# Tutorial 2: Matrices
**Week 0, Day 3: Linear Algebra**

**By Neuromatch Academy**

__Content creators:__ Ella Batty

__Content reviewers:__ Keith van Antwerp, Aderogba Bayo, Anoop Kulkarni, Pooya Pakarian

__Production editors:__ Siddharth Suresh, Ella Batty

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

---
# Tutorial Objectives

*Estimated timing of tutorial: 1 hour, 35 minutes*

During today, we will learn the basics of linear algebra, focusing on the topics that underlie the material on future days in the NMA Computational Neuroscience course. In this tutorial, we focus on matrices: their definition, their properties & operations, and especially on a geometrical intuition of them.

By the end of this tutorial, you will be able to :
* Explain matrices as a linear transformation and relate matrix properties to properties of that linear transformation
*  Perform matrix multiplication by hand
*  Define what eigenvalues/eigenvectors are

**Code Credit:**

Some elements of this problem set are from or inspired by https://openedx.seas.gwu.edu/courses/course-v1:GW+EngComp4+2019/about. In particular, we are using their `plot_linear_transformation` and `plot_linear_transformations` functions.

 Code under BSD 3-Clause License © 2019 Lorena A. Barba, Tingyu Wang. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

---
# Setup

```python
# Imports

import numpy as np
import matplotlib.pyplot as plt
```

```python
#@title Figure settings
import ipywidgets as widgets# interactive display
from ipywidgets import fixed
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @title Plotting functions
import numpy
from numpy.linalg import inv, eig
from math import ceil
from matplotlib import pyplot, ticker, get_backend, rc
from mpl_toolkits.mplot3d import Axes3D
from itertools import cycle

_int_backends = ['GTK3Agg', 'GTK3Cairo', 'MacOSX', 'nbAgg',
                 'Qt4Agg', 'Qt4Cairo', 'Qt5Agg', 'Qt5Cairo',
                 'TkAgg', 'TkCairo', 'WebAgg', 'WX', 'WXAgg', 'WXCairo']
_backend = get_backend()   # get current backend name

# shrink figsize and fontsize when using %matplotlib notebook
if _backend in _int_backends:
  fontsize = 4
  fig_scale = 0.75
else:
  fontsize = 5
  fig_scale = 1

grey = '#808080'
gold = '#cab18c'   # x-axis grid
lightblue = '#0096d6'  # y-axis grid
green = '#008367'  # x-axis basis vector
red = '#E31937'    # y-axis basis vector
darkblue = '#004065'

pink, yellow, orange, purple, brown = '#ef7b9d', '#fbd349', '#ffa500', '#a35cff', '#731d1d'

quiver_params = {'angles': 'xy',
                 'scale_units': 'xy',
                 'scale': 1,
                 'width': 0.012}

grid_params = {'linewidth': 0.5,
               'alpha': 0.8}

def set_rc(func):
  def wrapper(*args, **kwargs):
    rc('font', family='serif', size=fontsize)
    rc('figure', dpi=200)
    rc('axes', axisbelow=True, titlesize=5)
    rc('lines', linewidth=1)
    func(*args, **kwargs)
  return wrapper

@set_rc
def plot_vector(vectors, tails=None):
  ''' Draw 2d vectors based on the values of the vectors and the position of their tails.

  Parameters
  ----------
  vectors : list.
    List of 2-element array-like structures, each represents a 2d vector.

  tails : list, optional.
    List of 2-element array-like structures, each represents the coordinates of the tail
    of the corresponding vector in vectors. If None (default), all tails are set at the
    origin (0,0). If len(tails) is 1, all tails are set at the same position. Otherwise,
    vectors and tails must have the same length.

  Examples
  --------
  >>> v = [(1, 3), (3, 3), (4, 6)]
  >>> plot_vector(v)      # draw 3 vectors with their tails at origin
  >>> t = [numpy.array((2, 2))]
  >>> plot_vector(v, t)   # draw 3 vectors with their tails at (2,2)
  >>> t = [[3, 2], [-1, -2], [3, 5]]
  >>> plot_vector(v, t)   # draw 3 vectors with 3 different tails

  '''
  vectors = numpy.array(vectors)
  assert vectors.shape[1] == 2, "Each vector should have 2 elements."
  if tails is not None:
    tails = numpy.array(tails)
    assert tails.shape[1] == 2, "Each tail should have 2 elements."
  else:
    tails = numpy.zeros_like(vectors)

  # tile vectors or tails array if needed
  nvectors = vectors.shape[0]
  ntails = tails.shape[0]
  if nvectors == 1 and ntails > 1:
    vectors = numpy.tile(vectors, (ntails, 1))
  elif ntails == 1 and nvectors > 1:
    tails = numpy.tile(tails, (nvectors, 1))
  else:
    assert tails.shape == vectors.shape, "vectors and tail must have a same shape"

  # calculate xlimit & ylimit
  heads = tails + vectors
  limit = numpy.max(numpy.abs(numpy.hstack((tails, heads))))
  limit = numpy.ceil(limit * 1.2)   # add some margins

  figsize = numpy.array([2,2]) * fig_scale
  figure, axis = pyplot.subplots(figsize=figsize)
  axis.quiver(tails[:,0], tails[:,1], vectors[:,0], vectors[:,1], color=darkblue,
                  angles='xy', scale_units='xy', scale=1)
  axis.set_xlim([-limit, limit])
  axis.set_ylim([-limit, limit])
  axis.set_aspect('equal')

  # if xticks and yticks of grid do not match, choose the finer one
  xticks = axis.get_xticks()
  yticks = axis.get_yticks()
  dx = xticks[1] - xticks[0]
  dy = yticks[1] - yticks[0]
  base = max(int(min(dx, dy)), 1)   # grid interval is always an integer
  loc = ticker.MultipleLocator(base=base)
  axis.xaxis.set_major_locator(loc)
  axis.yaxis.set_major_locator(loc)
  axis.grid(True, **grid_params)

  # show x-y axis in the center, hide frames
  axis.spines['left'].set_position('center')
  axis.spines['bottom'].set_position('center')
  axis.spines['right'].set_color('none')
  axis.spines['top'].set_color('none')

@set_rc
def plot_transformation_helper(axis, matrix, *vectors, unit_vector=True, unit_circle=False, title=None):
  """ A helper function to plot the linear transformation defined by a 2x2 matrix.

  Parameters
  ----------
  axis : class matplotlib.axes.Axes.
    The axes to plot on.

  matrix : class numpy.ndarray.
    The 2x2 matrix to visualize.

  *vectors : class numpy.ndarray.
    The vector(s) to plot along with the linear transformation. Each array denotes a vector's
    coordinates before the transformation and must have a shape of (2,). Accept any number of vectors.

  unit_vector : bool, optional.
    Whether to plot unit vectors of the standard basis, default to True.

  unit_circle: bool, optional.
    Whether to plot unit circle, default to False.

  title: str, optional.
    Title of the plot.

  """
  assert matrix.shape == (2,2), "the input matrix must have a shape of (2,2)"
  grid_range = 20
  x = numpy.arange(-grid_range, grid_range+1)
  X_, Y_ = numpy.meshgrid(x,x)
  I = matrix[:,0]
  J = matrix[:,1]
  X = I[0]*X_ + J[0]*Y_
  Y = I[1]*X_ + J[1]*Y_
  origin = numpy.zeros(1)

  # draw grid lines
  for i in range(x.size):
    axis.plot(X[i,:], Y[i,:], c=gold, **grid_params)
    axis.plot(X[:,i], Y[:,i], c=lightblue, **grid_params)

  # draw (transformed) unit vectors
  if unit_vector:
    axis.quiver(origin, origin, [I[0]], [I[1]], color=green, **quiver_params)
    axis.quiver(origin, origin, [J[0]], [J[1]], color=red, **quiver_params)

  # draw optional vectors
  color_cycle = cycle([pink, darkblue, orange, purple, brown])
  if vectors:
    for vector in vectors:
      color = next(color_cycle)
      vector_ = matrix @ vector.reshape(-1,1)
      axis.quiver(origin, origin, [vector_[0]], [vector_[1]], color=color, **quiver_params)

  # draw optional unit circle
  if unit_circle:
    alpha =  numpy.linspace(0, 2*numpy.pi, 41)
    circle = numpy.vstack((numpy.cos(alpha), numpy.sin(alpha)))
    circle_trans = matrix @ circle
    axis.plot(circle_trans[0], circle_trans[1], color=red, lw=0.8)

  # hide frames, set xlimit & ylimit, set title
  limit = 4
  axis.spines['left'].set_position('center')
  axis.spines['bottom'].set_position('center')
  axis.spines['left'].set_linewidth(0.3)
  axis.spines['bottom'].set_linewidth(0.3)
  axis.spines['right'].set_color('none')
  axis.spines['top'].set_color('none')
  axis.set_xlim([-limit, limit])
  axis.set_ylim([-limit, limit])
  if title is not None:
    axis.set_title(title)

@set_rc
def plot_linear_transformation(matrix, *vectors, name = None, unit_vector=True, unit_circle=False):
  """ Plot the linear transformation defined by a 2x2 matrix using the helper
  function plot_transformation_helper(). It will create 2 subplots to visualize some
  vectors before and after the transformation.

  Parameters
  ----------
  matrix : class numpy.ndarray.
    The 2x2 matrix to visualize.

  *vectors : class numpy.ndarray.
    The vector(s) to plot along with the linear transformation. Each array denotes a vector's
    coordinates before the transformation and must have a shape of (2,). Accept any number of vectors.

  unit_vector : bool, optional.
    Whether to plot unit vectors of the standard basis, default to True.

  unit_circle: bool, optional.
    Whether to plot unit circle, default to False.

  """
  figsize = numpy.array([4,2]) * fig_scale
  figure, (axis1, axis2) = pyplot.subplots(1, 2, figsize=figsize)
  plot_transformation_helper(axis1, numpy.identity(2), *vectors, unit_vector=unit_vector, unit_circle=unit_circle, title='Before transformation')
  plot_transformation_helper(axis2, matrix, *vectors, unit_vector=unit_vector, unit_circle=unit_circle, title='After transformation')
  if name is not None:
    figure.suptitle(f'Population {name}')

def plot_eig_vec_transform(W):
  classic = 'k'
  vec_names = ['a', 'b','c','d','e','f','g', 'h']

  _, vecs = np.linalg.eig(W)
  vecs = vecs.T

  fig, axes = plt.subplots(1, 2, figsize=(2, 1))
  colors = plt.rcParams['axes.prop_cycle'].by_key()['color']

  for i in range(2):
    axes[i].set(xlim=[-3.5, 3.5], ylim=[-3.5,3.5])
    axes[i].axis('Off')
    axes[i].plot([0, 0], [-3.5, 3.5], classic, alpha=.4)
    axes[i].plot([-3.5, 3.5], [0, 0], classic, alpha=.4)

  for i_vec, vec in enumerate(vecs):
    axes[0].arrow(0, 0, vec[0], vec[1], head_width=.2, facecolor=colors[i_vec], edgecolor=colors[i_vec], length_includes_head=True)
    axes[0].annotate(vec_names[i_vec], xy=(vec[0]+np.sign(vec[0])*.15, vec[1]+np.sign(vec[1])*.15), color=colors[i_vec])

    transformed_vec = np.matmul(W, vec)
    axes[1].arrow(0, 0, transformed_vec[0], transformed_vec[1], head_width=.2, facecolor=colors[i_vec], edgecolor=colors[i_vec], length_includes_head=True)
    axes[1].annotate(vec_names[i_vec], xy=(transformed_vec[0]+np.sign(transformed_vec[0])*.15, transformed_vec[1]+np.sign(transformed_vec[1])*.15), color=colors[i_vec])

  axes[0].set_title('Before')
  axes[1].set_title('After')
```

