# Homework 1 Problem 3

In $2d$ electron gas, using Born-von Karman boundary conditions:

\begin{equation}
\begin{aligned}
\psi (x,y + Y) = \psi(x,y) \\
\psi (x + X,y) = \psi(x,y)
\end{aligned}
\end{equation}

Therefore:

\begin{equation}
\psi_{\vec{k}}(\vec{r}) = \frac{e^{i\vec{k}\cdot\vec{r}}}{\sqrt{S}}
\end{equation}

Energy and momentum eigen values of electrons are related to wave vector:

\begin{equation}
\begin{aligned}
E_{\vec{k}} = \frac{\hbar^2 k^2}{2m}\\
\vec{p} = \hbar \vec{k}
\end{aligned}
\end{equation}

Recall boundary conditions. They imply:

\begin{equation}
\begin{aligned}
e^{ik_x X} = e^{ik_y Y} = 1
\end{aligned}
\end{equation}

Therefore:

\begin{equation}
\begin{aligned}
k_x = n_x \frac{2\pi}{X}\\
k_y = n_y \frac{2\pi}{Y}
\end{aligned}
\end{equation}

In $k-Space$, it's obvious that in every area of size $\frac{2\pi}{X}\frac{2\pi}{Y}$ there is only one grid point. That means that there are $\frac{S}{(2\pi)^2}$ grid points per unit area of $k-Space$.

That further means that:

\begin{equation}
d^2 \vec{n} = \frac{S}{(2\pi)^2} d^2 \vec{k}
\end{equation}

Spin degeneracy is:

\begin{equation}
g_s = 2s+1 = 2
\end{equation}

Combining previous results, we can write equation for total number of electrons:

\begin{equation}
N = g_s \int \frac{d^2 \vec{k}}{(2\pi)^2} S \cdot f_{FD} (\vec{k})
\end{equation}

where Fermi-Dirac distribution is:

\begin{equation}
f_{FD} (\vec{k}) = \frac{1}{e^{\frac{E_{\vec{k}} - \mu}{kT}} + 1}
\end{equation}

Chemical potential at $T = 0K$ is called Fermi energy. To calculate it, we will use:

\begin{equation}
\lim_{T \to 0} f_{FD} (\vec{k}) = \lim_{T \to 0} \frac{1}{e^{\frac{E_{\vec{k}} - \mu}{kT}} + 1} = \theta(E_F - E_{\vec{k}})
\end{equation}

Using step function approximation of Fermi-Dirac distribution at low temperatures, we can easily calculate number of particles:

\begin{equation}
N = g_s \frac{S}{(2\pi)^2} \pi k_F^2
\end{equation}

Finally, as $n_S = \frac{N}{S}$, we obtain Fermi wave vector:

\begin{equation}
k_F = \sqrt{2\pi n_S}
\end{equation}

Knowing Fermi wave vector, we can easily calculate Fermi energy and Fermi velocity:

\begin{equation}
\begin{aligned}
E_F = \frac{\hbar^2 k_F^2}{2m} = \frac{\pi n_S \hbar^2}{m} \\
v_F = \frac{\hbar k_F}{m} = \frac{\hbar}{m} \sqrt{2\pi n_S}
\end{aligned}
\end{equation}

If we define $r_S$ by equation:

\begin{equation}
\frac{S}{N} = \frac{1}{n_S} = \pi r_S^2
\end{equation}

Then $n_S = \frac{1}{\pi r_S^2}$.

Bohr radius is:

\begin{equation}
a_0 = 0.529 \cdot 10^{-10}m
\end{equation}

```python
a_0 = 0.529

k_F = 2 ** 0.5 / a_0
```

```python
k_F #Unit is A^-1
```

    2.6733715734841117

```python
from scipy.constants import hbar, m_e, e

v_F = hbar / m_e * k_F * 10 ** 10
```

```python
v_F
```

    3094899.0720617673

```python
e_F = hbar ** 2 * k_F ** 2 / (2 * m_e) * 10 ** 20
e_F/e
```

    27.229620250262712

Therefore:

\begin{equation}
\begin{aligned}
k_F = \frac{1}{(r_S/a_0)}\cdot 2.67337 A^{-1}\\
v_F = \frac{1}{(r_S/a_0)}\cdot 3.00949 \cdot 10^{6} \frac{m}{s}\\
E_F = \frac{1}{(r_S/a_0)^2} \cdot 27.229 eV
\end{aligned}
\end{equation}

Degeneracy of electron gass is:

\begin{equation}
g(E) = \sum_{s, vec{k}} \delta(E_{\vec{k}} - E) \to g_s \int \frac{d^2 \vec{k}}{(2\pi)^2} S \cdot \delta(E_k - E)
\end{equation}

Changing variables $d^2 \vec{k} \to k dk d\phi$ and integrating over polar angle, we get:

\begin{equation}
g(E) =  \frac{g_s S}{2\pi} \int_0^{\infty} k dk \delta(E_k - E)
\end{equation}

