Run the below cell to import all relevant notebooks.

```python
import io, os, sys, types
from IPython import get_ipython
from nbformat import read
from IPython.core.interactiveshell import InteractiveShell

def find_notebook(fullname, path=None):
    """find a notebook, given its fully qualified name and an optional path

    This turns "foo.bar" into "foo/bar.ipynb"
    and tries turning "Foo_Bar" into "Foo Bar" if Foo_Bar
    does not exist.
    """
    name = fullname.rsplit('.', 1)[-1]
    if not path:
        path = ['']
    for d in path:
        nb_path = os.path.join(d, name + ".ipynb")
        #print('searching: %s'%nb_path)
        if os.path.isfile(nb_path):
            return nb_path
        # let import Notebook_Name find "Notebook Name.ipynb"
        nb_path = nb_path.replace("_", " ")
        #print('searching: %s' % nb_path)
        if os.path.isfile(nb_path):
            return nb_path

class NotebookLoader(object):
    """Module Loader for Jupyter Notebooks"""
    def __init__(self, path=None):
        self.shell = InteractiveShell.instance()
        self.path = path

    def load_module(self, fullname):
        """import a notebook as a module"""
        path = find_notebook(fullname, self.path)

        print ("importing Jupyter notebook from %s" % path)

        # load the notebook object
        with io.open(path, 'r', encoding='utf-8') as f:
            nb = read(f, 4)

        # create the module and add it to sys.modules
        # if name in sys.modules:
        #    return sys.modules[name]
        mod = types.ModuleType(fullname)
        mod.__file__ = path
        mod.__loader__ = self
        mod.__dict__['get_ipython'] = get_ipython
        sys.modules[fullname] = mod

        # extra work to ensure that magics that would affect the user_ns
        # actually affect the notebook module's ns
        save_user_ns = self.shell.user_ns
        self.shell.user_ns = mod.__dict__

        #print('Found %d cells'%len(nb.cells))
        try:
          for cell in nb.cells:
            if cell.cell_type == 'code':
                # transform the input to executable Python
                code = self.shell.input_transformer_manager.transform_cell(cell.source)
                # run the code in themodule
                exec(code, mod.__dict__)
        finally:
            self.shell.user_ns = save_user_ns
        return mod

class NotebookFinder(object):
    """Module finder that locates Jupyter Notebooks"""
    def __init__(self):
        self.loaders = {}

    def find_module(self, fullname, path=None):
        nb_path = find_notebook(fullname, path)
        if not nb_path:
            return

        key = path
        if path:
            # lists aren't hashable
            key = os.path.sep.join(path)

        if key not in self.loaders:
            self.loaders[key] = NotebookLoader(path)
        return self.loaders[key]

#  register the NotebookFinder with sys.meta_path
print('running importer')
sys.meta_path.append(NotebookFinder())
```

    running importer

```python
!pip install -U gif
```

    Requirement already up-to-date: gif in /home/avani/anaconda3/lib/python3.8/site-packages (3.0.0)
    Requirement already satisfied, skipping upgrade: Pillow>=7.1.2 in /home/avani/anaconda3/lib/python3.8/site-packages (from gif) (7.2.0)

```python
# Import all relevant functions
from sklearn import datasets
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

from utils import plot_decision_boundary, multi_class_signum, get_accuracy, get_prediction
from utils import plot_2D_input_datapoints, generate_gifs, normalize, signum

import matplotlib.pyplot as plt
import random
import numpy as np
import seaborn as sns; sns.set();
import pandas as pd
import math
import gif
import warnings
warnings.filterwarnings('ignore')
```

# Introduction to Perceptron Algorithm

The Perceptron Algorithm is a supervised learning algorithm of binary classifiers. A binary classifier is a function that decides whether an input data vector belongs to a specific class. In our case, we define our output class $\mathbf{y} = \{-1, +1\}$, where $+1$ denotes positive class and $-1$ denotes negative class.

Perceptron algorithm makes a fundamental assumption i.e., all datapoints are linearly separable.

In our perceptron algorithm problem, our goal is to classify a set of points into two classes in n-dimensional space. This classification is done by a hyperplane defined by a hypothesis function, $h_{\mathbf{w}}$ as follows:

\begin{equation}
\mathbf{X}: \mathbf{w^{\top}X + b} = 0
\end{equation}

where,
*   $\mathbf{m}  $ Number of examples/datapoints.
*   $\mathbf{d}  $ Number of features/dimensions.
*   $\mathbf{n}  $ Number of output categories.
*   $\mathbf{X} \in \mathbf{R^{m \times d}}$
*   $\mathbf{w} \in \mathbf{R^{d \times n}}$, Weights of the hyperplane
*   $\mathbf{b} \in \mathbf{R^n} $, Bias of the hyperplane

Instead of finding both $\mathbf{w}$ and $\mathbf{b}$, we make the following modifications to the above equation:

\begin{equation}
\mathbf{x_i} \rightarrow
\begin{bmatrix}
\mathbf{x_i} \\ 1
\end{bmatrix},
\mathbf{w} \rightarrow
\begin{bmatrix}
\mathbf{w} \\ \mathbf{b}
\end{bmatrix}
\end{equation}

When we generalize this modification for $\mathbf{X}$, our hyperplane necessarily goes through origin, which simplifies our hypothesis function as follows:

\begin{equation}
h_{\mathbf{w}} = \mathbf{w^{\top} X}
\end{equation}

Depending on the sign of this hypothesis function, it can predict three situations,