---

# Section 1: Intro to matrices

## Section 1.1: Matrices to solve systems of equations

```python
# @title Video 1: Systems of Equations
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1Aq4y1x7hf", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="3ecnOrMEh00", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

This video covers using a matrix to solve a linear system of equations.

 Click here for text recap of video

In a variety of contexts, we may encounter systems of linear equations like this one:

$$
\begin{align}
3x_1 + 2x_2 + x_3 &= y_1 \\
7x_1 + x_2 + 2x_3 &= y_2 \\
x_1 - x_2  - 2x_3 &= y_3 \\
\end{align}
$$
We may know all the x's and want to solve for y's, or we may know the y's and want to solve for the x's. We can solve this in several different ways but one especially appealing way is to cast it as a matrix-vector equation:

$$
\mathbf{W}\mathbf{x} = \mathbf{y}
$$
where
$$
\begin{align}
\mathbf{W} &= \begin{bmatrix} 3 & 2 & 1 \\ 7 & 1 & 2 \\ 1 &-1 &-2 \end{bmatrix}, \mathbf{x} = \begin{bmatrix} x_1 \\ x_2 \\ x_3 \end{bmatrix}, \mathbf{y} = \begin{bmatrix} y_1 \\ y_2 \\ y_3 \end{bmatrix}\\
\end{align}
$$

If we know $\mathbf{W}$ and $\mathbf{x}$, we can solve for $\mathbf{y}$ using matrix-vector multiplication. Each row of $\mathbf{y}$ is computed as the dot product of the equivalent row of $\mathbf{W}$ and $\mathbf{x}$.

If we know $\mathbf{W}$ and $\mathbf{y}$, we can sometimes solve for $\mathbf{x}$ by using the inverse of $\mathbf{W}$:
$$ \mathbf{x} = W^{-1}\mathbf{y} $$.
The reason this only sometimes works will be dived into later in this tutorial!

### Coding Exercise 1.1: Understanding neural transformations

We will look at a group of 2 LGN neurons which get input from 2 retinal neurons: we will call the population of LGN neurons population p. Below, we have the system of linear equations that dictates the neuron models for each population. $r_1$ and $r_2$ correspond to the retinal neural activities (of neuron 1 and 2). $g_{p_1}$ and  $g_{p_2}$ correspond to the responses of the LGN neurons 1 and 2 in population p.

$$
\begin{align}
r_1 + 3r_2 &= g_{p_1} \\
2r_1 + r_2 &= g_{p_2} \\
\end{align}
$$

1) Cast each this as a matrix-vector multiplication:

$$ \mathbf{g}_p = \mathbf{P}\mathbf{r} $$
where P is the weight matrix to population p.

2) Let's say we only recorded from the LGN cells (and know the weight matrix) and are trying to figure out how the retinal cells responded. Solve the matrix equation for the given LGN activities:

$$
\mathbf{g}_p = \begin{bmatrix}
16 \\
7 \\
\end{bmatrix}
$$

```python
# Create P (using np array)
P = np.array([[1, 3], [2, 1]])

# Create g_p (using np array)
g_p = np.array([16, 7])

# Solve for r (using np.linalg.inv)
r = np.linalg.inv(P) @ g_p

# Print r
print(r)
```

    [1. 5.]

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_2fa7cf85.py)

You should see the output [1, 5]

You can recover how the retinal neurons respond given the weight matrix and LGN responses! You have solved the system of equations using matrices. We can't always do this though: let's say we have a different group of 2 LGN neurons -  population q - with the following weight matrix from the retinal neurons.

$$
Q = \begin{bmatrix}
4 & 1 \\
8 & 2 \\
\end{bmatrix},
$$

As you can see if you uncomment and run the next code cell, we get an error if we try to invert this matrix to solve the equation. We'll find out more about this in the next sections.

```python
g_q = np.array([16, 7])
Q = np.array([[4, 1], [8, 2]])

print(np.linalg.inv(Q) @ g_q)
```

## Section 1.2: Matrices as linear transformations

*Estimated timing to here from start of tutorial: 20 min*

```python
# @title Video 2: Linear Transformations
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1KB4y1T7zM", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="N6UUV9tVIr8", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

Matrices can be thought of as enacting linear transformations. When multiplied with a vector, they transform it into another vector. In fact, they are transforming a grid of space in a linear manner: the origin stays in place and grid lines remain straight, parallel, and evenly spaced.

### Coding Exercise 1.2: Creating matrices for transformations

Come up with a matrix $A$ for which the corresponding linear transformation is reflection through the $y$ axis (flipping across the $y$ axis). For example, $\mathbf{x} = \begin{bmatrix}
2 \\
6  \\
\end{bmatrix}$ should become $\mathbf{b} = \begin{bmatrix}
-2 \\
6  \\
\end{bmatrix}$ when multiplied with $A$.

**Remember to think about where your basis vectors should end up! Then your matrix consists of the transformed basis vectors. Drawing out what you want to happen can help**

```python
A = np.array([[-1, 0], [0, 1]])

# Uncomment to visualize transformation
plot_linear_transformation(A)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_a86560cc.py)

*Example output:*

Come up with a matrix $A$ for which the corresponding linear transformation is projecting onto the $x$ axis. For example, $\bar{x} = \begin{bmatrix}
2 \\
3  \\
\end{bmatrix}$ should become $\bar{b} = \begin{bmatrix}
2 \\
0  \\
\end{bmatrix}$ when multiplied with $A$.

```python
A = np.array([[1, 0], [0, 0]])

# Uncomment to visualize transformation
plot_linear_transformation(A)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_8d909295.py)

*Example output:*

## Section 1.3: Rank & Null Space

*Estimated timing to here from start of tutorial: 35 min*

```python
# @title Video 3: Rank & Null Space
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1vw411R7eA", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="ay2p5rgkMcY", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

This video covers properties of matrices, such as range, rank, and null space, in the context of matrices enacting a linear transformation.

 Click here for text recap of video

Square matrices always result in vectors with the same dimensions (number of components) but can alter the dimensionality of the transformed space. Let's say you have a 3 x 3 matrix. You will transform from 3-dimensional vectors to 3-dimensional vectors. You will often be transforming from all of 3D space to all of 3D space. However, this isn't always the case! You could have a 3 x 3 matrix that always results in a vector that lies along a 2D plane through 3D space. This matrix would be transforming from a 3 dimensional vector space (all of R3) to a 2 dimensional vector space (the 2D plane).

Matrices that aren't square are enacting transformations that change the dimensionality of the vectors. If you have a 4 x 5 matrix, you are transforming 5-dimensional vectors to 4-dimensional vectors. Similarly, if you have a 4 x 2 matrix, you are transforming from 4-dimensional vectors to 2-dimensional vectors.

The **range of a matrix** is the set of all possible vectors it can lead to after a transformation. The **rank of a matrix** is the dimensionality of the range.

Sometimes, a matrix will transform a non-zero vector into a zero vector (the origin). The **null space** of a matrix is the set of all vectors that will be transformed into the origin.

### Think! 1.3: Neural coding

Let's return to the setup of the previous coding exercise: we have two populations of LGN neurons, p and q, responding to retinal neurons. Visualize the linear transformations of these matrices by running the next code cell. Then discuss the following questions:

1) What are the ranks of weight matrix P and Q?

2) What does the null space of these matrices correspond to in our neuroscience setting? **Advanced:** What do you think the dimensionality of the null space is for P and Q?

3) What is the intrinsic dimensionality of the population of neural responses in population p? How about in q? The intrinsic dimensionality is the minimal number of dimensions required for a complete representation of the data.

4) If we wanted to decode retinal neural activity from the LGN activities, would we always be able to completely recover the retinal activity when looking at population p? How about population q? What does this tell us about the information loss of the neural processing?

```python
# @markdown Execute to visualize linear transformations
P = np.array([[1, 3], [2, 1]])
plot_linear_transformation(P, name = 'p')

Q = np.array([[4, 1], [8, 2]])
plot_linear_transformation(Q, name = 'q')
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_95edac8e.py)

---
# Section 2: Eigenvalues & Eigenvectors

*Estimated timing to here from start of tutorial: 65 min*

```python
# @title Video 4: Eigenstuff
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1KK4y1M7Ez", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="l-c7ptT7znM", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

This video covers eigenvalues and eigenvectors.

Eigenvectors, $\mathbf{v}$ of a matrix $\mathbf{W}$ are vectors that, when multipled by the matrix, equal a scalar multiple of themselves. That scalar multiple is the corresponding eigenvalue, $\lambda$.

$$\mathbf{W}\mathbf{v} = \lambda\mathbf{v} $$

If we have one eigenvector for a matrix, we technically have an infinite amount: every vector along the span of that eigenvector is also an eigenvector. So, we often use the unit vector in that direction to summarize all the eigenvectors along that line.

We can find the eigenvalues and eigenvectors of a matrix in numpy using `np.linalg.eig`.

## Think! 2: Identifying transformations from eigenvectors

Earlier, we learned how to think about linear transformations in terms of where the standard basis vectors end up. We can also think about them in terms of eigenvectors.

Just by looking at eigenvectors before and after a transformation, can you describe what the transformation is in words? Try for each of the two plots below.

Note that I show an eigenvector for every eigenvalue. The x/y limits do not change in before vs after (so eigenvectors are showed scaled by the eigenvalues).

Here are some transformation words to jog your memory and guide discussion: contraction, expansion, horizontal vs vertical, projection onto an axis, reflection, and rotation.

```python
# @title
# @markdown Execute this cell to visualize vectors

W = np.array([[3, 0], [0, 1]])
plot_eig_vec_transform(W)
```

```python
# @title
# @markdown Execute this cell to visualize vectors

W = np.array([[0, 1], [1, 0]])
plot_eig_vec_transform(W)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_80704181.py)

As we saw above, looking at how just the eigenvectors change after a transformation can be very informative about what that transformation was.

---
# Section 3: Matrix multiplication

*Estimated timing to here from start of tutorial: 80 min*

```python
# @title Video 5: Matrix Multiplication
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1Rb4y1C7cE", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="OFyWfegC9Cs", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

We sometimes want to multiple two matrices together, instead of a matrix with a vector. Let's say we're multiplying matrices $\mathbf{A}$ and $\mathbf{B}$ to get $\mathbf{C}$:
$$ \mathbf{C} = \mathbf{A}\mathbf{B}$$.

We take the dot product of each row of A with each column of B. The resulting scalar is placed in the element of $\mathbf{C}$ that is the same row (as the row in A) and column (as the column in B). So the element of $\mathbf{C}$ at row 4 and column 2 is the dot product of the 4th row of $\mathbf{A}$ and the 2nd column of $\mathbf{B}$. We can write this in a formula as:

$$\mathbf{C}_{\text{row i, column j}} = \mathbf{A}_{\text{row i}} \cdot \mathbf{B}_{\text{column j}}$$

## Exercise 2: Computation corner

Break out the pen and paper - it's critical to implement matrix multiplication yourself to fully understand how it works.

Let's say we have 3 retina neurons and 2 LGN neurons. The weight matrix, $W$, between the retina and LGN neurons is:

$$
W = \begin{bmatrix}
3 &  2 & 1 \\
1 & 2 & 7 \\
\end{bmatrix},
$$

We are going to look at the activity at two time steps (each time step is a column). Our retina activity matrix, $R$, is:

$$
R= \begin{bmatrix}
0 &  1  \\
2 & 4 \\
5 & 1 \\
\end{bmatrix}
$$

Please compute the LGN neural activity, $G$, according to our linear model:

$$ G = WR $$.

Please calculate it 1) by-hand and then 2) using code. Check that the answers match!

```python
# Compute by hand first!
```

```python
# Define R
R = np.array([[3,2,1], [1,2,7]])

