 **Chapter 2: [Diffraction](CH2_00-Diffraction.ipynb)**

# Unic Cell Determination and Stereographic Projection

[Download](https://raw.githubusercontent.com/gduscher/MSE672-Introduction-to-TEM//main/Diffraction/CH2_09-Unit_Cell.ipynb)

[](
    https://colab.research.google.com/github/gduscher/MSE672-Introduction-to-TEM/blob/main/Diffraction/CH2_09-Unit_Cell.ipynb)

part of

 **[MSE672:  Introduction to Transmission Electron Microscopy](../_MSE672_Intro_TEM.ipynb)**

by Gerd Duscher, Spring 2021

Microscopy Facilities

Joint Institute of Advanced Materials

Materials Science & Engineering

The University of Tennessee, Knoxville

Background and methods to analysis and quantification of data acquired with transmission electron microscopes

## Load relevant python packages
### Check Installed Packages

```python
import sys
from pkg_resources import get_distribution, DistributionNotFound

def test_package(package_name):
    """Test if package exists and returns version or -1"""
    try:
        version = get_distribution(package_name).version
    except (DistributionNotFound, ImportError) as err:
        version = '-1'
    return version

# Colab setup ------------------
if 'google.colab' in sys.modules:
    !pip install pyTEMlib -q
# pyTEMlib setup ------------------
else:
    if test_package('pyTEMlib') < '0.2021.3.17':
        print('installing pyTEMlib')
        !{sys.executable} -m pip install  --upgrade pyTEMlib -q
# ------------------------------
print('done')
```

    installing pyTEMlib
    done

    WARNING: You are using pip version 21.0; however, version 21.0.1 is available.
    You should consider upgrading via the 'C:\Users\nosle\anaconda3\python.exe -m pip install --upgrade pip' command.

## Import numerical and plotting python packages
Import the python packages that we will use:

Beside the basic numerical (numpy) and plotting (pylab of matplotlib) libraries,

and some libraries from the book
* kinematic scattering library.

```python
# import matplotlib and numpy
#                       use "inline" instead of "notebook" for non-interactive plots
import sys
if 'google.colab' in sys.modules:
    %pylab --no-import-all inline
else:
    %pylab --no-import-all notebook

# additional package
import  itertools
from matplotlib import patches

# Import libraries from the book

# Import libraries from pyTEMlib
import pyTEMlib
import pyTEMlib.KinsCat as ks         # Kinematic sCattering Library
                             # Atomic form factors from Kirklands book

__notebook_version__ = '2021.02.17'
print('pyTEM version: ', pyTEMlib.__version__)
print('notebook version: ', __notebook_version__)
```

    Populating the interactive namespace from numpy and matplotlib
    Using KinsCat library version  0.5  by G.Duscher
    spglib not installed; Symmetry functions of spglib disabled
    pyTEM version:  0.2021.02.17
    notebook version:  2021.02.17

    C:\Users\nosle\anaconda3\lib\site-packages\pyUSID\viz\__init__.py:16: FutureWarning: Please use sidpy.viz.plot_utils instead of pyUSID.viz.plot_utils. pyUSID.plot_utils will be removed in a future release of pyUSID
      warn('Please use sidpy.viz.plot_utils instead of pyUSID.viz.plot_utils. '

## Unit Cell Determination

- The HOLZ rings will give the lattice repeat vector (reciprocal vector parallel to the zone axis).
- So tilting in [001] zone axis, the ZOLZ pattern will give you the [100] and [010] distance
- and the HOLZ ring radius the [001] distance.

> **This is the determination of the lattice parameter of a unit cell.**
>
>Thus, we see that one can determine 3D information from a single two dimensional pattern.
>
>It might be necessary to use other low order zone axes.

### Measurements

- Record HOLZ and ZOLZ patterns, if possible in one picture (use double illumination with different exposure times to enhance dynamic range), but with different convergence angles.
- If the angle of the ring is too large then your measurements may suffer from lens distortions.
- If the HOLZ ring is split measure the inner one.

### Z-Component of Unit Cell

If $H$ is the distance between the reciprocal-lattice planes parallel to the beam and $G_n$ is the projected radius of the HOLZ ring, then
\begin{eqnarray}
G_1&=& \left( \frac{2H}{\lambda}\right)^{1/2} = \sqrt{\frac{2H}{\lambda}}\\
G_2&=& 2\left( \frac{H}{\lambda}\right)^{1/2} = 2 \sqrt{\frac{H}{\lambda}}
\end{eqnarray}
for FOLZ and SOLZ.

Similar expressions can be developed for higher order HOLZ rings.

In real space you get for example for FOLZ:
\begin{equation}
\frac{1}{H}=\frac{2}{\lambda G_1^2} = \frac{2}{\lambda} \left(\frac{\lambda L}{r}\right)^{2}
\end{equation}

If you did use a zone axis which is not ${100}$, then you have to compare your result to calculated values.

Assuming you are looking down $[UVW]$ then we know:
\begin{equation}
\frac{1}{H}= |[UVW]|
\end{equation}

Now we have to calculate this $|[UVW]|$ for different structures:\\
#### for fcc:
\begin{equation}
\frac{1}{H}= \frac{a_0}{p(U^2+V^2+W^2)}
\end{equation}
with $a_0$ is the lattice parameter and $p=1$ for $U+V+W$ is odd; $p=2$ for $U+V+W$ is even.

#### for bcc:
the same relationship as for fcc is true for bcc but $p$ is different:  $p=2$ for $U$, $V$, and $W$ all odd; $p=1$ otherwise.

Look up other crystal systems.

If a ring is forbidden: you have to multiply your measurement $1/H_m$ with an integer $n$ to obtain the distance of the crystal.

## Lattice Centering

We are going to look at cubic structures to
- fcc
- bcc
- a-face
- b-face
- primitive or simple cubic

The maximal excitation error is chosen so that ZOLZ and FOLZ overlap and we can see the different centering

```python
def plot_spots(tags, ax):
    """Simple plotting for spot pattern"""

    points = tags['allowed']['g']
    ix = np.argsort((points**2).sum(axis=1))
    p = points[ix]
    Laue_zones = np.unique(p[:,2])
    ZOLZ = np.where(p[:,2] == Laue_zones[0])
    FOLZ = np.where(p[:,2] == Laue_zones[1])
    SOLZ = np.where(p[:,2] == Laue_zones[2])

    ax.scatter(p[ZOLZ,0], p[ZOLZ,1], color='red')
    ax.scatter(p[FOLZ,0], p[FOLZ,1], color='blue', alpha = 0.3)
    ax.scatter(p[SOLZ,0], p[SOLZ,1], color='green', alpha = 0.3)

    ax.set_aspect('equal')
    ax.set_title(tags['crystal_name'])
    ax.set_xlim(-40,40)
    ax.set_ylim(-40,40)

# load structure
tags = ks.structure_by_name('FCC Fe')

# add necessary parameters for kinematic scattering calculation
tags['acceleration_voltage_V'] = 200000
tags['convergence_angle_mrad'] = 0
tags['zone_hkl'] = [0, 0, 1] # incident neares zone axis: defines Laue Zones!!!!
tags['mistilt']  = np.array([0,0,0])  # mistilt in degrees

tags['Sg_max'] = 2.5 # 1/nm  maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = 15   # Highest evaluated Miller indices
# calulcuate kinematic scattering data
ks.kinematic_scattering(tags, False)

fig, ax = plt.subplots(nrows=2, ncols=3, figsize=(10,6))

# plot diffraction pattern
plot_spots(tags, ax[0, 0])
tags['crystal_name'] = 'FCC or I'
plot_spots(tags, ax[1, 2])

tags.update(ks.structure_by_name('BCC Fe'))
ks.kinematic_scattering(tags, False)

# plot diffraction pattern
plot_spots(tags, ax[1, 0])

tags['crystal_name'] = 'a-face'
tags['base'] = np.array([[0. , 0. , 0. ], [0, 1/2, 1/2]])
ks.kinematic_scattering(tags, False)
plot_spots(tags, ax[0, 1])

tags['crystal_name'] = 'b-face'
tags['base'] = np.array([[0. , 0. , 0. ], [1/2, 0, 1/2]])
ks.kinematic_scattering(tags, False)
plot_spots(tags, ax[1, 1])

tags['crystal_name'] = 'simple cubic'
tags['base'] = np.array([[0. , 0. , 0. ]])
tags['elements'] = ['Fe']
ks.kinematic_scattering(tags, False)
plot_spots(tags, ax[0, 2])

```

    <IPython.core.display.Javascript object>

To analyse an experimental pattern:

- Extend the pattern for the ZOLZ into the HOLZ ring and look for discrepancies.

## Laue Circle

The mistilt (angles in degrees) leads to a circular pattern in the ZOLZ.

Any mistilt will cause the Ewald sphere to cut through the projection plane in a circle:
the Laue circle. The maximal excitation error $S_{g_{max}}$ has to be rather small for this effect to appear.

The nearest zone axis will always be in the middle of the Laue circle.

If you encounter such a Laue circle try to minimize the circle by tilting towards the center.
You can try this out below in rhe

```python
# -----Input ----------
tags['mistilt']  = np.array([0., -2 , 0])
tags['Sg_max'] = .05 # 1/nm  maximum allowed excitation error ; This parameter is related to the thickness
# ---------------------

# add necessary parameters for kinematic scattering calculation
tags['acceleration_voltage_V'] = 200000
tags['convergence_angle_mrad'] = 0
tags['zone_hkl'] = [0, 0, 1] # incident neares zone axis: defines Laue Zones!!!!

tags['crystal_name'] = f"FCC with mistilt {tags['mistilt']}"
ks.kinematic_scattering(tags, False)
ks.plotSAED(tags)
```

    <IPython.core.display.Javascript object>

The next graph shows a cross section through the reciprocal space (with Ewald sphere).

The tilt out of zone axis (blue) leaves some spots in the middle with an high excitation error $s_g$ larger than the maximum allowed one. These spots (in the figure from 2 1/nm to 8 1/nm) are invisible in such a case. Because the Ewald sphere is a 3D object the cut of a sphere with a plane will give a circle.

```python
from pyTEMlib import animation
plt.figure()
animation.deficient_holz_line(exact_bragg=False, laue_zone=0, color='black')
animation.deficient_holz_line(exact_bragg=True, laue_zone=0, color='blue')
```

    <IPython.core.display.Javascript object>

## Stereographic Projection
There are a lot of problems in materials science you can solve with diffraction patterns in the TEM.

 For instance the orientation relationship two crystals have to each other. What is the grain boundary plane and so on.

The method to visualize such orientation relationships is the stereographic projection.

### Construction

The Schematic below shows the construction of  **Stereographic Projection** for cubic systems.

Draw the crystal in the middle of a sphere. Draw a line from the center of the sphere through the middle of each plane (must be normal to the plane). Mark where this line intersects the sphere (it is named P in figure above). From this point draw a line to the south or north pole so that you intersect the equatorial plane. If you have to go to the south pole, mark the intersection of this line with equatorial plane with a dot;
if you go to the north pole mark this intersection with a circle.We construct the point P'. This point represents uniquely one plane. The relevant area of the equatorial plane is a disk.

Now we can also project circumference of a circle. Note that all the planes perpendicular to a low order zone axis lay on such a circle. These circles show up as lines or as ovals in the stereographic projection .

Change the Miller indices to see the change

```python
# ------Input ----------
reflection = np.array([1,0, 1])
# -----------------------
if reflection[1] != 0:
    print('we only use a cross section so y is set to 0')
    reflection[1] = 0

R = 90 # 90 degrees projection sphere
x,y,z =reflection/np.linalg.norm(reflection)*R # Coordinates on sphere surface
x_projeted = (x*R/(R+z)) # x coordinate on stereographic projection plane
print(f'projected x-coordinate is {x_projeted:.2f} degree')

plt.figure()
plt.title(f'Cross Section of Stereographic Projection of {reflection} ')
sphere = plt.Circle(( 0. , 0. ), R , fill=False, linewidth=2)
plt.gca().set_aspect( 'equal')
plt.gca().add_artist(sphere)

plt.plot([-R*1.1,R*1.1], [0,0])
plt.text(0.04, 0, 'O', horizontalalignment='center', verticalalignment='bottom')
plt.text(-50, 0, 'projection plane', horizontalalignment='center', verticalalignment='bottom')
plt.ylim(-R*1.1,R*1.1)
plt.scatter(0,-R)
plt.text(0,-R*1.02, 'S', horizontalalignment='center', verticalalignment='top')
plt.scatter(0,0)

plt.plot([0, x], [0, z], label='diffracted wave vector')
plt.scatter(x, z)
plt.text(x*1.04, z, 'P', horizontalalignment='center', verticalalignment='bottom')

plt.plot([x, 0], [z, -R], label='connection to south pole')
plt.scatter(x_projeted, 0)
plt.text(x_projeted, -0.4, 'P\'', horizontalalignment='left', verticalalignment='top', )
plt.legend(loc='upper left');
```

    projected x-coordinate is 37.28 degree

    <IPython.core.display.Javascript object>

**Some features of the stereographic projection:**

- We can represent plane normals and directions in the same projection.
- The can read off the angles between the directions, because the angles are preserved in this projection. Possibly the most important feature of this projection.
- The zone axis is always 90$^{\rm o} $ away from any plane normal to its zone.
- All the planes normal to a particular zone will lay on a great circle (oval). The zone of the centrale pole is on the circumference of the whole projection.
- The angle between two planes is the angle between their normals measured with the Wulff net.
- We can add the symmetry elements of any particular crystal system.

### Wulff Plot

The result of the stereographic Projection of the holeprojeciton sphere is shown below. It is convenient to show the ** circles of the sphere** as a grid the Wulff Plot.

But first we define some helper functions.

```python
## ## Some helper functions first
def circumcenter(a,b,c):
    ax, ay = a
    bx, by = b
    cx, cy = c
    d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
    ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d
    uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d
    return (ux, uy)

def wulff_net(ax, density=10):

    outer_ring = plt.Circle(( 0. , 0. ), 90 , fill=False, linewidth=2)

    ax.set_aspect( 'equal')
    ax.add_artist( outer_ring )
    ax.spines['left'].set_position(('data', 0))
    ax.spines['bottom'].set_position(('data', 0))
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)

    ax.set_xlim(-94,94)
    ax.set_ylim(-94,94)

    for phi in range(density,90,density):
        phi_r = np.radians(phi)
        x,y = 90*np.sin(phi_r), 90*np.cos(phi_r)
        u,v = circumcenter([x,y],[-x,y],[0,90-phi])
        theta = np.degrees(np.arctan2( y-v, x-u))
        ax.add_patch(patches.Arc((0. , v), (v-90+phi)*2, (v-90+phi)*2, fill=False, edgecolor = 'gray', linewidth=.5, theta1=180-theta , theta2=theta))
        ax.add_patch(patches.Arc((0. , -v), (v-90+phi)*2, (v-90+phi)*2, fill=False, edgecolor = 'gray', linewidth=.5, theta1=-theta , theta2=180+theta))
        u,v = circumcenter([0,90],[0,-90],[phi,0])
        theta = np.degrees(np.arctan2(u, phi))
        radius = np.sqrt(u**2+ 90**2)

        theta = np.degrees(np.arctan2(  90, u))
        radius = np.abs(u-phi)
        ax.add_patch(patches.Arc((u, 0), radius*2, radius*2, fill=False, edgecolor = 'gray', linewidth=.5, theta1=180+theta , theta2=-180-theta))
        ax.add_patch(patches.Arc((-u, 0), radius*2, radius*2, fill=False, edgecolor = 'gray', linewidth=.5, theta1=theta , theta2=-theta))

def add_main_circles(ax):
    phi = 45
    phi_r = np.radians(phi)
    x,y = 90*np.sin(phi_r), 90*np.cos(phi_r)

    ax.plot([x,-x], [y,-y], color='blue')
    ax.plot([x,-x], [-y,y], color='blue')

    phi_r = np.radians(phi)
    x,y = 90*np.sin(phi_r), 90*np.cos(phi_r)
    u,v = circumcenter([x,y],[-x,y],[0,90-phi])
    theta = np.degrees(np.arctan2( y-v, x-u))
    ax.add_patch(patches.Arc((0., 90), np.sqrt(2)*180, np.sqrt(2)*180, fill=False, edgecolor = 'blue', linewidth=1, theta1=180+45 , theta2=-45))
    ax.add_patch(patches.Arc((0., -90), np.sqrt(2)*180, np.sqrt(2)*180, fill=False, edgecolor = 'blue', linewidth=1, theta1=45, theta2=180-45))
    ax.add_patch(patches.Arc((90., 0), np.sqrt(2)*180, np.sqrt(2)*180, fill=False, edgecolor = 'blue', linewidth=1, theta1=180-45, theta2=180+45))
    ax.add_patch(patches.Arc((-90., 0), np.sqrt(2)*180, np.sqrt(2)*180, fill=False, edgecolor = 'blue', linewidth=1, theta1=-45 , theta2=45))

```

Again change the Miller indices around to see where the reflections *lands*.

```python
# ------Input ----------
reflection = np.array([3, 1, 1])
# -----------------------
R = 90 # 90 degrees Ewald sphere
projected =(reflection/np.linalg.norm(reflection)*R) # Coordinates on ewals sphere surface

projected = projected*R/(R+projected[2]) # x coordinate on stereographic projection plane
if projected[2]>=0:
    print(f'Projected coordinates: {projected[:2]}')
else:
    print('negative l Miller index is not supported')
plt.figure()
plt.title(f'Stereographic Projection of {reflection}')
wulff_net(plt.gca(), density=10)
add_main_circles(plt.gca())
if projected[2]>=0:
    plt.scatter(projected[0], projected[1], color='red')
```

    Projected coordinates: [62.54886934 20.84962311]

    <IPython.core.display.Javascript object>

### Cubic Crystal Reflections

change the maximum Miller index around  to see what happens

```python
# ------Input ----------
hkl_max = 7
# -----------------------
h  = np.linspace(-hkl_max,hkl_max,2*hkl_max+1)  # all evaluated single Miller Indices
hkl  = np.array(list(itertools.product(h,h,h)), dtype=int) # all evaluated Miller indices
zero = np.where(np.linalg.norm(hkl)==0)
R = 90 # 90 degrees projection sphere

projected = []
reflections = []
for reflection in hkl:
    if reflection[2]>=0:
        if np.linalg.norm(reflection) >0:
            p = reflection/np.linalg.norm(reflection)*R# Coordinates on sphere surface
            projected.append(p*R/(R+p[2])) # x coordinate on stereographic projection plane
            reflections.append(reflection)
projected = np.array(projected)
reflections = np.array(reflections, dtype=int)

plt.figure()
plt.title(f'Stereographic Projection of hkl up to [{hkl_max}{hkl_max}{hkl_max}]' )
wulff_net(plt.gca(), density=10)
add_main_circles(plt.gca())
color=['orange', 'green', 'red'] + ['blue']*200
for index, spot in enumerate(projected):
    color_index = int(np.abs(reflections[index]).sum()-1)
    plt.scatter(spot[0], spot[1], color=color[color_index])
    if color_index<3:
        plt.text(spot[0], spot[1], f'{reflections[index]}',  horizontalalignment='left', verticalalignment='top')

```

    <IPython.core.display.Javascript object>

### Stereographic Projections for Any Symmertry and Any Orientation

We did already all the work in the earlier notebooks and now we can just plot those the projections of the allowed $\vec{g}$ vectors.

The stereographic projection is after all only a projection of allowed reflections.

Use a high ``hkl_max`` parameter (about 15) and you start seeing the Kikuchi bands (next [notebook](CH2-10-Kikuchi.ipynb))

Also see whether you can detect the 3-fold  symmetry in [111] zone axis.

```python
# ---Input ---------
hkl_max = 8
zone_axis = [0,0,1]
# ------------------

#Initialize the dictionary of the input
tags = {}
### Define Crystal
tags  = ks.structure_by_name('silicon')

### Define experimental parameters:
tags['acceleration_voltage_V'] = 200.0 *1000.0 #V
tags['new_figure'] = False
tags['plot FOV'] = 30
tags['convergence_angle_mrad'] = 0
tags['zone_hkl'] = np.array(zone_axis)  # incident neares zone axis: defines Laue Zones!!!!
tags['mistilt']  = np.array([0,0,0])  # mistilt in degrees
tags['Sg_max'] = 20 # 1/nm  maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = hkl_max   # Highest evaluated Miller indices

######################################
# Diffraction Simulation of Crystal #
######################################

ks.kinematic_scattering(tags, verbose = True)

hkl = tags['allowed']['g'][tags['allowed']['g'][:,2]>=0]

projected = []
reflections = []
for reflection in hkl:
    p = reflection/np.linalg.norm(reflection)*R# Coordinates on sphere surface
    projected.append(p*R/(R+p[2])) # x coordinate on stereographic projection plane
    reflections.append(reflection)
projected = np.array(projected)
reflections = np.array(reflections, dtype=int)

plt.figure()
plt.title(f'Stereographic Projection of hkl up to [{hkl_max}{hkl_max}{hkl_max}]' )
wulff_net(plt.gca(), density=10)
# add_main_circles(plt.gca())
color=['orange', 'green', 'red'] + ['blue']*100
alpha = [1, 1, 1] + [0.2]*100
for index, spot in enumerate(projected):
    color_index = int(np.abs(reflections[index]).sum()-1)
    plt.scatter(spot[0], spot[1], color=color[color_index], alpha = alpha[color_index])
    if color_index<3:
        plt.text(spot[0], spot[1], f'{reflections[index]}',  horizontalalignment='left', verticalalignment='top')

```

    reciprocal_unit_cell
    [[1.764 0.    0.   ]
     [0.    1.764 0.   ]
     [0.    0.    1.764]]
    The inner potential is 84619.583kV
    Magnitude of incident wave vector in material 392.0 1/nm and vacuum 398.7 1/nm
    The convergence angle of 0mrad = 0.00 1/nm
    Rotation angles are 0.0 deg and 90.0 deg
    Center of Ewald sphere  [  0.           0.         391.99390809]
    Of the 4912 tested reciprocal_unit_cell points, 4912 have an excitation error less than 20.00 1/nm
    Of the 4912 possible reflection 876 are allowed.
     There are 40 allowed reflections in the zero order Laue Zone
     There are 128 allowed reflections in the first order Laue Zone
     There are 80 allowed reflections in the second order Laue Zone
     There are 628 allowed reflections in the other higher order Laue Zones
    Length of zone axis vector in real space 0.567 nm
    There are 0 forbidden but dynamical activated diffraction spots:
    KinsCat's  "Kinematic_Scattering" finished

    <IPython.core.display.Javascript object>

### Just a Pretty Plot

```python
def add_main_planes(ax):
    ax.scatter(0, 0, color='blue', s=50)
    ax.text(0, 0, '[001]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(-90, 0, color='blue', s=50)
    ax.text(-90, 0, r'[0$\bar{1}$0]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(90, 0, color='blue', s=50)
    ax.text(90, 0, '[010]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(0, 90, color='blue', s=50)
    ax.text(0, 90, r'[$\bar{1}$00]', horizontalalignment='left', verticalalignment='top')
    ax.scatter(0, -90, color='blue', s=50)
    ax.text(0, -90, '[100]', horizontalalignment='left', verticalalignment='top')

    phi_r = np.radians(45)
    r = 46.6# 1/np.tan(phi_r/2)*20
    x,y = r*sin(phi_r), r*cos(phi_r)
    ax.scatter(x,-y, color='red', s=50)
    ax.text(x,-y, '[111]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(x,y, color='red', s=50)
    ax.text(x,y, r'[$\bar{1}$11]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(-x,y, color='red', s=50)
    ax.text(-x,y, r'[$\bar{1}\bar{1}$1]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(-x,-y, color='red', s=50)
    ax.text(-x,-y, r'[1,$\bar{1}$,1]', horizontalalignment='left',verticalalignment='top')

    ax.scatter(37.2, 0, color='green', s=50)
    ax.text(37.2, 0, '[011]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(-37.2, 0, color='green', s=50)
    ax.text(-37.2, 0, r'[0$\bar{1}$1]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(0, -37.2, color='green', s=50)
    ax.text(0, -37.2, r'[101]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(0, 37.2, color='green', s=50)
    ax.text(0,37.2 , r'[$\bar{1}$01]', horizontalalignment='left',verticalalignment='top')

    phi = 45
    phi_r = np.radians(phi)
    x,y = 90*sin(phi_r), 90*cos(phi_r)

    ax.scatter(-x, -y, color='green', s=50)
    ax.text(-x, -y, '[110]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(x,-y, color='green', s=50)
    ax.text(x,-y, r'[1$\bar{1}$0]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(x,y, color='green', s=50)
    ax.text(x,y, r'[$\bar{1}$10]', horizontalalignment='left',verticalalignment='top')
    ax.scatter(-x,y, color='green', s=50)
    ax.text(-x,y, r'[$\bar{1}\bar{1}$0]', horizontalalignment='left',verticalalignment='top')

plt.figure(figsize=(5, 5))

wulff_net(plt.gca(), density=10)
add_main_planes(plt.gca())
add_main_circles(plt.gca())
```

## Summary

Lot's of information can be gained with basic crystallogrpahy tools and trigonometry.

## Navigation

-   **Back: [Spot Diffraction Pattern](CH2_8-Spot_Diffraction_Pattern)**
-   **Next: [Kikuchi Lines](CH2_10-Kikuchi_Lines.ipynb)**
-   **Chapter 2: [Diffraction](CH2_00-Diffraction.ipynb)**
-   **List of Content: [Front](../_MSE672_Intro_TEM.ipynb)**

```python

```


```python
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:100% !important; }</style>"))
```

<style>.container { width:100% !important; }</style>

# Naive image anomaly detection on Fashion MNIST with Euclidean and Riemannian means, distances, and norms

The goal of this notebook is to evaluate to possibility to achieve anomaly detection in image databases with naive distances to centroids and norms using Euclidean and Riemannian representations. The methods considered being rudimentary, only simple AD setups where the objective is to discriminate between two classes of the Fashion MNIST dataset will be considered. A general approach to embed images into the space of covariance matrices is introduced.

**Table of contents:**

---

[1. Introduction and motivation](#intro)

[1.1. Experiments hyperparameters](#intro1)

---

[2. Analysis/Experiment](#analysis)

[2.1. Dataset loading](#analysis1)

[2.2. Dataset description and preprocessing into covariance matrices](#analysis2)

[2.3. AD with Riemannian distance to Fréchet mean](#analysis3)

[2.4. AD with norm of negated geodesic PCA](#analysis4)

[2.5. AD with norm of negated geodesic PCA with Giotto-TDA preprocessing](#analysis5)

[2.6. Role of Geomstats/Giotto-TDA in the analysis](#analysis6)

---

[3. Benchmark](#benchmark)

[3.1. Euclidean baseline: distance to Euclidean mean](#benchmark1)

[3.2. Euclidean baseline: distance to Euclidean mean after PCA](#benchmark2)

[3.3. Euclidean baseline: Mahalanobis distance to Euclidean mean](#benchmark3)

[3.4. Euclidean baseline: Mahalanobis distance to Euclidean mean after PCA](#benchmark4)

[3.5. Euclidean baseline: norm negated PCA](#benchmark5)

[3.6. Results comparison](#benchmark6)

---

[4. Limitations and perspectives](#limitations)

---

[5. References](#references)

---

```python
# tensorflow not amongst Geomstats’ and Giotto-tda’s requirements.txt
import sys
!{sys.executable} -m pip install tensorflow
!{sys.executable} -m pip install giotto-tda-nightly
```

    Requirement already satisfied: tensorflow in /home/blupon/anaconda3/lib/python3.7/site-packages (2.2.0)
    Requirement already satisfied: wheel>=0.26; python_version >= "3" in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (0.35.1)
    Requirement already satisfied: wrapt>=1.11.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.11.2)
    Requirement already satisfied: protobuf>=3.8.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (3.14.0)
    Requirement already satisfied: numpy<2.0,>=1.16.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.19.2)
    Requirement already satisfied: astunparse==1.6.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.6.3)
    Requirement already satisfied: google-pasta>=0.1.8 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (0.2.0)
    Requirement already satisfied: grpcio>=1.8.6 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.31.0)
    WARNING: Keyring is skipped due to an exception: Failed to unlock the collection!
    Collecting scipy==1.4.1; python_version >= "3"
      Using cached scipy-1.4.1-cp37-cp37m-manylinux1_x86_64.whl (26.1 MB)
    Requirement already satisfied: h5py<2.11.0,>=2.10.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (2.10.0)
    Requirement already satisfied: absl-py>=0.7.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (0.11.0)
    Requirement already satisfied: opt-einsum>=2.3.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (3.1.0)
    Requirement already satisfied: keras-preprocessing>=1.1.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.1.2)
    Requirement already satisfied: six>=1.12.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.15.0)
    Requirement already satisfied: tensorboard<2.3.0,>=2.2.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (2.2.2)
    Requirement already satisfied: tensorflow-estimator<2.3.0,>=2.2.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (2.2.0)
    Requirement already satisfied: termcolor>=1.1.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (1.1.0)
    Requirement already satisfied: gast==0.3.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorflow) (0.3.3)
    Requirement already satisfied: google-auth-oauthlib<0.5,>=0.4.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (0.4.2)
    Requirement already satisfied: setuptools>=41.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (50.3.1.post20201107)
    Requirement already satisfied: tensorboard-plugin-wit>=1.6.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (1.6.0)
    Requirement already satisfied: google-auth<2,>=1.6.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (1.27.0)
    Requirement already satisfied: requests<3,>=2.21.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (2.24.0)
    Requirement already satisfied: werkzeug>=0.11.15 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (1.0.1)
    Requirement already satisfied: markdown>=2.6.8 in /home/blupon/anaconda3/lib/python3.7/site-packages (from tensorboard<2.3.0,>=2.2.0->tensorflow) (3.3.3)
    Requirement already satisfied: requests-oauthlib>=0.7.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.3.0,>=2.2.0->tensorflow) (1.3.0)
    Requirement already satisfied: rsa<5,>=3.1.4; python_version >= "3.6" in /home/blupon/anaconda3/lib/python3.7/site-packages (from google-auth<2,>=1.6.3->tensorboard<2.3.0,>=2.2.0->tensorflow) (4.7)
    Requirement already satisfied: pyasn1-modules>=0.2.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from google-auth<2,>=1.6.3->tensorboard<2.3.0,>=2.2.0->tensorflow) (0.2.8)
    Requirement already satisfied: cachetools<5.0,>=2.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from google-auth<2,>=1.6.3->tensorboard<2.3.0,>=2.2.0->tensorflow) (4.2.1)
    Requirement already satisfied: chardet<4,>=3.0.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from requests<3,>=2.21.0->tensorboard<2.3.0,>=2.2.0->tensorflow) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from requests<3,>=2.21.0->tensorboard<2.3.0,>=2.2.0->tensorflow) (1.25.11)
    Requirement already satisfied: idna<3,>=2.5 in /home/blupon/anaconda3/lib/python3.7/site-packages (from requests<3,>=2.21.0->tensorboard<2.3.0,>=2.2.0->tensorflow) (2.10)
    Requirement already satisfied: certifi>=2017.4.17 in /home/blupon/anaconda3/lib/python3.7/site-packages (from requests<3,>=2.21.0->tensorboard<2.3.0,>=2.2.0->tensorflow) (2020.12.5)
    Requirement already satisfied: importlib-metadata; python_version < "3.8" in /home/blupon/anaconda3/lib/python3.7/site-packages (from markdown>=2.6.8->tensorboard<2.3.0,>=2.2.0->tensorflow) (2.0.0)
    Requirement already satisfied: oauthlib>=3.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from requests-oauthlib>=0.7.0->google-auth-oauthlib<0.5,>=0.4.1->tensorboard<2.3.0,>=2.2.0->tensorflow) (3.1.0)
    Requirement already satisfied: pyasn1>=0.1.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from rsa<5,>=3.1.4; python_version >= "3.6"->google-auth<2,>=1.6.3->tensorboard<2.3.0,>=2.2.0->tensorflow) (0.4.8)
    Requirement already satisfied: zipp>=0.5 in /home/blupon/anaconda3/lib/python3.7/site-packages (from importlib-metadata; python_version < "3.8"->markdown>=2.6.8->tensorboard<2.3.0,>=2.2.0->tensorflow) (3.4.0)
    Installing collected packages: scipy
      Attempting uninstall: scipy
        Found existing installation: scipy 1.6.3
        Uninstalling scipy-1.6.3:
          Successfully uninstalled scipy-1.6.3
    ERROR: After October 2020 you may experience errors when installing or updating packages. This is because pip will change the way that it resolves dependency conflicts.

    We recommend you use --use-feature=2020-resolver to test your packages with the new resolver before it becomes the default.

    giotto-tda 0.4.0 requires scipy>=1.5.0, but you'll have scipy 1.4.1 which is incompatible.
    giotto-tda-nightly 20210113.12 requires scipy>=1.5.0, but you'll have scipy 1.4.1 which is incompatible.
    geomstats 2.2.2 requires joblib==0.14.1, but you'll have joblib 1.0.1 which is incompatible.
    Successfully installed scipy-1.4.1
    Requirement already satisfied: giotto-tda-nightly in /home/blupon/anaconda3/lib/python3.7/site-packages (20210113.12)
    Requirement already satisfied: joblib>=0.16.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (1.0.1)
    Requirement already satisfied: plotly>=4.8.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (4.14.3)
    Requirement already satisfied: pyflagser>=0.4.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (0.4.4)
    Requirement already satisfied: python-igraph>=0.8.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (0.9.1)
    Requirement already satisfied: ipywidgets>=7.5.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (7.5.1)
    WARNING: Keyring is skipped due to an exception: Failed to unlock the collection!
    Collecting scipy>=1.5.0
      Using cached scipy-1.6.3-cp37-cp37m-manylinux1_x86_64.whl (27.4 MB)
    Requirement already satisfied: numpy>=1.19.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (1.19.2)
    Requirement already satisfied: scikit-learn>=0.23.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from giotto-tda-nightly) (0.23.2)
    Requirement already satisfied: retrying>=1.3.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from plotly>=4.8.2->giotto-tda-nightly) (1.3.3)
    Requirement already satisfied: six in /home/blupon/anaconda3/lib/python3.7/site-packages (from plotly>=4.8.2->giotto-tda-nightly) (1.15.0)
    Requirement already satisfied: texttable>=1.6.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from python-igraph>=0.8.2->giotto-tda-nightly) (1.6.3)
    Requirement already satisfied: traitlets>=4.3.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipywidgets>=7.5.1->giotto-tda-nightly) (5.0.5)
    Requirement already satisfied: ipython>=4.0.0; python_version >= "3.3" in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipywidgets>=7.5.1->giotto-tda-nightly) (7.19.0)
    Requirement already satisfied: widgetsnbextension~=3.5.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipywidgets>=7.5.1->giotto-tda-nightly) (3.5.1)
    Requirement already satisfied: nbformat>=4.2.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipywidgets>=7.5.1->giotto-tda-nightly) (5.0.8)
    Requirement already satisfied: ipykernel>=4.5.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipywidgets>=7.5.1->giotto-tda-nightly) (5.3.4)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from scikit-learn>=0.23.1->giotto-tda-nightly) (2.1.0)
    Requirement already satisfied: ipython-genutils in /home/blupon/anaconda3/lib/python3.7/site-packages (from traitlets>=4.3.1->ipywidgets>=7.5.1->giotto-tda-nightly) (0.2.0)
    Requirement already satisfied: pexpect>4.3; sys_platform != "win32" in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (4.8.0)
    Requirement already satisfied: backcall in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.2.0)
    Requirement already satisfied: pickleshare in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.7.5)
    Requirement already satisfied: setuptools>=18.5 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (50.3.1.post20201107)
    Requirement already satisfied: pygments in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (2.7.2)
    Requirement already satisfied: decorator in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (4.4.2)
    Requirement already satisfied: prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (3.0.8)
    Requirement already satisfied: jedi>=0.10 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.17.1)
    Requirement already satisfied: notebook>=4.4.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (6.1.4)
    Requirement already satisfied: jsonschema!=2.5.0,>=2.4 in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (3.2.0)
    Requirement already satisfied: jupyter-core in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (4.6.3)
    Requirement already satisfied: jupyter-client in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipykernel>=4.5.1->ipywidgets>=7.5.1->giotto-tda-nightly) (6.1.7)
    Requirement already satisfied: tornado>=4.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from ipykernel>=4.5.1->ipywidgets>=7.5.1->giotto-tda-nightly) (6.0.4)
    Requirement already satisfied: ptyprocess>=0.5 in /home/blupon/anaconda3/lib/python3.7/site-packages (from pexpect>4.3; sys_platform != "win32"->ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.6.0)
    Requirement already satisfied: wcwidth in /home/blupon/anaconda3/lib/python3.7/site-packages (from prompt-toolkit!=3.0.0,!=3.0.1,<3.1.0,>=2.0.0->ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.2.5)
    Requirement already satisfied: parso<0.8.0,>=0.7.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from jedi>=0.10->ipython>=4.0.0; python_version >= "3.3"->ipywidgets>=7.5.1->giotto-tda-nightly) (0.7.0)
    Requirement already satisfied: Send2Trash in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.5.0)
    Requirement already satisfied: pyzmq>=17 in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (19.0.2)
    Requirement already satisfied: terminado>=0.8.3 in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.9.1)
    Requirement already satisfied: prometheus-client in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.8.0)
    Requirement already satisfied: argon2-cffi in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (20.1.0)
    Requirement already satisfied: nbconvert in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (6.0.7)
    Requirement already satisfied: jinja2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (2.11.2)
    Requirement already satisfied: attrs>=17.4.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (20.3.0)
    Requirement already satisfied: pyrsistent>=0.14.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.17.3)
    Requirement already satisfied: importlib-metadata; python_version < "3.8" in /home/blupon/anaconda3/lib/python3.7/site-packages (from jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (2.0.0)
    Requirement already satisfied: python-dateutil>=2.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from jupyter-client->ipykernel>=4.5.1->ipywidgets>=7.5.1->giotto-tda-nightly) (2.8.1)
    Requirement already satisfied: cffi>=1.0.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.14.0)
    Requirement already satisfied: nbclient<0.6.0,>=0.5.0 in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.5.1)
    Requirement already satisfied: bleach in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (3.2.1)
    Requirement already satisfied: pandocfilters>=1.4.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.4.3)
    Requirement already satisfied: defusedxml in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.6.0)
    Requirement already satisfied: entrypoints>=0.2.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.3)
    Requirement already satisfied: mistune<2,>=0.8.1 in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.8.4)
    Requirement already satisfied: jupyterlab-pygments in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.1.2)
    Requirement already satisfied: testpath in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.4.4)
    Requirement already satisfied: MarkupSafe>=0.23 in /home/blupon/anaconda3/lib/python3.7/site-packages (from jinja2->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.1.1)
    Requirement already satisfied: zipp>=0.5 in /home/blupon/anaconda3/lib/python3.7/site-packages (from importlib-metadata; python_version < "3.8"->jsonschema!=2.5.0,>=2.4->nbformat>=4.2.0->ipywidgets>=7.5.1->giotto-tda-nightly) (3.4.0)
    Requirement already satisfied: pycparser in /home/blupon/anaconda3/lib/python3.7/site-packages (from cffi>=1.0.0->argon2-cffi->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (2.20)
    Requirement already satisfied: async-generator in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbclient<0.6.0,>=0.5.0->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.10)
    Requirement already satisfied: nest-asyncio in /home/blupon/anaconda3/lib/python3.7/site-packages (from nbclient<0.6.0,>=0.5.0->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (1.4.2)
    Requirement already satisfied: webencodings in /home/blupon/anaconda3/lib/python3.7/site-packages (from bleach->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (0.5.1)
    Requirement already satisfied: packaging in /home/blupon/anaconda3/lib/python3.7/site-packages (from bleach->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (20.4)
    Requirement already satisfied: pyparsing>=2.0.2 in /home/blupon/anaconda3/lib/python3.7/site-packages (from packaging->bleach->nbconvert->notebook>=4.4.1->widgetsnbextension~=3.5.0->ipywidgets>=7.5.1->giotto-tda-nightly) (2.4.7)
    Installing collected packages: scipy
      Attempting uninstall: scipy
        Found existing installation: scipy 1.4.1
        Uninstalling scipy-1.4.1:
          Successfully uninstalled scipy-1.4.1
    ERROR: After October 2020 you may experience errors when installing or updating packages. This is because pip will change the way that it resolves dependency conflicts.

    We recommend you use --use-feature=2020-resolver to test your packages with the new resolver before it becomes the default.

    tensorflow 2.2.0 requires scipy==1.4.1; python_version >= "3", but you'll have scipy 1.6.3 which is incompatible.
    geomstats 2.2.2 requires joblib==0.14.1, but you'll have joblib 1.0.1 which is incompatible.
    Successfully installed scipy-1.6.3

```python
import geomstats.backend as gs
from geomstats.learning.kmeans import RiemannianKMeans
import geomstats.geometry.spd_matrices as spd
from geomstats.learning.kmeans import FrechetMean
from geomstats.learning.pca import TangentPCA

from sklearn.metrics import roc_auc_score
from sklearn.covariance import EmpiricalCovariance
from sklearn.decomposition import PCA

from gtda.images import RadialFiltration
from gtda.images import DilationFiltration
from gtda.images import ErosionFiltration
from gtda.images import DensityFiltration
from gtda.images import Binarizer

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# make sure tensorflow doesn't use GPU to avoid problem of insufficient GPU memory to handle batch size
import os
os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
```

    INFO: Using numpy backend

# <a id='intro'>1. Introduction and motivation</a>

<div style="text-align: justify">
Anomaly detection (AD) in images is a useful tool in a large variety of applications. One can detect anomalies on images to raise an alarm in video surveillance, or to identify flaws for quality control. This AD problem is an active research subject in the machine learning and data processing community. The main difference between AD and classification lies in the supervision and the information available during training. Whereas classification requires representative training data for each mode, AD gathers a single or a set of classes in a unique normal mode and separates the latter from any different data. In order to do this, so-called unsupervised AD only uses normal samples during training to characterize the normality latent distribution, whereas semi-supervised AD additionally takes into account a minority of labelled anomalies to learn the latent boundary. AD remains a difficult task when normality is complex, e.g. multimodal, or when the data provided that defines the latent distribution of normality is not representative, which could lead to a latent boundary easily excluding normal samples in the test phase. Other typical difficulties include potentially infinite diversity when it comes to anomalies and excessive similarity between some normal samples and some anomalies. The following image illustrates what a generic AD setup looks like.
</div>

```python
# show image with python to avoid github error when displaying notebook in browser without jupyter
plt.figure(figsize=(14,9))
plt.imshow(mpimg.imread('multimodal_AD.png'))
```

<div style="text-align: justify">
In this notebook, we explore the performances of naive AD methods with simple distances and norms directly in representation space or after a PCA dimensionality reduction. The objective is to check where naive approaches stand with respect to the very simple AD task of separating two modes of data, i.e. normality is not multimodal and abnormality is not infinitely diverse. This amounts to discriminating between two classes with training data available only for one of the two. The dataset chosen for this experiment is Fashion MNIST. This choice is motivated by the will to practice on a simple enough dataset, avoiding the complexity of CIFAR10 and the lack of multiscale structure in MNIST.
</div>

<br>

<div style="text-align: justify">
Working on distance to centroids and norms asks the question of the representation space on which to compute those features. Following recent research putting forward the relevance of SPD matrices representations, the images will first be transformed into covariance matrices. This enables us to compare the performances of means and distances either defined in an Euclidean setting or a Riemannian setting, the Riemannian setting being provided by the SPD manifold. The Riemannian tools carry the hope that a well chosen manifold makes the distances between samples more relevant with respect to the AD task considered. This translates into the idea that a well chosen manifold is a manifold more adapted to the data distribution.
</div>

<br>

<div style="text-align: justify">
The next figure represents the AD pipelines considered in this notebook, with each notebook subsection implementing the corresponding method indicated on the left.
</div>

```python
# show image with python to avoid github error when displaying notebook in browser without jupyter
plt.figure(figsize=(20,13))
plt.imshow(mpimg.imread('ICLR2021_geomstatsChallenge_pipelines.png'))
```

## <a id='intro1'>1.1. Experiments hyperparameters</a>

```python
results_dic = {}
```

Choice of the Fashion MNIST classes we will separate, we choose 0 (T-shirt) and 2 (Pullover) to obtain a difficult separation problem, since these two classes are quite similar:

```python
class1 = 0
class2 = 2
```

Choice of the number of samples available to compute the mean, defining the normality centroid or reference point:

```python
nsamples = 1000
```

Choice of the covariance matrix type:

```python
cov_type = 'siegel2'
```

Choice of the image axis along which the covariance matrix will be computed:

```python
axis_to_cov = 2
```

Choice of the Giotto-TDA preprocessing filter, and of the binarization threshold for the binarization preceding the filter:

```python
# gfiltertype = 'density'
# gfiltertype = 'dilation'
# gfiltertype = 'erosion'
gfiltertype = 'radial'

binarize_threshold = 0.4
```

Choice of the number of components for PCA:

```python
n_components_PCA = 20
```

# <a id='analysis'>2. Analysis/Experiment</a>

## <a id='analysis1'>2.1. Dataset loading</a>

The so-called normal class seen during training in the unsupersived AD setup is class1, defined in the hyperparameters. The other class, which we try to separate from class1 will only be seen in test.

```python
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

x_train = x_train/255.
x_test = x_test/255.

filter_class = y_train==class1

x_train1 = x_train[filter_class,:,:]
y_train1 = y_train[filter_class]
x_train1 = x_train1[0:nsamples,:,:]
x_train1 = np.expand_dims(x_train1,axis=-1)

filter_class = y_test==class1
x_test1 = x_test[filter_class]
x_test1 = np.expand_dims(x_test1,axis=-1)

filter_class = y_test==class2
x_test2 = x_test[filter_class]
x_test2 = np.expand_dims(x_test2,axis=-1)
```

## <a id='analysis2'>2.2. Dataset description and preprocessing into covariance matrices</a>

To obtain a relevant SPD matrix to describe an image, transformations of the image will be accumulated, the resulting tensor will be reshaped according to the chosen covariance matrix axis in the hyperparameters, and finally the covariance matrix will be computed. To make this SPD as much relevant as possible, the append mean approach is considered, in order to associate the two first orders representations in the SPD matrix. It can be interpreted as the Riemannian space of non-centered multivariate Gaussian distributions, which has a geometry described by the Siegel metric [[1]](#[1]).

```python
def compute_transformation(x_train):
    x_train=x_train+(np.random.random(x_train.shape)/100)
    xout=[]
    xout.append(x_train)
    for i in range(-3,4,2):
        xtemp=tf.roll(x_train,i,axis=1)
        for j in range(-3,4,2):
            xout.append(tf.roll(xtemp,j,axis=2))
    xout.append(tf.image.flip_left_right(x_train))
    xout.append(tf.image.flip_up_down(x_train))
    xout.append(tf.image.rot90(x_train))
    xout.append(tf.image.rot90(x_train,k=2))
    xout.append(tf.image.rot90(x_train,k=3))
    xout=np.stack(xout,axis=-1)
    return xout
```

```python
def reshape_byaxis(xout,axis=1):
    xout=np.swapaxes(xout,axis,-1)
    return np.reshape(xout,[xout.shape[0],xout.shape[1]*xout.shape[2]*xout.shape[3],xout.shape[4]])
```

In the following function, three possible SPD representations can be computed: a covariance matrix, or two append mean SPD representations combining first and second order moments. As indicated in the code comments, these two append mean approaches stem from the literature [[1]](#[1]) [[2]](#[2]).

<br>

Append mean approach SPD representation with "siegel" option for **compute_cov()** (cf. next notebook cell):

<br>

\begin{equation}
\begin{pmatrix}
\Sigma+ \beta^2 \mu \mu^T & \beta \mu \\
\beta \mu^T & 1
\end{pmatrix}
\end{equation}

<br>

Append mean approach SPD representation with "siegel2" option for **compute_cov()** (cf. next notebook cell):

<br>

\begin{equation}
\begin{pmatrix}
\Sigma+ \beta \mu \mu^T & \beta \mu \\
\beta \mu^T & \beta
\end{pmatrix}
\end{equation}

<br>

In the previous equations, beta is a constant, sigma the covariance matrix, and mu the mean vector.

```python
def compute_cov(xout,option='simple'):
    resC=[]
    if option=='simple':
        for i in range(xout.shape[0]):
            C=np.cov(xout[i,:,:].T)
            resC.append(C)
    elif option=='siegel': # append mean from equation 2 here https://arxiv.org/pdf/1703.06817.pdf
        beta=.3 # Constant recommended in https://arxiv.org/pdf/1703.06817.pdf
        for i in range(xout.shape[0]):
            C=np.cov(xout[i,:,:].T)
            m=np.expand_dims(np.mean(xout[i,:,:],axis=0),axis=-1)
            resC.append(np.concatenate([np.concatenate([C+(beta**2)*(m*m.T),beta*m],axis=1),np.concatenate([beta*m,np.ones([1,1])],axis=0).T]))
    elif option=='siegel2': # append mean from lemma 3.1 here https://core.ac.uk/download/pdf/82584625.pdf
        beta=.3 #Constant recommended in https://arxiv.org/pdf/1703.06817.pdf
        for i in range(xout.shape[0]):
            C=np.cov(xout[i,:,:].T)
            m=np.expand_dims(np.mean(xout[i,:,:],axis=0),axis=-1)
            resC.append(np.concatenate([np.concatenate([C+beta*(m*m.T),beta*m],axis=1),np.concatenate([beta*m,beta*np.ones([1,1])],axis=0).T]))
    return np.array(resC)
```

```python
x_train1_transformations = compute_transformation(x_train1)
x_test1_transformations = compute_transformation(x_test1)
x_test2_transformations = compute_transformation(x_test2)

fig, ax = plt.subplots(2,11,figsize=(20,5))
for transf_index1 in range(2):
    for transf_index2 in range(11):
        ax[transf_index1,transf_index2].imshow(x_train1_transformations[0,:,:,0,transf_index2+11*(transf_index1==1)])
ax[0,5].set_title('Image transformations accumulated in order to compute \na covariance matrix along the horizontal or vertical axis\n', fontsize=20)
```

```python
x_train1_transformations = reshape_byaxis(x_train1_transformations, axis=axis_to_cov)
x_test1_transformations = reshape_byaxis(x_test1_transformations, axis=axis_to_cov)
x_test2_transformations = reshape_byaxis(x_test2_transformations, axis=axis_to_cov)

SPD_train1 = compute_cov(x_train1_transformations, option=cov_type)
SPD_test1 = compute_cov(x_test1_transformations, option=cov_type)
SPD_test2 = compute_cov(x_test2_transformations, option=cov_type)
```

```python
fig, ax = plt.subplots(1,2,figsize=(20,8))
ax[0].imshow(x_train1[3])
ax[0].set_title('Untransformed input image', fontsize=20)
ax[1].imshow(SPD_train1[3])
ax[1].set_title('SPD representation of the image \n(notice the append mean approach illuminating the last column & row)', fontsize=20)

fig, ax = plt.subplots(1,2,figsize=(20,8))
ax[0].imshow(x_test2[7])
ax[0].set_title('Untransformed input image', fontsize=20)
ax[1].imshow(SPD_test2[7])
ax[1].set_title('SPD representation of the image \n(notice the append mean approach illuminating the last column & row)', fontsize=20)
```

## <a id='analysis3'>2.3. AD with Riemannian distance to Fréchet mean</a>

A Riemannian metric must be chosen among those available in Geomstats, in order to compute the Fréchet mean and the distance to this mean

```python
dim = SPD_train1[0].shape[1]
# riem_metric = spd.SPDMetricAffine(dim)
riem_metric = spd.SPDMetricLogEuclidean(dim)
```

Compute Fréchet SPD matrices mean according to previously chosen SPD metric:

```python
FM = FrechetMean(riem_metric).fit(SPD_train1)

plt.imshow(FM.estimate_)
plt.title('Fréchet mean of our normal class training samples')
plt.show()
```

Compute distances of test samples to Fréchet mean:

```python
riemdist_test1 = riem_metric.dist(FM.estimate_,np.array(SPD_test1))
plt.hist(riemdist_test1, alpha=0.2, color='r', bins=100)

riemdist_test2 = riem_metric.dist(FM.estimate_,np.array(SPD_test2))
plt.hist(riemdist_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for the two classes')
plt.show()
```

ROC AUC achieved:

```python
print(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([riemdist_test1, riemdist_test2])))
```

    0.7279720000000001

```python
results_dic['dist_frechet_mean'] = roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([riemdist_test1, riemdist_test2]))
```

## <a id='analysis4'>2.4. AD with norm of negated geodesic PCA</a>

Use norm of negated geodesic PCA as detector [[3]](#[3]), with the previously computed Riemannian mean as PCA base point:

```python
tpca = TangentPCA(metric=riem_metric, n_components=n_components_PCA)
tpca = tpca.fit(SPD_train1, base_point=FM.estimate_)

tangent_projected_data_test1 = tpca.transform(SPD_test1)
tangent_projected_data_test2 = tpca.transform(SPD_test2)
```

```python
scores = []
for var in range(0,19):
    scores.append(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([np.sum(tangent_projected_data_test1[:,var:]**2,axis=1),np.sum(tangent_projected_data_test2[:,var:]**2,axis=1)])))

print("Best ROC AUC is: {} for norm of {} last PCA dimensions".format(max(scores), 20-scores.index(max(scores))))
```

    Best ROC AUC is: 0.8808370000000001 for norm of 14 last PCA dimensions

```python
negPCAscore_test1 = np.sum(tangent_projected_data_test1[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test1, alpha=0.2, color='r', bins=100)

negPCAscore_test2 = np.sum(tangent_projected_data_test2[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for the two classes')
plt.show()
```

```python
results_dic['negated geodesic PCA'] = max(scores)
```

## <a id='analysis5'>2.5. AD with norm of negated geodesic PCA with Giotto-TDA preprocessing</a>

```python
class giotto_preprocessing():
    def __init__(self, x_train, filter_name):
        self.filter_name = filter_name
        if self.filter_name == 'dilation':
            self.filter = DilationFiltration()
        elif self.filter_name == 'erosion':
            self.filter = ErosionFiltration()
        elif self.filter_name == 'density':
            self.filter = DensityFiltration()
        elif self.filter_name == 'radial':
            self.filter = RadialFiltration()
        else:
            raise NoValidGiottoFilterError
        self.filter.fit(x_train)

    def transform(self, x):
        return self.filter.transform(x)
```

Binarize data to be able to apply Giotto-TDA filter:

```python
binarizer = Binarizer(threshold=binarize_threshold)
x_train1 = binarizer.fit_transform(x_train1)

x_test1 = binarizer.transform(x_test1)
x_test2 = binarizer.transform(x_test2)
```

Define Giotto-TDA filter:

```python
gfilter = giotto_preprocessing(x_train1, gfiltertype)
```

Transform train and test data with Giotto-TDA filter and compute new SPD representations:

```python
x_train1_filtered = gfilter.transform(x_train1)

x_test1_filtered = gfilter.transform(x_test1)
x_test2_filtered = gfilter.transform(x_test2)
```

```python
x_train1_transformations_filtered = compute_transformation(x_train1_filtered)
x_test1_transformations_filtered = compute_transformation(x_test1_filtered)
x_test2_transformations_filtered = compute_transformation(x_test2_filtered)

x_train1_transformations_filtered = reshape_byaxis(x_train1_transformations_filtered, axis=axis_to_cov)
x_test1_transformations_filtered = reshape_byaxis(x_test1_transformations_filtered, axis=axis_to_cov)
x_test2_transformations_filtered = reshape_byaxis(x_test2_transformations_filtered, axis=axis_to_cov)

SPD_train1_filtered = compute_cov(x_train1_transformations_filtered, option=cov_type)
SPD_test1_filtered = compute_cov(x_test1_transformations_filtered, option=cov_type)
SPD_test2_filtered = compute_cov(x_test2_transformations_filtered, option=cov_type)
```

```python
fig, ax = plt.subplots(1,2,figsize=(20,8))
ax[0].imshow(x_train1_filtered[3])
ax[0].set_title('Filtered input image', fontsize=20)
ax[1].imshow(SPD_train1_filtered[3])
ax[1].set_title('SPD representation of the filtered image', fontsize=20)

fig, ax = plt.subplots(1,2,figsize=(20,8))
ax[0].imshow(x_test2_filtered[7])
ax[0].set_title('Filtered input image', fontsize=20)
ax[1].imshow(SPD_test2_filtered[7])
ax[1].set_title('SPD representation of the filtered image', fontsize=20)
```

A new Riemannian mean needs to be computed in order to compute the PCA base point:

```python
dim = SPD_train1_filtered[0].shape[1]
# riem_metric = spd.SPDMetricAffine(dim)
riem_metric = spd.SPDMetricLogEuclidean(dim)
```

```python
FM = FrechetMean(riem_metric).fit(SPD_train1_filtered)
FM.estimate_

plt.imshow(FM.estimate_)
plt.title('Fréchet mean of our normal class filtered training samples')
plt.show()
```

```python
tpca = TangentPCA(metric=riem_metric, n_components=n_components_PCA)
tpca = tpca.fit(SPD_train1_filtered, base_point=FM.estimate_)

tangent_projected_fdata_test1 = tpca.transform(SPD_test1_filtered)
tangent_projected_fdata_test2 = tpca.transform(SPD_test2_filtered)
```

```python
scores = []
for var in range(0,19):
    scores.append(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1_filtered)),np.ones(len(SPD_test1_filtered))]),np.concatenate([np.sum(tangent_projected_fdata_test1[:,var:]**2,axis=1),np.sum(tangent_projected_fdata_test2[:,var:]**2,axis=1)])))

print("Best ROC AUC is: {} for norm of {} last PCA dimensions".format(max(scores), 20-scores.index(max(scores))))
```

    Best ROC AUC is: 0.887529 for norm of 13 last PCA dimensions

```python
negPCAscore_test1 = np.sum(tangent_projected_fdata_test1[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test1, alpha=0.2, color='r', bins=100)

negPCAscore_test2 = np.sum(tangent_projected_fdata_test2[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for the two classes')
plt.show()
```

```python
results_dic['negated geodesic PCA with preprocessing filter'] = max(scores)
```

## <a id='analysis6'>2.6. Role of Geomstats/Giotto-TDA in the analysis</a>

<div style="text-align: justify">
The package geomstats allows the AD naive approach, i.e. the distance to the normality reference centroid, to be tested on the SPD manifold on which the data points are projected. The library realizes the most important computations in our experiments, which are the computations of a Riemannian mean and of a Riemannian distance. It also allows us to consider another AD naive approach, i.e. the one where the norm of the last PCA components constitutes an AD score, with its TangentPCA() function, which takes into account the manifold constraint. Finally, Giotto-TDA provides us with a useful preprocessing filter which leads to an improvement in the AD performances.
</div>

# <a id='benchmark'>3. Benchmark</a>

```python
class EuclideanMetric():

    def __init__(self, withMahalanobis=False, train_data=None, pca_reduce=False, n_components=20):
        self.withMahalanobis = withMahalanobis
        self.n_components = n_components
        self.pca_reduce = pca_reduce
        if self.pca_reduce:
            self.pca = PCA(n_components=self.n_components)
        if train_data is None:
            raise NoTrainDataError
        else:
            if self.pca_reduce:
                self.pca = self.pca.fit(train_data.reshape(train_data.shape[0], -1))
            self.centroid = self.init_centroid(train_data)
            if self.withMahalanobis:
                self.empiCov = self.init_Mahalanobis(train_data)
                self.cov = self.empiCov.covariance_

    def init_centroid(self, x):
        x = x.reshape(x.shape[0], -1)
        if self.pca_reduce:
            x = self.pca.transform(x)
        return np.sum(x, axis=0)/x.shape[0]

    def init_Mahalanobis(self, x):
        x = x.reshape(x.shape[0], -1)
        if self.pca_reduce:
            x = self.pca.transform(x)

        empiCov = EmpiricalCovariance()
        empiCov.fit(x)
        plt.imshow(empiCov.covariance_)
        plt.title('Mahalanobis Covariance Matrix')
        plt.show()
        return empiCov

    def dist_to_centroid(self, test_data):
        if self.withMahalanobis:
            return self.Mahalanobis(test_data)
        else:
            return self.Frobenius(test_data)

    def Frobenius(self, x):
        x = x.reshape(x.shape[0], -1)
        if self.pca_reduce:
            x = self.pca.transform(x)
        return np.sqrt(np.sum((np.subtract(x, self.centroid))**2, axis=1))

    def Mahalanobis(self, x):
        x = x.reshape(x.shape[0], -1)
        if self.pca_reduce:
            x = self.pca.transform(x)
        batch_scores = self.empiCov.mahalanobis(x)
        return np.sqrt(batch_scores)
```

## <a id='benchmark1'>3.1. Euclidean baseline: distance to Euclidean mean</a>

```python
baseline_metric = EuclideanMetric(withMahalanobis=False, train_data=SPD_train1, pca_reduce=False)
```

```python
plt.imshow(baseline_metric.centroid.reshape(29,29))
plt.title('Euclidean mean of our normal class training samples')
plt.show()
```

```python
eucldist_test1 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test1))
plt.hist(eucldist_test1, alpha=0.2, color='r', bins=100)

eucldist_test2 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test2))
plt.hist(eucldist_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for two classes')
plt.show()
```

```python
print(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2])))
```

    0.664795

```python
results_dic['distance to Euclidean mean'] = roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2]))
```

## <a id='benchmark2'>3.2. Euclidean baseline: distance to Euclidean mean after PCA</a>

```python
baseline_metric = EuclideanMetric(withMahalanobis=False, train_data=SPD_train1, pca_reduce=True, n_components=n_components_PCA)
```

```python
eucldist_test1 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test1))
plt.hist(eucldist_test1, alpha=0.2, color='r', bins=100)

eucldist_test2 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test2))
plt.hist(eucldist_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for two classes')
plt.show()
```

```python
print(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2])))
```

    0.659112

```python
results_dic['distance to Euclidean mean after PCA'] = roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2]))
```

## <a id='benchmark3'>3.3. Euclidean baseline: Mahalanobis distance to Euclidean mean</a>

```python
baseline_metric = EuclideanMetric(withMahalanobis=True, train_data=SPD_train1, pca_reduce=False, n_components=n_components_PCA)
```

```python
eucldist_test1 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test1))
plt.hist(eucldist_test1, alpha=0.2, color='r', bins=100)

eucldist_test2 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test2))
plt.hist(eucldist_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for two classes')
plt.show()
```

```python
print(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2])))
```

    0.863534

```python
results_dic['Mahalanobis distance to Euclidean mean'] = roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2]))
```

## <a id='benchmark4'>3.4. Euclidean baseline: Mahalanobis distance to Euclidean mean after PCA</a>

```python
baseline_metric = EuclideanMetric(withMahalanobis=True, train_data=SPD_train1, pca_reduce=True, n_components=n_components_PCA)
```

```python
eucldist_test1 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test1))
plt.hist(eucldist_test1, alpha=0.2, color='r', bins=100)

eucldist_test2 = baseline_metric.dist_to_centroid(test_data=np.array(SPD_test2))
plt.hist(eucldist_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for two classes')
plt.show()
```

```python
print(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2])))
```

    0.8198350000000001

```python
results_dic['Mahalanobis distance to Euclidean mean after PCA'] = roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([eucldist_test1, eucldist_test2]))
```

## <a id='benchmark5'>3.5. Euclidean baseline: norm negated PCA</a>

```python
baseline_metric = EuclideanMetric(withMahalanobis=False, train_data=SPD_train1, pca_reduce=True, n_components=n_components_PCA)
```

```python
tangent_projected_data_test1 = baseline_metric.pca.transform(SPD_test1.reshape(SPD_test1.shape[0], -1))
tangent_projected_data_test2 = baseline_metric.pca.transform(SPD_test2.reshape(SPD_test2.shape[0], -1))
```

```python
scores = []
for var in range(0,19):
    scores.append(roc_auc_score(np.concatenate([np.zeros(len(SPD_test1)),np.ones(len(SPD_test2))]),np.concatenate([np.sum(tangent_projected_data_test1[:,var:]**2,axis=1),np.sum(tangent_projected_data_test2[:,var:]**2,axis=1)])))

print("Best ROC AUC is: {} for norm of {} last PCA dimensions".format(max(scores), 20-scores.index(max(scores))))
```

    Best ROC AUC is: 0.8371390000000001 for norm of 4 last PCA dimensions

```python
negPCAscore_test1 = np.sum(tangent_projected_data_test1[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test1, alpha=0.2, color='r', bins=100)

negPCAscore_test2 = np.sum(tangent_projected_data_test2[:,scores.index(max(scores)):]**2,axis=1)
plt.hist(negPCAscore_test2, alpha=0.2, color='g', bins=100)

plt.title('Outlier Score for the two classes')
plt.show()
```

```python
results_dic['norm of negated PCA'] = max(scores)
```

## <a id='benchmark6'>3.6. Results comparison</a>

```python
print('ROC AUCs of the method considered:')
for key in results_dic:
    print(results_dic[key], '---->', key)
```

    ROC AUCs of the method considered:
    0.7279720000000001 ----> dist_frechet_mean
    0.8808370000000001 ----> negated geodesic PCA
    0.887529 ----> negated geodesic PCA with preprocessing filter
    0.664795 ----> distance to Euclidean mean
    0.659112 ----> distance to Euclidean mean after PCA
    0.863534 ----> Mahalanobis distance to Euclidean mean
    0.8198350000000001 ----> Mahalanobis distance to Euclidean mean after PCA
    0.8371390000000001 ----> norm of negated PCA

Negated geodesic PCA led to the best ROC AUC performance for the AD task considered. Giotto-TDA preprocessing filter led to a slight performances increase. We also notice the relatively good performances of the Mahalanobis distances in the Euclidean setup.

# <a id='limitations'>4. Limitations and perspectives</a>

# Limitations of this analysis

<div style="text-align: justify">
In this analysis, we projected data on the SPD manifold hoping for a better understanding of the data latent distribution on this mathematical structure. Good performances were obtained with the naive AD method harnessing the SPD representation, but a final conclusion regarding the absolute relevance of the specific manifold choice can not be reached. In order to reach such a conclusion, one would need to compare the projection of the same problem on a wide variety of manifolds. On the one hand, this camparison would allow to detect the best manifold available if any, on the other hand it would help understand the actual contribution of the manifold chosen here.
</div>

<br>

<div style="text-align: justify">
Additionally, the AD problem considered, as well as the competing methods implemented, remain naive in its complexity. Although naive, the methods provide in this specific case relatively interesting performances. A study progressively complexifying the AD problem solved would allow to better understand the limits of such naive methods, and the point after which machine learning with, for example, deep neural networks, is actually needed.
</div>

# Limitation of Geomstats and Giotto-TDA

Trainable SPD representations transformations would have allowed to go further in the Euclidean versus Riemannian comparison, enabling the comparison of more complex models.

# Proposed features for Geomstats and Giotto-TDA

<div style="text-align: justify">
Preprocessing dedicated to SPD matrices, or generating a useful SPD representation for various data types would be an interesting contribution to the machine learning on manifolds toolbox. An implementation of a SPD neural network, or so-called second order neural networks, with the Riemannian gradient and much attention given to the efficiency of the gradient computation, would be welcomed since a widely accepted reference code for this promising model has yet to appear. Moreover, such an implementation would enable more subtle comparisons between AD tasks on a variety of manifolds, to take a step back with respect to the Euclidean methods widely used.
</div>

# <a id='references'>5. References</a>

<a id='[1]'>[1]</a> "A Distance between Multivariate Normal Distributions Based in an Embedding into the Siegel Group" Miquel Calvo and Josep M. Oller

https://core.ac.uk/download/pdf/82584625.pdf

<a id='[2]'>[2]</a> "Second-order Convolutional Neural Networks" Kaicheng Yu and Mathieu Salzmann

https://arxiv.org/pdf/1703.06817.pdf

<a id='[3]'>[3]</a> "Modeling the Distribution of Normal Data in Pre-Trained Deep Features for Anomaly Detection" Oliver Rippel, Patrick Mertens and Dorit Merhof

https://arxiv.org/pdf/2005.14140.pdf

```python

```


# Molecular Dynamics: Lab 2

In part based on [Fortran code from Furio Ercolessi](http://www.fisica.uniud.it/~ercolessi/md/f90/) and the [CHEM126 course of Kalju Khan](http://web.chem.ucsb.edu/~kalju/chem126).

```
from IPython.core.display import HTML
css_file = '../ipython_notebook_styles/ngcmstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Open+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 1000px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1200px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Open Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:900px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Arvo', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Arvo', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Arvo', serif;
    margin-top:16px;
	font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Arvo', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Arvo', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 90%;
}

</style>

## Energy minimization

In lab 1 we worked with the Lennard-Jones potential. Other simple potentials include the **harmonic** potential

$$
\begin{equation}
  V = \frac{1}{2} k \left( r - r_{\text{eq}} \right)^2
\end{equation}
$$

with harmonic constant $k$ and equilibrium position $r_{\text{eq}}$, the **Kratzer** potential

$$
\begin{equation}
  V = D_0 \left( \frac{r-r_{\text{eq}}}{r} \right)^2
\end{equation}
$$

with dissociation energy $D_0$, and the **Morse** potential

$$
\begin{equation}
 V = D_0 \left[ 1 - e^{-\alpha (r - r_{\text{eq}})} \right]^2
\end{equation}
$$

where $\alpha$ is the Morse parameter.

### Carbon Monoxide

For example, carbon monoxide (CO) consists of one carbon atom (atomic weight 12.011) and one oxygen atom (atomic weight 15.999) separated by 1.1283 Angstroms. This can be modelled by

1. a harmonic potential with $k = 2743.0$ and $r_{\text{eq}} = 1.1283$ Angstroms, or
2. a Kratzer potential with $D_0 = 258.9$ kcal/mol and the same equilibrium distance, or
3. a Morse potential with $\alpha = 2.302$ Angstroms${}^{-1}$, and the same dissociation energy and equilibrium distance.

Using standard minimization algorithms (e.g. `scipy.optimize.minimize`) show that, for suitable initial guesses, all of the above potentials have minima at the expected location.

By plotting the potentials, understand why the success of the algorithms changes.

Briefly test how changing the potential changes the number of steps required by the algorithm.

Also compute the force $\frac{dV}{dr}$ at the minimum point for each potential to check that it vanishes as expected.

```
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.axes3d import Axes3D
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
rcParams['figure.figsize'] = (12,6)
from scipy.optimize import minimize
```

```

```

### Evolution

Using your velocity-Verlet integrator from lab 1, evolve the CO molecule using the Morse potential. Note that, unlike lab 1, the two particles now have different mass. It is perhaps easiest to scale it so that the carbon particle has mass $1$ and the oxygen mass $1.332029$. Do *not* use periodic boundaries! Try using $\Delta t = 0.001$ up to $t=1$. Start from the equilibrium position and show that it does not evolve. The move the location of the oxygen atom slightly and see how it evolves.

```

```

## Water

The paper of [Praprotnik, Janezic, and Mavri](http://dx.doi.org/10.1021/jp046158d) suggests a potential for a single water molecule (one oxygen atom and two hydrogen with atomic weights $1.008$) of

$$
\begin{equation}
  V = \sum_{l = 1}^2 D_0 \left[ 1 - e^{\alpha \Delta r_{OH_l}} \right]^2 + \frac{1}{2} k_{\theta} \Delta r_{HH}^2 + k_{r \theta} \Delta r_{HH} \left( \Delta r_{OH_1} + \Delta r_{OH_2} \right) + k_{rr} \Delta r_{OH_1} \Delta r_{OH_2}.
\end{equation}
$$

The first term is the Morse potential again, and the other terms are the intra-molecule bond potentials. $\Delta r_{OH_l} = r_{OH_l} - r_{OH_{\text{eq}}}$ is the stretch in the distance between the oxygen atom and the $l^{\text{th}}$ hydrogen atom $r_{OH_l}$ and its equilibrium value, and $\Delta r_{HH} = r_{HH_l} - r_{HH_{\text{eq}}}$ is the stretch in the distance between the hydrogen atoms. The parameter values are

1. $D_0 = 101.9188$ kcal/mol.
2. $\alpha = 2.567$ Angstrom${}^{-1}$
3. $k_{\theta} = 328.645606$ kcal/mol/Angstrom${}^2$
4. $k_{r \theta} = -211.4672$ kcal/mol/Angstrom${}^2$
5. $k_{rr} = 111.70765$ kcal/mol/Angstrom${}^2$
6. $r_{OH_{\text{eq}}} = 1$ Angstrom
7. $r_{HH_{\text{eq}}} = 1.633$ Angstrom

Fix the oxygen atom at the origin and start the hydrogen atoms at $(\pm 0.8, 0.6, 0)$. Use the minimization techniques to find the equilibrium position of the atoms, [comparing against the known structure of water](http://en.wikipedia.org/wiki/Water_model).

**NOTE**: if using `scipy`'s minimization routine, you may have to increase the tolerance as high as $10^{-4}$ to make it converge.

```

```

Using your integrator, evolve the molecule and see how it behaves.

```

```

FEEG6016 Simulation and Modelling/2014/Molecular Dynamics Lab 2.ipynb