\begin{equation}
sign(h_{\mathbf{w}}) = sign(\mathbf{w^{\top} X}) = \left\{
  \begin{array}{ll}
    \text{1 if } \; \mathbf{w^{\top} X} \text{ > 0}\\
    \text{-1 if } \; \mathbf{w^{\top} X} \text{ <= 0}
  \end{array}
  \right.
\end{equation}

In our binary classification problem, the hypothesis function, $h_{\mathbf{w}}$, classifies a particular datapoint $(x_i, y_i)$ correctly if,

\begin{equation}
sign(\mathbf{w^{\top}x_i}) = \mathbf{y_i} \equiv \mathbf{y_i \cdot w^{\top} x_i > 0}
\end{equation}

On the other hand, $h_{\mathbf{w}}$ incorrectly classifies a datapoint $(x_i, y_i)$ if,

\begin{equation}
sign(\mathbf{w^{\top}x_i}) \neq \mathbf{y_i} \equiv \mathbf{y_i \cdot w^{\top} x_i \leq 0}
\end{equation}

$\;$

$\;$

The quantity, $\mathbf{y \cdot w^{\top} X}$, is the margin or the distance between the datapoints in $\mathbf{X}$ and the hyperplane.

Consider our desired behavior: we would like to have $\mathbf{y \cdot w^{\top} X > 0}$ for all training examples, and penalize those $\mathbf{w}$ where $\mathbf{y \cdot w^{\top} X \leq 0}$.

Intuitively, we would like to have our loss function defined only for misclassified training datapoints. For the correctly classified ones, we define zero loss.

Hence, our loss function is as follows:

\begin{equation}
J = \sum_{x_i \in \varepsilon} - \mathbf{y_i \cdot w^{\top} x_i}
\end{equation}

where $\varepsilon$ is the set of misclassified samples i.e.,

\begin{equation}
\varepsilon = \{\mathbf{x_i | w^{\top} x_i < 0 }\}
\end{equation}

This loss is differentiable. For a batch of samples, we can now write our gradient descent update rule as follows:

\begin{equation}
\mathbf{w^{k+1} \leftarrow w^k} + \eta \sum_{x_i \in \varepsilon} \mathbf{y_i x_i}
\end{equation}

Now that we have seen an introduction on how the Perceptron model works, we will dive into the code implementations. Also, we will answer some open-ended questions such as:

1.   Effect of weight initialization
2.   Do we need biases?
3.   What happens if we have non-linearly separable dataset, like XOR problem?
4.   Training perceptron model on a partially separable dataset

# Example 1 - Toy dataset

```python
# Setting the seed to ensure reproducibility of experiments
np.random.seed(10)

# Getting only linearly separable dataset
separable = False
while not separable:
  samples = make_classification(n_samples=1000, n_features=2, n_redundant=0, n_informative=1, n_clusters_per_class=1, flip_y=-1)
  red = samples[0][samples[1] == 0]
  blue = samples[0][samples[1] == 1]
  separable = any([red[:, k].max() < blue[:, k].min() or red[:, k].min() > blue[:, k].max() for k in range(2)])

X, y = samples

# Adding column of ones to absorb bias b of the hyperplane into X
bias_ones = np.ones((len(X), 1))
X = np.hstack((X, bias_ones))

# Setting y from {0,1} to {-1, 1}
y[y == 0] = -1
y = y.reshape(-1, 1)
```

```python
plot_2D_input_datapoints(X[:, :2], y)
```

```python
y.shape
```

    (1000, 1)

```python
# Train-Val-Test split
X_train_val, X_test, Y_train_val, Y_test = train_test_split(X, y, shuffle=True, test_size = 0.2)
X_train, X_val, Y_train, Y_val = train_test_split(X_train_val, Y_train_val, test_size = 0.125)

print("Training dataset: ", X_train.shape)
print("Validation dataset: ", X_val.shape)
print("Test dataset: ", X_test.shape)
```

    Training dataset:  (700, 3)
    Validation dataset:  (100, 3)
    Test dataset:  (200, 3)

```python
# Normalizing X_train and absorbing weight b of the hyperplane
X_normalized_train = normalize(X_train[:, :2])

b_ones = np.ones((len(X_normalized_train), 1))
X_normalized_train = np.hstack((X_normalized_train, b_ones))
```

Note above that 1st and 2nd column are input data features and the 3rd column effectively translates to the bias column. Adding column of ones to the input data helps in easier matrix multiplication between the input data $X$ and weights $w$.

The pseudo-code for our perceptron algorithm is as follows:

```python
# Perceptron training algorithm
def train(X_train, Y_train, weights, learning_rate=1, total_epochs=100):

  """Training method for Perceptron.

  Parameters
  -----------

  X_train: ndarray (num_examples(rows) vs num_features(columns))
    Input dataset which perceptron will use to learn optimal weights

  Y_train: ndarray (num_examples(rows) vs class_labels(columns))
    Class labels for input data

  weights: ndarray (num_features vs n_output)
    Weights used to train the network and predict on test set

  learning_rate: int
    Learning rate use to learn and update weights

  total_epochs: int
    Max number of epochs to train the perceptron model
  """

  n_samples, _ = np.shape(X_train)
  history_weights = []
  epoch = 1

  # Number of missclassified points we would like to see in the train set.
  # While training, its value will change every epoch. If m==0, our training
  # error will be zero.
  m = 1

  # If the most recent weights gave 0 misclassifications, break the loop.
  # Else continue until total_epochs is completed.
  while m != 0 and epoch <= total_epochs:
    m = 0

    # Compute weighted inputs and predict class labels on training set.
    weights_transpose_x = np.dot(X_train, weights)
    weights_transpose_x = signum(weights_transpose_x)
    y_train_out = np.multiply(Y_train, weights_transpose_x)
    epoch += 1

    # Collecting misclassified indexes and count them
    y_miscls_idxs = np.argwhere(y_train_out <= 0)[:, 0]
    y_miscls_idxs = np.unique(y_miscls_idxs)
    m = len(y_miscls_idxs)

    # Calculate gradients and update weights
    dweights = np.dot((X_train[y_miscls_idxs]).T, Y_train[y_miscls_idxs])
    weights += (learning_rate/n_samples) * dweights
    weights = np.round(weights, decimals=4)

    # Append weights to visualize decision boundary later
    history_weights.append(weights)

  if m == 0 and epoch <= total_epochs:
    print("Training has stabilized with all points classified: ", epoch)
  else:
    print(f'Training completed at {epoch-1} epochs. {m} misclassified points remain.')

  return history_weights
```

```python
# Initializing weights to zero
_, n_features = np.shape(X_normalized_train)
_, n_outputs = np.shape(Y_train)
print(n_outputs)
weights = np.zeros((n_features, n_outputs))
```

    1

```python
print(X_normalized_train.shape,Y_train.shape,weights.shape)
```

    (700, 3) (700, 1) (3, 1)

```python
trained_weights = train(X_normalized_train, Y_train, weights, learning_rate=0.05, total_epochs=50)
```

    Training completed at 50 epochs. 2 misclassified points remain.

Visualizing decision boundary for training set

```python
generate_gifs(X_train, Y_train, trained_weights, 'train', path='content/training_decision_boundary.gif')
```

    Gif/image generated

```python
best_weights = trained_weights[-1]
train_acc, _ = get_prediction(X_train, Y_train, best_weights, get_acc=True)
val_acc, _ = get_prediction(X_val, Y_val, best_weights, get_acc=True)
test_acc, _ = get_prediction(X_test, Y_test, best_weights, get_acc=True)

print("Evaluation results")
print("Training accuracy: {:.3f}" .format(train_acc))
print("Validation accuracy: {:.3f}" .format(val_acc))
print("Test accuracy: {:.3f}" .format(test_acc))
```

    Evaluation results
    Training accuracy: 99.571
    Validation accuracy: 99.000
    Test accuracy: 99.500

```python
plot_decision_boundary(X_test, Y_test, best_weights, dataset_type='test')
```

## Effect of weight initialization

Previously, we had initialized our weights to zero. What would have happened if we had initialized our weights too large? Let's check that too.

```python
# Initializing large weights
weights = np.ones((n_features, n_outputs)) * 3
```

```python
trained_weights = train(X_normalized_train, Y_train, weights, learning_rate=0.05, total_epochs=1000)
```

    Training completed at 1000 epochs. 9 misclassified points remain.

We see a similar effect. After 1000 epochs, training has still not converged. Let's see when does it converge.

```python
trained_weights = train(X_normalized_train, Y_train, weights, learning_rate=0.05, total_epochs=10000)
```

    Training completed at 10000 epochs. 4 misclassified points remain.

We shall see about better strategies of weight initializations later in the course.

## Effect of biases

Bias are additional constants attached neurons and added to the weighted input before the activation function is applied.

Let's visualize this effect of adding and removing biases in perceptron. The characteristics of biases found here is broadly applicable to other neural networks algorithms.

We know that an equation of the line can be described as such: $y = w \times x + b$. From this equation we know that intercept $b$ allows our equation to translate in our 2D plane.

W.r.t our perceptron model, we had derived a similar analogy: $Y_{prediction} = \text{signum_activation}(weights \times X_{input} + bias$).

Let us define an equation of line as follows: $0.25x - 0.85y + 3 = 0$. If we consider this line as our decision boundary, our optimal weights $=[0.25, -0.85]$ and bias $=[3]$. We will generate a random set of points on either side of line but not on it (for simplicity).

```python
import numpy as np
```

```python
# Setting the seed to ensure reproducibility of experiments
np.random.seed(10)

X = np.random.randint(-10, 10, size=(1000, 2))

# Adding column of ones to absorb bias b of the hyperplane into X
bias_ones = np.ones((len(X), 1))
X = np.hstack((X, bias_ones))
```

```python
# our optimal weights
best_weights = np.array([[0.25], [-0.85], [3]])
```

```python
# Method to generate random set of points on either side of line but not on it.
def get_relevant_points(X, weights):
  y_pred = np.dot(X, weights)
  y_pred[y_pred < 0] = -1
  y_pred[y_pred > 0] = 1
  relevant_idxs = np.argwhere(y_pred != 0)[:, 0]
  on_the_line_idxs = np.argwhere(y_pred == 0)[:, 0]
  return y_pred, relevant_idxs, on_the_line_idxs
```

```python
y_pred, relevant_idxs, on_the_line_idxs = get_relevant_points(X, best_weights)

X_on_the_line = X[on_the_line_idxs]
y_on_the_line = y[on_the_line_idxs]

X = X[relevant_idxs]
y = y_pred[relevant_idxs]
```

```python
plot_decision_boundary(X, y, best_weights, dataset_type='test', bias='on', class_label_01_form='off', model_type='perceptron')
```

```python
# Removing the column of ones
X = X[:, :2]
X.shape
```

    (995, 2)

Note that after generating the samples above, we aren't appending a column of ones in $X$. Hence, when we multiply $X$ with weights $w$, we are not including the bias term.

```python
# Initializing weights to zero
_, n_features = np.shape(X)
_, n_outputs = np.shape(y)

non_bias_weights = np.zeros((n_features, n_outputs))
```

```python
non_bias_weights.shape
```

    (2, 1)

```python
# Removing bias term and training the network only with weights.
trained_weights = train(X, y, non_bias_weights, learning_rate=0.1)
```

    Training completed at 100 epochs. 181 misclassified points remain.

```python
generate_gifs(X, y, trained_weights, 'train', path='content/training_decision_boundary_with_non_bias_weights.gif', bias='off')
```

    Gif/image generated

From the above gif, we can see that the decision boundary (or line equation) always passes through $0$ and is of the form: $\text{const1} \times x + \text{const2} \times y = 0$. What happens if we include the bias term while training the perceptron model? Let's see.

```python
# Adding column of ones to absorb bias b of the hyperplane into X
bias_ones = np.ones((len(X), 1))
X = np.hstack((X, bias_ones))
```

```python
# Initializing weights to zero
_, n_features = np.shape(X)
_, n_outputs = np.shape(y)

weights_with_bias = np.zeros((n_features, n_outputs))
```

```python
weights_with_bias.shape
```

    (3, 1)

```python
# Adding bias term and training the network only with weights.
trained_weights = train(X, y, weights_with_bias, learning_rate=0.1, total_epochs=1000)
```

    Training has stabilized with all points classified:  616

```python
# Taking last 50 weights since creating gif files can take too much time
if len(trained_weights) > 100:
  last_50_trained_weights = trained_weights[len(trained_weights)-50:]
```

```python
generate_gifs(X, y, last_50_trained_weights, 'train', path='content/training_decision_boundary_weights_with_bias.gif', bias='on')
```

    Gif/image generated

From the above gif, we can see that bias helps translate the decision boundary by representing pattern that don't necessarily pass through zero. In effect, a bias value lets you to shift the activation function by learning an appropriate threshold. One bias unit is always sufficient since it will affect each neuron differently based on its weight with each unit. Generally, to successfully learn an ML model, it almost always helpful to assign bias to every non-input since without bias, weights would have thresholds that will always be zero.

## Partially separable dataset

Let's see how the perceptron learns the decision boundary on a partially separable dataset.

```python
# Initializing weights to zero
_, n_features = np.shape(X)
_, n_outputs = np.shape(y)

weights_with_bias = np.zeros((n_features, n_outputs))
```

```python
weights_with_bias.shape, X.shape, y.shape
```

    ((3, 1), (995, 3), (995, 1))

```python
# Collecting all indexes with class label = 1. Ramdomly sampling 5 indexes
# and setting class labels = -1
np.random.seed(10)
idxs_class_labels_one = np.argwhere(y == 1)[:, 0]
idxs_class_labels_one = random.sample(idxs_class_labels_one.tolist(), 5)
y[idxs_class_labels_one] = -1
```

```python
# Adding bias term and training the network only with weights.
trained_weights = train(X, y, weights_with_bias, learning_rate=0.1, total_epochs=1000)
```

    Training completed at 1000 epochs. 59 misclassified points remain.

```python
# Taking last 50 weights since creating gif files can take too much time
if len(trained_weights) > 100:
  last_50_trained_weights = trained_weights[len(trained_weights)-50:]
```

```python
generate_gifs(X, y, last_50_trained_weights, 'train', path='content/training_decision_boundary_partially_separable_dataset.gif')
```

    Gif/image generated

# Perceptron learning and the XOR problem

```python
# Setting the seed to ensure reproducibility of experiments
np.random.seed(10)

# Set the input data
X = np.array([[0, 0], [0, 1],
              [1, 0], [1, 1]])

# Set the labels, the correct results for the xor operation
y = np.array([[0], [1], [1], [0]])

# Adding column of ones to absorb bias b of the hyperplane into X
bias_ones = np.ones((len(X), 1))
X = np.hstack((X, bias_ones))
```

```python
# Initializing weights from uniform distribution
np.random.seed(312)
n_samples, n_features = np.shape(X)
_, n_outputs = np.shape(y)

limit = 1 / math.sqrt(n_features)
weights = np.random.uniform(-limit, limit, (n_features, n_outputs))
weights[-1] = 0
```

```python
# Train the network with the above weights
trained_weights = train(X, y, weights, learning_rate=0.1, total_epochs=50)
```

    Training completed at 50 epochs. 2 misclassified points remain.

```python
generate_gifs(X, y, trained_weights, 'train', path='content/xor_training_decision_boundary.gif')
```

    Gif/image generated

By training the perceptron model for 10 epochs we can clearly see from the gif above, that at a time, atleast 2 to atmost 3 of the points are missclassified.

This characteristic persists even if we train the perceptron model for higher number of epochs. The perceptron is unable to classify all the points correctly. Theoretically, the perceptron is unable to converge on a XOR dataset. Why do you think this is the case? The reason is because the classes in XOR are not linearly separable.

```python

```

```python

```


# General Announcements

* HW2 and Quiz 3 are now posted on Canvas
* Nataly has posted a poll about office hours on Canvas. We will switch them if there is strong availability at another time.
* Today's lecture supplements from the following texts:
  * Krane, Kenneth S. Introductory nuclear physics. John Wiley & Sons, 1991. (Sections 3.3 and Ch5)
  * Martin, James E. Physics for radiation protection: a handbook. John Wiley & Sons, 2006. (Sections 2.4-2.6)

```python
%matplotlib notebook
%matplotlib inline
import math
import matplotlib.pyplot as plt
import numpy as np
```

# Nuclear Models

## Learning objectives

- Summarize the history of atomic theory development.
- Recognize the radiation signatures that drove early atomic theory.
- List atomic models: Plum Pudding, Rutherford, Bohr, Bohr with Elliptical Orbits, Quantum Mechanical
- Differentiate various atomic models by name and physics.
- List nuclear models: Proton-electron, Proton-neutron, Liquid-Drop, Shell
- Differentiate nuclear models by name and physics.
- Identify the physics captured by various nuclear models.
- Explain the reason for the structure of most likely decays in the chart of the nuclides

## Discovery of Radioactivity

1896: Becquerel found that some U salts emitted "X-Rays" that fogged photographic plates.

1898: The Curies also found two elements that had these properties -- Radium and Polonium

Experiments with electrically charged plates impose a magnetic field (out of the page). Three types of radiation appeared to exist from various elements.

21.3 Radioactive Decay by Rice University is licensed under a Creative Commons Attribution 4.0 International License, except where otherwise noted. (https://opentextbc.ca/chemistry/chapter/21-3-radioactive-decay/)

- Alpha $(\alpha)$ particles are attracted to the negative plate and deflected by a relatively small amount.
- Beta $(\beta)$ particles are attracted to the positive plate and deflected by a larger amount.
- Gamma $(\gamma)$ particles seem to be unaffected.

### Exercise: Think-pair-share

Based on these experimental results:

1. What can one determine about the charges of the alpha, beta, and gamma particles?
2. What can one determine about the weights of the particles?

### Knowledge of the atom

What we've talked about in the last few lectures contributed to vital insights about the atom and its structure and how various components relate to one another.

- Quantized energy could be emitted from atoms
- Electrons can be broken off of atoms, and look the same regardless of what element they come from
- There must exist a positively charged component, because atoms are electrically neutral until electrons are removed.

### Plum Pudding Model

J.J. Thomson correctly identified the beta rays as electrons.
So, electrons must be somewhere in the atom. However, atoms seemed be generally electrically neutral.
Being british, J.J. Thomson equated this to plum pudding (year 1904):

He had no reason to believe that the charges were not uniformly distributed.

Explains:

- electrically neutral atoms
- size of the atom $(10^{-8} cm)$
- an ion is an atom from which an electron has been lost
- charge of a singly ionized atom is exactly one negative charge of an electron
- number of electrons equals approximately half of the atomic weight of the atom

Issues:
- Does not explain frequencies observed in optical spectra
- Where the rest of the mass comes from (electrons only account for small fraction of mass

### Rutherford Model

Gold foil experiment by Geiger (same one associated with the Geiger counter) and Marsden. Alpha particles bombarded a very thin gold sheet/foil. Reflected alphas were very unlikely to be observed if the Thomson model was correct.

In **1911** Rutherford surmised that a dense, positively charged thing must be at the center of the atom, perhaps with a diameter of $10^{-14}m$ or less. Rutherford also postulated a "neutral" particle may help contribute to this (it helped explain discrepencies between mass and charge)

Deficiency of Rutherford’s model:
According to classical electromagnetic theory, an accelerating charge must emit electromagnetic waves. Thus, an electron circling the nucleus (electron) would radiate away their kinetic energy, r would slowly decrease, and the atom will collapse.

### Bohr Model

In the 1880s, Balmer, Rydberg, and others had observed **discrete lines** in the wavelength spectrum of atomic hydrogen when atoms are excited.

The equation explaining these emission lines was well known by the early 1900s.

\begin{align}
\frac{1}{\lambda} &= R_H \left[\frac{1}{n_o^2} - \frac{1}{n^2} \right]\\
\mbox{where  }&\\
\lambda &= \mbox{the wavelength of electromagnetic radiation emitted in vacuum}\\
R_H &= \mbox{the Rydberg constant } \\
&\simeq  1.097373156850865 \times 10^7 m^{-1} \\
\implies \frac{1}{R} &=912 \mbox{ angstrom}\\
\end{align}

In the Bohr model, then, electrons were allowed only in certain orbits. Bohr's model had three postulates:
1) An electron in an atom can move about the nucleus in discrete stationary states without radiating.

2) The allowed stationary states of orbiting electrons are specifically only those orbits whose angular momentum is an integral multiple of $h/2\pi$

and satisfied the following relation:

\begin{align}
\frac{m_ev^2}{r} &= \frac{Ze^2}{4\pi\epsilon_or^2}\\
L &\equiv m_evr = n\frac{h}{2\pi}\\
\mbox{where }&\\
n&=1,2,3...\\
L&=\mbox{angular momentum}\\
v&=\mbox{velocity of the electron}\\
r&=\mbox{radius of the electron orbit}\\
\epsilon_o&=\mbox{permittivity of free space}\\
&= 8.85418782 \times 10^{-12} m^{-3} kg^{-1} s^4 A^2
\end{align}

Solving those relations for $r$ and $v$ :

\begin{align}
v_n &= \frac{Ze^2}{2\epsilon_onh}\\
r_n &= \frac{n^2h^2\epsilon_o}{\pi m_e Z e^2}
\end{align}

And finally,

3) An electron can jump from a higher energy state $E_2$ to a lower enegy orbit $E_1$ by emitting electromagnetic radiation of energy $h\nu$