# Define W
W = np.array([[0, 1], [2, 4], [5, 1]])

# Compute G
# in Python, we can use @ for matrix multiplication: matrix1 @ matrix2
G = W @ R

# Print values of G
print(G)
```

    [[ 1  2  7]
     [10 12 30]
     [16 12 12]]

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W0D3_LinearAlgebra/solutions/W0D3_Tutorial2_Solution_4b71ea28.py)

---
# Summary

*Estimated timing of tutorial: 1 hour, 35 minutes*

In this tutorial, you have learned how to think about matrices from the perspective of solving a system of equations and as a linear transformation of space. You have learned:
- Properties of a matrix, such as rank & null space
- How the invertibility of matrices relates to the linear transform they enact
- What eigenvalues/eigenvectors are and why they might be useful

We will be using this knowledge in many of the days in the NMA computational neuroscience course.


# Coherent dark states and polarization switching
Studying the effect of polarization switching on coherent dark states in a 9-level system. The system is made of two ground states with J = 1 and one excited state with J = 2 for a total of 11 levels. Basically just 9-level system studied in "Coherent dark states and polarization switching.ipynb" but with excited state having J = 2 so there are no polarization dark states. Note that the system actually has two polarization dark states which significantly complicates things.

## Imports
Start by importing the necessary packages

```python
%load_ext autoreload
%autoreload 2

import joblib
import matplotlib.pyplot as plt
plt.style.use("ggplot")
import numpy as np
import scipy
import qutip
from sympy import Symbol

from toy_systems.couplings import FirstRankCouplingJ, ToyEnergy
from toy_systems.dark_states import get_dark_states
from toy_systems.decays import CouplingDecay, ToyDecay
from toy_systems.hamiltonian import Hamiltonian
from toy_systems.operators import JRotation
from toy_systems.quantum_system import QuantumSystem
from toy_systems.states import Basis, BasisState, JQuantumNumbers, ToyQuantumNumbers
from toy_systems.utils import generate_P_op
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

## Set up states and basis
We start by defining the three states of the system: the ground states $|g0\rangle$ and $|g1\rangle$, and the excited state $|e\rangle$>

```python
g0s = [BasisState(qn=JQuantumNumbers(label="g0", J = 1, mJ = mJ)) for mJ in np.arange(-1,2)]
g1s = [BasisState(qn=JQuantumNumbers(label="g1", J = 1, mJ = mJ)) for mJ in np.arange(-1,2)]
es = [BasisState(qn=JQuantumNumbers(label="e", J = 2, mJ = mJ)) for mJ in np.arange(-2,3)]

# Define basis
basis = Basis(g0s+g1s+es)
basis.print()
```

    |0> = JQuantumNumbers(J=1, mJ=-1, label='g0')
    |1> = JQuantumNumbers(J=1, mJ=0, label='g0')
    |2> = JQuantumNumbers(J=1, mJ=1, label='g0')
    |3> = JQuantumNumbers(J=1, mJ=-1, label='g1')
    |4> = JQuantumNumbers(J=1, mJ=0, label='g1')
    |5> = JQuantumNumbers(J=1, mJ=1, label='g1')
    |6> = JQuantumNumbers(J=2, mJ=-2, label='e')
    |7> = JQuantumNumbers(J=2, mJ=-1, label='e')
    |8> = JQuantumNumbers(J=2, mJ=0, label='e')
    |9> = JQuantumNumbers(J=2, mJ=1, label='e')
    |10> = JQuantumNumbers(J=2, mJ=2, label='e')

## Define energies, couplings, decays and quantum system
I'm going to define the system in the rotating frame as usual.

### Energies

```python
δ = Symbol('delta') # Energy splitting between |g0> and |g1
Δ = Symbol('Delta') # Detuning of drive field from 0
E0 = ToyEnergy(g0s, -δ/2)
E1 = ToyEnergy(g1s, +δ/2)
Ee = ToyEnergy(es, Δ)
```

### Couplings
Will treat the problem as if it has two time dependent laser fields: one polarized along z and the other one along x. The polarization will rotate back and forth between the two directions

```python
Ωz = Symbol('Omega_z') # Drive field Rabi rate for z-polarization
Ωx = Symbol('Omega_x') # Drive field Rabi rate for x-polarization

# A condition to make sure the ground states don't get coupled to each other
def both_not_ground(state1, state2):
    return (state1.qn.label == 'e') or (state2.qn.label == 'e')

def both_not_excited(state1, state2):
    return not ((state1.qn.label == 'e') and (state2.qn.label == 'e'))

# Strings for time-dependence
time_dep_z = "(1-tanh(t/tau))/2"
time_dep_x = "(1+tanh(t/tau))/2"

coupling_z = FirstRankCouplingJ(Ωz, p_car=np.array((0,0,1)), other_conds = [both_not_ground, both_not_excited], time_dep = time_dep_z, time_args={"tau":1})
coupling_x = FirstRankCouplingJ(Ωx, p_car=np.array((1,0,0)), other_conds = [both_not_ground, both_not_excited], time_dep = time_dep_x, time_args={"tau":1})
```

### Decays
Defining a decay from all $|e\rangle$ to all $|g0\rangle$ and $|g1\rangle$ as permitted by angular momentum:

```python
# Define dipole couplings that connect excited state to ground states
decay_couplings = [
    FirstRankCouplingJ(1, p_car=np.array((1,0,0)), other_conds=[both_not_ground, both_not_excited]),
    FirstRankCouplingJ(1, p_car=np.array((0,1,0)), other_conds=[both_not_ground, both_not_excited]),
    FirstRankCouplingJ(1, p_car=np.array((0,0,1)), other_conds=[both_not_ground, both_not_excited]),
]

decays = [CouplingDecay(e, Symbol("Gamma"), decay_couplings) for e in es]
```

### Define a QuantumSystem
The QuantumSystem object combines the basis, Hamiltonian and decays to make setting parameters for time evolution using QuTiP more convenient.

```python
# Define the system
system = QuantumSystem(
    basis=basis,
    couplings=[E0, E1, Ee, coupling_z, coupling_x],
    decays=decays,
)

# Get representations of the Hamiltonian and the decays that will be accepted by qutip
Hqobj, c_qobj = system.get_qobjs()
```

## Time evolution
No matter what state the system starts in, it should always end up in the dark state, from which it will slowly evolve out since the dark state is not an eigenstate of the Hamiltonian.

```python
# Get a pointer to the time-evolution arguments
args = Hqobj.args
print("Keys for setting arguments:")
print(f"args = {args}")
```

    Keys for setting arguments:
    args = {'delta': 1, 'Delta': 1, 'tau': 1, 'Omega_z': 1, 'Omega_x': 1, 'Gamma': 1}

### Find bright and dark states
Determining the bright and dark states for the parameters that are used for time-evolution so can keep track of the population in the bright and dark states.

```python
# Set the parameters for the system
args['delta'] = 0.05
args['Delta'] = 0
args['Omega_z'] = 1
args['Omega_x'] = 1
args['Gamma'] = 1

# Find the dark and bright states for each mJ and each polarization
bright_states_z = []
dark_states_z = []
pol_dark_states_z = []
bright_states_x = []
dark_states_x = []
pol_dark_states_x = []
bright_states_y = []
dark_states_y = []
pol_dark_states_y = []

for g0, g1, e in zip(g0s, g1s, es[1:-1]):
    test_coupling_z = FirstRankCouplingJ(Ωz, p_car=np.array((0,0,1)), other_conds = [both_not_ground, both_not_excited])

    # Find the bright and dark states for z-polarization
    bright_states, dark_states, pol_dark_states = get_dark_states([g0, g1], e, [test_coupling_z])
    bright_states_z += bright_states
    dark_states_z += dark_states
    pol_dark_states_z += pol_dark_states

# Find the dark and bright states for x- and y-polarization by rotating the basis
# X-polarized
rotation_x = JRotation(np.pi/2, np.array((0,1,0)))
for bs in bright_states_z:
    bright_states_x.append(bs.apply_operator(basis, rotation_x))

for ds in dark_states_z:
    dark_states_x.append(ds.apply_operator(basis, rotation_x))

for ds in pol_dark_states_z:
    pol_dark_states_x.append(ds.apply_operator(basis, rotation_x))

# Y-polarized
rotation_y = JRotation(np.pi/2, np.array((1,0,0)))
for bs in bright_states_z:
    bright_states_y.append(bs.apply_operator(basis, rotation_y))

for ds in dark_states_z:
    dark_states_y.append(ds.apply_operator(basis, rotation_y))

for ds in pol_dark_states_z:
    pol_dark_states_y.append(ds.apply_operator(basis, rotation_y))

# Print the bright and dark states for each polarization:
print("X-polarization:\n")
for i, state in enumerate(bright_states_x):
    print(f"\n|Bx{i}> = ")
    state.print_state()

for i, state in enumerate(dark_states_x):
    print(f"\n|Dx{i}> = ")
    state.print_state()

print("\n\nY-polarization:\n")
for i, state in enumerate(bright_states_y):
    print(f"\n|By{i}> = ")
    state.print_state()

for i, state in enumerate(dark_states_y):
    print(f"\n|Dy{i}> = ")
    state.print_state()

print("\n\nZ-polarization:\n")
for i, state in enumerate(bright_states_z):
    print(f"\n|Bz{i}> = ")
    state.print_state()

for i, state in enumerate(dark_states_z):
    print(f"\n|Dz{i}> = ")
    state.print_state()
```

    X-polarization:

    |Bx0> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Bx1> =
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Bx2> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dx0> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dx1> =
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dx2> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    -0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    +0.5000+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    Y-polarization:

    |By0> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |By1> =
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |By2> =
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dy0> =
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000-0.5000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dy1> =
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    0.0000-0.5000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000-0.5000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dy2> =
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    0.0000+0.5000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.3536+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')
    0.0000-0.5000j x JQuantumNumbers(J=1, mJ=0, label='g1')
    +0.3536+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    Z-polarization:

    |Bz0> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')

    |Bz1> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')

    |Bz2> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

    |Dz0> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g0')
    +0.7071-0.0000j x JQuantumNumbers(J=1, mJ=-1, label='g1')

    |Dz1> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=0, label='g0')
    +0.7071-0.0000j x JQuantumNumbers(J=1, mJ=0, label='g1')

    |Dz2> =
    -0.7071+0.0000j x JQuantumNumbers(J=1, mJ=1, label='g0')
    +0.7071-0.0000j x JQuantumNumbers(J=1, mJ=1, label='g1')

