# Overview

### Contents

- **Introduction to Python**
  - The Python Interpreter
  - First Steps with Python
  - Importing Libraries
  - About the Data
  - Arrays and their Attributes
  - Getting Help
  - More on Arrays
  - Basic Data Visualization
- **Repeating Tasks with Loops**
  - Sequences
  - More Complex Loops
  - Iterators and Generators
- **Analyzing Data from Multiple Files**
  - Looping over Files
  - Generating a Plot
  - Putting it All Together
- **Conditional Evaluation**
  - Conditional Expressions in Python
  - Checking our Data
- **Creating Functions for Reuse**
  - Composing Multiple Functions
  - Cleaning Up our Analysis Code
  - Positional versus Keyword Arguments
  - Documenting Functions
- **Capstone: Fitting Linear Models**
  - About the Data
  - Introducing `statsmodels`
- **Python at the Command Line**
  - Our First Python Script
  - Alternative Command-Line Tools
  - Modularization
- **Understanding and Handling Errors**
- **Defensive Programming**
  - Assertions
  - Test-Driven Development
  - Testing for Quality Control
  - Unit Testing
- **Analyzing and Optimizing Performance**
  - Benchmarking
- **Capstone**
  - Your Tasks
  - Getting Started
- **Connecting to SQLite with Python**

# Introduction to Python

## The Python Interpreter

### Jupyter Notebook

## First Steps with Python

## Importing Libraries

## About the Data

The data are formatted such that:

- Each column is the monthly mean, January (1) through December (12)
- Each row is a year, starting from January 1948 (1) through December 2016 (69)

[More information on the data can be found here.](http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NCEP/.CPC/.GHCN_CAMS/.gridded/.deg0p5/.temp/)

## Arrays and their Attributes

**How many rows and columns are there in the `barrow` array?**

### Challenge

**What do each of the following code samples do?**

```py
barrow[0]
barrow[0,]
barrow[-1]
barrow[-3:-1]
```

### Slicing NumPy Arrays

### Challenge

**What's the mean monthly temperature in August of 2016? Converted to degrees Fahrenheit?**

Degrees F can be calculated from degrees K by the formula:

$$
T_F = \left(T_K \times \frac{9}{5}\right) - 459.67
$$

### Calculating on NumPy Arrays

**What is the overall mean temperature in any month in Barrow between 1948 and 2016 in degrees C?**

**How cold was the coldest February in Barrow, by monthly mean temperatures, in degrees C?**

### Challenge

**What's the minimum, maximum, and mean monthly temperature for August in Barrow, in degrees C?**

## Getting Help

## More on Arrays

**What is the mean temperature in 1948? In 1949? And so on...**

Remembering the difference between `axis = 0` and `axis = 1` is tricky, even for experienced Python programmers. Here are some helpful, visual reminders:

- [Integrating across rows or columns (StackOverflow)](https://stackoverflow.com/questions/25773245/ambiguity-in-pandas-dataframe-numpy-array-axis-definition/25774395#25774395)
- [Another way of visualizing the same thing (Site44)](http://using-python-in-research.site44.com/_images/axis.png)

## Basic Data Visualization

# Repeating Tasks with Loops

## Sequences

### Character Strings

### Lists and Tuples

### Performing Calculations with Lists

## More Complex Loops

### Challenge: Looping over Sequences

**Write a `for` loop that iterates through the letters of your favorite city, putting each letter inside a list. The result should be a list with an element for each letter.**

Hint: You can create an empty list like this:

```py
letters = []
```

Hint: You can confirm you have the right result by comparing it to:

```py
list("my favorite city")
```

### Challenge: Sequences and Mutability

**Which of the sequences we've learned about are immutable (i.e., they can't be changed)?**

- Strings are (immutable / mutable)?
- Lists are (immutable / mutable)?
- Tuples are (immutable / mutable)?

**And what does this mean for working with each data type?**

```py
"birds".upper()

[1, 2, 3].append(4)

(1, 2, 3)
```

## Iterators and Generators

# Analyzing Data from Multiple Files

## First Step: Looping over Files

## Second Step: Generating a Plot

## Third Step: Putting It All Together

### Challenge: Integrating over Multiple File Datasets

**For each location (each file), plot the difference between that location's mean temperature and the mean across all locations.**

Hint: One way to calculate the mean across five (5) files is by adding the 5 arrays together, then dividing by 5. You can add arrays together in a loop like this:

```py
# Start with an array full of zeros that is 69-elements long
running_total = np.zeros((69))

for fname in filenames:
    data = np.loadtxt(fname, delimiter = ',')
    running_total = running_total + data.mean(axis = 1)
```

Hint: How do you difference two arrays? Remember how the plus, `+`, and minus, `-`, operators work on arrays?

# Conditional Evaluation

This code can be represented by the following workflow.

### Challenge: Conditional Expressions

**How can you make this code print "Greater" by changing only one line?**

```py
a_number = 42

if a_number > 100:
    print('Greater')

else:
    print('Not greater')

print('Done')
```

**There are two (2) one-line changes you could make. Can you find them both?**

## Conditional Expressions in Python

**What do each of the following evaluate to, `True` or `False`?**

```py
1 < 2
1 <= 1
3 == 3
2 != 3
```

## Checking our Data

### Challenge: Fitting a Line over Multiple File Datasets

**Write a `for` loop, with an `if` statement inside, that calculates a line of best fit for each dataset's temperature anomalies and prints out a message as to whether that trend line is positive or negative.**

Hint: What we want to know about each trend line is whether, for:

```py
results = sm.OLS(y_data, x_data).fit()
b0, b1 = results.params
```

If `b1`, the slope of the line, is positive or negative.

# Creating Functions for Reuse

**What happens if we remove the keyword `return` from this function? Make this change and call the function again.**

## Composing Multiple Functions

Now that we've created a function that converts temperatures in degrees Kelvin to degrees Celsius, let's see if we can write a function that converts from degrees Celsius to degrees Fahrenheit.

$$
T_F = \left(T_C \times \frac{9}{5}\right) + 32
$$

## Cleaning Up our Analysis Code

## Positional versus Keyword Arguments

## Documenting Functions

### Challenge: Functions

**Create one (or both, for an extra challenge) of the following functions...**

- A function called `fences` that takes an input character string and surrounds it on both sides with another string, e.g., "pasture" becomes "|pasture|" or "@pasture@" if either "|" or "@" are provided.
- A function called `rescale` that takes an array and returns a corresponding array of values scaled to lie in the range 0.0 to 1.0.

Hint: Strings can be concatenated with the plus operator.

```py
'cat' + 's'
```

Hint: If $x_0$ and $x_1$ are the lowest and highest values in an array, respectively, then the replacement value for any element $x$, scaled to between 0.0 and 1.0, should be:

$$
\frac{x - x_0}{x_1 - x_0}
$$

# Capstone: Fitting Linear Models

We've seen the basics of the Python programming language. Now, let's get some hands-on experience applying what we've learned to real scientific data. **In this exercise, we'll see how to fit linear trends to data using a new Python library, `statsmodels`. After you see an initial example, you'll have time to extend the example on your own.**

## About the Data

The data are formatted such that:

- Each column is the monthly mean, January (1) through December (12)
- Each row is a year, starting from January 1948 (1) through December 2016 (69)

[More information on the data can be found here.](http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NCEP/.CPC/.GHCN_CAMS/.gridded/.deg0p5/.temp/)

## Introducing `statsmodels`

Without going into too much detail, our linear trend line has two components: a constant term ($\alpha$) and the slope of the trend line ($\beta$). Using linear algebra, we represent these two terms as two columns in a matrix. **To fit a linear model with a constant term, the first column is a column of ones.**

$$
\begin{align}
[\mathrm{Temp.\ anomaly}]&=[\mathrm{Some\ constant,\ }\alpha] + [\mathrm{Slope\ of\ trend\ line},\beta]\times[\mathrm{Year}]\\
\left[\begin{array}{r}
-2.04\\
-0.20\\
0.88\\
\vdots\\
\end{array}\right] &=
\left[\begin{array}{rr}
1 & 1948\\
1 & 1949\\
1 & 1950\\
\vdots & \vdots\\
\end{array}\right]
\left[\begin{array}{r}
\alpha\\
\beta\end{array}\right]
\end{align}
$$

## Challenge: Fitting a Line over Multiple Datasets

**Write a `for` loop, with an `if` statement inside, that calculates a line of best fit for each dataset's temperature anomalies and prints out a message as to whether that trend line is positive or negative.**

Hint: What we want to know about each trend line is whether, for:

```py
results = sm.OLS(y_data, x_data).fit()
b0, b1 = results.params
```

If `b1`, the slope of the line, is positive or negative. **So, to break that down:**

1. Loop over all the temperature files;
2. Calculate the temperature anomaly;
3. Fit an OLS regression to the anomaly data;
4. `print()` out whether the trend line is "positive" or "negative;"

### Hint: Looping over Files

```python
import glob

filenames = glob.glob('*.csv')
filenames
```

    ['barrow.temperature.csv',
     'reston.temperature.csv',
     'land_o_lakes.temperature.csv',
     'key_west.temperature.csv',
     'wvu.temperature.csv']

# Python at the Command Line

We've seen a lot of tools and techniques for improving our productivity through reproducible Python code. So far, however, we've been working exclusively within Jupyter Notebook. Jupyter Notebook is great for interactive, exploratory work in Python and encourages literate programming, as we discussed earlier. A Notebook is a great place to demonstrate to your future self or your peers how some Python code works.

But when it's time to scale-up your work and process data, you want to be on the command line, for all the reasons we saw when we discussed the Unix shell earlier.

Let's explore Python programs at the command line using the following *Python script,* `temp_extremes.py`.

```py
'''
Reports the min and max July temperatures for each file
that matches the given filename pattern.
'''

import csv
import os
import sys
import glob

def main():
    # Get the user-specified directory
    directory = sys.argv[1]

    # Pattern to use in searching for files
    filename_pattern = os.path.join(directory, '*temperature.csv')

    for filename in glob.glob(filename_pattern):
        july_temps = []

        # While the file is open...
        with open(filename, 'r') as stream:
            # Use a function to read the file
            reader = csv.reader(stream)

            # Each row is a year
            for row in reader:
                # Add this year's July temperature to the list
                july_temps.append(row[6])

        # A human-readable name for the file
        pretty_name = os.path.basename(filename)
        print(pretty_name, '--Hottest July mean temp. was', max(july_temps), 'deg K')
        print(pretty_name, '--Coolest July mean temp. was', min(july_temps), 'deg K')

if __name__ == '__main__':
    main()
```

We can run this script on the command line by typing the following:

```sh
$ python3 temp_extremes.py .
```

Remember that the single dot, `.` represents the current working directory, which is where all of our temperature CSV files are located.

## Our First Python Script

### Encapsulating to Keep the Namespace Clean

## Alternative Command-Line Tools

`sys.argv` is a rather crude tool for processing command-line arguments. There are a couple of alternatives I suggest you look into if you are going to be writing command-line programs in Python:

- `argparse`, another built-in library, that handles common cases in a systematic way. Check out [this tutorial](http://docs.python.org/dev/howto/argparse.html).
- `Fire`, [a very new Python module from Google](https://opensource.googleblog.com/2017/03/python-fire-command-line.html), which can turn any Python object (function, class, etc.) into a command-line API.

### Optional: Try out Python Fire

[Installation instructions and source code here.](https://github.com/google/python-fire)

## Modularization

### Installing Your Project as a Module

We haven't covered installing new Python modules, but when the time is right for you to package your code together as a single module (e.g., as `package_name`, in the example above), [consider installing your module "in development mode" first.](https://packaging.python.org/tutorials/distributing-packages/#working-in-development-mode)

# Understanding and Handling Errors

# Defensive Programming

## Assertions

### Assertations Regarding Type (Advanced)

### Challenge: Asserting Type in a Function

Recall the `celsius_to_fahr()` function we saw earlier. Implement a type-checking assertion that produces an `AssertionError` when the `temp_c` argument is not a number. Remember that there are two types of numbers we've see in Python so far:

- `float`
- `int`

You can decide whether the `celsius_to_fahr()` function should accept one or both of these types as inputs. **Don't forget to provide a helpful message as part of the `AssertionError`.**

```python
def celsius_to_fahr(temp_c):
    return (temp_c * (9/5)) + 32
```

### Assertions Regarding Inherited Type (Advanced)

**What if we used `OrderedDict` to represent our metadata?**

### Duck Typing (Advanced)

**For more information about types, classes, and how Python represents objects, see:**

- [Python 3 Documentation: The Python Data Model](https://docs.python.org/3/reference/datamodel.html#)

## Test-Driven Development

For example, suppose we need to find where two or more time series overlap. The range of each time series is represented as a pair of numbers, which are the time the interval started and ended. The output is the largest range that they all include.

### Challenge: Fix the Range Overlap Function

Fix `range_overlap()`; re-run `test_range_overlap()` after each change you make.

```python
def range_overlap(ranges):
    '''Return common overlap among a set of [low, high] ranges.'''
    for i, (low, high) in enumerate(ranges):
        if i == 0:
            lowest, highest = low, high
            continue

        lowest = max(lowest, low)
        highest = min(highest, high)

    if lowest >= highest:
        return None

    return (lowest, highest)

test_range_overlap()
```

## When in Doubt...

## Testing for Quality Control

## Unit Testing

# Analyzing and Optimizing Performance

> "Premature optimization is the root of all evil." - Sir Tony Hoare (later popularized by Donald Knuth)

## Benchmarking

## Line and Memory Profiling

Line and memory profilers aren't available in the Anaconda installation I had you use, [but you can read all about this topic on this excellent blog post.](https://www.huyng.com/posts/python-performance-analysis)

# Capstone

Now you have a chance to bring together everything you've learned in this Software Carpentry workshop, particularly:

- Using the Unix shell to download and manage delimited text data files;
- Importing data into Python;
- Using NumPy or other Python tools to summarize the data and diagnose any data issues;
- Cleaning and plotting the data using reproducible Python functions;

For this open-ended exercise, we'll use data from [the Woods Hole Oceanographic Institution's "Martha's Vineyard Coastal Observatory."](http://www.whoi.edu/page.do?pid=70177) **Choose which of the following datasets you want to work with:**

- [**The meteorological record**](http://www.whoi.edu/mvco/meteorological-data), which includes solar irradiance (`solar_campmt_m50[W/m^2]`) and rainfall (`rain_campmt[mm]`); [the order of the fields in the delimited files is listed here.](ftp://mvcodata.whoi.edu/pub/mvcodata/data/formats/MetDat_s.C99.txt)
- [**The oceanographic record**](http://www.whoi.edu/science/AOPE/mvco/data/OcnDat_s.html), which includes wave period and direction, ocean bottom temperature, and many other variables.

Each of these data sources has an online file directory:

- [**Directory to download meteorological records**](ftp://mvcodata.whoi.edu/pub/mvcodata/data/MetDat_s/)
- [**Directory to download oceanographic records**](ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/)

Once you've decided which dataset you want to work with, follow along with me using that particular record. I'm going to use the oceanographic data in this example.

## Your Tasks

1. Download 3 days worth of meteorological or oceanographic data files using `wget` or `curl` in the Unix shell.
2. Join the files together as one file using `cat` in the Unix shell.
3. Read the data into a Python session.
4. Create a time plot of a variable of your choice.
5. Filter the rows of the table to only daytime observations.
6. Write a function to calculate the *range* of values in air temperature (meteorological record)) or water temperature (oceanographic record). Apply this function teach of the 3+ days for which you obtained data.

## Getting Started

For this exercise, we want to work with multiple data files. Each data file in the index is one day, but we want to work with multiple days worth of data. **How can we quickly and conveniently download multiple data files from the web?**

This is something= the Unix shell is really great at automating. The WHOI dataset we're using exposes multiple data files at unique URLs. Below is an example file from the 120th day of 2018.

[ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018120_OcntDat_s.C99](ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018120_OcnDat_s.C99)

To download other days, we need only change one number in the URL:

```
ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018120_OcnDat_s.C99
ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018119_OcnDat_s.C99
ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018118_OcnDat_s.C99
...
```

How can we automate this with the Unix shell? First we need to figure out which shell program to use. Recall that the Unix shell offers many different small programs; depending on which variant of the Unix shell we're using, a certain program might not be available.

```sh
which wget
which curl
```

So you may have both installed. If you do, use `wget` instead of `curl`; both programs do the same thing.

### Downloading Online Datafiles with wget

Here's an example shell script to get us started. We iterate over three (3) days (118, 119, 120), storing each day number in a variable called `day`. In each iteration, we use `wget` to download the file, inserting that day number stored in the variable `day`.

```sh
cd
cd Desktop

for day in 118 119 120
do
  wget "ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018${day}_OcnDat_s.C99"
done
```

### Downloading Online Datafiles with curl

Unlike `wget`, the `curl` program prints the downloaded text data directly to the screen. **Remember how we dealt with taking screen output and *redirecting* it to a file?**

```sh
cd
cd Desktop

for day in 118 119 120
do
  curl "ftp://mvcodata.whoi.edu/pub/mvcodata/data/OcnDat_s/2018/2018${day}_OcnDat_s.C99" > 2018${day}_OcnDat_s.C99
done
```

## Good Luck!

**Try the rest of the Capstone on your own. Helpful hints are provided throughout. You're encouraged to work with a partner but feel free to work independently if that suits you.**

The hints below use only the packages we've seen in Python 3 so far, but if you're feeling adventurous, the `pandas` package has more and better tools for dealing with mixed, tabular data like the meteorology and oceanographic records here.

```py
import pandas as pd
```

For help getting started with `pandas`, check out [10 Minutes to Pandas](https://pandas.pydata.org/pandas-docs/stable/10min.html), in particular, the sections:

- [Getting data in/out - CSV](https://pandas.pydata.org/pandas-docs/stable/10min.html#csv)
- [Viewing data](https://pandas.pydata.org/pandas-docs/stable/10min.html#viewing-data)
- [Plotting data](https://pandas.pydata.org/pandas-docs/stable/10min.html#plotting)

## Hint: Combining Multiple Text Files in the Unix Shell

Remember the `cat` program?

```sh
cat 2018120_OcnDat_s.C99
```

You can provide multiple files to the `cat` program and it will combine them line-by-line.

```sh
cat 2018120_OcnDat_s.C99 2018119_OcnDat_s.C99 2018118_OcnDat_s.C99
```

But this just prints everything out to the screen. **Use redirection to store the output in a new file called `ocean.txt` or `met.txt`, depending on which data source you're using.**

## Hint: Reading in Data Using Python

The WHOI datasets are apparently space-delimited. They might also be called "fixed width" because each data column appears at the same distance along each line.

```python
import numpy as np

data = np.loadtxt('/home/arthur/Desktop/ocean.txt', delimiter = ' ')

# How many rows and columns?
data.shape
```

    (216, 24)

## Hint: Plotting Data in Python

Revisit your notes from when we did this earlier! Remember you need to import the plotting capability in Jupyter Notebook:

```py
import matplotlib.pyplot as pyplot
%matplotlib inline
```

## Hint: Filter Data Using Python

The fifth column contains the hour of the day. Recall that Python starts counting at zero, so column 5 is at position 4. The three dots (`...`) just mean "everything else" or, more specifically, "all the rows." Recall that with NumPy arrays, we count the number of rows, then columns: "rows comma columns."

```python
data[...,4]
```

    array([  0.,   0.,   0.,   1.,   1.,   1.,   2.,   2.,   2.,   3.,   3.,
             3.,   4.,   4.,   4.,   5.,   5.,   5.,   6.,   6.,   6.,   7.,
             7.,   7.,   8.,   8.,   8.,   9.,   9.,   9.,  10.,  10.,  10.,
            11.,  11.,  11.,  12.,  12.,  12.,  13.,  13.,  13.,  14.,  14.,
            14.,  15.,  15.,  15.,  16.,  16.,  16.,  17.,  17.,  17.,  18.,
            18.,  18.,  19.,  19.,  19.,  20.,  20.,  20.,  21.,  21.,  21.,
            22.,  22.,  22.,  23.,  23.,  23.,   0.,   0.,   0.,   1.,   1.,
             1.,   2.,   2.,   2.,   3.,   3.,   3.,   4.,   4.,   4.,   5.,
             5.,   5.,   6.,   6.,   6.,   7.,   7.,   7.,   8.,   8.,   8.,
             9.,   9.,   9.,  10.,  10.,  10.,  11.,  11.,  11.,  12.,  12.,
            12.,  13.,  13.,  13.,  14.,  14.,  14.,  15.,  15.,  15.,  16.,
            16.,  16.,  17.,  17.,  17.,  18.,  18.,  18.,  19.,  19.,  19.,
            20.,  20.,  20.,  21.,  21.,  21.,  22.,  22.,  22.,  23.,  23.,
            23.,   0.,   0.,   0.,   1.,   1.,   1.,   2.,   2.,   2.,   3.,
             3.,   3.,   4.,   4.,   4.,   5.,   5.,   5.,   6.,   6.,   6.,
             7.,   7.,   7.,   8.,   8.,   8.,   9.,   9.,   9.,  10.,  10.,
            10.,  11.,  11.,  11.,  12.,  12.,  12.,  13.,  13.,  13.,  14.,
            14.,  14.,  15.,  15.,  15.,  16.,  16.,  16.,  17.,  17.,  17.,
            18.,  18.,  18.,  19.,  19.,  19.,  20.,  20.,  20.,  21.,  21.,
            21.,  22.,  22.,  22.,  23.,  23.,  23.])

The `range()` function in Python returns a list of consecutive integers between the first and the second number. The `np.in1d` function tests each element of our "hour" column to see if it is in the list of numbers from 7 to 18, inclusive.

```python
np.in1d(data[...,4], range(7,19))
```

    array([False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False], dtype=bool)

To filter the full NumPy array to just those rows where the "hour" is between 7 and 18 (inclusive), we can take this vector of `True` and `False` values and put inside brackets, as below. Remember that the three dots (`...`) just mean "everything else" or, more specifically, "all the columns."

```python
daytime = data[np.in1d(data[...,4], range(7,19)),...]
daytime.shape
```

    (108, 24)

## Hint: Creating a Function to Calculate Temperature Ranges

The *range* of daytime temperatures for a given day is the daily maximum temperature minus the daily minimum temperature. Create a single function to do this, then call it at least 3 times, once for each unique day in your dataset.

**How can you present this function with just the data for a given day?** There are a few different ways. In both datasets, the 4th column (column 3 in Python, where we start counting from zero) has an integer representing the day of the month.

```python
daytime[...,3]
```

    array([ 28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,
            29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,
            29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,  29.,
            29.,  29.,  29.,  29.,  29.,  29.,  30.,  30.,  30.,  30.,  30.,
            30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,
            30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,
            30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.,  30.])

```python
daytime[...,3] == 28
```

    array([ True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
            True,  True,  True,  True,  True,  True,  True,  True,  True,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False,
           False, False, False, False, False, False, False, False, False], dtype=bool)

For another way to get all the records associated with a single day, remember that every day has the same number of records. Now that we've filtered to just the daytime records, there should be 36 records per day.

```python
daytime.shape
```

    (108, 24)

```python
108 / 3
```

    36.0

We can slice the first 36 rows of our data table to obtain the first day. The second day would then be rows 37 through 72, and so on.

```python
daytime[0:36,3]
```

    array([ 28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,  28.,
            28.,  28.,  28.])

# Connecting to SQLite with Python

```python
cursor.close()
connection.close()
```

## Best Practices with Database Connections


 **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

```