This explained the discreteness, as electrons might radiate energy only when moving from one allowed orbit to another.

Electron’s total energy under a certain orbit:

\begin{align}
E_n &= \frac{-m_e(Ze^2)^2}{8\epsilon_o^2 n^2 h^2}\\
\end{align}

Energy difference between two allowed orbits:

\begin{align}
\Delta E_{n\rightarrow n_o} &= h\nu_{n\rightarrow n_o} \\
 &=  \frac{-m_e(Ze^2)^2}{8\epsilon_o^2 h^2}\left[\frac{1}{n_o^2} - \frac{1}{n^2}\right]\\
\end{align}

This model explained a lot.

### Bohr Model : Elliptic Orbits

Fine structure: Spectral lines are consisted of a number of lines very close together.

- Except the quantum number $n$, there are some energy levels lying close to one another.
- Sommerfeld postulated elliptic orbits as well as circular orbits and introduced another quantum number to describe the **angular momentum** of orbits.

This helped explain the fine structure that was observed. However, Sommerfeld’s theory predicted more lines than were observed in experiments.

Thus, a new quantum number  $n$  needed an _ad hoc_ selection rule to limit the number of predicted lines.

Splitting of the spectral lines was observed: A third quantum number $m$ needed to be introduced to revise Bohr’s model. also predicted more lines