```python
args['tau'] = 1e-5

# Generate a Qobj representing the initial state
# psi0 = (1*g0s[0]).qobj(basis)
psi0 = (1*dark_states_z[0]).qobj(basis)

# Operators for getting probability of being in each state as a function of time
P_g0 = generate_P_op(g0s, basis)
P_g1 = generate_P_op(g1s, basis)
P_e = generate_P_op(es, basis)
P_B_z = generate_P_op(bright_states_z, basis)
P_D_z = generate_P_op(dark_states_z, basis)
P_B_x = generate_P_op(bright_states_x, basis)
P_D_x = generate_P_op(dark_states_x, basis)
P_B_y = generate_P_op(bright_states_y, basis)
P_D_y = generate_P_op(dark_states_y, basis)

P_ops = [P_g0, P_g1, P_e, P_B_z, P_D_z, P_B_x, P_D_x, P_B_y, P_D_y]

# Times at which result is requested
times = np.linspace(-10,10,1001)/args["delta"]

# Setting the max_step is sometimes necessary
options = qutip.solver.Options(method = 'adams', nsteps=10000, max_step=1e0, rhs_reuse=False)

# Setup a progress bar
pb = qutip.ui.progressbar.EnhancedTextProgressBar()

# Run the time-evolution
result = qutip.mesolve(Hqobj, psi0, times, c_ops = c_qobj, args = args, e_ops = P_ops, progress_bar=pb, options = options)
```

     Total run time:   0.75s*] Elapsed 0.75s / Remaining 00:00:00:00

```python
fig, ax = plt.subplots(3, 1, figsize = (16,18))
ax[0].plot(times, result.expect[0], label = "P_g0")
ax[0].plot(times, result.expect[1], label = "P_g1", ls = '--')
ax[0].plot(times, result.expect[2], label = "P_e")
ax[0].legend()
ax[0].set_ylabel("Population in each state")
ax[0].set_title("Energy eigenstate basis")

ln = []
ln +=ax[1].plot(times, result.expect[3], label = "P_B_z")
ln +=ax[1].plot(times, result.expect[4], label = "P_D_z")
ln +=ax[1].plot(times, result.expect[2], label = "P_e")
ax[1].set_ylabel("Population in each state")
ax[1].set_title("Dark and bright state basis for z-polarization")
ax1c = ax[1].twinx()
ax1c.grid(False)
ln +=coupling_z.plot_time_dep(times, args, ax = ax1c, c = 'k', ls = '--', label = "z-pol mag")
ax1c.set_ylabel("Magnitude of z-polarization")
ax[1].legend(ln, [l.get_label() for l in ln])

ln2 = []
ln2+=ax[2].plot(times, result.expect[5], label = "P_B_x")
ln2+=ax[2].plot(times, result.expect[6], label = "P_D_x")
ln2+=ax[2].plot(times, result.expect[2], label = "P_e")
ax[2].legend()
ax[2].set_ylabel("Population in each state")
ax[2].set_title("Dark and bright state basis for x-polarization")
ax2c = ax[2].twinx()
ax2c.grid(False)
ln2 +=coupling_x.plot_time_dep(times, args, ax = ax2c, c = 'k', ls = '--', label = "x-pol mag")
ax2c.set_ylabel("Magnitude of x-polarization")
ax[2].legend(ln2, [l.get_label() for l in ln2])

print(f"\nPopulation in excited state at the end: {result.expect[2][-1]*100:.1e} %")
print(f"Photons per unit time: {scipy.integrate.trapezoid(result.expect[2], x = times)/(times[-1]-times[0]):.2e}")
```

So what is happening here?:
- I'm starting the system in the z-polarization coherent dark state with mJ = -1.
- For x-polarization the initial state is also completely completely darl
- There is a small amount of population in the bright and excited state, with the amount depending on the value of $\delta$ (an probably also the other parameters to some extent)
- Not much changes when the polarization is flipped since the dark state for z-polarization is also dark for x-polarization
- The next question is: Can population be transferred to the bright state for x if the polarization switch is done with the right time dynamics?

## Trying different rise times

```python
# Set the rise time
args["tau"] = 10

# Generate a Qobj representing the initial state
# psi0 = (1*g0s[0]).qobj(basis)
psi0 = (1*dark_states_z[0]).qobj(basis)

P_ops = [P_g0, P_g1, P_e, P_B_z, P_D_z, P_B_x, P_D_x, P_B_y, P_D_y]

# Times at which result is requested
times = np.linspace(-10,10,1001)/args["delta"]

# Setting the max_step is sometimes necessary
options = qutip.solver.Options(method = 'adams', nsteps=10000, max_step=1e0, rhs_reuse=False)

# Setup a progress bar
pb = qutip.ui.progressbar.EnhancedTextProgressBar()

# Run the time-evolution
result = qutip.mesolve(Hqobj, psi0, times, c_ops = c_qobj, args = args, e_ops = P_ops, progress_bar=pb, options = options)
```

     Total run time:   0.75s*] Elapsed 0.75s / Remaining 00:00:00:00

```python
fig, ax = plt.subplots(3, 1, figsize = (16,18))
ax[0].plot(times, result.expect[0], label = "P_g0")
ax[0].plot(times, result.expect[1], label = "P_g1", ls = '--')
ax[0].plot(times, result.expect[2], label = "P_e")
ax[0].legend()
ax[0].set_ylabel("Population in each state")
ax[0].set_title("Energy eigenstate basis")

ln = []
ln +=ax[1].plot(times, result.expect[3], label = "P_B_z")
ln +=ax[1].plot(times, result.expect[4], label = "P_D_z")
ln +=ax[1].plot(times, result.expect[2], label = "P_e")
ax[1].set_ylabel("Population in each state")
ax[1].set_title("Dark and bright state basis for z-polarization")
ax1c = ax[1].twinx()
ax1c.grid(False)
ln +=coupling_z.plot_time_dep(times, args, ax = ax1c, c = 'k', ls = '--', label = "z-pol mag")
ax1c.set_ylabel("Magnitude of z-polarization")
ax[1].legend(ln, [l.get_label() for l in ln])

ln2 = []
ln2+=ax[2].plot(times, result.expect[5], label = "P_B_x")
ln2+=ax[2].plot(times, result.expect[6], label = "P_D_x")
ln2+=ax[2].plot(times, result.expect[2], label = "P_e")
ax[2].legend()
ax[2].set_ylabel("Population in each state")
ax[2].set_title("Dark and bright state basis for x-polarization")
ax2c = ax[2].twinx()
ax2c.grid(False)
ln2 +=coupling_x.plot_time_dep(times, args, ax = ax2c, c = 'k', ls = '--', label = "x-pol mag")
ax2c.set_ylabel("Magnitude of x-polarization")
ax[2].legend(ln2, [l.get_label() for l in ln2])

print(f"\nPopulation in excited state at the end: {result.expect[2][-1]*100:.1e} %")
print(f"Photons per unit time: {scipy.integrate.trapezoid(result.expect[2], x = times)/(times[-1]-times[0]):.2e}")
```

Takeaways:
- With just one switch, a slower switch seems to be better
- However, if we can do multiple switches (as we can) then shorter rise time might be better

## Continuous polarization switching

### Redefine couplings with periodic switching

```python
# Strings for time-dependence
time_dep_z = "(1-tanh(asin(sin(2*pi*t/T))*T/tau))/2"
time_dep_x = "(1+tanh(asin(sin(2*pi*t/T))*T/tau))/2"

periodic_coupling_z = FirstRankCouplingJ(Ωz, p_car=np.array((0,0,1)), other_conds = [both_not_ground, both_not_excited],
                                         time_dep = time_dep_z, time_args={"tau":1, "T":1})
periodic_coupling_x = FirstRankCouplingJ(Ωx, p_car=np.array((1,0,0)), other_conds = [both_not_ground, both_not_excited],
                                         time_dep = time_dep_x, time_args={"tau":1, "T":1})
```

```python
# Define dipole couplings that connect excited state to ground states
decay_couplings = [
    FirstRankCouplingJ(1, p_car=np.array((1,0,0)), other_conds=[both_not_ground, both_not_excited]),
    FirstRankCouplingJ(1, p_car=np.array((0,1,0)), other_conds=[both_not_ground, both_not_excited]),
    FirstRankCouplingJ(1, p_car=np.array((0,0,1)), other_conds=[both_not_ground, both_not_excited]),
]

decays = [CouplingDecay(e, Symbol("Gamma"), decay_couplings) for e in es]
```

### Define a QuantumSystem
The QuantumSystem object combines the basis, Hamiltonian and decays to make setting parameters for time evolution using QuTiP more convenient.

```python
# Define the system
periodic_system = QuantumSystem(
    basis=basis,
    couplings=[E0, E1, Ee, periodic_coupling_z, periodic_coupling_x],
    decays=decays,
)

# Get representations of the Hamiltonian and the decays that will be accepted by qutip
Hqobj_periodic, c_qobj_periodic = periodic_system.get_qobjs()
visualizer = Visualizer(system, vertical={"label":50}, horizontal={"label":0, "mJ":40})
```

### Time evolution

```python
# Get a pointer to the time-evolution arguments
args = Hqobj_periodic.args
print("Keys for setting arguments:")
print(f"args = {args}")
```

    Keys for setting arguments:
    args = {'delta': 1, 'Delta': 1, 'tau': 1, 'T': 1, 'Omega_z': 1, 'Omega_x': 1, 'Gamma': 1}

```python
args['delta'] = 0.05
args['Delta'] = 0
args['Omega_z'] = 1
args['Omega_x'] = 1
args['Gamma'] = 1
args['tau'] = 0.1
args['T'] = 10

# Generate a Qobj representing the initial state
# psi0 = (1*g0s[0]).qobj(basis)
psi0 = (1*dark_states_z[0]).qobj(basis)

P_ops = [P_g0, P_g1, P_e, P_B_z, P_D_z, P_B_x, P_D_x, P_B_y, P_D_y]

# Times at which result is requested
times = np.linspace(-10,10,1001)/args["delta"]

# Setting the max_step is sometimes necessary
options = qutip.solver.Options(method = 'adams', nsteps=10000, max_step=1e0, rhs_reuse=False)

# Setup a progress bar
pb = qutip.ui.progressbar.EnhancedTextProgressBar()

# Run the time-evolution
result = qutip.mesolve(Hqobj_periodic, psi0, times, c_ops = c_qobj_periodic, args = args, e_ops = P_ops, progress_bar=pb, options = options)
```

     Total run time:   0.95s*] Elapsed 0.95s / Remaining 00:00:00:00