Using $\delta(f(x)) = \frac{\delta(x-x_0)}{|f'(x_0)|}$, we get:

\begin{equation}
g(E) = \frac{g_s S}{2\pi} \frac{m}{\hbar^2} \int_0^{\infty} dk \delta(k - \frac{1}{\hbar}\sqrt{2mE})
\end{equation}

Finally:

\begin{equation}
g(E) = \frac{g_s m S}{2\pi \hbar^2}
\end{equation}

# Homework 1 Problem 4

Recall some expressions from previous problem.

```python
from scipy.constants import k, e, epsilon_0, m_e, hbar, pi
from sympy import pprint
import numpy as np

def fermi_temperature(concentration):
    return fermi_energy(concentration) / k

def fermi_energy(concentration):
    return pi * concentration * hbar ** 2 / m_e
```

```python
n_s = [10 ** 19, 10 ** 14]
```

```python
for n in n_s:
    print("concentration = ", str(n), r"$m^{-2}$")
    print("Fermi Temperature = ", fermi_temperature(n), "K")
```

    concentration =  10000000000000000000 $m^{-2}$
    Fermi Temperature =  27779.860455981565 K
    concentration =  100000000000000 $m^{-2}$
    Fermi Temperature =  0.27779860455981564 K

Estimate of electrostatic energy is:

\begin{equation}
E_e = \frac{e^2}{4 \pi \epsilon_0 r_s}
\end{equation}

```python
e_e = e ** 2 / (4 * pi * epsilon_0 * a_0)
e_e/e * 10 ** 10
```

    27.220501610492526

In terms of $r_s/a_0$, we have:

\begin{equation}
E_e = \frac{1}{r_s/a_0} \cdot 27.2205 eV
\end{equation}

```python
e_e/e_F * 10 ** 10
```

    0.9996651205677355

We conclude that:

\begin{equation}
\frac{E_e}{E_F} \approx \frac{r_S}{a_0}
\end{equation}

```python
def r_s(concentration):
    return 1 / np.sqrt(pi * concentration)

def electrostatic_energy(concentration):
    r = r_s(concentration)
    return e ** 2 / (4 * pi * epsilon_0 * r)
```

```python
for n in n_s:
    print("concentration = ", str(n), r"$m^{-2}$")
    print("Fermi Energy = ", fermi_energy(n)/e, "eV")
    print("Electrostatic Energy = ", electrostatic_energy(n)/e, "eV")
    print("Ee/EF = ", electrostatic_energy(n)/fermi_energy(n))
```

    concentration =  10000000000000000000 $m^{-2}$
    Fermi Energy =  2.393882342709907 eV
    Electrostatic Energy =  8.070988571692807 eV
    Ee/EF =  3.371505954029611
    concentration =  100000000000000 $m^{-2}$
    Fermi Energy =  2.3938823427099072e-05 eV
    Electrostatic Energy =  0.025522706855738462 eV
    Ee/EF =  1066.163795955252

In case of smaller concentrations, electrostatic energy is more important then in case of smaller concentrations.

```python

```


```python
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

Here are some basic — and some more surprising — features of the iPython Notebook
that has been used to build this collection of astronomy examples.

```python
>>> n = 0
>>> for i in range(5):
...     n += i
...
>>> print(n)
```

    10

```python
# Exception tracebacks are attractive and detailed

1/0
```

```python
!pwd
```

    /home/brandon/astronomy-notebooks/An-Introduction

```python
!cal 1 2013
```

        January 2013
    Su Mo Tu We Th Fr Sa
           1  2  3  4  5
     6  7  8  9 10 11 12
    13 14 15 16 17 18 19
    20 21 22 23 24 25 26
    27 28 29 30 31

```python
files = !ls /usr/bin
```

```python
%load http://matplotlib.org/mpl_examples/api/radar_chart.py
```

```python
%timeit '-'.join(('abc', 'def', 'ghi'))
%timeit '-'.join(['abc', 'def', 'ghi'])
```

    125 ns ± 8.59 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
    171 ns ± 14.1 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

```python
from IPython.display import Image, HTML, Latex, YouTubeVideo
```

```python
# Inline media

f = '../Talks/tiangong-1-headline.png'
Image(filename=f)
```

```python
HTML('')
```

    /home/brandon/.v/astronomy-notebooks/lib/python3.7/site-packages/IPython/core/display.py:717: UserWarning: Consider using IPython.display.IFrame instead
      warnings.warn("Consider using IPython.display.IFrame instead")

```python
YouTubeVideo('F4rFuIb1Ie4')  # Fernando Pérez at PyConCA
```

```python
from sympy.interactive import init_printing
init_printing()

from sympy import *
x, y = symbols('x y')
eq = ((x + y)**2 * (x + 1))
```

```python
eq
```

```python
expand(eq)
```

```python
Latex(r'The Taylor series for $e^x$ is:'
      r'$$\sum_{x=0}^\infty {x^n / n!}$$')
```

The Taylor series for $e^x$ is:$$\sum_{x=0}^\infty {x^n / n!}$$

## XKCD Style

Recently, @jakevdp decided that his example plots looked too serious,
and wanted them to look more like hand-drawn plots in xkcd.

http://jakevdp.github.com/blog/2012/10/07/xkcd-style-plots-in-matplotlib/


# The heat equation in 1D

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

In this tutorial, we test a very classical lattice Boltzmann scheme $\DdQq{1}{3}$ on the heat equation.

The problem reads
$$
\begin{gathered} \drondt u = \mu \drondxx u, \quad t>0, \quad x\in(0,1),\\ u(0) = u(1) = 0, \end{gathered}
$$
where $\mu$ is a constant scalar.

```python
%matplotlib inline
```

## The scheme $\DdQq{1}{3}$

The numerical simulation of this equation by a lattice Boltzmann scheme consists in the approximatation of the solution on discret points of $(0,1)$ at discret instants.

To simulate this system of equations, we use the $\DdQq{1}{3}$ scheme given by

* three velocities $v_0=0$, $v_1=1$, and $v_2=-1$, with associated distribution functions $\fk{0}$, $\fk{1}$, and $\fk{2}$,
* a space step $\dx$ and a time step $\dt$, the ration $\lambda=\dx/\dt$ is called the scheme velocity,
* three moments
  $$ \mk{0}=\sum_{i=0}^{2} \fk{i}, \quad \mk{1}= \sum_{i=0}^{2} v_i \fk{i}, \quad \mk{2}= \frac{1}{2} \sum_{i=0}^{2} v_i^2 \fk{i},$$

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

In order to use the formalism of the package pylbm, we introduce the three polynomials that define the moments: $P_0 = 1$, $P_1=X$, and $P_2=X^2/2$, such that
$$ \mk{k} = \sum_{i=0}^2 P_k(v_i) \fk{i}.$$

The transformation $(\fk{0}, \fk{1}, \fk{2})\mapsto(\mk{0},\mk{1}, \mk{2})$ is invertible if, and only if, the polynomials $(P_0,P_1,P_2)$ is a free set over the stencil of velocities.

The lattice Boltzmann method consists to compute the distribution functions $\fk{0}$, $\fk{1}$, and $\fk{2}$ in each point of the lattice $x$ and at each time $t^n=n\dt$.
A step of the scheme can be read as a splitting between the relaxation phase and the transport phase:

* relaxation:
$$
    \begin{aligned}\mks{1}(t,x)&=(1-s_1)\mk{1}(t,x)+s_1\mke{1}(t,x),\\ \mks{2}(t,x)&=(1-s_2)\mk{2}(t,x)+s_2\mke{2}(t,x).\end{aligned}
$$

* m2f:
$$
    \begin{aligned}\fks{0}(t,x)&\;=\mk{0}(t,x)-2\mks{2}(t,x), \\ \fks{1}(t,x)&\;=\mks{1}(t,x)/2+\mks{2}(t,x), \\ \fks{2}(t,x)&\;=-\mks{1}(t,x)/2+\mks{2}(t,x).\end{aligned}
    $$

* transport:
$$
    \begin{aligned} \fk{0}(t+\dt, x)&\;=\fks{0}(t,x), \\ \fk{1}(t+\dt, x)&\;=\fks{1}(t,x-\dx), \\ \fk{2}(t+\dt, x)&\;=\fks{2}(t,x+\dx). \end{aligned}
    $$

* f2m:
$$
    \begin{aligned}\mk{0}(t+\dt,x)&\;=\fk{0}(t+\dt,x)+\fk{1}(t+\dt,x)+\fk{2}(t+\dt,x), \\ \mk{1}(t+\dt,x)&\;=\fk{1}(t+\dt,x)-\fk{2}(t+\dt,x), \\ \mk{2}(t+\dt,x)&\;=\tfrac{1}{2}\fk{1}(t+\dt,x)+\tfrac{1}{2}\fk{2}(t+\dt,x).\end{aligned}
$$

The moment of order $0$, $\mk{0}$, being conserved during the relaxation phase,
a diffusive scaling $\dt=\dx^2$, yields to the following equivalent equation
$$\drondt\mk{0} = 2\bigl(\tfrac{1}{s_1}-\tfrac{1}{2}\bigr) \drondxx\mke{2} + \grandO(\dx^2),$$

if $\mke{1}=0$.
In order to be consistent with the heat equation, the following choice is done:
$$\mke{2}=\tfrac{1}{2}u, \qquad s_1 = \frac{2}{1+2\mu}, \qquad s_2=1.$$

## Using pylbm

pylbm uses Python dictionary to describe the simulation. In the following, we will build this dictionary step by step.

### The geometry

In pylbm, the geometry is defined by a box and a label for the boundaries.

```python
import pylbm
import numpy as np

xmin, xmax = 0., 1.
dico_geom = {
    'box': {'x': [xmin, xmax], 'label':0},
}
geom = pylbm.Geometry(dico_geom)
print(geom)
geom.visualize(viewlabel=True);
```

### The stencil

pylbm provides a class stencil that is used to define the discret velocities of the scheme. In this example, the stencil is composed by the velocities $v_0=0$, $v_1=1$ and $v_2=-1$ numbered by $[0,1,2]$.

```python
dico_sten = {
    'dim': 1,
    'schemes':[{'velocities': list(range(3))}],
}
sten = pylbm.Stencil(dico_sten)
print(sten)
sten.visualize();
```

### The domain

In order to build the domain of the simulation, the dictionary should contain the space step $\dx$ and the stencils of the velocities (one for each scheme).

We construct a domain with $N=10$ points in space.

```python
N = 10
dx = (xmax-xmin)/N
dico_dom = {
    'box': {'x': [xmin, xmax], 'label':0},
    'space_step': dx,
    'schemes': [
        {
            'velocities': list(range(3)),
        }
    ],
}
dom = pylbm.Domain(dico_dom)
print(dom)
dom.visualize();
```

### The scheme

In pylbm, a simulation can be performed by using several coupled schemes. In this example, a single scheme is used and defined through a list of one single dictionary. This dictionary should contain:

* 'velocities': a list of the velocities
* 'conserved_moments': a list of the conserved moments as sympy variables
* 'polynomials': a list of the polynomials that define the moments
* 'equilibrium': a list of the equilibrium value of all the moments
* 'relaxation_parameters': a list of the relaxation parameters ($0$ for the conserved moments)
* 'init': a dictionary to initialize the conserved moments

(see the documentation for more details)

The scheme velocity could be taken to $1/\dx$ and the inital value of $u$ to

$$ u(t=0,x) = \sin(\pi x).$$

```python
import sympy as sp

def solution(x, t):
    return np.sin(np.pi*x)*np.exp(-np.pi**2*mu*t)

# parameters
mu = 1.
la = 1./dx
s1 = 2./(1+2*mu)
s2 = 1.
u, X = sp.symbols('u, X')

dico_sch = {
    'dim': 1,
    'scheme_velocity': la,
    'schemes':[
        {
            'velocities': list(range(3)),
            'conserved_moments': u,
            'polynomials': [1, X, X**2/2],
            'equilibrium': [u, 0., .5*u],
            'relaxation_parameters': [0., s1, s2],
        }
    ],
}

sch = pylbm.Scheme(dico_sch)
print(sch)
```

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

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

            - polynomials

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

            - equilibria

                ⎡  u  ⎤
                ⎢     ⎥
                ⎢ 0.0 ⎥
                ⎢     ⎥
                ⎣0.5⋅u⎦

            - relaxation parameters

                ⎡       0.0       ⎤
                ⎢                 ⎥
                ⎢0.666666666666667⎥
                ⎢                 ⎥
                ⎣       1.0       ⎦

        - moments matrices

            ⎡1  1    1 ⎤
            ⎢          ⎥
            ⎢0  10  -10⎥
            ⎢          ⎥
            ⎣0  50  50 ⎦

        - inverse of moments matrices

            ⎡1    0    -1/50⎤
            ⎢               ⎥
            ⎢0  1/20   1/100⎥
            ⎢               ⎥
            ⎣0  -1/20  1/100⎦

### The simulation

A simulation is built by defining a correct dictionary.

We combine the previous dictionaries to build a simulation. In order to impose the homogeneous Dirichlet conditions in $x=0$ and $x=1$, the dictionary should contain the key 'boundary_conditions' (we use pylbm.bc.Anti_bounce_back function).

```python
dico = {
    'box': {'x':[xmin, xmax], 'label':0},
    'space_step': dx,
    'scheme_velocity': la,
    'schemes':[
        {
            'velocities': list(range(3)),
            'conserved_moments': u,
            'polynomials': [1, X, X**2/2],
            'equilibrium': [u, 0., .5*u],
            'relaxation_parameters': [0., s1, s2],
        }
    ],
    'init': {u:(solution,(0.,))},
    'boundary_conditions': {
        0: {'method': {0: pylbm.bc.AntiBounceBack,}},
    },
    'generator': 'numpy'
}

sol = pylbm.Simulation(dico)
print(sol)
```

    +------------------------+
    | Simulation information |
    +------------------------+

        +--------------------+
        | Domain information |
        +--------------------+
            - spatial dimension: 1
            - space step: 0.1
            - with halo:
                bounds of the box: [-0.05] x [1.05]
                number of points: [12]
            - without halo:
                bounds of the box: [0.05] x [0.95]
                number of points: [10]

            +----------------------+
            | Geometry information |
            +----------------------+
                - spatial dimension: 1
                - bounds of the box: [0. 1.]
                - labels: [0, 0]

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

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

                - polynomials

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

                - equilibria

                    ⎡  u  ⎤
                    ⎢     ⎥
                    ⎢ 0.0 ⎥
                    ⎢     ⎥
                    ⎣0.5⋅u⎦

                - relaxation parameters

                    ⎡       0.0       ⎤
                    ⎢                 ⎥
                    ⎢0.666666666666667⎥
                    ⎢                 ⎥
                    ⎣       1.0       ⎦

            - moments matrices

                ⎡1  1    1 ⎤
                ⎢          ⎥
                ⎢0  10  -10⎥
                ⎢          ⎥
                ⎣0  50  50 ⎦

            - inverse of moments matrices

                ⎡1    0    -1/50⎤
                ⎢               ⎥
                ⎢0  1/20   1/100⎥
                ⎢               ⎥
                ⎣0  -1/20  1/100⎦

### Run a simulation

Once the simulation is initialized, one time step can be performed by using the function one_time_step.

We compute the solution of the heat equation at $t=0.1$. And, on the same graphic, we plot the initial condition, the exact solution and the numerical solution.

```python
import numpy as np
import sympy as sp
import pylab as plt
import pylbm

u, X, LA = sp.symbols('u, X, LA')

def solution(x, t):
    return np.sin(np.pi*x)*np.exp(-np.pi**2*mu*t)

xmin, xmax = 0., 1.
N = 128
mu = 1.
Tf = .1
dx = (xmax-xmin)/N # spatial step
la = 1./dx
s1 = 2./(1+2*mu)
s2 = 1.
dico = {
    'box':{'x': [xmin, xmax], 'label': 0},
    'space_step': dx,
    'scheme_velocity': la,
    'schemes': [
        {
            'velocities': list(range(3)),
            'conserved_moments': u,
            'polynomials': [1, X/LA, X**2/(2*LA**2)],
            'equilibrium': [u, 0., .5*u],
            'relaxation_parameters': [0., s1, s2],
        }
    ],
    'init': {u: (solution, (0.,))},
    'boundary_conditions': {
        0: {'method': {0: pylbm.bc.AntiBounceBack,}},
    },
    'parameters': {LA: la},
    'generator': 'numpy'
}

sol = pylbm.Simulation(dico)
x = sol.domain.x
y = sol.m[u]

plt.figure(1)
plt.plot(x, y, 'k', label='initial')

while sol.t < 0.1:
    sol.one_time_step()

plt.plot(x, sol.m[u], 'b', label=r'$D_1Q_3$')
plt.plot(x, solution(x, sol.t),'r', label='exact')
plt.title('Heat equation t={0:5.3f}'.format(sol.t))
plt.legend();
```


# Physics 420/580 Final Exam
## December 19, 2019 2pm-5pm  L1-150

Do three of the four following problems.  If you attempt all four, your best three will be used for your mark.  Use the Jupyter notebook, inserting your code and any textual answers/explanations in cells between the questions. (Feel free to add additional cells!)  Marks will be given based on how clearly you demonstrate your understanding.

There are no restrictions on downloading from the internet, eclass,  or the use of books, notes, or any other widely available computing resources.  However, **you are not allowed** to communicate with each other or collaborate in any way and uploading to the internet or sending or receiving direct communications is not appropriate.  You are required to sit the exam in L1-150, and to upload it before you leave.

When you are finished, upload the jupyter notebook to eclass.

Also be careful to save the notebook periodically and double check **that you upload the file that has your answers in it!**

```python

```

# The Normal Distribution

 Write a simple program that:

 a.  Calculates and plots the function
 $$G(x;\mu,\sigma)=1000e^{-\frac{(x-\mu)^2}{2\sigma^2}}$$ with an average value $\mu=10$, width $\sigma=2$.  This is proportional to the normal distribution.

```python

```

The three dimensional normal distribution is the product of normal distributions in the three coordinates:
\begin{equation} G(\vec{x}; \vec{x_0},\sigma, A)=Ae^{-\frac{(\vec{x}-\vec{x_0})^2}{2\sigma^2}}\end{equation}

Set $\vec{x_0}=(6,8,0)$ and throw 100000 random points by throwing the three coordinates independently with $\sigma=2$ for each coordinate.

Histogram and plot the distributions for each coordinate.  Comment on how you pick the right number of bins for your histogram.

```python

```

Now histogram and plot the distribution of $r=|\vec{x}-\vec{x_0}|$.

```python

```

This should look like the analytic 3d normal distribution in spherical coordinates:
\begin{equation}G(r;\sigma,A)=4\pi A r^2 e^{-\frac{r^2}{2\sigma^2}}.\end{equation}  We derived this equation by converting $G(\vec{x}; \vec{x_0},\sigma, A)$ into spherical coordinates and integrating over $\theta$ and $\phi.$

Determine A for 100000 points by numerically integrating:
\begin{equation}100000=\int_0^{20} G(r;\sigma,A)dr =A\int_0^{20} G(r;\sigma,1)dr\end{equation}

With a known A, you can now replot your histogram and overlay the calculation of $G(r;\sigma,A)$.  To have $G(r;\sigma,A)$ overlay properly you will need to include the bin widths, which you can do either by the exact integral or the approximation: $\int_{r_i}^{r_{i+1}}G(r;\sigma,A)dr \approx G(r;\sigma,A)\Delta r$, where $r_i$ is the start of the i-th bin and $r_{i+1}$ is the start of the next bin, and $\Delta r=r_{i+1}-r_i$.

```python

```

# Monte Carlo

A radioactive source is placed into a large water Cerenkov detector (which is able to detect high energy electrons/gammas in pure water by looking at visible photons from the Cerenkov process.)  Before the source is deployed, the detector measures background radiation at a rate of 200 events/second.  The source produces two types of events:
1.  single gamma rays with a rate of 100 events/second
2. events that produce both a gamma ray and a neutron at a rate of 100 events/second.  The gamma ray is detected immediately after creation with an efficiency of 50% (for both gamma rays from the source- the singles and the gamma/neutron coincidences).  The neutron is detected after a capture time, which is quantified with a mean of 250 microseconds. In math, this says that the time distribution of neutron captures is $$P(t)=250\times 10^{-6} e^{-t/(250\times 10^{-6})},$$ where t is the time difference (in seconds) between gamma/neutron creation and neutron capture. The neutron detection efficiency is. 40%.    The time distribution of neutron captures is $$P(t)=250\times 10^{-6} e^{-t/(250\times 10^{-6})},$$ where t is the time difference (in seconds) between gamma/neutron creation and neutron capture.

Write a simple MC to model 1000 seconds of running time.   Generate the times for background events, the single gamma rays from the source and the gamma/neutron coincidences.  Then generate the neutron capture times and finally apply the detection efficiencies for each type of event.

Histogram and plot:

1.  The times of each type of event (on an x-axis with a scale of 0-1000 seconds)
2.  The intervals between a detected event and the next detected event (from 0-10 ms).  To do this you might need to make an array with the times of all detections, sort it and then calculate the time from one event to the next.
3.  Repeat plots 1 and 2 for the case where the background rate is 1000 events/second.

```python

```

```python

```

# Solving and Fitting

An object is viewed by 6 theodolites (telescopes that allow you to measure angles with respect to the vertical($\theta$) and with respect to a horizontal($\phi$).  The locations of the telescopes and the measured angles are given in the code cell below (all with respect to a single coordinate system,) which you can run in order to avoid cutting and pasting.

Since each theodolite gives you two quantities we have 12 measurements.  There are three unknowns- the x, y, and z coordinates of a point on an object.  Consequently the system of equations is overdetermined, and we would typically be unable to solve.

In a case like this, we "solve" the system of equations by fitting.

Write a function that calculates the theta and phi angles (spherical coordinates) given an arbitrary point position and an theodolite position.

Using this function, write a second function that calculates the residuals of the calculation with respect to the data. Then find the position of the target by fitting the calculations to the data.

Extract the uncertainties on the position.  In order to do this, you will need to estimate the size of the uncertainty in the angles to incorporate into the residual calculation.  One way to do this is by histogramming the residuals ((data-calculation)/uncertainty) and getting a sense of how big they are- if the uncertainties are correct the RMS/width of the residuals distribution should be about 1.

```python

```

```python
import numpy as np
positions =  np.array([[-0.0114519 ,  9.99999344,  6.69130799],
       [ 6.33116335, -7.74056655,  3.59009715],
       [-9.85887257, -1.67410623,  4.56138703],
       [ 4.09466118,  9.12325325,  7.32437587],
       [-3.37815507, -9.41212348,  3.67927878],
       [-5.4274145 , -8.39899827,  6.8865273 ]])
thetaMeasures = np.array( [2.3164460126412245, 1.6313464103926718, 1.6970719353033004, 2.533624050255211, 1.6289243873579955, 1.8159707640506253] )
phiMeasures = np.array( [-0.9844385514247201, 1.8432675248986576, 0.5831709514637226, -2.132723211863188, 1.2215622389804748, 1.0853416750730511] )

print('Theodolite x  \t Theodolite y \t Theodolite z \t Meas.theta \t Meas. phi')
for i in range(0,6):
    print('%f  \t %f \t %f \t %f \t %f '%(positions[i,0],positions[i,1],positions[i,2],thetaMeasures[i],phiMeasures[i]))

```

    Theodolite x  	 Theodolite y 	 Theodolite z 	 Meas.theta 	 Meas. phi
    -0.011452  	 9.999993 	 6.691308 	 2.316446 	 -0.984439
    6.331163  	 -7.740567 	 3.590097 	 1.631346 	 1.843268
    -9.858873  	 -1.674106 	 4.561387 	 1.697072 	 0.583171
    4.094661  	 9.123253 	 7.324376 	 2.533624 	 -2.132723
    -3.378155  	 -9.412123 	 3.679279 	 1.628924 	 1.221562
    -5.427415  	 -8.398998 	 6.886527 	 1.815971 	 1.085342

```python

```

# Ordinary Differential Equations

Consider the mechanics problem shown in the diagram below.  We have a rigid bar $l=4$ meters long, with mass $m=20$ kg, at an angle $\theta$ to the floor and leaning against a wall.  The center of mass of the bar is at coordinates (x,y).  The normal force against the wall we designate as $N_1,$ while the normal force to the floor is $N_2.$  The coefficient of (sliding) friction with the floor is $\mu_2=0.2$, while the coefficient of friction with the wall is $\mu_1=0.4.$

Calculate how long it takes for the bar to fall to the floor.  The acceleration due to gravity is $g.$  Start with an initial condition of $\theta_0= 1.15$ radians, and $\omega_0=\dot{\theta}(0)=0.$

From the mechanics perspective, this involves setting up Newton's second law for the acceleration of the center of mass, and using the torque equation for the rotation of the bar around the center of mass:
\begin{align}\\
-mg+N_2+\mu_1 N_1&=m\ddot{y}\\
N_1-\mu_2 N_2=m\ddot{x}\\
(N_1+\mu_2 N_2)\frac{l}{2} \sin\theta - (N_2-\mu_1 N_1) \frac{l}{2}\cos\theta &=I\ddot{\theta}\\
\end{align}

For a bar, the moment of inertia $I=\frac{1}{12}ml^2.$

The geometry of the situation means that we can't simply plug the equation above into an ODE integrator.  Instead, we need to relate $x$ and $y$ to the angle $\theta:$
\begin{align}\\
x&=\frac{l}{2}\cos\theta\\
\dot{x}&=-\frac{l}{2}\sin\theta \dot{\theta}\\
\ddot{x}&=-\frac{l}{2}\sin\theta \ddot{\theta}-\frac{l}{2}\cos\theta \dot{\theta}^2\\
y&=\frac{l}{2}\sin\theta\\
\dot{y}&=\frac{l}{2}\cos\theta \dot{\theta}\\
\ddot{y}&=\frac{l}{2}\cos\theta \ddot{\theta}-\frac{l}{2}\sin\theta \dot{\theta}^2\\
\end{align}

Now substitute these equations for $\ddot{x}$ and $\ddot{y}$ into the original equation for Newton's laws.
Solve the ODE numerically.  For each step, you will need to solve the set of simultaneous equations for $\ddot{\theta}$, $N_1,$ and $N_2$.

Plot $\theta$ as a function of time $t$.  Graphically find the time the bar hits the floor ($\theta=0.$)
Plot $N_1$ and $N_2$ as a function of time $t$.  Discuss any mathematical assumptions in our model that might not reflect reality and how the model would need to change to accommodate them.

```python

```

```python

```

When you are finished, upload your completed exam onto eClass.  Double check that the uploaded exam contains your latest work!

```python

```


# Monte Carlo Calculation of π

### Christina C. Lee

### Category: Numerics

### Monte Carlo Physics Series
* [Monte Carlo: Calculation of Pi](../Numerics_Prog/Monte-Carlo-Pi.ipynb)
* [Monte Carlo Markov Chain](../Numerics_Prog/Monte-Carlo-Markov-Chain.ipynb)
* [Monte Carlo Ferromagnet](../Prerequisites/Monte-Carlo-Ferromagnet.ipynb)
* [Phase Transitions](../Prerequisites/Phase-Transitions.ipynb)

## Monte Carlo- Random Numbers to Improve Calculations

When one hears "Monte Carlo", most people might think of something like this:

Monte Carlo, Monaco: known for extremely large amounts of money, car racing, no income taxes,and copious gambling.

In addition to Monaco, Europe, Las Vegas decided to host a Monte Carlo-themed casino as well.  So during the Manhattan project, when the best minds in the United States were camped out in the New Mexican desert, they had plenty of inspiration from Las Vegas, and plenty of difficult problems to work on in the form of quantifying the inner workings of nuclei.  Enrico Fermi first played with these ideas, but Stanislaw Ulam invented the modern Monte Carlo Markov Chain later.

At the same time, these scientists now had computers at their disposal.  John von Neumann programmed Ulam's algorithm onto ENIAC (Electronic Numerical Integrator and Computer), the very first electronic, general purpose computer, even though it did still run on vacuum tubes.

That still doesn't answer, why do random numbers actually help us solve problems?

Imagine you are visiting a new city for the first time (maybe Monte Carlo). You only have a day or two, and you want to really get to know the city.  You have two options for your visit

* Hit the tourist sites you researched online
* Wander around.  Try and communicate with the locals.  Find an out-of-the-way restaurant and sample food not tamed for foreigners.  Watch people interact.  Get lost.

Both are legitimate ways to see the city.  But depending on what you want, you might choose a different option.  The same goes for exploring physics problems.  Sometimes you want to go in and study just everything you knew about beforehand, but sometimes you need to wander around, not hit everything, but get a better feeling for what everything might be like.

## Buffon's Needle: Calculation of π
Even back in the 18th century, Georges-Louis Leclerc, Comte de Buffon posed a problem in geometric probability. Nowadays, we use a slightly different version of that problem to calculate π and illustrate Monte Carlo simulations.

Suppose we have a square dartboard, and someone with really bad, completely random aim, even though he/she always at least hits inside the dartboard.  We then inscribe a circle inside that dartboard. After an infinite number of hits, what is the ratio between hits in the circle, and hits in the square?

\begin{equation}
    f= \frac{N_{circle}}{N_{square}} =\frac{\text{Area of circle}}{\text{Area of square}} =\frac{\pi r^2}{4 r^2}= \frac{\pi}{4}
\end{equation}
\begin{equation}
    \pi = 4 f
\end{equation}

## Onto the Code!

```julia
using Statistics
using Plots
gr()
```

    Plots.GRBackend()

We will generate our random numbers on the unit interval.  Thus the radius in our circumstance is $.5$.

 Write a function `incircle(r2)` such that if `r2` is in the circle, it returns true, else, it returns false.  We will use this with the Julia function `filter`.  Assume `r2` is the radius squared, and already centered around the middle of the unit circle

```julia
function incircle(r2)
    if r2<.25
        return true
    else
        return false
    end
end
```

    incircle (generic function with 1 method)

```julia
#The number of darts we will throw at the board.  We will see how accurate different numbers are
N=[10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000];
# We will perform each number multiple times in order to calculate error bars
M=15;
```

```julia
πapprox=zeros(Float64,length(N),M);

for ii in 1:length(N)
    for jj in 1:M

        #popular our array with random numbers on the unit interval
        X=rand(N[ii],2)

        #calculate their radius squared
        R2=(X[:,1].-0.5).^2.0.+(X[:,2].-0.5).^2

        # 4*number in circle / total number
        πapprox[ii,jj]=4.0*length(filter(incircle,R2))/N[ii];

    end
end
```

```julia
# Get our averages and standard deviations
πave=mean(πapprox,dims=2);
πstd=std(πapprox,dims=2);
```

## Analysis

So that was a nice, short little piece of code.  Lets plot it now to see means.

```julia
plot(N,π*ones(length(N)),xscale=:log10);

for j in 1:M
    scatter!(N,πapprox[:,j]);
end

scatter!(N,πave,yerr=πstd)
plot!(xlabel="Number of Darts",ylabel="pi Estimate",
        title="Monte Carlo Estimate of pi",legend=:false)
```

When we have fewer numbers of points, our estimates vary much more wildly, and much further from 3.1415926 .
But, at least, the guesses from our different runs all seem equally distributed around the correct value, so it seems we have no systematic error.

As we get up to $10^4$, our estimate starts getting much more accurate and consistent.

```julia
plot(N,πstd,xscale=:log10)

plot!(xlabel="N points"
    ,ylabel="standard deviation"
    ,title="Dependence of Monte Carlo Error on Number of Points")
```

So what we guessed in the first plot about dispersion in estimate, we quantify here in this plot.  When we only have 10 darts, the guesses vary by up to .3, but when we get down to 1,000 trials, we are starting to be consistent to .0002

```julia
plot(N,πave,xscale=:log10,label="π Guess")
plot!(N,π*ones(length(N)),label="π")

plot!(xlabel="N steps"
    ,ylabel="Average of 15 runs"
    ,title="Overall Averages")
```

Now lets just make a graphical representation of what we've been doing this whole time.  Plot our points on a unit square, and color the ones inside the circle a different color.

```julia
X=rand(1000);
Y=rand(1000);
R2=(X.-0.5).^2.0.+(Y.-0.5).^2;
Xc=[];
Yc=[]
for ii in 1:length(X)
    if R2[ii]<.25
        push!(Xc,X[ii]);
        push!(Yc,Y[ii]);
    end
end

```

```julia
scatter(X,Y)
scatter!(Xc,Yc)
plot!(aspect_ratio=1,xlabel="X",ylabel="Y",legend=:false,
        title="Dartboard")
```

That's all folks!
Now here's a picture of some pie to congratulate you on calculating π.

<sub>By Scott Bauer, USDA ARS - This image was released by the Agricultural Research Service, the research agency of the United States Department of Agriculture, with the ID K7252-47 (next).This tag does not indicate the copyright status of the attached work. A normal copyright tag is still required. See Commons:Licensing for more information.English | français | македонски | +/−, Public Domain, https://commons.wikimedia.org/w/index.php?curid=264106 </sub>

@article{Markov,
author = {A.  Markov , A},
year = {2006},
month = {12},
pages = {591 - 600},
title = {An Example of Statistical Investigation of the Text Eugene Onegin Concerning the Connection of Samples in Chains},
volume = {19},
journal = {Science in Context},
doi = {10.1017/S0269889706001074}
}

```julia

```


# MLGeometry guide

This introduction demonstrates how to use MLGeometry to:
1. Generate a hypersurface.
2. Build a bihomogeneous neural network.
3. Use the model to compute numerical Calabi-Yau metrics with the embedding method.
4. Plot $\eta$ on a rational curve.

## Configure imports

Import tensorflow_probability to use the L-BFGS optimizer:

```python
import sympy as sp
import tensorflow as tf
import tensorflow.python.keras.backend as K
import tensorflow_probability as tfp
```

```python
import MLGeometry as mlg
from MLGeometry import bihomoNN as bnn
```

Import the libraries to plot the $\eta$ on the rational curve (see the last section):

```python
import math
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

## Set a random seed (optional)
Some random seed might be bad for numerical calulations. If there are any errors during the training, you may want to try a different seed.

```python
np.random.seed(42)
tf.random.set_seed(42)
```

## Define a hypersurface
First define a set of coordinates and a function as sympy symbols:

```python
z0, z1, z2, z3, z4 = sp.symbols('z0, z1, z2, z3, z4')
Z = [z0,z1,z2,z3,z4]
f = z0**5 + z1**5 + z2**5 + z3**5 + z4**5 + 0.5*z0*z1*z2*z3*z4
```

Then define a hypersurface as a collection of points which solve the equation f = 0, using the `Hypersurface` class in the `mlg.hypersurface` module. The parameter n_pairs is the number of random pairs of points used to form the random lines in $\mathbf{CP}^{N+1}$. Then we take the intersections of those random lines and the hypersurface. By Bezout's theorem, each line intersects the hypersurface in precisely d points where d is the number of homogeneous coordinates. So the total number of points is d * n_pairs.

```python
n_pairs = 10000
HS_train = mlg.hypersurface.Hypersurface(Z, f, n_pairs)
HS_test = mlg.hypersurface.Hypersurface(Z, f, n_pairs)
```

The Hypersurface class will take care of the patchwork automatically. Let's use the `list_patches` function to check the number of points on each patch:

```python
HS_train.list_patches()
```

    Number of Patches: 5
    Points on patch 1 : 9933
    Points on patch 2 : 10015
    Points on patch 3 : 10249
    Points on patch 4 : 10011
    Points on patch 5 : 9792

You can also invoke this method on one of the patches to check the distribution on the subpatches:

```python
HS_train.patches[0].list_patches()
```

    Number of Patches: 4
    Points on patch 1 : 2474
    Points on patch 2 : 2468
    Points on patch 3 : 2547
    Points on patch 4 : 2444

The Hypersurface class contains some symbolic and numerical methods as well, which will be introduced elsewhere.

## Training with Tensorflow
The following steps are similar to a regular Tensorflow training process.
### Generate datasets
The `mlg.tf_dataset.generate_dataset` function converts a hypersurface to a Tensorflow Dataset, which has four componets: the points on the hypersurface, the volume form $\small \Omega \wedge \bar\Omega$, the mass reweighting the points distribution and the restriction which restricts the Kähler metric to a subpatch. The restriction contains an extra linear transformation so that points on different affine patches can all be processed in one call. It is also possible to generate a dataset only on one affine patch.

```python
train_set = mlg.tf_dataset.generate_dataset(HS_train)
test_set = mlg.tf_dataset.generate_dataset(HS_test)
```

Shuffle and batch the datasets:

```python
train_set = train_set.shuffle(HS_train.n_points).batch(1000)
test_set = test_set.shuffle(HS_test.n_points).batch(1000)
```

Let's look at what is inside a dataset:

```python
points, Omega_Omegabar, mass, restriction = next(iter(train_set))
print(points)
```

    tf.Tensor(
    [[-0.28939024-0.7013853j   0.36498785+0.31731284j  1.        -0.j
      -0.83973217-0.04013346j  0.69238067-0.44357434j]
     [-0.2632903 +0.00763338j  1.        +0.j         -0.3904083 -0.03917403j
      -0.07155564+0.36113203j  0.80278397+0.58611786j]
     [-0.9368906 +0.12442758j  0.21154833+0.6303546j  -0.3895686 +0.4952412j
      -0.22148535-0.86754274j  1.        -0.j        ]
     ...
     [-0.3757828 -0.8230363j  -0.176058  +0.23105958j -0.36762843+0.82377625j
      -0.28792354-0.0678029j   1.        -0.j        ]
     [ 0.38600475+0.27455673j  0.80249757+0.5882626j   1.        +0.j
      -0.0039644 +0.08065715j -0.46647677+0.11512545j]
     [ 0.8101118 -0.5754197j   1.        +0.j         -0.13839988-0.06433091j
      -0.17594224+0.5278059j  -0.47444513+0.20035468j]], shape=(1000, 5), dtype=complex64)

### Build a bihomogeneous neural network

The `mlg.bihomoNN` module provides the necessary layers (e.g. `Bihomogeneous` and `Dense` ) to construct the Kähler potential with a bihomogeneous neural network. Here is an example of a two-hidden-layer network (k = 4) with 70 and 100 hidden units:

```python
class Kahler_potential(tf.keras.Model):
    def __init__(self):
        super(Kahler_potential, self).__init__()
        # The first layer transforms the complex points to the bihomogeneous form.
        # The number of the outputs is d^2, where d is the number of coordinates.
        self.bihomogeneous = bnn.Bihomogeneous()
        self.layer1 = bnn.Dense(5**2, 70, activation=tf.square)
        self.layer2 = bnn.Dense(70, 100, activation=tf.square)
        self.layer3 = bnn.Dense(100, 1)

    def call(self, inputs):
        x = self.bihomogeneous(inputs)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = tf.math.log(x)
        return x
```

```python
model = Kahler_potential()
```

Define the Kähler metric $g_{i \bar j} = \partial_i\bar\partial_{\bar j} K$ and the volume form $d\mu_g = \det g_{i \bar j}$:

```python
@tf.function
def volume_form(points, Omega_Omegabar, mass, restriction):

    kahler_metric = mlg.complex_math.complex_hessian(tf.math.real(model(points)), points)
    volume_form = tf.matmul(restriction, tf.matmul(kahler_metric, restriction, adjoint_b=True))
    volume_form = tf.math.real(tf.linalg.det(volume_form))

    # Calculate the normalization constant to make the overall integration as 1
    # It is a batchwise calculation but we expect it to converge to a constant eventually
    weights = mass / tf.reduce_sum(mass)
    factor = tf.reduce_sum(weights * volume_form / Omega_Omegabar)

    return volume_form / factor
```

### Train the model with Adam and L-BFGS
#### Adam
Setup the keras optmizer as `Adam` and the loss function as one of weighted loss in the `mlg.loss` module. Some available functions are `weighted_MAPE`, `weighted_MSE`, `max_error` and `MAPE_plus_max_error`. They are weighted with the mass formula since the points on the hypersurface are distributed according to the Fubini-Study measure while the measure used in the integration is determined by the volume form $\small \Omega \wedge \bar\Omega$.

```python
optimizer = tf.keras.optimizers.Adam()
loss_func = mlg.loss.weighted_MAPE
```

Loop over the batches and train the network:

```python
max_epochs = 500
epoch = 0
while epoch < max_epochs:
    epoch = epoch + 1
    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(train_set):
        with tf.GradientTape() as tape:
            det_omega = volume_form(points, Omega_Omegabar, mass, restriction)
            loss = loss_func(Omega_Omegabar, det_omega, mass)
            grads = tape.gradient(loss, model.trainable_weights)
        optimizer.apply_gradients(zip(grads, model.trainable_weights))
    if epoch % 50 == 0:
        print("epoch %d: loss = %.5f" % (epoch, loss))
```

    epoch 50: loss = 0.06240
    epoch 100: loss = 0.05430
    epoch 150: loss = 0.05022
    epoch 200: loss = 0.01916
    epoch 250: loss = 0.01290
    epoch 300: loss = 0.01227
    epoch 350: loss = 0.01163
    epoch 400: loss = 0.01132
    epoch 450: loss = 0.01031
    epoch 500: loss = 0.01081

Let's check the loss of the test dataset. First define a function to calculate the total loss over the whole dataset:

```python
def cal_total_loss(dataset, loss_function):
    total_loss = tf.constant(0, dtype=tf.float32)
    total_mass = tf.constant(0, dtype=tf.float32)

    for step, (points, Omega_Omegabar, mass, restriction) in enumerate(dataset):
        det_omega = volume_form(points, Omega_Omegabar, mass, restriction)
        mass_sum = tf.reduce_sum(mass)
        total_loss += loss_function(Omega_Omegabar, det_omega, mass) * mass_sum
        total_mass += mass_sum
    total_loss = total_loss / total_mass

    return total_loss.numpy()
```

Check the results of MAPE and MSE:

```python
sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE)
E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE)
print("sigma_test = %.5f" % sigma_test)
print("E_test = %.5f" % E_test)
```

    sigma_test = 0.01105
    E_test = 0.00022

You can also check the error of the Monte Carlo integration, estimated by:

$$\delta \sigma = \frac{1}{\sqrt{N_p}} {\left( \int_X (|\eta - 1_X| - \sigma)^2 d\mu_{\Omega}\right)}^{1/2},$$

where $N_p$ is the number of points on the hypersurface and $\sigma$ is the `weighted_MAPE` loss, and

$$\eta = \frac{\det \omega}{\small \Omega \wedge \bar \Omega}$$

```python
def delta_sigma_square_test(y_true, y_pred, mass):
    weights = mass / K.sum(mass)
    return K.sum((K.abs(y_true - y_pred) / y_true - sigma_test)**2 * weights)

delta_sigma = cal_total_loss(test_set, delta_sigma_square_test)
print("delta_simga = %.5f" % delta_sigma)
```

    delta_simga = 0.00010

#### L-BFGS
As elaborated in our paper, when the network getting more complicated, L-BFGS converges faster than Adam near the minima. It is recommanded to use it after pretraining with Adam. However, L-BFGS is not in the standard Tensorflow library so the training process is slightly different: (Only ~20 iterations are shown here. In a real case you may want to set the `max_epochs` to ~1000)

```python
# The displayed max_epochs will be three to four times this value since iter + 1 everytime the function
# is invoked, which also happens during the evaluation of the function itself and its gradient
max_epochs = 5

# Setup the function to be optimized by L-BFGS

train_func = mlg.lbfgs.function_factory(model, loss_func, train_set)

# Setup the inital values and train
init_params = tf.dynamic_stitch(train_func.idx, model.trainable_variables)
results = tfp.optimizer.lbfgs_minimize(value_and_gradients_function=train_func,
                                        initial_position=init_params,
                                        max_iterations=max_epochs)
# Update the model after the last loop
train_func.assign_new_model_parameters(results.position)
```

    Iter: 1 loss: 0.0104599521
    Iter: 2 loss: 1.98670208
    Iter: 3 loss: 0.169171184
    Iter: 4 loss: 0.0170233361
    Iter: 5 loss: 0.0103792194
    Iter: 6 loss: 0.0102512939
    Iter: 7 loss: 0.0106646018
    Iter: 8 loss: 0.0102152973
    Iter: 9 loss: 0.0101271793
    Iter: 10 loss: 0.0100452136
    Iter: 11 loss: 0.0100203073
    Iter: 12 loss: 0.0098494282
    Iter: 13 loss: 0.0114825852
    Iter: 14 loss: 0.00984716602
    Iter: 15 loss: 0.00980117545
    Iter: 16 loss: 0.00973520614
    Iter: 17 loss: 0.00973185431

Note that the definition of the volume form is already in the `mlg.lbfgs` module. Also note that the standard L-BFGS does not support multi-batch training. You can still batch the dataset in case the GPU is out of memory, but the parameters are only updated after a whole epoch.

You can also check the test dataset:

```python
sigma_test = cal_total_loss(test_set, mlg.loss.weighted_MAPE)
E_test = cal_total_loss(test_set, mlg.loss.weighted_MSE)
print("sigma_test = %.5f" % sigma_test)
print("E_test = %.5f" % E_test)
```

    sigma_test = 0.01040
    E_test = 0.00021

### $\eta$ on the rational curve

Now let's retrict our model to a subspace and check the local behavior of $\eta$. With the quintic 3-fold f = 0, we can choose the embedding

$$(z_0, -z_0, z_1, 0, -z_1),$$

and the local coordinate system defined by $t = z_1 / z_0$. Using shperical coordinates $(\theta, \phi)$, it can be embedded into $\mathbb{R}^3$ by:

$$z_0 = \sin \theta \cos \phi, \qquad z_1= \sin \theta \sin \phi + i \cos \phi$$

So first sample the points on the rational curve:

```python
theta, phi = np.linspace(0.001,np.pi+0.001, 400), np.linspace(0.001, 2*np.pi+0.001, 400)
eps = 0.0001 + 0.0001j

R = []
points_list = []
for j in phi:
    for i in theta:
        t = complex(math.sin(i)*math.sin(j), math.cos(i)) / (math.sin(i)*math.cos(j))
        if np.absolute(t) <= 1:
            # The Bihomogeneous layer will remove the zero entries automatically.
            # So here we add a small number eps to avoid being removed
            points_list.append([1+eps, -1+eps, t+eps, 0+eps, -t+eps])
        else:
            # Use the symmetry:
            points_list.append([1+eps, -1+eps, 1/t+eps, 0+eps, -1/t+eps])
```

Use this set of points to generate the rational curve with norm_coordinate = z0 and max_grad_coordinate = z1:

```python
rc = mlg.hypersurface.Hypersurface(Z, f, points=points_list, norm_coordinate=0, max_grad_coordinate=0)
rc_dataset = mlg.tf_dataset.generate_dataset(rc).batch(rc.n_points)
```

Calculate $\eta$:

```python
points, Omega_Omegabar, mass, restriction = next(iter(rc_dataset))
det_omega = volume_form(points, Omega_Omegabar, mass, restriction)
eta = (det_omega / Omega_Omegabar).numpy()
```

Convert to Cartesian coordinates:

```python
R = eta.reshape(400, 400)
THETA, PHI = np.meshgrid(theta, phi)
X = R * np.sin(THETA) * np.cos(PHI)
Y = R * np.sin(THETA) * np.sin(PHI)
ZZ = R * np.cos(THETA)
```

Plot the figure:

```python
fig = plt.figure()
ax = fig.add_subplot(1,1,1, projection='3d')
ax.set_zlim3d(-1.0, 1.0)
plot = ax.plot_surface(
    X, Y, ZZ, rstride=1, cstride=1, cmap=plt.cm.YlGnBu_r,
    linewidth=0, antialiased=False)
```

$\eta$ is expected to approach the constant function 1 as k increases.