The theory failed to applied to more complicated atoms (multiplet structure is observed).

Further change of Bohr’s theory can no longer resolve the difficulties.

### Quantum Mechanical Model

The electrons are no longer modeled as particles moving in orbits. Instead, they are modeled as a standing wave around the nucleus. The magnitude of that wave reflects the probability of finding the electron in that locations.

- Schrodinger’s new approach:  **the wave function**
- The electrons are no longer point particles; they are visualized as a standing wave around the nucleus.

The three quantum numbers ($n$, $l$ , $m$) arose from the theory naturally - no ad hoc selection rules were needed.

A fourth quantum number $m_s$ was introduced to explain:
- Multiple fine-line structure
- Splitting of lines in a strong magnetic field ( the **anomalous Zeeman effect**)

The $m_s$ quantum number accounted for the inherent angular momentum of the electron equal to $\pm\frac{h}{2\pi}$. Later, in 1928, Dirac would show that this fourth quantum number also arises from the wave equation.

## Nuclear Models

### Fundamental properties of the nucleus

Some simple facts were determined about the mass of the nucleus.

1. The **masses of atoms** were very nearly whole numbers if one defines the atomic mass unit as:
\begin{align}
1 u = \frac{1}{12}M(^{12}C)
\end{align}
2. The **mass contribution from electrons** is quite **small**
  - In $^{12}C$, the six electrons together only weigh $0.00329u$

Also, electron scattering experiments yielded details about the density of the nucleus and its components.
**The density of protons** inside the spherical nuclei was well understood to be a function of the radius, dropping off appreciably at the boundary of the nucleus.

\begin{align}
\rho_p(r) &= \frac{\rho_p^o}{1+e^{\frac{(r-R)}{a}}} \left[\frac{\mbox{protons}}{fm^3}\right]\\
\mbox{where }&\\
r &= \mbox{distance to center of nucleus}\\
R &= \mbox{total 'radius' of the nucleus}\\
a &= \mbox{surface thickness}\\
\rho_p^o &= \int\int\int \rho_p(r)dV\\
&= 4\pi\int_0^\infty r^2\rho_p(r)dr\\
&= Z
\end{align}

Also, the R value, the radius of the nucleus was seen empirically to be proportional to $A^{1/3}$.

```python
def rho_p(r, rho_po, radius, a):
    denom = 1 + math.exp((r - radius)/a)
    return rho_po/denom
```

```python
# For 16O (from table 3.2)
o_rho_po = 0.156 # fm^{-3}
o_radius = 2.61 # fm
o_a = 0.513

# For 109Ag (from table 3.2)
ag_rho_po = 0.157 # fm^{-3}
ag_radius = 5.33 # fm
ag_a = 0.523

# For 208Pb (from table 3.2)
pb_rho_po = 0.159 # fm^{-3}
pb_radius = 6.65 # fm
pb_a = 0.526

r = np.arange(0, 10, 0.1)
to_plot_o = np.arange(0., 100.)
to_plot_ag = np.arange(0., 100.)
to_plot_pb = np.arange(0., 100.)

for i in range(0, 100):
    to_plot_o[i]  = rho_p(r[i], o_rho_po, o_radius, o_a)
    to_plot_ag[i] = rho_p(r[i], ag_rho_po, ag_radius, ag_a)
    to_plot_pb[i] = rho_p(r[i], pb_rho_po, pb_radius, pb_a)

plt.plot(r, to_plot_o, label="$^{16}O$")
plt.plot(r, to_plot_ag, label="$^{109}Ag$")
plt.plot(r, to_plot_pb, label="$^{208}Pb$")
plt.ylabel("Proton density ($fm^{-3}$)")
plt.xlabel("distance from center $(fm)$")
plt.legend()
```

# Review from last lecture

### Proton Electron Model

This model sought primarily to explain the wholeness of mass numbers. To do so, it simply assumed that all heavier nuclei were composed of multiples of the hydrogen nucleus (assumed to be a single proton), which has the smallest mass(1 amu).

For this to match what was known about atomic charge, electrons would need to be in the nucleus to cancel some, but not all, of the positive charge from the protons.

**In this model** an atom $^A_ZX$ would have a nucleus containing A protons and (A − Z) electrons with Z electrons surrounding the nucleus. This postulated extra electrons -- to explain it, the mass of the electrons was assumed to make a negligible contribution.

Two difficulties with this P-E Model:

1. Predicted angular momentum (spin) of the nuclei did not always agree with experiment.
   **Protons and electrons both have half integer spin**, so when an even number are combined, whole integer spin should result. For example, **the model predicted integer spin for Beryllium, while experiments predicted half-integer spin.** Similarly, **the model predicted half-integer spin in nitrogen but experimental results show nitrogen has integer spin.**

2. Uncertainty Principle

\begin{align}
\Delta p \Delta x &\ge \frac{h}{4\pi}
\end{align}

If an electron is in the nucleus, then $\Delta x\simeq10^{-14}m$. Accordingly:
\begin{align}
min(\Delta_p) = 1.1\times 10^{-20}J m^{-1}s
\end{align}
Since the electron’s total energy is
\begin{align}
E &= T +m_oc^2 \\
&=	\sqrt{p^2c^2 +m^2_oc^4}\\
\implies &\\
&\forall p = \Delta p : E \simeq T = 20 MeV\\
\end{align}

Since the electron's rest-mass energy is 0.51 MeV and beta particles emitted by atoms seldom have energies above a few MeV, something was wrong with this calculation.

You can do the same calculation for the proton. Because it has a much higher mass, there is no discrepancy. The energy of a free proton confined to a nucleus is its rest-mass energy (931 MeV), with $T<1MeV$.

### 1932: Chadwick discovers the neutron

Chadwick discovers the existence of a chargeless particle with a mass just slightly greater than that of the proton.

\begin{align}
m_n &= 1.008665 u\\
m_p &= 1.007276 u
\end{align}

### Proton-Neutron Model

- **1932** Chadwick discovered the neutron.
- **1932** Heisenberg first suggested every nucleus is composed of only protons and neutrons.

**In this model**, a nucleus with a mass number A contains Z protons and N = A − Z neutrons.

- This P-N Model avoids the failures of the P-E model.
- Also is consistent with experimental results regarding radioactivity
- Since the neutron has half-integral spin, the A neutrons and protons give appropriate spin for the total atom in all cases.

Challenges for this model:

- Because of Coulombic repulsive forces in the nucleus, a 'nuclear force' must hold the nucleus together
- To hold the protons and neutrons together:
      nuclear force : p-n , n-n , p-p
      1.  inside the nucleus: nuclear force (attractive)
      2. outside the nucleus: Coulombic force (repulsive)

The energy needed to separate nucleus into $p_s$ & $n_s$ : binding energy

In the image below, the nuclear potential well is shown.

**The above image was reproduced from Shultis, J. K. and Faw, R. E. “Fundamentals of Nuclear Science and Engineering” (2016).**

## Nuclear Stability

**Figure** Graph of isotopes by type of nuclear decay. Orange and blue nuclides are unstable, with the black squares between these regions representing stable nuclides. The unbroken line passing below many of the nuclides represents the theoretical position on the graph of nuclides for which proton number is the same as neutron number. The graph shows that elements with more than 20 protons must have more neutrons than protons, in order to be stable.

**The above image was reproduced from Shultis, J. K. and Faw, R. E. “Fundamentals of Nuclear Science and Engineering” (2016).**

- many more stable isotopes with even N and/or Z
- in a heavy nucleus, the neutrons and protons tend to group themselves into subunits of 2 neutrons and 2 protons.
- when either Z or N equals 8, 20, 50, 82, or 126, there are relatively greater numbers of stable nuclides.

### Shell Model

Recall from your chemistry class how ionization energy for electrons varies at it fills orbital shells.

**The above image was reproduced from Figure 5.1 in Krane, K. “Introductory Nuclear Physics” (1988).**

We also see strong effects on the atomic radius based on filled electron orbital shells. This implies a stable structure when shells are filled.

To remove a single proton or neutron from the nucleus, we use a term called the "separation energy". That is, the energy needed to remove that nucleon from the nucleus. Below, let's look at the two proton and two neutron separation energies for some nucleons of constant N (isotones).

**The above image was reproduced from Figure 5.2 in Krane, K. “Introductory Nuclear Physics” (1988).**

The liquid drop model cannot explain the abnormal high stable nuclides(magic numbers:2,8,20,28,50,82,126). Thus, the nuclear shell model is another attempt at explaining the structure of the nucleus.

**The above image was reproduced from Figure 5.7 in Krane, K. “Introductory Nuclear Physics” (1988).**

> The filling of shells in $^{15}O$ and $^{17}O$. The filled proton shells do not contribute to the structure; the properties of the ground state are determined by the odd neutron.

We can see effects of the nucleon shells in the chart of the nuclides too. These help explain the stable isotopes that exist on magic number lines as well as "islands of stability".

**The shell model assumes**: (Shrodinger’s wave eq.)
Each nucleon moves independently.
Each nucleon moves in a potential well.

When the model’s quantum-mechanical wave eq. is solved, the nucleons are found to distribute themselves into a number of energy levels.
Filled shells are indicated by large gaps between each adjacent energy level.