```python
fig, ax = plt.subplots(3, 1, figsize = (16,18))
ax[0].plot(times, result.expect[0], label = "P_g0")
ax[0].plot(times, result.expect[1], label = "P_g1", ls = '--')
ax[0].plot(times, result.expect[2], label = "P_e")
ax[0].legend()
ax[0].set_ylabel("Population in each state")
ax[0].set_title("Energy eigenstate basis")

ln = []
ln +=ax[1].plot(times, result.expect[3], label = "P_B_z")
ln +=ax[1].plot(times, result.expect[4], label = "P_D_z")
ln +=ax[1].plot(times, result.expect[2], label = "P_e")
ax[1].set_ylabel("Population in each state")
ax[1].set_title("Dark and bright state basis for z-polarization")
ax1c = ax[1].twinx()
ax1c.grid(False)
ln +=periodic_coupling_z.plot_time_dep(times, args, ax = ax1c, c = 'k', ls = '--', label = "z-pol mag")
ax1c.set_ylabel("Magnitude of z-polarization")
ax[1].legend(ln, [l.get_label() for l in ln])

ln2 = []
ln2+=ax[2].plot(times, result.expect[5], label = "P_B_x")
ln2+=ax[2].plot(times, result.expect[6], label = "P_D_x")
ln2+=ax[2].plot(times, result.expect[2], label = "P_e")
ax[2].legend()
ax[2].set_ylabel("Population in each state")
ax[2].set_title("Dark and bright state basis for x-polarization")
ax2c = ax[2].twinx()
ax2c.grid(False)
ln2 +=periodic_coupling_x.plot_time_dep(times, args, ax = ax2c, c = 'k', ls = '--', label = "x-pol mag")
ax2c.set_ylabel("Magnitude of x-polarization")
ax[2].legend(ln2, [l.get_label() for l in ln2])

print(f"\nPopulation in excited state at the end: {result.expect[2][-1]*100:.1e} %")
print(f"Photons per unit time: {scipy.integrate.trapezoid(result.expect[2], x = times)/(times[-1]-times[0]):.2e}")
```

### Parameter scan
Is there some value of polarization switching frequency or rise time that works better than others?

```python
default_args = {}
default_args['delta'] = 0.05
default_args['Delta'] = 0
default_args['Omega_z'] = 1
default_args['Omega_x'] = 1
default_args['Gamma'] = 1
default_args['tau'] = 0.1
default_args['T'] = 10

# Define function that will be executed in parallel
def parallel_func(args):
    # Generate a Qobj representing the initial state
    # psi0 = (1*g0s[0]).qobj(basis)
    psi0 = (1*dark_states_z[0]).qobj(basis)

    P_ops = [P_g0, P_g1, P_e, P_B_z, P_D_z, P_B_x, P_D_x, P_B_y, P_D_y]

    # Times at which result is requested
    times = np.linspace(-10,10,1001)/args["delta"]

    # Setting the max_step is sometimes necessary
    options = qutip.solver.Options(method = 'adams', nsteps=10000, max_step=1e0, rhs_reuse=True)

    # Setup a progress bar
    pb = qutip.ui.progressbar.EnhancedTextProgressBar()

    # Run the time-evolution
    result = qutip.mesolve(Hqobj_periodic, psi0, times, c_ops = c_qobj_periodic, args = args, e_ops = P_ops,
                           progress_bar=pb, options = options)

    # Calculate rate at which photons are emitted
    rate = scipy.integrate.trapezoid(result.expect[2], x = times)/(times[-1]-times[0])

    return rate
```

#### Switching frequency

```python
# Define values period to use
Ts = np.linspace(0.01, 10, 100)

# Generate list of arguments to pass to time evaluaation
args_list = [default_args.copy() | {'T': T} for T in Ts]

rates = np.array(joblib.Parallel(n_jobs = 10, backend='loky', verbose=True)(joblib.delayed(parallel_func)(args) for args in args_list))
```

    [Parallel(n_jobs=10)]: Using backend LokyBackend with 10 concurrent workers.
    [Parallel(n_jobs=10)]: Done  30 tasks      | elapsed:    7.6s
    [Parallel(n_jobs=10)]: Done 100 out of 100 | elapsed:   23.9s finished

```python
fig, ax = plt.subplots(figsize = (16,9))
ax.plot(Ts, rates)
ax.set_xlabel("T", fontsize = 16)
ax.set_ylabel("Photon emission rate", fontsize = 16)
```

So the faster the switching the better apparently

#### Rise time

```python
# Define values period to use
taus = np.linspace(0.01, 10, 100)

# Generate list of arguments to pass to time evaluaation
args_list = [default_args.copy() | {'tau': tau} for tau in taus]

rates = np.array(joblib.Parallel(n_jobs = 10, backend='loky', verbose=True)(joblib.delayed(parallel_func)(args) for args in args_list))
```

    [Parallel(n_jobs=10)]: Using backend LokyBackend with 10 concurrent workers.
    [Parallel(n_jobs=10)]: Done  30 tasks      | elapsed:  1.6min
    [Parallel(n_jobs=10)]: Done 100 out of 100 | elapsed:  2.1min finished

```python
fig, ax = plt.subplots(figsize = (16,9))
ax.plot(taus, rates)
ax.set_xlabel("tau", fontsize = 16)
ax.set_ylabel("Photon emission rate", fontsize = 16)
```

```python

```

examples/Coherent dark states and polarization switching-11-level system.ipynb


# Start-to-Finish Example: Unit Testing `GiRaFFE_NRPy`: Induction Equation

## Author: Patrick Nelson

## This module validates the routine to compute the flux of $\epsilon_{ijk} v^j B^k$ for `GiRaFFE`.

**Notebook Status:** Validated

**Validation Notes:** This module validates the routines in [Tutorial-GiRaFFE_NRPy-Afield_flux_handwritten](Tutorial-GiRaFFE_NRPy-Afield_flux_handwritten.ipynb).

### NRPy+ Source Code for this module:
* [GiRaFFE_NRPy/GiRaFFE_NRPy_Afield_flux_handwritten.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_Afield_flux_handwritten.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-Afield_flux_handwritten.ipynb) Generates the symbolic expressions needed to compute the flux term of the induction equation right-hand side.

## Introduction:

This notebook validates our algorithm to compute the flux of $\epsilon_{ijk} v^j B^k$ through cell faces, which contributes to the right-hand side of the evolution equation for $A_i$, for use in `GiRaFFE_NRPy`. Because the original `GiRaFFE` used staggered grids and we do not, we can not trivially do a direct comparison to the old code. Instead, we will compare the numerical results with the expected analytic results.

It is, in general, good coding practice to unit test functions individually to verify that they produce the expected and intended output. Here, we expect our functions to produce the correct cross product between the velocity and magnetic field in an arbitrary spacetime. To that end, we will choose functions with relatively simple analytic forms.

TERRENCE SAYS: wording is confusing here

In this test, we will generate analytic forms for the magnetic field and three-velocity. We will need to do this in a $7 \times 7 \times 7$ cube in order to run the PPM routine on the data to generate the inputs compute the non-gauge terms of the RHS of the induction equation, since it would be impractical to usefully spoof the left- and right-face values for the HLLE solver. We care about this here, because we are comparing against an analytic expression and not the old code.