### Bringing them all together: the semiemperical mass formula

Let us attempt to summarize our current understanding of the atom by combining various terms that we know are important to the structure of the nucleus.

First, we are going to express this concept in terms of binding energy per nucleon. We are starting with the concept that the binding energy B is proportional to the increase in nucleons, A.

$
B = [Zm(^{1}H) + Nm_{n} - m(^{A}X)]c^2
$

If we can come up with an approach to determine B, we can predict the mass of $^{A}X$

First,
1. volume term: volume binding energy is proportional to the number of nucleons (i.e. # of nucleons.) **liquid drop**

Volume Term = $a_{v}A$

2. Surface term: Nucleons near the surface of the nucleus are not completely surrounded by other nucleons as interior nucleons. **liquid drop**

Surface Term = $a_{s}A^{2/3}$

(this is because the surface area of the nuclueus is proportional to $R^{2}$, or $A^{2/3}$ since $R \propto A$)

3. Coulomb term: Coulombic repulsive force decrease the stability of nucleons (reduces the BE ). **liquid drop**

Coulomb Term = $a_{c}Z(Z-1)A^{1/3}$

4. Symmetry term: a departure from symmetry (N=Z) tends to reduce nuclear stability. (Fig.3.10 ) **shell model**

Symmetry Term = $a_{sym}\frac{(A-2Z)^2}{A}$

5. pairing term : pairing neutrons and protons are more stable. (even N or Z; odd N or Z ; even N (Z), odd Z (N)) (Fig.3.11 & Fig.3.12) **shell model**

Pairing Term = $a_{p}A^{-3/4}$

Combining all the five terms we've discussed...

\begin{align}
BE &= a_{v}A - a_{s}A^{2/3} - a_{c}Z(Z-1)A^{-1/3} - a_{sym}\frac{(A-2Z)^2}{A} \pm a_{p}A^{-3/4}
\end{align}

where
* $a_{v}$ = 15.5MeV
* $a_{s}$ = 16.8MeV
* $a_{c}$ = 0.72MeV
* $a_{sym}$ = 23MeV
* $a_{p}$ = 34 MeV

Higher binding energy $\implies$ a more stable nuclide.

**The above image was reproduced from Figure 3.17 in Krane, K. “Introductory Nuclear Physics” (1988).**
> The contributions of the various terms in the semiemperical mass formula to the binding energy per nucleon

In our next lecture, we will discuss binding energy calculations. However, if we know that the difference between the constituent parts of the atom and its measured mass is the binding energy, we can use our binding energy formula to calculate a **semiemperical mass**.

\begin{align}
M(Z,A) &= Zm(^{1}H)-Nm_{n}-B(Z,A)/c^{2}
\end{align}

This is the **semiemperical mass formula**

For constant A, we will see a parabola dependent on Z and M.

### Exercise: Think-pair-share

Given a constant A of several isotopes, what do you think a lower mass says about the binding energy?

**The above image was reproduced from Figure 3.18 in Krane, K. “Introductory Nuclear Physics” (1988).**
> Mass chains for A=125 and A=128. For A=125, note how the energy differences between neighboring isotopes increase as we go further from the stable member at the energy minimum. For A=128, note the effect of the pairing term; in particular $^{128}I$ can decay in either direction and it is energetically possible for $^{128}Te$ to decay directly to $^{128}Xe$ by the process known as double $\beta$ decay.

```python

```

```python

```


```python
# This cell is added by sphinx-gallery
# It can be customized to whatever you like
%matplotlib inline
```

.. role:: html(raw)
   :format: html

Understanding the Haar Measure
==============================

.. meta::
    :property="og:description": Learn all about the Haar measure and how to randomly sample quantum states.

    :property="og:image": https://pennylane.ai/qml/_images/spherical_int_dtheta.png

.. related::

    quantum_volume Quantum volume
    qsim_beyond_classical Beyond classical computing with qsim
    tutorial_barren_plateaus Barren plateaus

*Author: PennyLane dev team. Posted: 22 March 2021. Last updated: 22 March 2021.*

If you've ever dug into the literature about random quantum circuits,
variational ansatz structure, or anything related to the structure and
properties of unitary operations, you've likely come across a statement like the
following: "Assume that $U$ is sampled uniformly at random from the Haar
measure".  In this demo, we're going to unravel this cryptic statement and take
an in-depth look at what it means. You'll gain an understanding of the general
concept of *measure*, the Haar measure and its special properties, and you'll
learn how to sample from it using tools available in PennyLane and other
scientific computing frameworks. By the end of this demo, you'll be able to
include that important statement in your own work with confidence!

Note

To get the most out of this demo, it is helpful if you are familiar with
   `integration of multi-dimensional functions
   <https://en.wikipedia.org/wiki/Multiple_integral>`__, the `Bloch sphere
   <https://en.wikipedia.org/wiki/Bloch_sphere>`__, and the conceptual ideas
   behind `decompositions
   <https://en.wikipedia.org/wiki/Matrix_decomposition>`__ and factorizations of
   unitary matrices (see, e.g., 4.5.1 and 4.5.2 of [#NandC2000]_).

Measure
-------

`Measure theory <https://en.wikipedia.org/wiki/Measure_(mathematics)>`__ is a
branch of mathematics that studies things that are measurable---think length,
area, or volume, but generalized to mathematical spaces and even higher
dimensions. Loosely, the measure tells you about how "stuff" is distributed and
concentrated in a mathematical set or space. An intuitive way to understand
measure is to think about a sphere. An arbitrary point on a sphere can be
parametrized by three numbers---depending on what you're doing, you may use
Cartesian coordinates $(x, y, z)$, or it may be more convenient to use
spherical coordinates $(\rho, \phi, \theta)$.

Suppose you wanted to compute the volume of a solid sphere with radius
$r$.  This can be done by integrating over the three coordinates
$\rho, \phi$, and $\theta$. Your first thought here may be to simply
integrate each parameter over its full range, like so:

\begin{align}V = \int_0^{r} \int_0^{2\pi} \int_0^{\pi} d\rho~ d\phi~ d\theta = 2\pi^2 r\end{align}

But, we know that the volume of a sphere of radius $r$ is
$\frac{4}{3}\pi r^3$, so what we got from this integral is clearly wrong!
Taking the integral naively like this doesn't take into account the structure of
the sphere with respect to the parameters. For example, consider
two small, infinitesimal elements of area with the same difference in
$\theta$ and $\phi$, but at different values of $\theta$:

.. figure:: /demonstrations/haar_measure/spherical_int_dtheta.png
    :align: center
    :width: 50%

    |

Even though the differences $d\theta$ and $d\phi$ themselves are the
same, there is way more "stuff" near the equator of the sphere than there is
near the poles. We must take into account the value of $\theta$ when
computing the integral! Specifically, we multiply by the function
$\sin\theta$---the properties of the $\sin$ function mean that the
most weight will occur around the equator where $\theta=\pi/2$, and the
least weight near the poles where $\theta=0$ and $\theta=\pi$.

Similar care must be taken for $\rho$.  The contribution to volume of
parts of the sphere with a large $\rho$ is far more than for a small
$\rho$---we should expect the contribution to be proportional to
$\rho^2$, given that the surface area of a sphere of radius $r$ is
$4\pi r^2$.

On the other hand, for a fixed $\rho$ and $\theta$, the length of
the $d\phi$ is the same all around the circle. If put all these facts
together, we find that the actual expression for the integral should look like
this:

\begin{align}V = \int_0^r \int_0^{2\pi} \int_0^{\pi} \rho^2 \sin \theta~ d\rho~ d\phi~
    d\theta = \frac{4}{3}\pi r^3\end{align}

These extra terms that we had to add to the integral, $\rho^2 \sin
\theta$, constitute the *measure*. The measure weights portions of the sphere
differently depending on where they are in the space. While we need to know the
measure to properly integrate over the sphere, knowledge of the measure also
gives us the means to perform another important task, that of sampling points in
the space uniformly at random. We can't simply sample each parameter from the
uniform distribution over its domain---as we experienced already, this doesn't
take into account how the sphere is spread out over space. The measure describes
the distribution of each parameter and gives a recipe for sampling them in order
to obtain something properly uniform.

The Haar measure
----------------

Operations in quantum computing are described by unitary matrices.
Unitary matrices, like points on a sphere, can be expressed in terms of a fixed
set of coordinates, or parameters. For example, the most general single-qubit rotation
implemented in PennyLane (:class:`~.pennylane.Rot`) is expressed in terms of three
parameters like so,

\begin{align}U(\phi, \theta, \omega) = \begin{pmatrix} e^{-i(\phi + \omega)/2}
                        \cos(\theta/2) & -e^{i(\phi - \omega)/2} \sin(\theta/2)
                        \\ e^{-i(\phi - \omega)/2} \sin(\theta/2) & e^{i(\phi +
                        \omega)/2} \cos(\theta/2) \end{pmatrix}.\end{align}

For every dimension $N$, the unitary matrices of size $N \times N$
constitute the *unitary group* $U(N)$. We can perform operations on
elements of this group, such as apply functions to them, integrate over them, or
sample uniformly over them, just as we can do to points on a sphere. When we do
such tasks with respect to the sphere, we have to add the measure in order to
properly weight the different regions of space. The *Haar measure* provides the
analogous terms we need for working with the unitary group.

For an $N$-dimensional system, the Haar measure, often denoted by
$\mu_N$, tells us how to weight the elements of $U(N)$. For
example, suppose $f$ is a function that acts on elements of $U(N)$,
and we would like to take its integral over the group. We must write this
integral with respect to the Haar measure, like so:

\begin{align}\int_{V \in U(N)} f(V) d\mu_N(V).\end{align}

As with the measure term of the sphere, $d\mu_N$ itself can be broken down
into components depending on individual parameters.  While the Haar
measure can be defined for every dimension $N$, the mathematical form gets
quite hairy for larger dimensions---in general, an $N$-dimensional unitary
requires at least $N^2 - 1$ parameters, which is a lot to keep track of!
Therefore we'll start with the case of a single qubit $(N=2)$, then show
how things generalize.

Single-qubit Haar measure
~~~~~~~~~~~~~~~~~~~~~~~~~

The single-qubit case provides a particularly nice entry point because we can
continue our comparison to spheres by visualizing single-qubit states on the
Bloch sphere. As expressed above, the measure provides a recipe for sampling
elements of the unitary group in a properly uniform manner, given the structure
of the group. One useful consequence of this is that it provides a method to
sample quantum *states* uniformly at random---we simply generate Haar-random
unitaries, and apply them to a fixed basis state such as $\vert 0\rangle$.

We'll see how this works in good time. First, we'll take a look at what happens
when we ignore the measure and do things *wrong*. Suppose we sample quantum
states by applying unitaries obtained by the parametrization above, but sample
the angles $\omega, \phi$, and $\theta$ from the flat uniform
distribution between $[0, 2\pi)$ (fun fact: there is a measure implicit in
this kind of sampling too! It just has a constant value, because each point is
equally likely to be sampled).

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

# set the random seed
np.random.seed(42)

# Use the mixed state simulator to save some steps in plotting later
dev = qml.device('default.mixed', wires=1)

@qml.qnode(dev)
def not_a_haar_random_unitary():
    # Sample all parameters from their flat uniform distribution
    phi, theta, omega = 2 * np.pi * np.random.uniform(size=3)
    qml.Rot(phi, theta, omega, wires=0)
    return qml.state()

num_samples = 2021

not_haar_samples = [not_a_haar_random_unitary() for _ in range(num_samples)]
```

In order to plot these on the Bloch sphere, we'll need to do one more
step, and convert the quantum states into Bloch vectors.

```python
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])

# Used the mixed state simulator so we could have the density matrix for this part!
def convert_to_bloch_vector(rho):
    """Convert a density matrix to a Bloch vector."""
    ax = np.trace(np.dot(rho, X)).real
    ay = np.trace(np.dot(rho, Y)).real
    az = np.trace(np.dot(rho, Z)).real
    return [ax, ay, az]

not_haar_bloch_vectors = np.array([convert_to_bloch_vector(s) for s in not_haar_samples])
```

With this done, let's find out where our "uniformly random" states ended up:

```python
def plot_bloch_sphere(bloch_vectors):
    """ Helper function to plot vectors on a sphere."""
    fig = plt.figure(figsize=(6, 6))
    ax = fig.add_subplot(111, projection='3d')
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1)

    ax.grid(False)
    ax.set_axis_off()
    ax.view_init(30, 45)
    ax.dist = 7

    # Draw the axes (source: https://github.com/matplotlib/matplotlib/issues/13575)
    x, y, z = np.array([[-1.5,0,0], [0,-1.5,0], [0,0,-1.5]])
    u, v, w = np.array([[3,0,0], [0,3,0], [0,0,3]])
    ax.quiver(x, y, z, u, v, w, arrow_length_ratio=0.05, color="black", linewidth=0.5)

    ax.text(0, 0, 1.7, r"|0⟩", color="black", fontsize=16)
    ax.text(0, 0, -1.9, r"|1⟩", color="black", fontsize=16)
    ax.text(1.9, 0, 0, r"|+⟩", color="black", fontsize=16)
    ax.text(-1.7, 0, 0, r"|–⟩", color="black", fontsize=16)
    ax.text(0, 1.7, 0, r"|i+⟩", color="black", fontsize=16)
    ax.text(0,-1.9, 0, r"|i–⟩", color="black", fontsize=16)

    ax.scatter(
        bloch_vectors[:,0], bloch_vectors[:,1], bloch_vectors[:, 2], c='#e29d9e', alpha=0.3
    )

plot_bloch_sphere(not_haar_bloch_vectors)
```

You can see from this plot that even though our parameters were sampled from a
uniform distribution, there is a noticeable amount of clustering around the poles
of the sphere. Despite the input parameters being uniform, the output is very
much *not* uniform. Just like the regular sphere, the measure is larger near
the equator, and if we just sample uniformly, we won't end up populating that
area as much. To take that into account we will need to sample from the proper
Haar measure, and weight the different parameters appropriately.

For a single qubit, the Haar measure looks much like the case of a sphere,
minus the radial component. Intuitively, all qubit state vectors have length
1, so it makes sense that this wouldn't play a role here. The parameter that
we will have to weight differently is $\theta$, and in fact the
adjustment in measure is identical to that we had to do with the polar axis of
the sphere, i.e., $\sin \theta$. In order to sample the $\theta$
uniformly at random in this context, we must sample from the distribution
$\hbox{Pr}(\theta) = \sin \theta$. We can accomplish this by setting up
a custom probability distribution with
`rv_continuous <https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.rv_continuous.html#scipy.stats.rv_continuous>`__
in ``scipy``.

```python
from scipy.stats import rv_continuous

class sin_prob_dist(rv_continuous):
    def _pdf(self, theta):
        # The 0.5 is so that the distribution is normalized
        return 0.5 * np.sin(theta)

# Samples of theta should be drawn from between 0 and pi
sin_sampler = sin_prob_dist(a=0, b=np.pi)

@qml.qnode(dev)
def haar_random_unitary():
    phi, omega = 2 * np.pi * np.random.uniform(size=2) # Sample phi and omega as normal
    theta = sin_sampler.rvs(size=1) # Sample theta from our new distribution
    qml.Rot(phi, theta, omega, wires=0)
    return qml.state()

haar_samples = [haar_random_unitary() for _ in range(num_samples)]
haar_bloch_vectors = np.array([convert_to_bloch_vector(s) for s in haar_samples])

plot_bloch_sphere(haar_bloch_vectors)
```

We see that when we use the correct measure, our qubit states are now
much better distributed over the sphere. Putting this information together,
we can now write the explicit form for the single-qubit Haar measure:

\begin{align}d\mu_2 = \sin \theta d\theta \cdot d\omega \cdot d\phi.\end{align}

Show me more math!
~~~~~~~~~~~~~~~~~~

While we can easily visualize the single-qubit case, this is no longer
possible when we increase the number of qubits. Regardless, we can still
obtain a mathematical expression for the Haar measure in arbitrary
dimensions. In the previous section, we expressed the Haar measure in terms of
a set of parameters that can be used to specify the unitary group
$U(2)$. Such a parametrization is not unique, and in fact there are
multiple ways to *factorize*, or decompose an $N$-dimensional unitary
operation into a set of parameters.

Many of these parametrizations come to us from the study of photonics.  Here,
arbitrary operations are broken down into elementary operations involving only
a few parameters which correspond directly to parameters of the physical
apparatus used to implement them (beamsplitters and phase shifts). Rather than
qubits, such operations act on modes, or *qumodes*. They are expressed as
elements of the $N$-dimensional `special unitary group
<https://en.wikipedia.org/wiki/Special_unitary_group>`__. This group, written
as $SU(N)$, is the continuous group consisting of all $N \times N$
unitary operations with determinant 1 (essentially like $U(N)$, minus
a potential global phase).

Note

Elements of $SU(N)$ and $U(N)$ can still be considered as
    multi-qubit operations in the cases where $N$ is a power of 2, but
    they must be translated from continuous-variable operations into qubit
    operations. (In PennyLane, this can be done by feeding the unitaries to
    the :class:`~.pennylane.QubitUnitary` operation directly. Alternatively,
    one can use *quantum compilation* to express the operations as a sequence
    of elementary gates such as Pauli rotations and CNOTs.)

.. admonition:: Tip

   If you haven't had many opportunities to work in terms of qumodes, the
   `Strawberry Fields documentation
   <https://strawberryfields.ai/photonics/concepts/photonics.html>`__ is a
   good starting point.

For example, we saw already above that for $N=2$, we can write

\begin{align}U(\phi, \theta, \omega) = \begin{pmatrix} e^{-i(\phi + \omega)/2}
                       \cos(\theta/2) & -e^{i(\phi - \omega)/2} \sin(\theta/2)
                       \\ e^{-i(\phi - \omega)/2} \sin(\theta/2) & e^{i(\phi +
                       \omega)/2} \cos(\theta/2) \end{pmatrix}.\end{align}

This unitary can be factorized as follows:

\begin{align}U(\phi, \theta, \omega) =
       \begin{pmatrix}
         e^{-i\omega/2} & 0 \\ 0 & e^{i\omega/2}
       \end{pmatrix}
       \begin{pmatrix}
         \cos(\theta/2) & -\sin(\theta/2) \\ \sin(\theta/2) & \cos(\theta/2)
       \end{pmatrix}
      \begin{pmatrix}
         e^{-i\phi/2} & 0 \\ 0 & e^{i\phi/2}
       \end{pmatrix}\end{align}