When this notebook is run, the difference between the approximate and exact right-hand sides will be output to text files that can be found in the same directory as this notebook. These will be read in in [Step 3](#convergence), and used there to confirm second order convergence of the algorithm.

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#setup): Set up core functions and parameters for unit testing the A2B algorithm
    1. [Step 1.a](#metric) Set analytic 3-metric data
    1. [Step 1.b](#magnetic) Set analytic magnetic field
    1. [Step 1.c](#velocity) Set analytic Valencia three-velocity
    1. [Step 1.d](#functions) Generate C functions to write the test data
    1. [Step 1.e](#free_parameters) Set free parameters in the code
    1. [Step 1.f](#module) Generate `GiRaFFE_NRPy` Files
    1. [Step 1.g](#electric_field) Calculate the $E_i$ field contribution to $\partial_t A_i$
    1. [Step 1.h](#exact_flux) Calculate the *exact* flux of $\epsilon_{ijk} v^j B^k$
1. [Step 2](#mainc): `Afield_flux_unit_test.c`: The Main C Code
    1. [Step 2.a](#compile_run): Compile and run the code
1. [Step 3](#convergence): Code validation: Verify that relative error in numerical solution converges to zero at the expected order
1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set up core functions and parameters for unit testing the A2B algorithm \[Back to [top](#toc)\]

$$\label{setup}$$

We'll start by appending the relevant paths to `sys.path` so that we can access sympy modules in other places. Then, we'll import NRPy+ core functionality and set up a directory in which to carry out our test. We must also set the desired finite differencing order.

```python
import os, sys, shutil        # Standard Python modules for multiplatform OS-level functions
# First, we'll add the parent directory to the list of directories Python will check for modules.
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)
nrpy_dir_path = os.path.join("..","..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

from outputC import outCfunction, outputC, lhrh # NRPy+: Core C code output module
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import finite_difference as fin  # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp         # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm   # NRPy+: Reference metric support
import cmdline_helper as cmd     # NRPy+: Multi-platform Python command-line interface

Ccodesdir = "Start-to-Finish-UnitTests/Afield_flux_UnitTest/"

# First remove C code output directory if it exists
# Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty
shutil.rmtree(Ccodesdir, ignore_errors=True)
# Then create a fresh directory
cmd.mkdir(Ccodesdir)

outdir = os.path.join(Ccodesdir,"output/")
cmd.mkdir(outdir)

thismodule = "Start_to_Finish_UnitTest-GiRaFFE_NRPy-Afield_flux"
Use_Shock_Data = True
```

## Step 1.a: Set analytic 3-metric data \[Back to [top](#toc)\]
$$\label{metric}$$

First, we'll set some arbitrary functions for the three-metric $\gamma_{ij}$, the shift vector $\beta^i$, and the lapse function $\alpha$. We will need these to compute the characteristic speeds in each direction. In addition, we will need the metric determinant to exactly compute the electric field at the test point.

We will use this basic form for $\gamma_{ij}$. We will thus write a function with the following arbitrary equations.

\begin{align}
\gamma_{xx} &= ax^3 + by^3 + cz^3 + dy^2 + ez^2 + 1 \\
\gamma_{yy} &= gx^3 + hy^3 + lz^3 + mx^2 + nz^2 + 1 \\
\gamma_{zz} &= px^3 + qy^3 + rz^3 + sx^2 + ty^2 + 1. \\
\gamma_{xy} &= \frac{1}{10} \exp\left(-\left((x-b)^2+(y-c)^2+(z-d)^2\right)\right) \\
\gamma_{xz} &= \frac{1}{10} \exp\left(-\left((x-g)^2+(y-h)^2+(z-l)^2\right)\right) \\
\gamma_{yz} &= \frac{1}{10} \exp\left(-\left((x-n)^2+(y-o)^2+(z-p)^2\right)\right), \\
\end{align}

```python
a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t = par.Cparameters("REAL",thismodule,["a","b","c","d","e","f","g","h","l","m","n","o","p","q","r","s","t"],1e300)
# Note that the above default value allows us to set these directly in the C code
M_PI  = par.Cparameters("#define",thismodule,["M_PI"], "")

par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()
x = rfm.xx_to_Cart[0]
y = rfm.xx_to_Cart[1]
z = rfm.xx_to_Cart[2]

gammaDD = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gammaDD","sym01",DIM=3)
betaU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","betaU",DIM=3)
alpha = gri.register_gridfunctions("AUXEVOL","alpha")
```

\begin{align}
\gamma_{xx} &= ax^3 + by^3 + cz^3 + dy^2 + ez^2 + 1 \\
\gamma_{yy} &= gx^3 + hy^3 + lz^3 + mx^2 + nz^2 + 1 \\
\gamma_{zz} &= px^3 + qy^3 + rz^3 + sx^2 + ty^2 + 1. \\
\gamma_{xy} &= \frac{1}{10} \exp\left(-\left((x-b)^2+(y-c)^2+(z-d)^2\right)\right) \\
\gamma_{xz} &= \frac{1}{10} \exp\left(-\left((x-g)^2+(y-h)^2+(z-l)^2\right)\right) \\
\gamma_{yz} &= \frac{1}{10} \exp\left(-\left((x-n)^2+(y-o)^2+(z-p)^2\right)\right), \\
\end{align}

```python
gammaDD[0][0] = a*x**3 + b*y**3 + c*z**3 + d*y**2 + e*z**2 + sp.sympify(1)
gammaDD[1][1] = g*x**3 + h*y**3 + l*z**3 + m*x**2 + n*z**2 + sp.sympify(1)
gammaDD[2][2] = p*x**3 + q*y**3 + r*z**3 + s*x**2 + t*y**2 + sp.sympify(1)
gammaDD[0][1] = sp.Rational(1,10) * sp.exp(-((x-b)**2 + (y-c)**2 + (z-d)**2))
gammaDD[0][2] = sp.Rational(1,10) * sp.exp(-((x-g)**2 + (y-h)**2 + (z-l)**2))
gammaDD[1][2] = sp.Rational(1,10) * sp.exp(-((x-n)**2 + (y-o)**2 + (z-p)**2))

betaU[0] = sp.sympify(0)#(sp.sympify(2)/M_PI) * sp.atan(a*x + b*y + c*z)
betaU[1] = sp.sympify(0)#(sp.sympify(2)/M_PI) * sp.atan(b*x + c*y + a*z)
betaU[2] = sp.sympify(0)#(sp.sympify(2)/M_PI) * sp.atan(c*x + a*y + b*z)

alpha = sp.sympify(1)#sp.sympify(1) - sp.sympify(1) / (sp.sympify(2) + x**2 + y**2 + z**2)

metric_gfs_to_print = [
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD00"),rhs=gammaDD[0][0]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD01"),rhs=gammaDD[0][1]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD02"),rhs=gammaDD[0][2]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD11"),rhs=gammaDD[1][1]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD12"),rhs=gammaDD[1][2]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD22"),rhs=gammaDD[2][2]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU0"),rhs=betaU[0]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU1"),rhs=betaU[1]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU2"),rhs=betaU[2]),
                       lhrh(lhs=gri.gfaccess("aux_gfs","alpha"),rhs=alpha),
                      ]

desc = "Calculate the metric gridfunctions"
name = "calculate_metric_gfs"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *restrict params,REAL *restrict xx[3],REAL *restrict auxevol_gfs",
    body     = fin.FD_outputC("returnstring",metric_gfs_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")
```

    Output C function calculate_metric_gfs() to file Start-to-Finish-UnitTests/Afield_flux_UnitTest/calculate_metric_gfs.h

## Step 1.b: Valencia three-velocity \[Back to [top](#toc)\]
$$\label{velocity}$$

Here, we'll generate some functions for the velocity. Let's choose arctangents, since those have asymptotes that can be easily manipulated to prevent accidentally setting superluminal speeds.
\begin{align}
\bar{v}^x &= \frac{1}{5\pi} \arctan(a(x-k) + by + cz) \\
\bar{v}^y &= \frac{1}{5\pi} \arctan(bx + c(y-k) + az) \\
\bar{v}^z &= \frac{1}{5\pi} \arctan(cx + ay + b(z-k)) \\
\end{align}
We offset by $k$ so we have smooth data in the region of interest when `Use_Shock_Data` is false.

If we want to add a jump at the origin, we can simply add $\max(0,5x)$ to the argument of the arctangent. This will add a shock in the $x$-direction. The maximum will be described without the use of if statements using the `Min_Max_and_Piecewise_Expressions` module.

```python
import Min_Max_and_Piecewise_Expressions as noif

offset = sp.sympify(5)
args = ixp.zerorank1()
args[0] = a*(x-offset) + b*y + c*z
args[1] = b*x + c*(y-offset) + a*z
args[2] = c*x + a*y + b*(z-offset)
if Use_Shock_Data:
    for i in range(3):
        args[i] += noif.max_noif(0,5*x)

ValenciavU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","ValenciavU")
for i in range(3):
    ValenciavU[i] = (sp.Rational(1,5)/M_PI)*sp.atan(args[i])
```

## Step 1.c: Magnetic field \[Back to [top](#toc)\]
$$\label{magnetic}$$

We'll also need some functions for the magnetic field. Exponentials sound fun.
\begin{align}
B^x &= \exp\left(\frac{1}{10}(ey+fz)\right) \\
B^y &= \exp\left(\frac{1}{10}(fz+dx)\right) \\
B^z &= \exp\left(\frac{1}{10}(dx+ey)\right) \\
\end{align}
In this case, we'll add $\max{0,5x}$ to the field to add the jump.

```python
BU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","BU")
BU[0] = sp.exp(sp.Rational(1,10)*(e*y+f*z))
BU[1] = sp.exp(sp.Rational(1,10)*(f*z+d*x))
BU[2] = sp.exp(sp.Rational(1,10)*(d*x+e*y))
if Use_Shock_Data:
    for i in range(3):
        BU[i] += noif.max_noif(0,5*x)
```

## Step 1.d: Generate C functions to write the test data \[Back to [top](#toc)\]
$$\label{functions}$$

```python
BU_to_print = [
                lhrh(lhs=gri.gfaccess("out_gfs","BU0"),rhs=BU[0]),
                lhrh(lhs=gri.gfaccess("out_gfs","BU1"),rhs=BU[1]),
                lhrh(lhs=gri.gfaccess("out_gfs","BU2"),rhs=BU[2]),
               ]

desc = "Calculate sample magnetic field data"
name = "calculate_BU"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *params,REAL *xx[3],REAL *auxevol_gfs",
    body     = fin.FD_outputC("returnstring",BU_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")

ValenciavU_to_print = [
                       lhrh(lhs=gri.gfaccess("out_gfs","ValenciavU0"),rhs=ValenciavU[0]),
                       lhrh(lhs=gri.gfaccess("out_gfs","ValenciavU1"),rhs=ValenciavU[1]),
                       lhrh(lhs=gri.gfaccess("out_gfs","ValenciavU2"),rhs=ValenciavU[2]),
                      ]

desc = "Calculate sample velocity data"
name = "calculate_ValenciavU"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *params,REAL *xx[3],REAL *auxevol_gfs",
    body     = fin.FD_outputC("returnstring",ValenciavU_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts="AllPoints,Read_xxs")
```

    Output C function calculate_BU() to file Start-to-Finish-UnitTests/Afield_flux_UnitTest/calculate_BU.h
    Output C function calculate_ValenciavU() to file Start-to-Finish-UnitTests/Afield_flux_UnitTest/calculate_ValenciavU.h

## Step 1.e: Set free parameters in the code \[Back to [top](#toc)\]
$$\label{free_parameters}$$

We also need to create the files that interact with NRPy's C parameter interface.

```python
# Step 3.d.i: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
NGHOSTS  = par.Cparameters("int",thismodule,["NGHOSTS"], 3)
# Step 3.d.ii: Set free_parameters.h
with open(os.path.join(Ccodesdir,"free_parameters.h"),"w") as file:
    file.write("""
// Override parameter defaults with values based on command line arguments and NGHOSTS.
// We'll use this grid. It has one point and one ghost zone.
params.NGHOSTS = 3;
params.Nxx0 = atoi(argv[1]);
params.Nxx1 = atoi(argv[2]);
params.Nxx2 = atoi(argv[3]);
params.Nxx_plus_2NGHOSTS0 = params.Nxx0 + 2*params.NGHOSTS;
params.Nxx_plus_2NGHOSTS1 = params.Nxx1 + 2*params.NGHOSTS;
params.Nxx_plus_2NGHOSTS2 = params.Nxx2 + 2*params.NGHOSTS;
// Step 0d: Set up space and time coordinates
// Step 0d.i: Declare \Delta x^i=dxx{0,1,2} and invdxx{0,1,2}, as well as xxmin[3] and xxmax[3]:
const REAL xxmin[3] = {-0.1,-0.1,-0.1};
const REAL xxmax[3] = { 0.1, 0.1, 0.1};

params.dxx0 = (xxmax[0] - xxmin[0]) / ((REAL)params.Nxx0);
params.dxx1 = (xxmax[1] - xxmin[1]) / ((REAL)params.Nxx1);
params.dxx2 = (xxmax[2] - xxmin[2]) / ((REAL)params.Nxx2);
//printf("dxx0,dxx1,dxx2 = %.5e,%.5e,%.5e\\n",params.dxx0,params.dxx1,params.dxx2);
params.invdx0 = 1.0 / params.dxx0;
params.invdx1 = 1.0 / params.dxx1;
params.invdx2 = 1.0 / params.dxx2;
\n""")

# Generates declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))
```

## Step 1.f: Generate `GiRaFFE_NRPy` Files \[Back to [top](#toc)\]
$$\label{module}$$

Here, we generate the functions we want to test by calling the function found [here](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_PPM.py) and documented in [this tutorial](Tutorial-Start_to_Finish-GiRaFFE_NRPy-PPM.ipynb).

```python
subdir = "PPM"
cmd.mkdir(os.path.join(Ccodesdir,subdir))
import GiRaFFE_NRPy.GiRaFFE_NRPy_PPM as PPM
PPM.GiRaFFE_NRPy_PPM(os.path.join(Ccodesdir,subdir))
```

## Step 1.g: Calculate the $E_i$ field contribution to $\partial_t A_i$ \[Back to [top](#toc)\]
$$\label{electric_field}$$

Here, we generate the functions necessary to calculate the electric flux on the cell faces, which is the algorithm we are specifically trying to test.

```python
import GiRaFFE_NRPy.GiRaFFE_NRPy_Afield_flux_handwritten as Af

# We will pass values of the gridfunction on the cell faces into the function. This requires us
# to declare them as C parameters in NRPy+. We will denote this with the _face infix/suffix.
alpha_face = gri.register_gridfunctions("AUXEVOL","alpha_face")
gamma_faceDD = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gamma_faceDD","sym01")
beta_faceU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","beta_faceU")

# We'll need some more gridfunctions, now, to represent the reconstructions of BU and ValenciavU
# on the right and left faces
Valenciav_rU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","Valenciav_rU",DIM=3)
B_rU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","B_rU",DIM=3)
Valenciav_lU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","Valenciav_lU",DIM=3)
B_lU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","B_lU",DIM=3)

AD = ixp.register_gridfunctions_for_single_rank1("EVOL","AD",DIM=3)

subdir = "RHSs"
cmd.mkdir(os.path.join(Ccodesdir,subdir))
Af.GiRaFFE_NRPy_Afield_flux(os.path.join(Ccodesdir,subdir))

subdir = "FCVAL"
cmd.mkdir(os.path.join(Ccodesdir,subdir))
# We also need to interpolate the metric values to cell faces, which is easily done with this module:
import GiRaFFE_NRPy.GiRaFFE_NRPy_Metric_Face_Values as FCVAL
FCVAL.GiRaFFE_NRPy_FCVAL(os.path.join(Ccodesdir,subdir))
```

## Step 1.h: Calculate the *exact* flux of $\epsilon_{ijk} v^j B^k$ \[Back to [top](#toc)\]
$$\label{exact_flux}$$

Here, we generate a function to analytically calculate the electric flux on the cell faces for comparison. We'll need to import the Levi-Civita tensor for this.

```python
import GRHD.equations as gh
gh.compute_sqrtgammaDET(gammaDD)
LeviCivitaDDD = ixp.LeviCivitaTensorDDD_dim3_rank3(gh.sqrtgammaDET)

driftvU = ixp.zerorank1()
for i in range(3):
    # TODO: don't use _face metric gridfunctions once we're in curved space!
    driftvU[i] = alpha*ValenciavU[i]-betaU[i]

A_rhsD = ixp.zerorank1()
for i in range(3):
    for j in range(3):
        for k in range(3):
            A_rhsD[i] += LeviCivitaDDD[i][j][k]*driftvU[j]*BU[k]

A_rhsD_to_print = [
                   lhrh(lhs=gri.gfaccess("rhs_gfs","AD0"),rhs=A_rhsD[0]),
                   lhrh(lhs=gri.gfaccess("rhs_gfs","AD1"),rhs=A_rhsD[1]),
                   lhrh(lhs=gri.gfaccess("rhs_gfs","AD2"),rhs=A_rhsD[2]),
                  ]

desc = "Calculate analytic electric field, part of the right-hand side of AD"
name = "calculate_E_exactD"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *params,REAL *xx[3],const REAL *auxevol_gfs,REAL *rhs_gfs",
    body     = fin.FD_outputC("returnstring",A_rhsD_to_print,params="outCverbose=False").replace("IDX4","IDX4S"),
    loopopts ="AllPoints,Read_xxs")

```

    Output C function calculate_E_exactD() to file Start-to-Finish-UnitTests/Afield_flux_UnitTest/calculate_E_exactD.h

# Step 2: `Afield_flux_unit_test.c`: The Main C Code \[Back to [top](#toc)\]
$$\label{mainc}$$

Now that we have our vector potential and analytic magnetic field to compare against, we will start writing our unit test. We'll also import common C functionality, define `REAL`, the number of ghost zones, and the faces, and set the standard macros for NRPy+ style memory access. After the preliminaries and setting data on our grid, we compare the output between `calculate_E_exactD()` and `calculate_E_field_flat_all_in_one()`.

```python
%%writefile $Ccodesdir/Afield_flux_unit_test.c

// These are common packages that we are likely to need.
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "string.h" // Needed for strncmp, etc.
#include "stdint.h" // Needed for Windows GCC 6.x compatibility
#include <time.h>   // Needed to set a random seed.

#define REAL double
#include "declare_Cparameters_struct.h"

REAL a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t;

// Standard NRPy+ memory access:
#define IDX3S(i,j,k) ( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * (k) ) )
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )
#define IDX4ptS(g,idx) ( (idx) + (Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2) * (g) )

#define LOOP_REGION(i0min,i0max, i1min,i1max, i2min,i2max) \
  for(int i2=i2min;i2<i2max;i2++) for(int i1=i1min;i1<i1max;i1++) for(int i0=i0min;i0<i0max;i0++)

```

    Writing Start-to-Finish-UnitTests/Afield_flux_UnitTest//Afield_flux_unit_test.c

We'll now define the gridfunction names.

```python
%%writefile -a $Ccodesdir/Afield_flux_unit_test.c

// Let's also #define the NRPy+ gridfunctions
#define VALENCIAVU0GF 0
#define VALENCIAVU1GF 1
#define VALENCIAVU2GF 2
#define BU0GF 3
#define BU1GF 4
#define BU2GF 5
#define VALENCIAV_RU0GF 6
#define VALENCIAV_RU1GF 7
#define VALENCIAV_RU2GF 8
#define B_RU0GF 9
#define B_RU1GF 10
#define B_RU2GF 11
#define VALENCIAV_LU0GF 12
#define VALENCIAV_LU1GF 13
#define VALENCIAV_LU2GF 14
#define B_LU0GF 15
#define B_LU1GF 16
#define B_LU2GF 17
#define GAMMA_FACEDD00GF 18
#define GAMMA_FACEDD01GF 19
#define GAMMA_FACEDD02GF 20
#define GAMMA_FACEDD11GF 21
#define GAMMA_FACEDD12GF 22
#define GAMMA_FACEDD22GF 23
#define BETA_FACEU0GF 24
#define BETA_FACEU1GF 25
#define BETA_FACEU2GF 26
#define ALPHA_FACEGF 27
#define GAMMADD00GF 28
#define GAMMADD01GF 29
#define GAMMADD02GF 30
#define GAMMADD11GF 31
#define GAMMADD12GF 32
#define GAMMADD22GF 33
#define BETAU0GF 34
#define BETAU1GF 35
#define BETAU2GF 36
#define ALPHAGF 37
#define GAMMA_FACEUU00GF 38
#define GAMMA_FACEUU01GF 39
#define GAMMA_FACEUU02GF 40
#define GAMMA_FACEUU11GF 41
#define GAMMA_FACEUU12GF 42
#define GAMMA_FACEUU22GF 43
#define GAMMAUU00GF 44
#define GAMMAUU01GF 45
#define GAMMAUU02GF 46
#define GAMMAUU11GF 47
#define GAMMAUU12GF 48
#define GAMMAUU22GF 49
#define NUM_AUXEVOL_GFS 50

#define AD0GF 0
#define AD1GF 1
#define AD2GF 2
#define NUM_EVOL_GFS 3
```

    Appending to Start-to-Finish-UnitTests/Afield_flux_UnitTest//Afield_flux_unit_test.c

Now, we'll handle the different A2B codes. There are several things to do here. First, we'll add `#include`s to the C code so that we have access to the functions we want to test. We must also create a directory and copy the files to that directory. We will choose to do this in the subfolder `A2B` relative to this tutorial.

```python
%%writefile -a $Ccodesdir/Afield_flux_unit_test.c

// Some specific definitions needed for this file
typedef struct __gf_and_gz_struct__ {
  REAL *gf;
  int gz_lo[4],gz_hi[4];
} gf_and_gz_struct;

const int VX=0,VY=1,VZ=2,BX=3,BY=4,BZ=5;
const int NUM_RECONSTRUCT_GFS = 6;

#include "PPM/reconstruct_set_of_prims_PPM_GRFFE_NRPy.c"
#include "PPM/loop_defines_reconstruction_NRPy.h"
#include "FCVAL/interpolate_metric_gfs_to_cell_faces.h"

#include "calculate_metric_gfs.h"
#include "calculate_BU.h"
#include "calculate_ValenciavU.h"
#include "calculate_E_exactD.h"

// These are the functions we want to test.
#include "RHSs/calculate_E_field_flat_all_in_one.h"

```

    Appending to Start-to-Finish-UnitTests/Afield_flux_UnitTest//Afield_flux_unit_test.c

Now, we'll write the main method. First, we'll set up the grid. In this test, we cannot use only one point. To properly perform reconstruction and have a point at the center of the grid that's ideal for convergence testing, we'll need at least a $9 \times 9 \times 9$ grid; for the higher resolution, we'll double the interior points in one direction, yielding a $11 \times 11 \times 11$ grid. Then, we'll write the magnetic fields and drift velocity. After that, we'll calculate the electric field contribution to the $A_i$ RHS in two ways.

```python
%%writefile -a $Ccodesdir/Afield_flux_unit_test.c

int main(int argc, const char *argv[]) {
    paramstruct params;
#include "set_Cparameters_default.h"

    // Step 0c: Set free parameters, overwriting Cparameters defaults
    //          by hand or with command-line input, as desired.
#include "free_parameters.h"
#include "set_Cparameters-nopointer.h"

    // We'll define our grid slightly different from how we normally would. We let our outermost
    // ghostzones coincide with xxmin and xxmax instead of the interior of the grid. This means
    // that the ghostzone points will have identical positions so we can do convergence tests of them.    // Step 0d.ii: Set up uniform coordinate grids
    REAL *xx[3];
    xx[0] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS0);
    xx[1] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS1);
    xx[2] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS2);
    for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) xx[0][j] = xxmin[0] + (j-NGHOSTS)*dxx0;
    for(int j=0;j<Nxx_plus_2NGHOSTS1;j++) xx[1][j] = xxmin[1] + (j-NGHOSTS)*dxx1;
    for(int j=0;j<Nxx_plus_2NGHOSTS2;j++) xx[2][j] = xxmin[2] + (j-NGHOSTS)*dxx2;

    //for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) printf("x[%d] = %.5e\n",j,xx[0][j]);

    // This is the array to which we'll write the NRPy+ variables.
    REAL *auxevol_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);
    REAL *rhs_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);
    REAL *rhs_exact_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_EVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);

    a = (double)(rand()%20-10);
    b = (double)(rand()%20-10);
    c = (double)(rand()%20-10);
    d = (double)(rand()%20-10);
    e = (double)(rand()%20-10);
    f = (double)(rand()%20-10);
    g = (double)(rand()%20-10);
    h = (double)(rand()%20-10);
    l = (double)(rand()%20-10);
    m = (double)(rand()%20-10);
    n = (double)(rand()%20-10);
    o = (double)(rand()%20-10);
    p = (double)(rand()%20-10);
    q = (double)(rand()%20-10);
    r = (double)(rand()%20-10);
    s = (double)(rand()%20-10);
    t = (double)(rand()%20-10);
    //printf("a,b,c,d,e,f = %f,%f,%f,%f,%f,%f\n",a,b,c,d,e,f);

    // First, calculate the test metric on our grid:
    calculate_metric_gfs(&params,xx,auxevol_gfs);

    // Calculate the initial data and the exact solution.
    calculate_BU(&params,xx,auxevol_gfs);
    calculate_ValenciavU(&params,xx,auxevol_gfs);
    //for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) xx[0][j] -= 0.5*dxx0;
    calculate_E_exactD(&params,xx,auxevol_gfs,rhs_exact_gfs);

    // Set up the pointers and constants for the reconstruction procedure.
    gf_and_gz_struct in_prims[NUM_RECONSTRUCT_GFS], out_prims_r[NUM_RECONSTRUCT_GFS], out_prims_l[NUM_RECONSTRUCT_GFS];

    const int Nxxp2NG012 = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;
    REAL temporary[Nxxp2NG012];

    int ww=0;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*VALENCIAVU0GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_RU0GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_LU0GF;
    ww++;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*VALENCIAVU1GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_RU1GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_LU1GF;
    ww++;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*VALENCIAVU2GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_RU2GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*VALENCIAV_LU2GF;
    ww++;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*BU0GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*B_RU0GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*B_LU0GF;
    ww++;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*BU1GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*B_RU1GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*B_LU1GF;
    ww++;
    in_prims[ww].gf      = auxevol_gfs + Nxxp2NG012*BU2GF;
      out_prims_r[ww].gf = auxevol_gfs + Nxxp2NG012*B_RU2GF;
      out_prims_l[ww].gf = auxevol_gfs + Nxxp2NG012*B_LU2GF;
    ww++;

    // Prims are defined AT ALL GRIDPOINTS, so we set the # of ghostzones to zero:
    for(int i=0;i<NUM_RECONSTRUCT_GFS;i++) for(int j=1;j<=3;j++) { in_prims[i].gz_lo[j]=0; in_prims[i].gz_hi[j]=0; }
    // Left/right variables are not yet defined, yet we set the # of gz's to zero by default:
    for(int i=0;i<NUM_RECONSTRUCT_GFS;i++) for(int j=1;j<=3;j++) { out_prims_r[i].gz_lo[j]=0; out_prims_r[i].gz_hi[j]=0; }
    for(int i=0;i<NUM_RECONSTRUCT_GFS;i++) for(int j=1;j<=3;j++) { out_prims_l[i].gz_lo[j]=0; out_prims_l[i].gz_hi[j]=0; }

   int which_prims_to_reconstruct[6] = {VX, VY, VZ, BX, BY, BZ};
   int num_prims_to_reconstruct = 6;

    // In each direction, perform the PPM reconstruction procedure.
    // Then, add the fluxes to the RHS as appropriate.
    for(int flux_dirn=0;flux_dirn<3;flux_dirn++) {
        // This function is housed in the file: "reconstruct_set_of_prims_PPM_GRFFE_NRPy.c"
        interpolate_metric_gfs_to_cell_faces(&params,auxevol_gfs,flux_dirn+1);
        reconstruct_set_of_prims_PPM_GRFFE_NRPy(&params, auxevol_gfs, flux_dirn+1, num_prims_to_reconstruct,
                                                which_prims_to_reconstruct, in_prims, out_prims_r, out_prims_l, temporary);
        for(int count=0;count<=1;count++) {
            // This function is written to be general, using notation that matches the forward permutation added to AD2,
            // i.e., [F_HLL^x(B^y)]_z corresponding to flux_dirn=0, count=1.
            // The SIGN parameter is necessary because
            // -E_z(x_i,y_j,z_k) = 0.25 ( [F_HLL^x(B^y)]_z(i+1/2,j,k)+[F_HLL^x(B^y)]_z(i-1/2,j,k)
            //                           -[F_HLL^y(B^x)]_z(i,j+1/2,k)-[F_HLL^y(B^x)]_z(i,j-1/2,k) )
            // Note the negative signs on the reversed permutation terms!

            // By cyclically permuting with flux_dirn, we
            // get contributions to the other components, and by incrementing count, we get the backward permutations:
            // Let's suppose flux_dirn = 0. Then we will need to update Ay (count=0) and Az (count=1):
            //     flux_dirn=count=0 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (0+1+0)%3=AD1GF <- Updating Ay!
            //        (flux_dirn)%3 = (0)%3 = 0               Vx
            //        (flux_dirn-count+2)%3 = (0-0+2)%3 = 2   Vz .  Inputs Vx, Vz -> SIGN = -1 ; 2.0*((REAL)count)-1.0=-1 check!
            //     flux_dirn=0,count=1 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (0+1+1)%3=AD2GF <- Updating Az!
            //        (flux_dirn)%3 = (0)%3 = 0               Vx
            //        (flux_dirn-count+2)%3 = (0-1+2)%3 = 1   Vy .  Inputs Vx, Vy -> SIGN = +1 ; 2.0*((REAL)count)-1.0=2-1=+1 check!
            // Let's suppose flux_dirn = 1. Then we will need to update Az (count=0) and Ax (count=1):
            //     flux_dirn=1,count=0 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (1+1+0)%3=AD2GF <- Updating Az!
            //        (flux_dirn)%3 = (1)%3 = 1               Vy
            //        (flux_dirn-count+2)%3 = (1-0+2)%3 = 0   Vx .  Inputs Vy, Vx -> SIGN = -1 ; 2.0*((REAL)count)-1.0=-1 check!
            //     flux_dirn=count=1 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (1+1+1)%3=AD0GF <- Updating Ax!
            //        (flux_dirn)%3 = (1)%3 = 1               Vy
            //        (flux_dirn-count+2)%3 = (1-1+2)%3 = 2   Vz .  Inputs Vy, Vz -> SIGN = +1 ; 2.0*((REAL)count)-1.0=2-1=+1 check!
            // Let's suppose flux_dirn = 2. Then we will need to update Ax (count=0) and Ay (count=1):
            //     flux_dirn=2,count=0 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (2+1+0)%3=AD0GF <- Updating Ax!
            //        (flux_dirn)%3 = (2)%3 = 2               Vz
            //        (flux_dirn-count+2)%3 = (2-0+2)%3 = 1   Vy .  Inputs Vz, Vy -> SIGN = -1 ; 2.0*((REAL)count)-1.0=-1 check!
            //     flux_dirn=2,count=1 -> AD0GF+(flux_dirn+1+count)%3 = AD0GF + (2+1+1)%3=AD1GF <- Updating Ay!
            //        (flux_dirn)%3 = (2)%3 = 2               Vz
            //        (flux_dirn-count+2)%3 = (2-1+2)%3 = 0   Vx .  Inputs Vz, Vx -> SIGN = +1 ; 2.0*((REAL)count)-1.0=2-1=+1 check!
            calculate_E_field_flat_all_in_one(&params,
              &auxevol_gfs[IDX4ptS(VALENCIAV_RU0GF+(flux_dirn)%3, 0)],&auxevol_gfs[IDX4ptS(VALENCIAV_RU0GF+(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(VALENCIAV_LU0GF+(flux_dirn)%3, 0)],&auxevol_gfs[IDX4ptS(VALENCIAV_LU0GF+(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(B_RU0GF        +(flux_dirn)%3, 0)],&auxevol_gfs[IDX4ptS(B_RU0GF        +(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(B_LU0GF        +(flux_dirn)%3, 0)],&auxevol_gfs[IDX4ptS(B_LU0GF        +(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(B_RU0GF        +(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(B_LU0GF        +(flux_dirn-count+2)%3, 0)],
              &auxevol_gfs[IDX4ptS(GAMMA_FACEDD00GF,0)],&auxevol_gfs[IDX4ptS(GAMMA_FACEDD01GF,0)],&auxevol_gfs[IDX4ptS(GAMMA_FACEDD02GF,0)],
              &auxevol_gfs[IDX4ptS(GAMMA_FACEDD11GF,0)],&auxevol_gfs[IDX4ptS(GAMMA_FACEDD12GF,0)],&auxevol_gfs[IDX4ptS(GAMMA_FACEDD22GF,0)],
              &auxevol_gfs[IDX4ptS(BETA_FACEU0GF+flux_dirn,0)],&auxevol_gfs[IDX4ptS(BETA_FACEU0GF+(flux_dirn-count+2)%3,0)],&auxevol_gfs[IDX4ptS(ALPHA_FACEGF,0)],
              &rhs_gfs[IDX4ptS(AD0GF+(flux_dirn+1+count)%3,0)], 2.0*((REAL)count)-1.0, flux_dirn);
        }
    }

    char filename[100];
    sprintf(filename,"out%d-numer.txt",Nxx0);
    FILE *out2D = fopen(filename, "w");
    // We print the difference between approximate and exact numbers.
    int i0 = Nxx_plus_2NGHOSTS0/2;
    int i1 = Nxx_plus_2NGHOSTS1/2;
    int i2 = Nxx_plus_2NGHOSTS2/2;

    printf("Numerical: %.15e\n",rhs_gfs[IDX4S(AD0GF,i0,i1,i2)]);
    printf("Analytic:  %.15e\n",rhs_exact_gfs[IDX4S(AD0GF,i0,i1,i2)]);
    printf("Numerical: %.15e\n",rhs_gfs[IDX4S(AD1GF,i0,i1,i2)]);
    printf("Analytic:  %.15e\n",rhs_exact_gfs[IDX4S(AD1GF,i0,i1,i2)]);
    printf("Numerical: %.15e\n",rhs_gfs[IDX4S(AD2GF,i0,i1,i2)]);
    printf("Analytic:  %.15e\n\n",rhs_exact_gfs[IDX4S(AD2GF,i0,i1,i2)]);

    //printf("i0,i1,i2 = %d,%d,%d\n",i0,i1,i2);
    fprintf(out2D,"%.16e\t%.16e\t%.16e\t %e %e %e\n",
            rhs_exact_gfs[IDX4S(AD0GF,i0,i1,i2)]-rhs_gfs[IDX4S(AD0GF,i0,i1,i2)],
            rhs_exact_gfs[IDX4S(AD1GF,i0,i1,i2)]-rhs_gfs[IDX4S(AD1GF,i0,i1,i2)],
            rhs_exact_gfs[IDX4S(AD2GF,i0,i1,i2)]-rhs_gfs[IDX4S(AD2GF,i0,i1,i2)],
            xx[0][i0],xx[1][i1],xx[2][i2]
            );
    fclose(out2D);
}

```

    Appending to Start-to-Finish-UnitTests/Afield_flux_UnitTest//Afield_flux_unit_test.c

## Step 2.a: Compile and run the code

$$\label{compile_run}$$

Now that we have our file, we can compile it and run the executable.

```python
cmd.C_compile(os.path.join(Ccodesdir,"Afield_flux_unit_test.c"), os.path.join(outdir,"Afield_flux_unit_test"))

# Change to output directory
os.chdir(outdir)

cmd.Execute(os.path.join("Afield_flux_unit_test"),"2 2 2","out2.txt")
# To do a convergence test, we'll also need a second grid with twice the resolution.
cmd.Execute(os.path.join("Afield_flux_unit_test"),"4 4 4","out4.txt")
```

    Compiling executable...
    (EXEC): Executing `gcc -Ofast -fopenmp -march=native -funroll-loops Start-to-Finish-UnitTests/Afield_flux_UnitTest/Afield_flux_unit_test.c -o Start-to-Finish-UnitTests/Afield_flux_UnitTest/output/Afield_flux_unit_test -lm`...
    (BENCH): Finished executing in 0.6064903736114502 seconds.
    Finished compilation.
    (EXEC): Executing `taskset -c 0,1,2,3,4,5 ./Afield_flux_unit_test 2 2 2`...
    (BENCH): Finished executing in 0.20341801643371582 seconds.
    (EXEC): Executing `taskset -c 0,1,2,3,4,5 ./Afield_flux_unit_test 4 4 4`...
    (BENCH): Finished executing in 0.20840764045715332 seconds.

# Step 3: Code validation: Verify that relative error in numerical solution converges to zero at the expected order \[Back to [top](#toc)\]
$$\label{convergence}$$

For testing purposes, we have only checked these algorithms on a small grid. By construction, we have only guaranteed ourselves output from the functions we are testing at a point, so we will simply print the convergence order at that point after processing our outputs below.

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

Data1 = np.loadtxt("out2-numer.txt")
Data2 = np.loadtxt("out4-numer.txt")
""
def conv_order(lowres,highres):
    order = np.log2(np.abs(lowres/highres))
    if Use_Shock_Data:
        if order < 0.7 or order > 2.3:
            sys.exit(1)
    else:
        if order < 1.7 or order > 2.3:
            sys.exit(1)
    return order

print("The following quantities converge at the listed order (should be ~1 for Shock data, ~2 otherwise):")
print("A_rhsD0: "+str(conv_order(Data1[0],Data2[0])))
print("A_rhsD1: "+str(conv_order(Data1[1],Data2[1])))
print("A_rhsD2: "+str(conv_order(Data1[2],Data2[2])))
```

    The following quantities converge at the listed order (should be ~1 for Shock data, ~2 otherwise):
    A_rhsD0: 2.0234555321265675
    A_rhsD1: 1.0081097780531285
    A_rhsD2: 1.1291244848636108

# Step 4: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]

$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-A2B.pdf](Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-A2B.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface

# Change to NRPy directory
os.chdir("../../../")

cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-Afield_flux",location_of_template_file=os.path.join(".."))
```

    Created Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-Afield_flux.tex, and
        compiled LaTeX file to PDF file Tutorial-Start_to_Finish_UnitTest-
        GiRaFFE_NRPy-Afield_flux.pdf