The middle operation is a beamsplitter; the other two operations are phase
shifts.  We saw earlier that for $N=2$, $d\mu_2 = \sin\theta
d\theta d\omega d\phi$---note how the parameter in the beamsplitter
contributes to the measure in a different way than those of the phase
shifts. As mentioned above, for larger values of $N$ there are multiple
ways to decompose the unitary. Such decompositions rewrite elements in
$SU(N)$ acting on $N$ modes as a sequence of operations acting
only on 2 modes, $SU(2)$, and single-mode phase shifts.  Shown below are
three examples [#deGuise2018]_, [#Clements2016]_, [#Reck1994]_:

.. figure:: /demonstrations/haar_measure/unitaries.png
   :align: center
   :width: 95%

In these graphics, every wire is a different mode. Every box represents an
operation on one or more modes, and the number in the box indicates the number
of parameters.  The boxes containing a ``1`` are simply phase shifts on
individual modes. The blocks containing a ``3`` are $SU(2)$ transforms
with 3 parameters, such as the $U(\phi, \theta, \omega)$ above. Those
containing a ``2`` are $SU(2)$ transforms on pairs of modes with 2
parameters, similar to the 3-parameter ones but with $\omega = \phi$.

Although the decompositions all produce the same set of operations, their
structure and parametrization may have consequences in practice.  The first [#deGuise2018]_
has a particularly convenient form that leads to a recursive definition
of the Haar measure. The decomposition is formulated recursively such that an
$SU(N)$ operation can be implemented by sandwiching an $SU(2)$
transformation between two $SU(N-1)$ transformations, like so:

|

.. figure:: /demonstrations/haar_measure/sun.svg
   :align: center
   :width: 80%

|

The Haar measure is then constructed recursively as a product of 3
terms. The first term depends on the parameters in the first $SU(N-1)$
transformation; the second depends on the parameters in the lone $SU(2)$
transformation; and the third term depends on the parameters in the other
$SU(N-1)$ transformation.

$SU(2)$ is the "base case" of the recursion---we simply have the Haar measure
as expressed above.

|

.. figure:: /demonstrations/haar_measure/su2_haar.svg
   :align: center
   :width: 25%

|

Moving on up, we can write elements of $SU(3)$ as a sequence of three
$SU(2)$ transformations. The Haar measure $d\mu_3$ then consists
of two copies of $d\mu_2$, with an extra term in between to take into
account the middle transformation.

|

.. figure:: /demonstrations/haar_measure/su3_haar.svg
   :align: center
   :width: 80%

|

For $SU(4)$ and upwards, the form changes slightly, but still follows
the pattern of two copies of $d\mu_{N-1}$ with a term in between.

|

.. figure:: /demonstrations/haar_measure/su4_premerge.svg
   :align: center
   :width: 90%

|

For larger systems, however, the recursive composition allows for some of the
$SU(2)$ transformations on the lower modes to be grouped. We can take
advantage of this and aggregate some of the parameters:

|

.. figure:: /demonstrations/haar_measure/su4_triangle_merge.svg
   :align: center
   :width: 100%

|

This leads to one copy of $d\mu_{N-1}$, which we'll denote as
$d\mu_{N-1}^\prime$, containing only a portion of the full set of terms
(as detailed in [#deGuise2018]_, this is called a *coset measure*).

|

.. figure:: /demonstrations/haar_measure/su4_haar.svg
   :align: center
   :width: 100%

|

Putting everything together, we have that

\begin{align}d\mu_N = d\mu_{N-1}^\prime \times \sin \theta_{N-1}
   \sin^{2(N-2)}\left(\frac{\theta_{N-1}}{2}\right) d\theta_{N-1} d\omega_{N-1} \times d\mu_{N-1}\end{align}

The middle portion depends on the value of $N$, and the parameters
$\theta_{N-1}$ and $\omega_{N-1}$ contained in the $(N-1)$'th
$SU(N)$ transformation. This is thus a convenient, systematic way to
construct the $N$-dimensional Haar measure for the unitary group. As a
final note, even though unitaries can be parametrized in different ways, the
underlying Haar measure is *unique*. This is a consequence of it being an
invariant measure, as will be shown later.

Haar-random matrices from the $QR$ decomposition
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Nice-looking math aside, sometimes you just need to generate a large number of
high-dimensional Haar-random matrices. It would be very cumbersome to sample
and keep track of the distributions of so many parameters; furthermore, the
measure above requires you to parametrize your operations in a fixed way.
There is a much quicker way to perform the sampling by taking a (slightly
modified) `QR decomposition
<https://en.wikipedia.org/wiki/QR_decomposition>`__ of complex-valued
matrices.  This algorithm is detailed in [#Mezzadri2006]_, and consists of the
following steps:

1. Generate an $N \times N$ matrix $Z$ with complex numbers $a+bi$
   where both $a$ and $b$ are normally distributed with mean 0 and variance 1
   (this is sampling from the distribution known as the *Ginibre ensemble*).
2. Compute a QR decomposition $Z = QR$.
3. Compute the diagonal matrix $\Lambda = \hbox{diag}(R_{ii}/|R_{ii}|)$.
4. Compute $Q^\prime = Q \Lambda$, which will be Haar-random.

```python
from numpy.linalg import qr

def qr_haar(N):
    """Generate a Haar-random matrix using the QR decomposition."""
    # Step 1
    A, B = np.random.normal(size=(N, N)), np.random.normal(size=(N, N))
    Z = A + 1j * B

    # Step 2
    Q, R = qr(Z)

    # Step 3
    Lambda = np.diag([R[i, i] / np.abs(R[i, i]) for i in range(N)])

    # Step 4
    return np.dot(Q, Lambda)
```

Let's check that this method actually generates Haar-random unitaries
by trying it out for $N=2$ and plotting on the Bloch sphere.

```python
@qml.qnode(dev)
def qr_haar_random_unitary():
    qml.QubitUnitary(qr_haar(2), wires=0)
    return qml.state()

qr_haar_samples = [qr_haar_random_unitary() for _ in range(num_samples)]
qr_haar_bloch_vectors = np.array([convert_to_bloch_vector(s) for s in qr_haar_samples])
plot_bloch_sphere(qr_haar_bloch_vectors)
```

As expected, we find our qubit states are distributed uniformly over the
sphere.  This particular method is what's implemented in packages like
``scipy``'s `unitary_group
<https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.unitary_group.html>`__
function.

Now, it's clear that this method works, but it is also important to
understand *why* it works.  Step 1 is fairly straightforward---the base of our
samples is a matrix full of complex values chosen from a typical
distribution. This isn't enough by itself, since unitary matrices also
have constraints---their rows and columns must be orthonormal.
These constraints are where step 2 comes in---the outcome of a generic
QR decomposition consists of an *orthonormal* matrix $Q$, and and upper
triangular matrix $R$. Since our original matrix was complex-valued, we end
up with a $Q$ that is in fact already unitary. But why not stop there? Why
do we then perform steps 3 and 4?

Steps 3 and 4 are needed because, while the QR decomposition yields a unitary,
it is not a unitary that is properly Haar-random. In [#Mezzadri2006]_, it is
explained that a uniform distribution over unitary matrices should also yield
a uniform distribution over the *eigenvalues* of those matrices, i.e., every
eigenvalue should be equally likely. Just using the QR decomposition out of
the box produces an *uneven* distribution of eigenvalues of the unitaries!
This discrepancy stems from the fact that the QR decomposition is not unique.
We can take any unitary diagonal matrix $\Lambda$, and re-express the decomposition
as $QR = Q\Lambda \Lambda^\dagger R = Q^\prime R^\prime$. Step 3 removes this
redundancy by fixing a $\Lambda$ that depends on $R$, leading to a unique
value of $Q^\prime = Q \Lambda$, and a uniform distribution of eigenvalues.

.. admonition:: Try it!

   Use the ``qr_haar`` function above to generate random unitaries and construct
   a distribution of their eigenvalues. Then, comment out the lines for steps 3 and
   4 and do the same---you'll find that the distribution is no longer uniform.
   Check out reference [#Mezzadri2006]_ for additional details and examples.

Fun (and not-so-fun) facts
--------------------------

We've now learned what the Haar measure is, and both an analytical and
numerical means of sampling quantum states and unitary operations uniformly at
random. The Haar measure also has many neat properties that play a role in
quantum computing.

Invariance of the Haar measure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Earlier, we showed that the Haar measure is used when integrating functions over
the unitary group:

\begin{align}\int_{V \in U(N)} f(V) d\mu_N(V).\end{align}

One of the defining features of the Haar measure is that it is both left and
right *invariant* under unitary transformations. That is,

\begin{align}\int_{V \in U(N)} f(\color{red}{W}V) d\mu_N(V) =  \int_{V \in U(N)} f(V\color{red}{W}) d\mu_N(V) =  \int_{V \in U(N)} f(V) d\mu_N(V).\end{align}

This holds true for *any* other $N\times N$ unitary $W$! A
consequence of such invariance is that if $V$ is Haar-random, then so is
$V^T,$ $V^\dagger,$ and any product of another unitary matrix and
$V$ (where the product may be taken on either side).

Another consequence of this invariance has to do with the structure of the entries
themselves: they must all come from the same distribution. This is because the
measure remains invariant under permutations, since permutations are unitary---
the whole thing still has to be Haar random no matter how the entries are ordered,
so all distributions must be the same.  The specific distribution is complex
numbers $a+bi$ where both $a$ and $b$ has mean 0 and variance
$1/N$ [#Meckes2014]_ (so, much like Ginibre ensemble we used in the QR decomposition
above, but with a different variance and constraints due to orthonormality).

Concentration of measure
~~~~~~~~~~~~~~~~~~~~~~~~

An unfortunate (although interesting) property of the Haar measure is that it
suffers from the phenomenon of `concentration of measure
<https://en.wikipedia.org/wiki/Concentration_of_measure>`__. Most of the
"stuff" in the space concentrates around a certain area, and this gets worse
as the size of the system increases. You can see the beginnings of by looking
at the sphere. For the 3-dimensional sphere, we saw graphically how there is
concentration around the equator, and how the measure takes that into account
with the additional factor of $\sin \theta$. This property becomes
increasingly prominent for `higher-dimensional spheres
<https://en.wikipedia.org/wiki/N-sphere>`__.

.. important::

   The concentration described here is not referring to what we witnessed
   earlier on, when we sampled quantum states (points on the Bloch sphere)
   incorrectly and found that they clustered around the poles. However, that
   is not unrelated. Concentration of measure refers to where the measure
   itself is concentrated, and which parts of the space should be more heavily
   weighted. For the case of the sphere, it is the equatorial area, and when
   we didn't sample properly and take that concentration into account, we
   obtained an uneven distribution.

Let's consider an $N$-dimensional unit sphere. Points on the sphere, or
vectors in this space, are parametrized by $N-1$ real coordinates.
Suppose we have some function $f$ that maps points on that sphere to
real numbers. Sample a point $x$ on that sphere from the uniform
measure, and compute the value of $f(x)$. How close do you think the
result will be to the mean value of the function, $E[f]$, over the
entire sphere?

A result called `Levy's lemma
<https://en.wikipedia.org/wiki/Concentration_of_measure#Concentration_on_the_sphere>`__
[#Gerken2013]_, [#Hayden2006]_ expresses how likely it is that $f(x)$ is a specific
distance away from the mean. It states that, for an $x$ selected
uniformly at random, the probability that $f(x)$ deviates from
$E[f]$ by some amount $\epsilon$ is bounded by:

\begin{align}\hbox{Pr}(|f(x) - E[f]| \ge \epsilon) \leq 2 \exp\left[-\frac{N\epsilon^2}{9\pi^3 \eta^2}\right].\end{align}

A constraint on the function $f$ is that it must be `Lipschitz
continuous <https://en.wikipedia.org/wiki/Lipschitz_continuity>`__, where
$\eta$ is the *Lipschitz constant* of the function. The important aspect
here is the likelihood of deviating significantly from the mean by an amount
$\epsilon$ decreases exponentially with $\epsilon.$ Furthermore,
increasing the dimension $N$ also makes the deviation exponentially less
likely.

Now, this result seems unrelated to quantum states---it concerns higher-
dimensional spheres. However, recall that a quantum state vector is a complex
vector whose squared values sum to 1, similar to vectors on a sphere. If you
"unroll" a quantum state vector of dimension $N = 2^n$ by stacking its
real and complex parts, you end with a vector of length $2 \cdot 2^{n}$
which ends up behaving just like a unit vector on the sphere in this
dimension. Given that measure concentrates on spheres, and quantum state
vectors can be converted to vectors on spheres, functions on random quantum
states will also demonstrate concentration.

This is bad news! To do useful things in quantum computing, we need a lot of
qubits. But the more qubits we have, the more our randomly sampled states will
look the same (specifically, random states will concentrate around the
maximally entangled state [#Hayden2006]_). This has important consequences for
near-term algorithms (as detailed in the next section), and any algorithm that
involves uniform sampling of quantum states and operations.

Haar measure and barren plateaus
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Suppose you are venturing out to solve a new problem using an algorithm such
as the :doc:`variational quantum eigensolver </demos/tutorial_vqe>`. A
critical component of such methods is the choice of :doc:`variational ansatz
</glossary/circuit_ansatz>`. Having now learned a bit about the properties of
the Haar measure, you may think it would make sense to use this for the
parametrization. Variational ansaetze are, after all, parametrized quantum
circuits, so why not choose an ansatz that corresponds directly to a
parametrization for Haar-random unitaries?  The initial parameter selection
will give you a state in the Hilbert space uniformly at random. Then, since
this ansatz spans the entire Hilbert space, you're guaranteed to be able to
represent the target ground state with your ansatz, and it should be able to
find it with no issue ... right?

Unfortunately, while such an ansatz is extremely *expressive* (i.e., it is
capable of representing any possible state), these ansaetze actually suffer
the most from the barren plateau problem [#McClean2018]_, [#Holmes2021]_.
:doc:`Barren plateaus </demos/tutorial_barren_plateaus>` are regions in the
cost landscape of a parametrized circuit where both the gradient and its
variance approach 0, leading the optimizer to get stuck in a local minimum.
This was explored recently in the work of [#Holmes2021]_, wherein closeness to
the Haar measure was actually used as a metric for expressivity. The closer
things are to the Haar measure, the more expressive they are, but they are
also more prone to exhibiting barren plateaus.

.. figure:: /demonstrations/haar_measure/holmes-costlandscapes.png
   :align: center
   :width: 50%

   Image source: [#Holmes2021]_. A highly expressive ansatz that can access
   much of the space of possible unitaries (i.e., an ansatz capable of
   producing unitaries in something close to a Haar-random manner) is very
   likely to have flat cost landscapes and suffer from the barren plateau
   problem.

It turns out that the types of ansaetze know as *hardware-efficient ansaetze*
also suffer from this problem if they are "random enough" (this notion will be
formalized in a future demo). It was shown in [#McClean2018]_ that this is a
consequence of the concentration of measure phenomenon described above. The
values of gradients and variances can be computed for classes of circuits on
average by integrating with respect to the Haar measure, and it is shown that
these values decrease exponentially in the number of qubits, and thus huge
swaths of the cost landscape are simply and fundamentally flat.

Conclusion
----------

The Haar measure plays an important role in quantum computing---anywhere
you might be dealing with sampling random circuits, or averaging over
all possible unitary operations, you'll want to do so with respect
to the Haar measure.

There are two important aspects of this that we have yet to touch upon,
however. The first is whether it is efficient to sample from the Haar measure---given
that the number of parameters to keep track of is exponential in the
number of qubits, certainly not. But a more interesting question is do we
*need* to always sample from the full Haar measure?  The answer to this is
"no" in a very interesting way. Depending on the task at hand, you may be able
to take a shortcut using something called a *unitary design*. In an upcoming
demo, we will explore the amazing world of unitary designs and their
applications!

References
----------

.. [#NandC2000]

    M. A. Nielsen, and I. L. Chuang (2000) "Quantum Computation and Quantum Information",
    Cambridge University Press.

.. [#deGuise2018]

    H. de Guise, O. Di Matteo, and L. L. Sánchez-Soto. (2018) "Simple factorization
    of unitary transformations", `Phys. Rev. A 97 022328
    <https://journals.aps.org/pra/abstract/10.1103/PhysRevA.97.022328>`__.
    (`arXiv <https://arxiv.org/abs/1708.00735>`__)

.. [#Clements2016]

    W. R. Clements, P. C. Humphreys, B. J. Metcalf, W. S. Kolthammer, and
    I. A. Walmsley (2016) “Optimal design for universal multiport
    interferometers”, \ `Optica 3, 1460–1465
    <https://www.osapublishing.org/optica/fulltext.cfm?uri=optica-3-12-1460&id=355743>`__.
    (`arXiv <https://arxiv.org/abs/1603.08788>`__)

.. [#Reck1994]

   M. Reck, A. Zeilinger, H. J. Bernstein, and P. Bertani (1994) “Experimental
   realization of any discrete unitary operator”, `Phys. Rev. Lett.73, 58–61
   <https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.73.58>`__.

.. [#Mezzadri2006]

    F. Mezzadri (2006) "How to generate random matrices from the classical compact groups".
    (`arXiv <https://arxiv.org/abs/math-ph/0609050>`__)

.. [#Meckes2014]

    E. Meckes (2019) `"The Random Matrix Theory of the Classical Compact Groups"
    <https://case.edu/artsci/math/esmeckes/Haar_book.pdf>`_, Cambridge University Press.

.. [#Gerken2013]

    M. Gerken (2013) "Measure concentration: Levy's Lemma"
    (`lecture notes <http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.679.2560>`__).

.. [#Hayden2006]

    P. Hayden, D. W. Leung, and A. Winter (2006) "Aspects of generic
    entanglement", `Comm. Math. Phys. Vol. 265, No. 1, pp. 95-117
    <https://link.springer.com/article/10.1007%2Fs00220-006-1535-6>`__.
    (`arXiv <https://arxiv.org/abs/quant-ph/0407049>`__)

.. [#McClean2018]

    J. R. McClean, S. Boixo, V. N. Smelyanskiy, R. Babbush, and H. Neven
    (2018) "Barren plateaus in quantum neural network training
    landscapes", `Nature Communications, 9(1)
    <http://dx.doi.org/10.1038/s41467-018-07090-4>`__.
    (`arXiv <https://arxiv.org/abs/1803.11173>`__)

.. [#Holmes2021]

    Z. Holmes, K. Sharma, M. Cerezo, and P. J. Coles (2021) "Connecting ansatz
    expressibility to gradient magnitudes and barren plateaus". (`arXiv
    <https://arxiv.org/abs/2101.02138>`__)