# $\texttt{GiRaFFE}$: General Relativistic Force-Free Electrodynamics
$$\label{top}$$

## Authors: Patrick Nelson, Zach Etienne, George Vopal & Maria Babiuc-Hamilton
### Formatting improvements courtesy Brandon Clark

[comment]: <> (Abstract: TODO)

**Module Status:**  In progress

**Validation Notes:** This module is under active development -- do ***not*** use the resulting C code output for doing science.

### NRPy+ Source Code for this module: [GiRaFFE_HO/GiRaFFE_Higher_Order.py](../edit/GiRaFFE_HO/GiRaFFE_Higher_Order.py)

## Introduction:

The original $\texttt{GiRaFFE}$ code, as presented in [the original paper](https://arxiv.org/pdf/1704.00599.pdf), exists as a significant modification to $\texttt{IllinoisGRMHD}$. As such, it used a third-order reconstruction algorithm with a slope limiter (Colella et al's piecewise parabolic method, or PPM) to handle spatial derivatives in the general relativistic force-free electrodynamics (GRFFE) equations. However the GRFFE equations do not generally permit shocks, so a more optimal approach would involve finite differencing all derivatives in the GRFFE equations. As it happens, NRPy+ was designed to generate C codes involving complex tensorial expressions and finite difference spatial derivatives, with finite-differencing order a freely-specifiable parameter.

The purpose of this notebook is to rewrite the equations of GRFFE as used in the original $\texttt{GiRaFFE}$ code so that all derivatives that appear are represented numerically as finite difference derivatives. As we will see, the largest complication stems from derivatives of magnetic fields--requiring judicious application of the chain and product rules.

The GRFFE evolution equations (from eq. 13 of the [original paper](https://arxiv.org/pdf/1704.00599.pdf)) we wish to encode in the NRPy+ version of $\texttt{GiRaFFE}$ are as follows:

* $\partial_t \tilde{S}_i = - \partial_j \left( \alpha \sqrt{\gamma} T^j_{{\rm EM} i} \right) + \frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}$: [Link to Step 2](#4metric)
* $\partial_t A_i = \epsilon_{ijk} v^j B^k - \partial_i (\alpha \Phi - \beta^j A_j)$: [Link to Step 5](#construct_ai)
* $\partial_t [\sqrt{\gamma} \Phi] = -\partial_j (\alpha\sqrt{\gamma}A^j - \beta^j [\sqrt{\gamma} \Phi]) - \xi \alpha [\sqrt{\gamma} \Phi]$: [Link to Step 5](#construct_ai)

Here, the densitized spatial Poynting flux one-form $\tilde{S}_i = \sqrt{\gamma} S_i$ (and $S_i$ comes from $S_{\mu} -n_{\nu} T^{\nu}_{{\rm EM} \mu}$), and $(\Phi, A_i)$ is the vector potential. We will solve these PDEs using the method of lines, where the right-hand sides of these equations (involving no explicit time derivatives) will be constructed using NRPy+.

### A Note on Notation:

As is standard in NRPy+,

* Greek indices refer to four-dimensional quantities where the zeroth component indicates temporal (time) component.
* Latin indices refer to three-dimensional quantities. This is somewhat counterintuitive since Python always indexes its lists starting from 0. As a result, the zeroth component of three-dimensional quantities will necessarily indicate the first *spatial* direction.

For instance, in calculating the first term of [$T_{\rm EM}^{\mu\nu}$](#em_tensor) (specifically, ${\rm Term\ 1} = b^2 u^\mu u^\nu$), we use Greek indices:

```
T4EMUU = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        # Term 1: b^2 u^{\mu} u^{\nu}
        T4EMUU[mu][nu] = smallb2*u4U[mu]*u4U[nu]\
```

When we calculate [$\beta_i = \gamma_{ij} \beta^j$](#4metric), we use Latin indices:
```
betaD = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        betaD[i] += gammaDD[i][j] * betaU[j]
```

As a corollary, any expressions involving mixed Greek and Latin indices will need to offset one set of indices by one: A Latin index in a four-vector will be incremented and a Greek index in a three-vector will be decremented (however, the latter case does not occur in this tutorial notebook). This can be seen when we handle the second term of [$\partial_t \tilde{S}_i$](#construct_si) (or, more specifically, the second term thereof: $\frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}$):
```
# The second term: \alpha \sqrt{\\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu} / 2
for i in range(DIM):
    for mu in range(4):
        for nu in range(4):
            Stilde_rhsD[i] += alpsqrtgam * T4EMUU[mu][nu] * g4DDdD[mu][nu][i+1] / 2
```

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

This notebook is organized as follows

1. [Step 1](#initializenrpy): Set up the needed NRPy+ infrastructure and declare core gridfunctions used by $\texttt{GiRaFFE}$
1. [Step 2](#4metric): Build the four metric $g_{\mu\nu}$, its inverse $g^{\mu\nu}$ and spatial derivatives $g_{\mu\nu,i}$ from ADM 3+1 quantities $\gamma_{ij}$, $\beta^i$, and $\alpha$
1. [Step 3](#t_derivatives): $T^{\mu\nu}_{\rm EM}$ and its derivatives
    1. [Step 3.a](#uibi): $u^i$ and $b^i$ and related quantities
    1. [Step 3.b](#em_tensor): Construct all components of the electromagnetic stress-energy tensor $T^{\mu \nu}_{\rm EM}$
    1. [Step 3.c](#em_tensor_derivatives): Derivatives of the electromagnetic stress-energy tensor
        1. [Step 3.c.i](#capitalbideriv): Derivatives of $B^i$
        1. [Step 3.c.ii](#bideriv):  Derivatives of $b^i$
        1. [Step 3.c.iii](#b2deriv): Derivative of $b^2$
        1. [Step 3.c.iv](#gupijderiv): Derivatives of $g^{\mu\nu}$
    1. [Step 3.d](#alltogether): Putting it together: $\partial_j T^{j}_{{\rm EM} i}$
        1. [Step 3.d.i](#alltogether1): Putting it together: Term 1
        1. [Step 3.d.ii](#alltogether2): Putting it together: Term 2
        1. [Step 3.d.iii](#alltogether3): Putting it together: Term 3
1. [Step 4](#construct_si): Construct the evolution equation for $\tilde{S}_i$
1. [Step 5](#construct_ai): Construct the evolution equations for $A_i$ and $[\sqrt{\gamma}\Phi]$
    1. [Step 5.a](#aux): Construct some useful auxiliary gridfunctions for the other evolution equations
    1. [Step 5.b](#complete_construct_ai): Complete the construction of the evolution equations for $A_i$ and $[\sqrt{\gamma}\Phi]$
1. [Step 6](#code_validation): Code Validation against `GiRaFFE_HO.GiRaFFE_Higher_Order` NRPy+ Module
1. [Step 7](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set up the needed NRPy+ infrastructure and declare core gridfunctions used by $\texttt{GiRaFFE}$ \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

1. Set some basic NRPy+ parameters. E.g., set the spatial dimension parameter to 3 and the finite differencing order to 4.
1. Next, declare some gridfunctions that are provided as input to the equations:
    1. $\alpha$, $\beta^i$, and $\gamma_{ij}$: These ADM 3+1 metric quantities are declared in the ADMBase Einstein Toolkit thorn, and are assumed to be made available to $\texttt{GiRaFFE}$ at this stage.
    1. The Valencia 3-velocity $v^i_{(n)}$ and vector potential $A_i$: Declared by $\texttt{GiRaFFE}$, and will have their initial values set in the separate thorn **GiRaFFEfood_HO**.
    1. The magnetic field as measured by a normal observer $B^i$: The quantities evolved forward in time in $\texttt{GiRaFFE}$ do not include the Valencia 3-velocity, so this quantity is not automatically updated. Instead, we compute it based on the evolved quantity $\tilde{S}_i$ and $B^i = \epsilon^{ijk} \partial_j A_k$ (where $A_k$ is another evolved quantity and $\epsilon^{ijk}$ is the Levi-Civita tensor). $B^i$ is evaluated using finite differences of $A_k$ in a separate function, though it can only be evaluated consistently on the interior of the grid. In the ghost zones, we will have to use lower-order derivatives.

```python
import NRPy_param_funcs as par
import indexedexp as ixp
import grid as gri
import finite_difference as fin
from outputC import *

#Step 1.0: Set the spatial dimension parameter to 3.
par.set_parval_from_str("grid::DIM", 3)
DIM = par.parval_from_str("grid::DIM")

# Step 1.1: Set the finite differencing order to 4.
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", 4)

thismodule = "GiRaFFE_NRPy"

# M_PI will allow the C code to substitute the correct value
M_PI = par.Cparameters("#define",thismodule,"M_PI","")
# ADMBase defines the 4-metric in terms of the 3+1 spacetime metric quantities gamma_{ij}, beta^i, and alpha
gammaDD = ixp.register_gridfunctions_for_single_rank2("AUX","gammaDD", "sym01",DIM=3)
betaU   = ixp.register_gridfunctions_for_single_rank1("AUX","betaU",DIM=3)
alpha   = gri.register_gridfunctions("AUX","alpha")
# GiRaFFE uses the Valencia 3-velocity and A_i, which are defined in the initial data module(GiRaFFEfood)
ValenciavU = ixp.register_gridfunctions_for_single_rank1("AUX","ValenciavU",DIM=3)
AD = ixp.register_gridfunctions_for_single_rank1("EVOL","AD",DIM=3)
# B^i must be computed at each timestep within GiRaFFE so that the Valencia 3-velocity can be evaluated
BU = ixp.register_gridfunctions_for_single_rank1("AUX","BU",DIM=3)
```

# Step 2: Build the four metric $g_{\mu\nu}$, its inverse $g^{\mu\nu}$ and spatial derivatives $g_{\mu\nu,i}$ from ADM 3+1 quantities $\gamma_{ij}$, $\beta^i$, and $\alpha$ \[Back to [top](#toc)\]
$$\label{4metric}$$

Notice that the time evolution equation for $\tilde{S}_i$
$$
\partial_t \tilde{S}_i = - \partial_j \left( \alpha \sqrt{\gamma} T^j_{{\rm EM} i} \right) + \frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}
$$
contains $\partial_i g_{\mu \nu} = g_{\mu\nu,i}$. We will now focus on evaluating this term.

The four-metric $g_{\mu\nu}$ is related to the three-metric $\gamma_{ij}$, index-lowered shift $\beta_i$, and lapse $\alpha$ by
$$
g_{\mu\nu} = \begin{pmatrix}
-\alpha^2 + \beta^k \beta_k & \beta_j \\
\beta_i & \gamma_{ij}
\end{pmatrix}.
$$
This tensor and its inverse have already been built by the u0_smallb_Poynting__Cartesian.py module ([documented here](Tutorial-u0_smallb_Poynting-Cartesian.ipynb)), so we can simply load the module and import the variables.

```python
# Step 1.2: import u0_smallb_Poynting__Cartesian.py to set
#           the four metric and its inverse. This module also sets b^2 and u^0.
import u0_smallb_Poynting__Cartesian.u0_smallb_Poynting__Cartesian as u0b
u0b.compute_u0_smallb_Poynting__Cartesian(gammaDD,betaU,alpha,ValenciavU,BU)

betaD = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        betaD[i] += gammaDD[i][j] * betaU[j]
# Error check: fixed = to +=

# We will now pull in the four metric and its inverse.
g4DD = ixp.zerorank2(DIM=4)
g4UU = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        g4DD[mu][nu] = u0b.g4DD[mu][nu]
        g4UU[mu][nu] = u0b.g4UU[mu][nu]
```

Next we compute spatial derivatives of the metric, $\partial_i g_{\mu\nu} = g_{\mu\nu,i}$, written in terms of the three-metric, shift, and lapse. Simply taking the derivative of the expression for $g_{\mu\nu}$ above, we find
$$
g_{\mu\nu,l} = \begin{pmatrix}
-2\alpha \alpha_{,l} + \beta^k_{\ ,l} \beta_k + \beta^k \beta_{k,l} & \beta_{j,l} \\
\beta_{i,l} & \gamma_{ij,l}
\end{pmatrix}.
$$

Notice the derivatives of the shift vector with its indexed lowered, $\beta_{i,j} = \partial_j \beta_i$. This can be easily computed in terms of the given ADMBase quantities $\beta^i$ and $\gamma_{ij}$ via:
\begin{align}
\beta_{i,j} &= \partial_j \beta_i \\
            &= \partial_j (\gamma_{ik} \beta^k) \\
            &= \gamma_{ik} \partial_j\beta^k + \beta^k \partial_j \gamma_{ik} \\
\beta_{i,j} &= \gamma_{ik} \beta^k_{\ ,j} + \beta^k \gamma_{ik,j}.
\end{align}

Since this expression mixes Greek and Latin indices, we will declare this as a 4 dimensional quantity, but only set the three spatial components of its last index (that is, leaving $l=0$ unset).

So, we will first set
$$ g_{00,l} = \underbrace{-2\alpha \alpha_{,l}}_{\rm Term\ 1} + \underbrace{\beta^k_{\ ,l} \beta_k}_{\rm Term\ 2} + \underbrace{\beta^k \beta_{k,l}}_{\rm Term\ 3} $$

```python
# Step 1.2, cont'd: Build spatial derivatives of the four metric
# Step 1.2.a: Declare derivatives of grid functions. These will be handled by FD_outputC
alpha_dD   = ixp.declarerank1("alpha_dD")
betaU_dD   = ixp.declarerank2("betaU_dD","nosym")
gammaDD_dD = ixp.declarerank3("gammaDD_dD","sym01")

# Step 1.2.b: These derivatives will be constructed analytically.
betaDdD    = ixp.zerorank2()
g4DDdD     = ixp.zerorank3(DIM=4)

for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            # \gamma_{ik} \beta^k_{,j} + \beta^k \gamma_{ik,j}
            betaDdD[i][j] += gammaDD[i][k] * betaU_dD[k][j] + betaU[k] * gammaDD_dD[i][k][j]
#Error check: changed = to += above

# Step 1.2.c: Set the 00 components
# Step 1.2.c.i: Term 1: -2\alpha \alpha_{,l}
for l in range(DIM):
    g4DDdD[0][0][l+1] = -2*alpha*alpha_dD[l]

# Step 1.2.c.ii: Term 2: \beta^k_{\ ,l} \beta_k
for l in range(DIM):
    for k in range(DIM):
        g4DDdD[0][0][l+1] += betaU_dD[k][l] * betaD[k]

# Step 1.2.c.iii: Term 3: \beta^k \beta_{k,l}
for l in range(DIM):
    for k in range(DIM):
        g4DDdD[0][0][l+1] += betaU[k] * betaDdD[k][l]
```

Now we will contruct the other components of $g_{\mu\nu,l}$. We will first construct
$$ g_{i0,l} = g_{0i,l} = \beta_{i,l}, $$
then
$$ g_{ij,l} = \gamma_{ij,l} $$

```python
# Step 1.2.d: Set the i0 and 0j components
for l in range(DIM):
    for i in range(DIM):
        # \beta_{i,l}
        g4DDdD[i+1][0][l+1] = g4DDdD[0][i+1][l+1] = betaDdD[i][l]

#Step 1.2.e: Set the ij components
for l in range(DIM):
    for i in range(DIM):
        for j in range(DIM):
            # \gamma_{ij,l}
            g4DDdD[i+1][j+1][l+1] = gammaDD_dD[i][j][l]
```

# Step 3: $T^{\mu\nu}_{\rm EM}$ and its derivatives \[Back to [top](#toc)\]
$$\label{t_derivatives}$$

Now that the metric and its derivatives are out of the way, let's return to the evolution equation for $\tilde{S}_i$,
$$
\partial_t \tilde{S}_i = - \partial_j \left( \alpha \sqrt{\gamma} T^j_{{\rm EM} i} \right) + \frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}.
$$
We turn our focus now to $T^j_{{\rm EM} i}$ and its derivatives. To this end, we start by computing $T^{\mu \nu}_{\rm EM}$ (from eq. 27 of [Paschalidis & Shapiro's paper on their GRFFE code](https://arxiv.org/pdf/1310.3274.pdf)):

$$\boxed{T^{\mu \nu}_{\rm EM} = b^2 u^{\mu} u^{\nu} + \frac{b^2}{2} g^{\mu \nu} - b^{\mu} b^{\nu}.}$$

Notice that $T^{\mu\nu}_{\rm EM}$ is written in terms of

* $b^\mu$, the 4-component magnetic field vector, related to the comoving magnetic field vector $B^i_{(u)}$
* $u^\mu$, the 4-velocity
* $g^{\mu \nu}$, the inverse 4-metric

However, $\texttt{GiRaFFE}$ has access to only the following quantities, requiring in the following sections that we write the above quantities in terms of the following ones:

* $\gamma_{ij}$, the 3-metric
* $\alpha$, the lapse
* $\beta^i$, the shift
* $A_i$, the vector potential
* $B^i$, the magnetic field (we assume only in the grid interior, not the ghost zones)
* $\left[\sqrt{\gamma}\Phi\right]$, the zero-component of the vector potential $A_\mu$, times the square root of the determinant of the 3-metric
* $v_{(n)}^i$, the Valencia 3-velocity
* $u^0$, the zero-component of the 4-velocity

## Step 3.a: $u^i$ and $b^i$ and related quantities \[Back to [top](#toc)\]
$$\label{uibi}$$

We begin by importing what we can from [u0_smallb_Poynting__Cartesian.py](../edit/u0_smallb_Poynting__Cartesian/u0_smallb_Poynting__Cartesian.py). We will need the four-velocity $u^\mu$, which is related to the Valencia 3-velocity $v^i_{(n)}$ used directly by $\texttt{GiRaFFE}$ (see also [Duez, et al, eqs. 53 and 56](https://arxiv.org/pdf/astro-ph/0503420.pdf))
\begin{align}
u^i &= u^0 (\alpha v^i_{(n)} - \beta^i), \\
u_j &= \alpha u^0 \gamma_{ij} v^i_{(n)},
\end{align}
where $v^i_{(n)}$ is the Valencia three-velocity. These have already been constructed in terms of the Valencia 3-velocity and other 3+1 ADM quantities by the [u0_smallb_Poynting__Cartesian.py](../edit/u0_smallb_Poynting__Cartesian/u0_smallb_Poynting__Cartesian.py) module, so we can simply import these variables:

```python
# Step 2.0: u^i, b^i, and related quantities
# Step 2.0.a: import the four-velocity, as written in terms of the Valencia 3-velocity
uD = ixp.register_gridfunctions_for_single_rank1("AUX","uD")
uU = ixp.register_gridfunctions_for_single_rank1("AUX","uU")
u4upperZero = gri.register_gridfunctions("AUX","u4upperZero")

for i in range(DIM):
    uD[i] = u0b.uD[i].subs(u0b.u0,u4upperZero)
    uU[i] = u0b.uU[i].subs(u0b.u0,u4upperZero)

```

We also need the magnetic field 4-vector $b^{\mu}$, which is related to the magnetic field by [eqs. 23, 24, and 31 in Duez, et al](https://arxiv.org/pdf/astro-ph/0503420.pdf):
\begin{align}
b^0 &= \frac{1}{\sqrt{4\pi}} B^0_{\rm (u)} = \frac{u_j B^j}{\sqrt{4\pi}\alpha}, \\
b^i &= \frac{1}{\sqrt{4\pi}} B^i_{\rm (u)} = \frac{B^i + (u_j B^j) u^i}{\sqrt{4\pi}\alpha u^0}, \\
\end{align}
where $B^i$ is the variable tracked by the HydroBase thorn in the Einstein Toolkit. Again, these have already been built by the [u0_smallb_Poynting__Cartesian.py](../edit/u0_smallb_Poynting__Cartesian/u0_smallb_Poynting__Cartesian.py) module, so we can simply import the variables.

```python
# Step 2.0.b: import the small b terms
smallb4U = ixp.zerorank1(DIM=4)
smallb4D = ixp.zerorank1(DIM=4)
for mu in range(4):
    smallb4U[mu] = u0b.smallb4U[mu].subs(u0b.u0,u4upperZero)
    smallb4D[mu] = u0b.smallb4D[mu].subs(u0b.u0,u4upperZero)

smallb2 = u0b.smallb2etk.subs(u0b.u0,u4upperZero)

```

## Step 3.b: Construct all components of the electromagnetic stress-energy tensor $T^{\mu \nu}_{\rm EM}$ \[Back to [top](#toc)\]
$$\label{em_tensor}$$

We now have all the pieces to calculate the stress-energy tensor,
$$T^{\mu \nu}_{\rm EM} = \underbrace{b^2 u^{\mu} u^{\nu}}_{\rm Term\ 1} +
\underbrace{\frac{b^2}{2} g^{\mu \nu}}_{\rm Term\ 2}
- \underbrace{b^{\mu} b^{\nu}}_{\rm Term\ 3}.$$
Because $u^0$ is a separate variable, we will create a temporary variable $u^\mu=\left( u^0, u^i \right)$.

```python
# Step 2.1: Construct the electromagnetic stress-energy tensor
# Step 2.1.a: Set up the four-velocity vector
u4U = ixp.zerorank1(DIM=4)
u4U[0] = u4upperZero
for i in range(DIM):
    u4U[i+1] = uU[i]

# Step 2.1.b: Build T4EMUU itself
T4EMUU = ixp.zerorank2(DIM=4)
for mu in range(4):
    for nu in range(4):
        # Term 1: b^2 u^{\mu} u^{\nu}
        T4EMUU[mu][nu] = smallb2*u4U[mu]*u4U[nu]

for mu in range(4):
    for nu in range(4):
        # Term 2: b^2 / 2 g^{\mu \nu}
        T4EMUU[mu][nu] += smallb2*g4UU[mu][nu]/2

for mu in range(4):
    for nu in range(4):
        # Term 3: -b^{\mu} b^{\nu}
        T4EMUU[mu][nu] += -smallb4U[mu]*smallb4U[nu]
```

## Step 3.c: Derivative components of the electromagnetic stress-energy tensor \[Back to [top](#toc)\]
$$\label{em_tensor_derivatives}$$

If we look at the evolution equation, we see that we will need spatial  derivatives of $T^{\mu\nu}_{\rm EM}$. When confronted with derivatives of complicated expressions, it is generally convenient to declare those expressions as gridfunctions themselves, allowing NRPy+ to take finite-difference derivatives of the expressions. This can even reduce the truncation error associated with the finite differences, because the alternative is to use a function of several finite-difference derivatives, allowing more error to accumulate than the extra gridfunction will introduce. While we will use that technique for some of the subexpressions of $T^{\mu\nu}_{\rm EM}$, we don't want to rely on it for the whole expression; doing so would require us to take the derivative of the magnetic field $B^i$, which is itself found by finite-differencing the vector potential $A_k$. Thus $B^i$ cannot be *consistently* defined in ghost zones. To potentially reduce numerical errors induced by inconsistent finite differencing, we will differentiate $T^{\mu\nu}_{\rm EM}$ term-by-term so that finite-difference derivatives of $A_i$ appear.

We will now now take these spatial derivatives of $T^{\mu\nu}_{\rm EM}$, applying the chain rule until it is only in terms of basic gridfunctions and their derivatives: $\alpha$, $\beta^i$, $\gamma_{ij}$, $A_k$, and the four-velocity $u^i$. Along the way, we will also set up useful temporary variables representing the steps of the chain rule. (Notably, *all* of these quantities will be written in terms of $A_i$ and its derivatives):

* $B^i$ (already computed in terms of $A_k$, via $B^i = \epsilon^{ijk} \partial_j A_k$),
* $B^i_{,l}$,
* $b^i$ and $b_i$ (already computed),
* $b^i_{,k}$,
* $b^2$ (already computed),
* and $\left(b^2\right)_{,j}$.

(The variables not already computed will not be seen by the ETK, as they are written in terms of $A_k$ and its derivatives; they simply help to organize the NRPy+ code.)

So then,
\begin{align}
\partial_j T^{j}_{{\rm EM} i} &= \partial_j (g_{\mu i} T^{\mu j}_{\rm EM}) \\
&= \partial_j \left[g_{\mu i} \left(b^2 u^j u^\mu + \frac{b^2}{2} g^{j\mu} - b^j b^\mu\right)\right] \\
&= \underbrace{g_{\mu i,j} T^{\mu j}_{\rm EM}}_{\rm Term\ A} + g_{\mu i} \left( \underbrace{\partial_j \left(b^2 u^j u^\mu \right)}_{\rm Term\ B} + \underbrace{\partial_j \left(\frac{b^2}{2} g^{j\mu}\right)}_{\rm Term\ C} - \underbrace{\partial_j \left(b^j b^\mu\right)}_{\rm Term\ D} \right) \\
\end{align}
Following the product and chain rules for each term, we find that
\begin{align}
{\rm Term\ B} &= \partial_j (b^2 u^j u^\mu) \\
              &= \partial_j b^2 u^j u^\mu + b^2 \partial_j u^j u^\mu + b^2 u^j \partial_j u^\mu \\
              &= \underbrace{\left(b^2\right)_{,j} u^j u^\mu}_{\rm To\ Term\ 3\ below} + \underbrace{b^2 u^j_{,j} u^\mu + b^2 u^j u^{\mu}_{,j}}_{\rm To\ Term\ 2\ below} \\
{\rm Term\ C} &= \partial_j \left(\frac{b^2}{2} g^{j\mu}\right) \\
              &= \frac{1}{2} \left( \partial_j b^2 g^{j\mu} + b^2 \partial_j g^{j\mu} \right) \\
              &= \underbrace{\frac{1}{2} \left(b^2\right)_{,j} g^{j\mu}}_{\rm To\ Term\ 3\ below} + \underbrace{\frac{b^2}{2} g^{j\mu}_{\ ,j}}_{\rm To\ Term\ 2\ below} \\
{\rm Term\ D} &= \partial_j (b^j b^\mu) \\
              &= \underbrace{b^j_{,j} b^\mu + b^j b^\mu_{,j}}_{\rm To\ Term\ 2\ below}\\
\end{align}

So,
\begin{align}
\partial_j T^{j}_{{\rm EM} i} &= g_{\mu i,j} T^{\mu j}_{\rm EM} \\
&+ g_{\mu i} \left(\left(b^2\right)_{,j} u^j u^\mu +b^2 u^j_{,j} u^\mu + b^2 u^j u^{\mu}_{,j} + \frac{1}{2}\left(b^2\right)_{,j} g^{j\mu} + \frac{b^2}{2} g^{j\mu}_{\ ,j} + b^j_{,j} b^\mu + b^j b^\mu_{,j}\right);
\end{align}
We will rearrange this once more, collecting the $\left(b^2\right)_{,j}$ terms together, noting that Term A above becomes Term 1 below:
\begin{align}
\partial_j  T^{j}_{{\rm EM} i} =& \
\underbrace{g_{\mu i,j} T^{\mu j}_{\rm EM}}_{\rm Term\ 1} \\
& + \underbrace{g_{\mu i} \left( b^2 u^j_{,j} u^\mu + b^2 u^j u^\mu_{,j} + \frac{b^2}{2} g^{j\mu}_{\ ,j} + b^j_{,j} b^\mu + b^j b^\mu_{,j} \right)}_{\rm Term\ 2} \\
& + \underbrace{g_{\mu i} \left( \left(b^2\right)_{,j} u^j u^\mu + \frac{1}{2} \left(b^2\right)_{,j} g^{j\mu} \right).}_{\rm Term\ 3} \\
\end{align}

**List of Derivatives**
$$\label{dlist}$$

Note that this is in terms of the derivatives of several other quantities:

* [Step 3.c.i](#capitalbideriv): $B^i_{,l}$: Since $b^i$ is itself a function of $B^i$, we will first need the derivatives $B^i_{,l}$ in terms of the evolved quantity $A_k$ (the vector potential).
* [Step 3.c.ii](#bideriv): $b^i_{,k}$: Once we have $B^i_{,l}$ we can evaluate derivatives of $b^i$, $b^i_{,k}$
* [Step 3.c.iii](#b2deriv): The derivative of $b^2 = g_{\mu\nu} b^\mu b^\nu$, $\left(b^2\right)_{,j}$
* [Step 3.c.iv](#gupijderiv): Derivatives of $g^{\mu\nu}$, $g^{\mu\nu}_{\ ,k}$

### Step 3.c.i: Derivatives of $B^i$ \[Back to [List of Derivatives](#dlist)\]
$$\label{capitalbideriv}$$

First, we will build the derivatives of the magnetic field. Since $b^i$ is a function of $B^i$, we will start from the definition of $B^i$ in terms of $A_k$, $B^i = \frac{[ijk]}{\sqrt{\gamma}} \partial_j A_k$ (eq. 18 of [the original paper](https://arxiv.org/pdf/1704.00599.pdf)). We will first apply the product rule, noting that the symbol $[ijk]$ consists purely of the integers $-1, 0, 1$ and thus can be treated as a constant in this process.
\begin{align}
B^i_{,l} &= \partial_l \left( \frac{[ijk]}{\sqrt{\gamma}} \partial_j A_k \right)  \\
         &= [ijk] \partial_l \left( \frac{1}{\sqrt{\gamma}}\right) \partial_j A_k + \frac{[ijk]}{\sqrt{\gamma}} \partial_l \partial_j A_k \\
         &= [ijk]\left(-\frac{\gamma_{,l}}{2\gamma^{3/2}}\right) \partial_j A_k + \frac{[ijk]}{\sqrt{\gamma}} \partial_l \partial_j A_k \\
         &= -\frac{\gamma_{,l}}{2\gamma} \left( \frac{[ijk]}{\sqrt{\gamma}} \partial_j A_k \right) + \frac{[ijk]}{\sqrt{\gamma}} \partial_l \partial_j A_k \\
\end{align}
Now, we will substitute back in for the definition of the Levi-Civita tensor: $\epsilon^{ijk} = [ijk] / \sqrt{\gamma}$. Then we will substitute the magnetic field $B^i$ back in.
\begin{align}
B^i_{,l} &= -\frac{\gamma_{,l}}{2\gamma} \epsilon^{ijk} \partial_j A_k + \epsilon^{ijk} \partial_l \partial_j A_k \\
         &= -\frac{\gamma_{,l}}{2\gamma} B^i + \epsilon^{ijk} A_{k,jl}, \\
\end{align}

Thus, the expression we are left with for the derivatives of the magnetic field is:
\begin{align}
B^i_{,l} &= \underbrace{-\frac{\gamma_{,l}}{2\gamma} B^i}_{\rm Term\ 1} + \underbrace{\epsilon^{ijk} A_{k,jl}}_{\rm Term\ 2}, \\
\end{align}
where $\epsilon^{ijk} = [ijk] / \sqrt{\gamma}$ is the antisymmetric Levi-Civita tensor and $\gamma$ is the determinant of the three-metric.

```python
# Step 2.2: Derivatives of the electromagnetic stress-energy tensor

# We already have a handy function to define the Levi-Civita symbol in WeylScalars
import WeylScal4NRPy.WeylScalars_Cartesian as weyl
# Initialize the Levi-Civita tensor by setting it equal to the Levi-Civita symbol
LeviCivitaSymbolDDD = weyl.define_LeviCivitaSymbol_rank3()
LeviCivitaTensorDDD = ixp.zerorank3()
LeviCivitaTensorUUU = ixp.zerorank3()

gammaUU = ixp.register_gridfunctions_for_single_rank2("AUX","gammaUU","sym01")
gammadet = gri.register_gridfunctions("AUX","gammadet")
gammaUU, gammadet = ixp.symm_matrix_inverter3x3(gammaDD)

for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            LeviCivitaTensorDDD[i][j][k] = LeviCivitaSymbolDDD[i][j][k] * sp.sqrt(gammadet)
            LeviCivitaTensorUUU[i][j][k] = LeviCivitaSymbolDDD[i][j][k] / sp.sqrt(gammadet)
# Error check: Was not actually setting LeviCivitaTensorDDD

AD_dD = ixp.declarerank2("AD_dD","nosym")

# Step 2.2.a: Construct the derivatives of the magnetic field.
gammadet_dD = ixp.declarerank1("gammadet_dD")

AD_dDD = ixp.declarerank3("AD_dDD","sym12")
# The other partial derivatives of B^i
BUdD = ixp.zerorank2()
for i in range(DIM):
    for l in range(DIM):
        # Term 1: -\gamma_{,l} / (2\gamma) B^i
        BUdD[i][l] = -gammadet_dD[l]*BU[i]/(2*gammadet)

for i in range(DIM):
    for l in range(DIM):
        for j in range(DIM):
            for k in range(DIM):
                # Term 2: \epsilon^{ijk} A_{k,jl}
                BUdD[i][l] += LeviCivitaTensorUUU[i][j][k] * AD_dDD[k][j][l]
```

### Step 3.c.ii: Derivatives of $b^i$ \[Back to [List of Derivatives](#dlist)\]
$$\label{bideriv}$$

Starting from the definition
$$
b^i = \frac{B^i + (u_j B^j) u^i}{\sqrt{4\pi}\alpha u^0},
$$
we will now compose the spatial derivatives: $b^i_{\ ,k}$.

First we apply the quotient rule:
\begin{align}
b^i_{,k} &= \frac{\left(\sqrt{4\pi}\alpha u^0\right) \partial_k \left(B^i + (u_j B^j) u^i\right) - \left(B^i + (u_j B^j) u^i\right) \partial_k \left(\sqrt{4\pi}\alpha u^0\right)}{\left(\sqrt{4\pi}\alpha u^0\right)^2} \\
&= \frac{1}{\sqrt{4 \pi}} \frac{\left(\alpha u^0\right) \partial_k \left(B^i + (u_j B^j) u^i\right) - \left(B^i + (u_j B^j) u^i\right) \partial_k \left(\alpha u^0\right)}{\left(\alpha u^0\right)^2} \\
\end{align}
Note that $\left( \alpha u^0 \right)$ is being used as its own gridfunction, so $\partial_k \left(\alpha u^0\right)$ will be finite-differenced by NRPy+ directly. We will also apply the product rule to the term $\partial_k \left(B^i + (u_j B^j) u^i\right) = B^i_{,k} + u_{j,k} B^j u^i + u_j B^j_{,k} u^i + u_j B^j u^i_{,k}$. So,
$$ b^i_{,k} = \frac{1}{\sqrt{4 \pi}} \frac{\left(\alpha u^0\right)  \left(B^i_{,k} + u_{j,k} B^j u^i + u_j B^j_{,k} u^i + u_j B^j u^i_{,k}\right) - \left(B^i + (u_j B^j) u^i\right) \partial_k \left(\alpha u^0\right)}{\left(\alpha u^0\right)^2}. $$

It will be easier to code this up if we rearrange these terms to group together the terms that involve contractions over $j$. Doing that, we find
$$
b^i_{,k} = \frac{\overbrace{\alpha u^0 B^i_{,k} - B^i \partial_k (\alpha u^0)}^{\rm Term\ Num1} + \overbrace{\left( \alpha u^0 \right) \left( u_{j,k} B^j u^i + u_j B^j_{,k} u^i + u_j B^j u^i_{,k} \right)}^{\rm Term\ Num2.a} - \overbrace{\left( u_j B^j u^i \right) \partial_k \left( \alpha u^0 \right) }^{\rm Term\ Num2.b}}{\underbrace{\sqrt{4 \pi} \left( \alpha u^0 \right)^2}_{\rm Term\ Denom}}.
$$

```python
u0alpha = gri.register_gridfunctions("AUX","u0alpha")
u0alpha = alpha * u4upperZero
u0alpha_dD = ixp.declarerank1("u0alpha_dD")
uU_dD = ixp.declarerank2("uU_dD","nosym")
uD_dD = ixp.declarerank2("uD_dD","nosym")

# Step 2.2.b: Construct derivatives of the small b vector
# smallbUdD represents the derivative of smallb4U
smallbUdD = ixp.zerorank2()
for i in range(DIM):
    for k in range(DIM):
        # Term Num1: \alpha u^0 B^i_{,k} - B^i \partial_k (\alpha u^0)
        smallbUdD[i][k] += u0alpha*BUdD[i][k]-BU[i]*u0alpha_dD[k]

for i in range(DIM):
    for k in range(DIM):
        for j in range(DIM):
            # Term Num2.a: terms that require contractions over k, and thus an extra loop.
            # ( \alpha u^0 ) (  u_{j,k} B^j u^i
            #                 + u_j B^j_{,k} u^i
            #                 + u_j B^j u^i_{,k} )
            smallbUdD[i][k] += u0alpha*(uD_dD[j][k]*BU[j]*uU[i]\
                                         +uD[j]*BUdD[j][k]*uU[i]\
                                         +uD[j]*BU[j]*uU_dD[i][k])

for i in range(DIM):
    for k in range(DIM):
        for j in range(DIM):
            #Term 2.b (More contractions over k): ( u_j B^j u^i ) ( \alpha u^0 ),k
            smallbUdD[i][k] += -(uD[j]*BU[j]*uU[i])*u0alpha_dD[k]

for i in range(DIM):
    for k in range(DIM):
        # Term Denom: Divide the numerator by sqrt(4 pi) * (alpha u^0)^2
        smallbUdD[i][k] /= sp.sqrt(4*M_PI) * u0alpha * u0alpha

```

### Step 3.c.iii: Derivative of $b^2$ \[Back to [List of Derivatives](#dlist)\]
$$\label{b2deriv}$$

Here, we will take the derivative of $b^2 = g_{\mu\nu} b^\mu b^\nu$. Using the product rule,
\begin{align}
\left(b^2\right)_{,j} &= \partial_j \left( g_{\mu\nu} b^\mu b^\nu \right) \\
                      &= g_{\mu\nu,j} b^\mu b^\nu + g_{\mu\nu} b^\mu_{,j} b^\nu + g_{\mu\nu} b^\mu b^\nu_{,j} \\
\end{align}
Recall that, by definition, the metric must be symmetric; that is, $g_{\mu\nu} = g_{\nu\mu}$. Also, consider that we can freely rename bound indices. So, if we swap $\mu$ and $\nu$ in the final term above, $g_{\mu\nu} b^\mu b^\nu_{,j} = g_{\nu\mu} b^\nu b^\mu_{,j}$ becomes $g_{\mu\nu} b^\nu b^\mu_{,j} = g_{\mu\nu} b^\mu_{,j} b^\nu$. This allows us to combine the final two terms:
\begin{align}
\left(b^2\right)_{,j} &= g_{\mu\nu,j} b^\mu b^\nu + 2 g_{\mu\nu} b^\mu_{,j} b^\nu.
\end{align}
We have already defined the spatial derivatives of the four-metric $g_{\mu\nu,j}$ in [this section](#step3); we have also defined the spatial derivatives of spatial components of $b^\mu$, $b^i_{,k}$ in [this section](#bideriv). Notice the above expression for $\partial_j T^j_{{\rm EM} i}$ requires spatial derivatives of the *zeroth* component of $b^\mu$ as well, $b^0_{,j}$, which we will now compute. Starting with the definition, and applying the quotient rule:
\begin{align}
b^0 &= \frac{u_k B^k}{\sqrt{4\pi}\alpha}, \\
\rightarrow b^0_{,j} &= \frac{1}{\sqrt{4\pi}} \frac{\alpha \left( u_{k,j} B^k + u_k B^k_{,j} \right) - u_k B^k \alpha_{,j}}{\alpha^2} \\
    &= \frac{\alpha u_{k,j} B^k + \alpha u_k B^k_{,j} - \alpha_{,j} u_k B^k}{\sqrt{4\pi} \alpha^2}.
\end{align}
We will first code the numerator, and then divide through by the denominator.

```python
# Step 2.2.c: Construct the derivative of b^2
# First construct the derivative b^0_{,j}
# This four-vector will make b^2 simpler:
smallb4UdD = ixp.zerorank2(DIM=4)
# Fill in the zeroth component
for j in range(DIM):
    for k in range(DIM):
        # The numerator:  \alpha u_{k,j} B^k
        #               + \alpha u_k B^k_{,j}
        #               - \alpha_{,j} u_k B^k
        smallb4UdD[0][j+1] +=   alpha*uD_dD[k][j]*BU[k] \
                               + alpha*uD[k]*BUdD[k][j] \
                               - alpha_dD[j]*uD[k]*BU[k]
for j in range(DIM):
    # Divide through by the denominator: \sqrt{4\pi} \alpha^2
    smallb4UdD[0][j+1] /= sp.sqrt(4*M_PI)*alpha*alpha
```

At this point, both $b^0_{\ ,j}$ and $b^i_{\ ,j}$ have been computed, but one exists inconveniently in the $4\times 4$ component `smallb4UdD[][]` and the other in the $3\times 3$ component `smallbUdD[][]`. So that we can perform full implied sums over $g_{\mu\nu} b^\mu_{,j} b^\nu$ more conveniently, we will now store all information from `smallbUdD[i][j]` into `smallb4UdD[i+1][j+1]`:

```python
# Now, we'll fill out the rest of the four-vector with b^i_{,j} that we derived above.
for i in range(DIM):
    for j in range(DIM):
        smallb4UdD[i+1][j+1] = smallbUdD[i][j]

```

Using 4-component (Greek-indexed) quantities, we can now complete our construction of
$$\left(b^2\right)_{,j} = g_{\mu\nu,j} b^\mu b^\nu + 2 g_{\mu\nu} b^\mu_{,j} b^\nu:$$

```python
smallb2_dD = ixp.zerorank1()
for j in range(DIM):
    for mu in range(4):
        for nu in range(4):
            #   g_{\mu\nu,j} b^\mu b^\nu
            # + 2 g_{\mu\nu} b^\mu_{,j} b^\nu
            smallb2_dD[j] +=   g4DDdD[mu][nu][j+1]*smallb4U[mu]*smallb4U[nu] \
                             + 2*g4DD[mu][nu]*smallb4UdD[mu][j+1]*smallb4U[nu]

```

### Step 3.c.iv: Derivatives of $g^{\mu\nu}$ \[Back to [List of Derivatives](#dlist)\]

We will need derivatives of the inverse four-metric, as well. Let us begin with $g^{00}$: since $g^{00} = -1/\alpha^2$ ([Gourgoulhon, eq. 4.49](https://arxiv.org/pdf/gr-qc/0703035.pdf)), $$g^{00}_{\ ,k} = \frac{2 \alpha_{,k}}{\alpha^3}$$
$$\label{gupijderiv}$$

```python
# Step 2.2.d: Construct derivatives of the components of g^{\mu\nu}
g4UUdD = ixp.zerorank3(DIM=4)

for k in range(DIM):
    # 2 \alpha_{,k} / \alpha^3
    g4UUdD[0][0][k+1] = 2*alpha_dD[k]/alpha**3
```

Now, we will code the $g^{i0}_{\ ,k}$ and $g^{0j}_{\ ,k}$ components. According to [Gourgoulhon, eq. 4.49](https://arxiv.org/pdf/gr-qc/0703035.pdf), $g^{i0} = g^{0i} = \beta^i/\alpha^2$, so $$g^{i0}_{\ ,k} = g^{0i}_{\ ,k} = \frac{\alpha^2 \beta^i_{,k} - 2 \beta^i \alpha \alpha_{,k}}{\alpha^4}$$ by the quotient rule. So, we'll code
$$
g^{i0}_{\ ,k} = g^{0i}_{\ ,k} =
\underbrace{\frac{\beta^i_{,k}}{\alpha^2}}_{\rm Term\ 1}
- \underbrace{\frac{2 \beta^i \alpha_{,k}}{\alpha^3}}_{\rm Term\ 2}
$$

```python
for k in range(DIM):
    for i in range(DIM):
        # Term 1:                                    \beta^i_{,k} / \alpha^2
        g4UUdD[i+1][0][k+1] = g4UUdD[0][i+1][k+1] = betaU_dD[i][k] / alpha**2

for k in range(DIM):
    for i in range(DIM):
        # Term 2:              -2 \beta^i \alpha_{,k} / \alpha^3
        g4UUdD[i+1][0][k+1] += -2 * betaU[i] * alpha_dD[k] / alpha**3
        g4UUdD[0][i+1][k+1] += -2 * betaU[i] * alpha_dD[k] / alpha**3
# Error check: Term 1 -> Term 2; fixed = -> +=
#              Fixed extraneous factor of alpha and incorrect sign.
```

We will also need derivatives of the spatial part of the inverse four-metric: since $g^{ij} = \gamma^{ij} - \frac{\beta^i \beta^j}{\alpha^2}$ ([Gourgoulhon, eq. 4.49](https://arxiv.org/pdf/gr-qc/0703035.pdf)),
\begin{align}
g^{ij}_{\ ,k} &= \gamma^{ij}_{\ ,k} - \frac{\alpha^2 \partial_k (\beta^i \beta^j) - \beta^i \beta^j \partial_k \alpha^2}{(\alpha^2)^2} \\
&= \gamma^{ij}_{\ ,k} - \frac{\alpha^2\beta^i \beta^j_{,k}+\alpha^2\beta^i_{,k} \beta^j-2\beta^i \beta^j \alpha \alpha_{,k}}{\alpha^4}. \\
&= \gamma^{ij}_{\ ,k} - \frac{\alpha\beta^i \beta^j_{,k}+\alpha\beta^i_{,k} \beta^j-2\beta^i \beta^j \alpha_{,k}}{\alpha^3} \\
g^{ij}_{\ ,k} &= \underbrace{\gamma^{ij}_{\ ,k}}_{\rm Term\ 1} - \underbrace{\frac{\beta^i \beta^j_{,k}}{\alpha^2}}_{\rm Term\ 2} - \underbrace{\frac{\beta^i_{,k} \beta^j}{\alpha^2}}_{\rm Term\ 3} + \underbrace{\frac{2\beta^i \beta^j \alpha_{,k}}{\alpha^3}}_{\rm Term\ 4}. \\
\end{align}

```python
gammaUU_dD = ixp.declarerank3("gammaUU_dD","sym01")

# The spatial derivatives of the spatial components of the four metric:
# Term 1: \gamma^{ij}_{\ ,k}
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            g4UUdD[i+1][j+1][k+1] = gammaUU_dD[i][j][k]

# Term 2: - \beta^i \beta^j_{,k} / \alpha^2
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            g4UUdD[i+1][j+1][k+1] += -betaU[i]*betaU_dD[j][k]/alpha**2

# Term 3: - \beta^i_{,k} \beta^j / \alpha^2
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            g4UUdD[i+1][j+1][k+1] += -betaU_dD[i][k]*betaU[j]/alpha**2

# Term 4: 2\beta^i \beta^j \alpha_{,k}\alpha^3
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            g4UUdD[i+1][j+1][k+1] += 2*betaU[i]*betaU[j]*alpha_dD[k]/alpha**3

```

## Step 3.d: Putting it together: Constructing $\partial_j  T^{j}_{{\rm EM} i}$ \[Back to [top](#toc)\]
$$\label{alltogether}$$

So, we can now put it all together, starting from the expression we derived above in [Step 3.c](#em_tensor_derivatives):
\begin{align}
\partial_j  T^{j}_{{\rm EM} i} =& \
\underbrace{g_{\mu i,j} T^{\mu j}_{\rm EM}}_{\rm Term\ 1} \\
& + \underbrace{g_{\mu i} \left( b^2 u^j_{,j} u^\mu + b^2 u^j u^\mu_{,j} + \frac{b^2}{2} g^{j\mu}_{\ ,j} + b^j_{,j} b^\mu + b^j b^\mu_{,j} \right)}_{\rm Term\ 2} \\
& + \underbrace{g_{\mu i} \left( \left(b^2\right)_{,j} u^j u^\mu + \frac{1}{2} \left(b^2\right)_{,j} g^{j\mu} \right).}_{\rm Term\ 3} \\
\end{align}

### Step 3.d.i: Putting it together: Term 1 \[Back to [top](#toc)\]
$$\label{alltogether1}$$

We will now construct this term by term. Term 1 is straightforward: $${\rm Term\ 1} = g_{\mu i,j} T^{\mu j}_{\rm EM}.$$
**Error check: fixed typo $\gamma$ -> $g$**

```python
# Step 2.2.e: Construct TEMUDdD_contracted itself
# Step 2.2.e.i
TEMUDdD_contracted = ixp.zerorank1()
for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 1:                g_{\mu i,j} T^{\mu j}_{\rm EM}
            TEMUDdD_contracted[i] += g4DDdD[mu][i+1][j+1] * T4EMUU[mu][j+1]
```

### Step 3.d.ii: Putting it together: Term 2 \[Back to [top](#toc)\]
$$\label{alltogether2}$$

We will now add $${\rm Term\ 2} = g_{\mu i} \left( \underbrace{b^2 u^j_{,j} u^\mu}_{\rm Term\ 2a} + \underbrace{b^2 u^j u^\mu_{,j}}_{\rm Term\ 2b} + \underbrace{\frac{b^2}{2} g^{j\mu}_{\ ,j}}_{\rm Term\ 2c} + \underbrace{b^j_{,j} b^\mu}_{\rm Term\ 2d} + \underbrace{b^j b^\mu_{,j}}_{\rm Term\ 2e} \right)$$ to $\partial_j  T^{j}_{{\rm EM} i}$. These are the terms that involve contractions over $k$ (but no metric derivatives like Term 1 had).

All terms have been computed in terms of gridfunctions and their derivatives, except we will find it convenient to define the derivative of the 4-velocity `u4UdD[]` in terms of finite difference derivatives of $u^0=$ `u4upperZero` and $u^i=$ `uU[]`:

```python
# Step 2.2.e.ii: Compose derivatives of u4U:
u4UdD = ixp.zerorank2(DIM=4)
u4upperZero_dD = ixp.declarerank1("u4upperZero_dD") # We defined u4upperZero as a separate gridfunction so as not to confuse
                                              # NRPy+'s finite difference generator code.
for i in range(DIM):
    u4UdD[0][i+1] = u4upperZero_dD[i]
for i in range(DIM):
    for j in range(DIM):
        u4UdD[i+1][j+1] = uU_dD[i][j]

# Step 2.2.e.iii: Add terms 2a--2e to TEMUDdD_contracted[i]
for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 2a: g_{\mu i} b^2 u^j_{,j} u^\mu
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb2*uU_dD[j][j]*u4U[mu]

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 2b: g_{\mu i} b^2 u^j u^\mu_{,j}
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb2*uU[j]*u4UdD[mu][j+1]

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 2c: g_{\mu i} b^2 g^{j \mu}_{,j} / 2
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb2*g4UUdD[j+1][mu][j+1]/2

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 2d: g_{\mu i} b^j_{,j} b^\mu
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallbUdD[j][j]*smallb4U[mu]

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 2e: g_{\mu i} b^j b^\mu_{,j}
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb4U[j+1]*smallb4UdD[mu][j+1]
# Error check: fixed k -> mu in smallb4UdD indexing
```

### Step 3.d.iii: Putting it together: Term 3 \[Back to [top](#toc)\]
$$\label{alltogether3}$$

Now, we will add $${\rm Term\ 3} = g_{\mu i} \left( \underbrace{\left(b^2\right)_{,j} u^j u^\mu}_{\rm Term\ 3a} + \underbrace{\frac{1}{2} \left(b^2\right)_{,j} g^{j\mu}}_{\rm Term\ 3b} \right).$$

```python
# Step 2.2.e.iii
for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 3a: g_{\mu i} ( b^2 )_{,j} u^j u^\mu
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb2_dD[j]*uU[j]*u4U[mu]

for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            # Term 3b: g_{mu i} ( b^2 )_{,j} g^{j\mu} / 2
            TEMUDdD_contracted[i] += g4DD[mu][i+1]*smallb2_dD[j]*g4UU[j+1][mu]/2

```

# Step 4: Construct the evolution equation for $\tilde{S}_i$ \[Back to [top](#top)\]
$$\label{construct_si}$$

Finally, we will return our attention to the time evolution equation (from eq. 13 of the [original paper](https://arxiv.org/pdf/1704.00599.pdf)),
\begin{align}
\partial_t \tilde{S}_i &= - \partial_j \left( \alpha \sqrt{\gamma} T^j_{{\rm EM} i} \right) + \frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu} \\
                       &= -T^j_{{\rm EM} i} \partial_j (\alpha \sqrt{\gamma}) - \alpha \sqrt{\gamma} \partial_j T^j_{{\rm EM} i} + \frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu} \\
                       &= \underbrace{-g_{i\mu} T^{\mu j}_{\rm EM} \partial_j (\alpha \sqrt{\gamma})
}_{\rm Term\ 1} - \underbrace{\alpha \sqrt{\gamma} \partial_j T^j_{{\rm EM} i}}_{\rm Term\ 2} + \underbrace{\frac{1}{2} \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu}}_{\rm Term\ 3} .
\end{align}
We will first take derivatives of $\alpha \sqrt{\gamma}$, then construct each term in turn.
$$\label{step7}$$

```python
# Step 3.0: Construct the evolution equation for \tilde{S}_i
# Here, we set up the necessary machinery to take FD derivatives of alpha * sqrt(gamma)
alpsqrtgam = gri.register_gridfunctions("AUX","alpsqrtgam")
alpsqrtgam = alpha*sp.sqrt(gammadet)
alpsqrtgam_dD = ixp.declarerank1("alpsqrtgam_dD")

Stilde_rhsD = ixp.zerorank1()
# The first term: g_{i\mu} T^{\mu j}_{\rm EM} \partial_j (\alpha \sqrt{\gamma})
for i in range(DIM):
    for j in range(DIM):
        for mu in range(4):
            Stilde_rhsD[i] += -g4DD[i+1][mu]*T4EMUU[mu][j+1]*alpsqrtgam_dD[j]
# Error check: mu needs to run 0 to 3

# The second term: \alpha \sqrt{\gamma} \partial_j T^j_{{\rm EM} i}
for i in range(DIM):
    Stilde_rhsD[i] += -alpsqrtgam * TEMUDdD_contracted[i]

# The third term: \alpha \sqrt{\gamma} T^{\mu \nu}_{\rm EM} \partial_i g_{\mu \nu} / 2
for i in range(DIM):
    for mu in range(4):
        for nu in range(4):
            Stilde_rhsD[i] += alpsqrtgam * T4EMUU[mu][nu] * g4DDdD[mu][nu][i+1] / 2

```

# Step 5: Construct the evolution equations for $A_i$ and $[\sqrt{\gamma}\Phi]$ \[Back to [top](#toc)\]
$$\label{construct_ai}$$

We will also need to evolve the vector potential $A_i$. This evolution is given as eq. 17 in the [$\texttt{GiRaFFE}$](https://arxiv.org/pdf/1704.00599.pdf) paper:
$$\boxed{\partial_t A_i = \epsilon_{ijk} v^j B^k - \partial_i (\underbrace{\alpha \Phi - \beta^j A_j}_{\rm AevolParen}),}$$
where $\epsilon_{ijk} = [ijk] \sqrt{\gamma}$ is the antisymmetric Levi-Civita tensor, the drift velocity $v^i = u^i/u^0$, $\gamma$ is the determinant of the three metric, $B^k$ is the magnetic field, $\alpha$ is the lapse, and $\beta$ is the shift.
We will also need the scalar electric potential $\Phi$, which is evolved by eq. 19:
$$\boxed{\partial_t [\sqrt{\gamma} \Phi] = -\partial_j (\underbrace{\alpha\sqrt{\gamma} \gamma^{ij} A_i - \beta^j [\sqrt{\gamma} \Phi]}_{\rm PevolParenU[j]}) - \xi \alpha [\sqrt{\gamma} \Phi],}$$
with $\xi$ chosen as a damping factor.

## Step 5.a: Construct some useful auxiliary gridfunctions for the other evolution equations \[Back to [top](#toc)\]
$$\label{aux}$$

After declaring a  some needed quantities, we will also define the parenthetical terms (underbrace above) that we need to take derivatives of. That way, we can take finite-difference derivatives easily. Note that the above equations incorporate the fact that $\gamma^{ij}$ is the appropriate raising operator for $A_i$: $A^j = \gamma^{ij} A_i$. This is correct since $n_\mu A^\mu = 0$, where $n_\mu$ is a normal to the hypersurface, so $A^0=0$ (according to Sec. II, subsection C of [the "Improved EM gauge condition" paper of Etienne *et al*](https://arxiv.org/pdf/1110.4633.pdf)).

```python
# Step 4.0: Construct the evolution equations for A_i and sqrt(gamma)Phi
# Step 4.0.a: Construct some useful auxiliary gridfunctions for the other evolution equations
xi = par.Cparameters("REAL",thismodule,"xi",0.1) # The damping factor

# Define sqrt(gamma)Phi as psi6Phi
psi6Phi = gri.register_gridfunctions("EVOL","psi6Phi")
Phi = psi6Phi / sp.sqrt(gammadet)

# We'll define a few extra gridfunctions to avoid complicated derivatives
AevolParen  = gri.register_gridfunctions("AUX","AevolParen")
PevolParenU = ixp.register_gridfunctions_for_single_rank1("AUX","PevolParenU")

# {\rm AevolParen} = \alpha \Phi - \beta^j A_j
AevolParen = alpha*Phi
for j in range(DIM):
    AevolParen += -betaU[j] * AD[j]

# {\rm PevolParenU[j]} = \alpha\sqrt{\gamma} \gamma^{ij} A_i - \beta^j [\sqrt{\gamma} \Phi]
for j in range(DIM):
    PevolParenU[j] = -betaU[j] * psi6Phi
    for i in range(DIM):
        PevolParenU[j] += alpha * sp.sqrt(gammadet) * gammaUU[i][j] * AD[i]

AevolParen_dD  = ixp.declarerank1("AevolParen_dD")
PevolParenU_dD = ixp.declarerank2("PevolParenU_dD","nosym")

```

## Step 5.b: Complete the construction of the evolution equations for $A_i$ and $[\sqrt{\gamma}\Phi]$ \[Back to [top](#toc)\]
$$\label{complete_construct_ai}$$

Now to set the evolution equations ([eqs. 17 and 19](https://arxiv.org/pdf/1704.00599.pdf)), recalling that the drift velocity $v^i = u^i/u^0$:
\begin{align}
\partial_t A_i &= \epsilon_{ijk} v^j B^k - \partial_i (\alpha \Phi - \beta^j A_j) \\
               &= \epsilon_{ijk} \frac{u^j}{u^0} B^k - {\rm AevolParen\_dD[i]} \\
\partial_t [\sqrt{\gamma} \Phi] &= -\partial_j \left(\left(\alpha\sqrt{\gamma}\right)A^j - \beta^j [\sqrt{\gamma} \Phi]\right) - \xi \alpha [\sqrt{\gamma} \Phi] \\
                                &= -{\rm PevolParenU\_dD[j][j]} - \xi \alpha [\sqrt{\gamma} \Phi]. \\
\end{align}

```python
# Step 4.0.b: Construct the actual evolution equations for A_i and sqrt(gamma)Phi
A_rhsD = ixp.zerorank1()
psi6Phi_rhs = sp.sympify(0)

for i in range(DIM):
    A_rhsD[i] = -AevolParen_dD[i]
    for j in range(DIM):
        for k in range(DIM):
            A_rhsD[i] += LeviCivitaTensorDDD[i][j][k]*(uU[j]/u4upperZero)*BU[k]

psi6Phi_rhs = -xi*alpha*psi6Phi
for j in range(DIM):
    psi6Phi_rhs += -PevolParenU_dD[j][j]
```

# Step 6:  Code Validation against `GiRaFFE_HO.GiRaFFE_Higher_Order` NRPy+ Module \[Back to [top](#toc)\]
$$\label{code_validation}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for the $\texttt{GiRaFFE}$ evolution equations and auxiliary quantities we intend to use between
1. this tutorial and
2. the NRPy+ [GiRaFFE_HO.GiRaFFE_Higher_Order](../edit/GiRaFFE_HO/GiRaFFE_Higher_Order.py) module.

```python
# Reset the list of gridfunctions, as registering a gridfunction
#   twice will spawn an error.
gri.glb_gridfcs_list = []

import GiRaFFE_HO.GiRaFFE_Higher_Order as gho
gho.GiRaFFE_Higher_Order()

print("Consistency check between GiRaFFE_Higher_Order tutorial and NRPy+ module: ALL SHOULD BE ZERO.")

print("u0alpha - gho.u0alpha = " + str(u0alpha - gho.u0alpha))
print("alpsqrtgam - gho.alpsqrtgam = " + str(alpsqrtgam - gho.alpsqrtgam))
print("AevolParen - gho.AevolParen = " + str(AevolParen - gho.AevolParen))
print("psi6Phi_rhs - gho.psi6Phi_rhs = " + str(psi6Phi_rhs - gho.psi6Phi_rhs))
for i in range(DIM):
    print("uU["+str(i)+"] - gho.uU["+str(i)+"] = " + str(uU[i] - gho.uU[i]))
    print("uD["+str(i)+"] - gho.uD["+str(i)+"] = " + str(uD[i] - gho.uD[i]))
    print("PevolParenU["+str(i)+"] - gho.PevolParenU["+str(i)+"] = " + str(PevolParenU[i] - gho.PevolParenU[i]))
    print("Stilde_rhsD["+str(i)+"] - gho.Stilde_rhsD["+str(i)+"] = " + str(Stilde_rhsD[i] - gho.Stilde_rhsD[i]))
    print("A_rhsD["+str(i)+"] - gho.A_rhsD["+str(i)+"] = " + str(A_rhsD[i] - gho.A_rhsD[i]))
```

    Consistency check between GiRaFFE_Higher_Order tutorial and NRPy+ module: ALL SHOULD BE ZERO.
    u0alpha - gho.u0alpha = 0
    alpsqrtgam - gho.alpsqrtgam = 0
    AevolParen - gho.AevolParen = 0
    psi6Phi_rhs - gho.psi6Phi_rhs = 0
    uU[0] - gho.uU[0] = 0
    uD[0] - gho.uD[0] = 0
    PevolParenU[0] - gho.PevolParenU[0] = 0
    Stilde_rhsD[0] - gho.Stilde_rhsD[0] = 0
    A_rhsD[0] - gho.A_rhsD[0] = 0
    uU[1] - gho.uU[1] = 0
    uD[1] - gho.uD[1] = 0
    PevolParenU[1] - gho.PevolParenU[1] = 0
    Stilde_rhsD[1] - gho.Stilde_rhsD[1] = 0
    A_rhsD[1] - gho.A_rhsD[1] = 0
    uU[2] - gho.uU[2] = 0
    uD[2] - gho.uD[2] = 0
    PevolParenU[2] - gho.PevolParenU[2] = 0
    Stilde_rhsD[2] - gho.Stilde_rhsD[2] = 0
    A_rhsD[2] - gho.A_rhsD[2] = 0

# Step 7: 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-GiRaFFE_Higher_Order.pdf](Tutorial-GiRaFFE_Higher_Order.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
!jupyter nbconvert --to latex --template latex_nrpy_style.tplx Tutorial-GiRaFFE_Higher_Order.ipynb
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_Higher_Order.tex
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_Higher_Order.tex
!pdflatex -interaction=batchmode Tutorial-GiRaFFE_Higher_Order.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-GiRaFFE_Higher_Order.ipynb to latex
    [NbConvertApp] Writing 121855 bytes to Tutorial-GiRaFFE_Higher_Order.tex
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode
    This is pdfTeX, Version 3.14159265-2.6-1.40.18 (TeX Live 2017/Debian) (preloaded format=pdflatex)
     restricted \write18 enabled.
    entering extended mode


```python
%%html

<form action="javascript:code_toggle()"><input type="submit" id="toggleButton" value="Show Code"></form>
```

```javascript
%%javascript
IPython.OutputArea.prototype._should_scroll = function(lines) {
    return false;
}
```

# Provincial Achievement Test Scores

## Introduction

TODO: Improve discussion and add quantitative results if there's interest in this notebook to do that.

Every year, the province of Alberta runs standardized testing for grades 6 and 9 for primary courses under the blanket identifier of Provincial Achievement Tests in order to assess how well the students preform. The results of these test are open source and readily downloaded from the Alberta Education website. In this notebook we're going to download and manipulate the data direct from Alberta education, and see if we can easily identify under and over performing school districts. Time permitting, we might even toss these onto a map using another open data set from Alberta education which contains the addresses of every school in Alberta. Using this data in combination with the provincial testing scores, we will likely be able to easily identify which school districts/schools are performing best and worst.

## Wrangling the data

First let's download the data directly from the Alberta Education website and toss it in a Pandas data frame

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from numpy import nan as Nan

df_ero = pd.read_excel("https://education.alberta.ca/media/3680591/pat-multiyear-sch-list.xlsx")
```

That was pretty easily done as those are hosted excel spreadsheets. So, we don't even have to save the file locally, we can toss it straight in a pandas frame.

```python
school_results = df_ero.copy()
school_results.head(1)
```

```python
# Let's also collect diploma exam results
diploma_results = pd.read_excel('https://education.alberta.ca/media/3680580/diploma-multiyear-sch-list-annual.xlsx')
diploma_results.head(3)
```

```python
diploma_results = diploma_results.rename(columns = {"Diploma Course":"Course Name"})

```

Where the above data format is going to be annoying to work with in order to plot/sort some data. Instead, let's whip this data table into "long form" so that we can manipulate, analyze and plot this data more easily. We do this with the code below. Notice how now we have multiple duplicate entries for "Authority Name" and "School Name" columns, as well as a handy year column for each row.

```python
import re
import time
schools_reshaped = school_results.copy()
start = time.time()

# If there's a year in the column we want to split that bad boy
def splitter(string):
    r = re.compile(r'\d{4}|\S.*$')
    return r.findall(string)

cols = list(schools_reshaped)[0:8]
years = ['2013', '2014', '2015', '2016', '2017']

# Being lazy and creating duplicate columns with a year index. It's the same
# accross the board but we need them for the next step.
# The key is to not respect your RAM.
for year in years:
    for names in cols:
        schools_reshaped[str(year +" "+names)] = schools_reshaped[names]

schools_reshaped.columns = pd.MultiIndex.from_tuples([tuple(splitter(c)) for c in schools_reshaped.columns])
schools_reshaped = schools_reshaped.stack(0).reset_index(1)

end = time.time()
print(end - start)
schools_reshaped.rename(columns={'level_1': "Year"}, inplace=True)
#schools_reshaped[["School Name", "Course Name", "Sch Enrol", "Year", "Sch Writing"]].loc[schools_reshaped['Year'] == '2013']

# Sort by school name.
schools_reshaped=schools_reshaped.sort_values('School Name')

del schools_reshaped["Form"]
del schools_reshaped["Language"]

```

```python
diploma_reshaped = diploma_results.copy()
cols = list(diploma_results)[0:6]
years = ['2013', '2014', '2015', '2016', '2017']

# Being lazy and creating duplicate columns with a year index. It's the same
# accross the board but we need them for the next step.
# The key is to not respect your RAM.
for year in years:
    for names in cols:
        diploma_reshaped[str(year +" "+names)] = diploma_reshaped[names]

diploma_reshaped.columns = pd.MultiIndex.from_tuples([tuple(splitter(c)) for c in diploma_reshaped.columns])
diploma_reshaped = diploma_reshaped.stack(0).reset_index(1)

end = time.time()
print(end - start)
diploma_reshaped.rename(columns={'level_1': "Year"}, inplace=True)
del diploma_reshaped["Sch Exam Mark Acc Sig"]
del diploma_reshaped["Sch Exam Mark Exc Sig"]

print(list(diploma_reshaped))

diploma_reshaped = diploma_reshaped.rename(columns = {"Sch School Mark % Acc":"Sch % Acc of Writing",
                                                      "Sch School Mark % Exc": 'Sch % Exc of Writing',
                                                      "Sch Exam Mark % Exc":"Sch Part 1 % Exc",
                                                     "Sch Exam Mark % Acc":"Sch Part 1 % Acc"})

diploma_reshaped = diploma_reshaped[["Year",
                                     "Authority Name",
                                     "Course Name",
                                     "School Name",
                                     "Sch % Acc of Writing",
                                    "Sch % Exc of Writing",
                                    "Sch Part 1 % Exc",
                                    "Sch Part 1 % Acc"]]
# Sort by school name.
#diploma_reshaped=diploma_reshaped.sort_values('School Name')
diploma_reshaped.head(1)
```

Excellent. Now that the data have been reshaped into a "long form" they'll be a lot easier to work with when it comes to plotting and analysis. So, let's start to get an idea at the score distributions between schools and districts by using this dataframe as a back end to an interactive widget.

## Interactive Graph

Before we start any more "involved" analysis let's take a moment to plot these data by year to get an idea of what we're working with. In the widget below `_type` controls whether we're looking at individual schools or the school authority, `name` is the name of the school/authority, `subject` changes the subject, and `name2` is optional and will display another school/authority to compare with. Note that switching to school is a little slower, as that data set requires some set up before we can put it nicely into the widget. Also note that not all subjects are offered in each school, and they're filtered down buy what subjects were offered in the school/authority under `name`.

```python
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
from plotly.offline import init_notebook_mode, iplot
import plotly.graph_objs as go
from ipywidgets import interact
init_notebook_mode(connected=True)
from ipywidgets import Dropdown

schools_reshaped = pd.merge(schools_reshaped, diploma_reshaped, on = ["Year",
                                     "Course Name",
                                     "School Name",
                                     "Authority Name",
                                     "Sch % Acc of Writing",
                                     "Sch % Exc of Writing",
                                     "Sch Part 1 % Acc",
                                     "Sch Part 1 % Exc"], how = 'outer')
#schools_reshaped[schools_reshaped["Course Name"] == "Biology 30"]
```

```python
# Now let's do the same with school districts
# print(list(schools_reshaped))

def traces(name, subject, school_or_auth):
    result = None
    divisor = None
    y = None
    y2 = None
    y3 = None
    if school_or_auth == "Authority Name":
        result = schools_reshaped[schools_reshaped[school_or_auth] == name]
        divisor = result.groupby("Year")['Sch Enrol'].sum() - result.groupby("Year")["Sch Absent"].sum()
        y =  100 * (result.groupby("Year")["Sch Acc"].sum() - result.groupby("Year")["Sch Exc"].sum() )/divisor
        y2 = 100 * result.groupby("Year")['Sch Exc'].sum()/divisor
        y3 = 100 * result.groupby("Year")['Sch Below'].sum()/divisor

    if school_or_auth == "School Name":
        result = schools_reshaped[schools_reshaped[school_or_auth] == name]
        divisor = result['Sch Writing']
        y = (result['Sch % Acc of Writing']-result['Sch % Exc of Writing'])# - result['Sch Exc']) / divisor
        y2 = result['Sch % Exc of Writing'] #/ divisor
        y3 =  result['Sch % Below of Writing']# / divisor

    result = result[result['Course Name'] == subject]

    trace1 = go.Bar(x=result['Year'], y=y,
                    name=" ".join([name, '% at or above acceptable standard']))#,

    trace2 = go.Bar(x=result['Year'],
                    y=y2,
                    name= " ".join([name, '% achieved a standard of excellence']))#,

    trace3 = go.Bar(x=result['Year'],
                    y=y3,
                    name = " ".join([name,"% below acceptable standard"]))#,

    return [trace1, trace2, trace3]

def compare_results( _type, name, subject, name2 = []):

    print(name, subject, _type)

    data = traces(name, subject, _type)

    if name2:
        data2 = traces(name2, subject, _type)
        data = data + data2

    layout = go.Layout(title=subject,
                xaxis=dict(title='Year'),
                yaxis=dict(title='Percentage',
                      range = [0,100])
                      )

    fig = go.Figure(data=data, layout=layout)
    iplot(fig)

def course_drop(_type, name):
    courses = list(schools_reshaped['Course Name'].unique())
    filtered_course_list = []

    for course in courses:
        result = schools_reshaped[schools_reshaped[_type] == name]
        result = result[result['Course Name'] == course]
        if _type == "School Name":
            y = result['Sch % Acc of Writing']

        if _type == "Authority Name":
           y = result.groupby("Year")["Sch Acc"].sum() - result.groupby("Year")["Sch Exc"].sum()

        if y.isnull().sum() > 4 or y.empty == True:
            # No course for school, do nothing
            #filtered_course_list.append(course)

            continue
        else:
            # if something exists, we'll count ita
            filtered_course_list.append(course)

    if len(filtered_course_list) == 0:
        # TODO: make an empty thing instead of pretending they do math
        filtered_course_list.append("Mathematics 6")
    return filtered_course_list

course_widget = Dropdown()

type_widget = Dropdown(options = ["School Name", "Authority Name"], value = "School Name")

name_widget = Dropdown()
name_widget2 = Dropdown()

def update2(*args):
    a = sorted(list(map(str, list(schools_reshaped[type_widget.value].unique()))))
    name_widget.options = a
    name_widget2.options =  a
    name_widget2.value = None
    # course_widget.options = course_drop(type_widget.value, x_widget.value)
    name_widget.value = a[0]

def update(*args):
    course_widget.options = course_drop(type_widget.value, name_widget.value)

name_widget.observe(update)
#type_widget.observe(update)
type_widget.observe(update2)

interact(compare_results,
        _type = type_widget,
         name = name_widget,
         subject =  course_widget,
         name2 = name_widget2
        )

```

Fantastic. Now we can compare which schools do well and which do poorly and in what subject. I note that the first school/authority `name` is used to filter out subjects that they don't have data for. That means that you might not see all their choices if youre using them in `name2`. I also note that if a school/authority has no test scores, then it defauls to a blank grid for mathematics 9.

## How Do Differences in Funding Affect Student Performance?

The code below assumes you've downloaded all the PDFs off of the Alberta education site containing funding information from each district. If you don't have it you can either download those pdfs yourself (not recommended) or get them from our swift container `callysto-open-data` called `district_funding.csv`, of course, it is downloading it directly from swift for you.

Most of the code below is just wrangling data and making plots of that data. However, what we're doing is gathering all our funding data, combining it with our data frames and then plotting it. What we'll then have is the performance of each district against the provincial average in terms of test scores for each ear and subject, as well as a graph of how those test scores were affected by differences in _total_ funding. In order to do that, we plot the density of funding and performance grades for the entire province, and then fit a line to it in order to judge positive/negative coorelation between funding and grade performance.

```python

# If you don't have the LAT LONG data, uncomment the line below and run this cell .
temp_df = schools_reshaped.copy()

```

```python
def convert(x):
    try:
        return x.astype(int)
    except:
        return x

def get_funding_data(paths = "FundingPdf/*.pdf"):
    from tika import parser
    import requests
    import glob
    import re
    data = []
    count = 0
    for file in glob.iglob(paths):
        parsedPDF = parser.from_file(file)

        name = file.split("/")[-1]
        name = name.replace(".pdf", "")
        name = name.replace("-", " ").title()
        name = re.sub("[\(\[].*?[\)\]]", "", name)
        try:
            name = name.replace(" No ", " No. ")
        except:
            pass

        try:
            name = name.replace(" Ltd", " Ltd. ")
        except:
            pass

        name = name.strip()

        estimated_funding = None
        projected_funding =  None
        estimated_enroll =  None
        projected_enroll =  None
        year = None

        for line in parsedPDF['content'].split('\n')[::-1]:

            if "TOTAL FUNDING" in line:
                estimate_funding = line.split()[2].replace('$',"").replace(",","")
                projected_funding = line.split()[3].replace('$',"").replace(",","")

            if "As of " in line:

                try:
                    print(int(line.split()[-1]))
                    year = line.split()[-1]
                except:
                    pass

            if "Funded Enrolment for Grades 1 - 12" in line:

                estimated_enroll = line.split()[7].replace(",", "")
                projected_enroll = line.split()[9].replace(",", "")
            elif "Enrolment for Grades 1 - 12" in line:

                estimated_enroll = line.split()[6].replace(",", "")
                projected_enroll = line.split()[8].replace(",", "")

        data.append([name, estimate_funding, projected_funding, estimated_enroll, projected_enroll, year])

    df = pd.DataFrame(data, columns = ["Authority Name", "Estimated Funding", "Projected Funding", "Estimated 1-12", "Projected 1-12","Year"])
    df.to_csv("district_funding.csv")
```

```python

# Add district funding
try:
    funding = pd.read_csv("https://swift-yeg.cloud.cybera.ca:8080/v1/AUTH_233e84cd313945c992b4b585f7b9125d/callysto-open-data/district_funding.csv")
    del funding["Unnamed: 0"]
except:
    get_funding_data()

# This is to fix a pandas "gotcha" concerning integer arrays and NaN types.
# (as in it doesn't handle it and converts to float)

funding["Year"] = np.nan_to_num(funding["Year"]).astype(int)
funding["Estimated 1-12"] = np.nan_to_num(funding["Estimated 1-12"]).astype(int)
funding["Projected 1-12"] = np.nan_to_num(funding["Projected 1-12"]).astype(int)
# Don't need this year's data.
funding = funding[funding.Year != 2018]
#funding = funding[funding.Year != np.nan]

```

```python

# testing = pd.merge()
temp_df = schools_reshaped[["Authority Name", "School Name"]]
testing = pd.merge(funding, temp_df, how='left', on = ["Authority Name"]).drop_duplicates()

#
# There's a panda's gotcha with NaN types in integer columns so we have to
# go through all this crap to deal with it.
testing["Year"] = np.nan_to_num(testing["Year"]).astype(int)
testing["Year"] = np.nan_to_num(testing["Year"]).astype(str)
testing["Year"] = testing["Year"].replace('0', Nan)
combined_frame = pd.merge(schools_reshaped, testing, how='left',  on=['Authority Name',"School Name", "Year"])

# create funding per student.
combined_frame["Est Fund Per Student"] = combined_frame["Estimated Funding"]/combined_frame["Estimated 1-12"]
combined_frame["Proj Fund Per Student"] = combined_frame["Projected Funding"]/combined_frame["Projected 1-12"]

temp_df.drop_duplicates();

```

```python
density_plot_frame = combined_frame.sort_values("Year").copy()# .replace(0, np.NaN)

import seaborn as sns
from pylab import *
from matplotlib import animation
import numpy.ma as ma
from scipy.stats import mstats

density_plot_frame["Acc Differential Part 1"] = density_plot_frame['Sch % Acc of Writing'] - density_plot_frame['Sch Part 1 % Acc']
density_plot_frame["Exc Differential Part 1"] = density_plot_frame['Sch % Exc of Writing'] - density_plot_frame['Sch Part 1 % Exc']
density_plot_frame["Acc Differential Part 2"] = density_plot_frame['Sch % Acc of Writing'] - density_plot_frame['Sch Part 2 % Acc']
density_plot_frame["Exc Differential Part 2"] = density_plot_frame['Sch % Exc of Writing'] - density_plot_frame['Sch Part 2 % Exc']

density_plot_frame = density_plot_frame.rename(columns={'Sch % Exc of Writing': 'School Grade Percentage Excellent',
                        'Sch % Acc of Writing': 'School Grade Percentage Acceptable',
                        'Sch % Below of Writing': 'School Grade Percentage Unacceptable',
                        'Sch Part 1 % Acc':'Provincial/Diploma Exam Percent Acceptable',
                        'Sch Part 2 % Acc': 'Provincial Exam Percent Acceptable, Part 2',
                        'Sch Part 1 % Exc':'Provincial/Diploma Exam Percent Excellent',
                        'Sch Part 2 % Exc':'Provincial Exam Percent Excellent, Part 2',
                        'Acc Differential Part 1': "Acceptable Grade Differential",
                        'Acc Differential Part 2': "Acceptable Grade Differential, Provincial Part 2",
                        'Exc Differential Part 1': "Excellent Grade Differential",
                        'Exc Differential Part 2': "Excellent Grade Differential, Provincial Part 2"})

def make_density(category, year, subject, Authority=False, filter=False):
    YEARS = list(density_plot_frame["Year"].unique())

    x = density_plot_frame[density_plot_frame["Course Name"] == subject]
    grade = x[[category, "Year"]]
    funding = x[["Est Fund Per Student", "Year"]]
    points = x[["Est Fund Per Student", "Year", category, "Authority Name"]]

    if Authority:
        f, ax = plt.subplots(figsize=(7, 7))

        for i, year in enumerate(YEARS):
                line = points[points["Year"] == year][category]
                downline = line.mean() - line.std()
                upline = line.mean() + line.std()
                line = line.mean()
                if i == 0:
                    label = "Provincial Mean"
                    label1= "Provincial 1sd"

                else:
                    label = ""
                    label1 = ""
                plt.axhline(y=line,xmin= (i+.1)/(len(YEARS)),
                            xmax = (i+1-.1)/(len(YEARS)),
                            c="g",
                            label = label)
                plt.axhline(y=downline,xmin= (i+.1)/(len(YEARS)),
                            xmax = (i+1-.1)/(len(YEARS)),
                            c="purple",
                            label = label1)
                plt.axhline(y=upline,
                            xmin= (i+.1)/(len(YEARS)),
                            xmax = (i+1-.1)/(len(YEARS)),
                            c="purple",
                            label = "")

        plt.style.use('ggplot')
        points = points[points["Authority Name"] == Authority]
        grade = grade.dropna()
        dd = pd.melt(points[["Year", category]], id_vars = ["Year"], var_name = [category])
        title = ''.join([Authority, "\n", subject])

        try:
            sns.boxplot(x="Year", y="value", data=dd, hue=category)
            sns.swarmplot(x="Year", y="value", data=dd, color="0.25")
        except:
            title = ''.join([Authority, "\n", subject," No data"])
        plt.title(title)

    else:
        f, (ax1, ax2) = plt.subplots(2, figsize=(9, 9))
        plt.tight_layout(pad=4)
       # plt.subplot(2,1,1)
        points = points.dropna()
        if year:
            x = points[points["Year"] == year][category]
            y = points[points["Year"] == year]["Est Fund Per Student"]
            if filter:
                t_f = points[points["Year"] == year][[category, "Est Fund Per Student"]]

        else:
            x = points[category]
            y = points["Est Fund Per Student"]
            if filter:
                t_f = points[[category, "Est Fund Per Student"]]

        x1 = x.quantile(0.25)
        x2 = x.quantile(0.75)
        y1 = y.quantile(0.25)
        y2 = y.quantile(0.75)

        ax1.plot([x1,x1], [y1,y2], c ='r', label = "Box contains\n50% of data")
        ax1.plot([x1,x2], [y1,y1], c='r')
        ax1.plot([x1,x2], [y2,y2], c='r')
        ax1.plot([x2,x2], [y1,y2], c='r')

        try:
            # To get an idea for the trend I"m plotting al ine.
            # That said these errors are likely VERY non guassian
            # I don't feel like plotting them -- too deep in rabbit hole
            # to go down another. SO keep in mind these are "trends"
            # and shouldn't be read into beyond a positive/negative
            # correlation.

            if filter:
                # Filter outliers by one stadard dev. (VERY AGRESSIVE)
                top1 = t_f[category].mean() + t_f[category].std()
                top2 = t_f["Est Fund Per Student"].mean() + t_f["Est Fund Per Student"].std()
                bottom1 = t_f[category].mean() - t_f[category].std()
                bottom2 = t_f["Est Fund Per Student"].mean() - t_f["Est Fund Per Student"].std()
                t_f = t_f[t_f[category] < top1]
                t_f = t_f[t_f[category] > bottom1]
                tf = t_f[t_f["Est Fund Per Student"] < top2]
                tf = t_f[t_f["Est Fund Per Student"] > bottom2]

                x = tf[category]
                y = tf["Est Fund Per Student"]

            limits = x
            fit, V = np.polyfit(x, y, deg=1, cov=True)

            # 62 percentile. Though probably not really
            # as this calculation requires the errors to be normally distributed.
            error = 2*np.sqrt(np.diag(V))

            label = ''.join(["Line of best fit\n",
                            str(round(fit[0],2)),
                            "±",
                            str(round(error[0])),
                            "x + ",
                            str(round(fit[1],2)),
                            "±",
                            str(round(error[1],2))])

            ax1.plot(limits, fit[0] * limits + fit[1],
                     color='purple',
                     label = label)
            ax1.plot(limits,
                     (fit[0]+ error[0]) * limits + fit[1] + error[1],
                     color = 'orange',
                    label = "")
            ax1.plot(limits,
                     (fit[0]- error[0]) * limits + fit[1] - error[1],
                     color = 'orange',
                    label = "")

            test = fit[0] * x + fit[1]
            residual = y - test

        # Naked exception because I'm a rule breaker.
        except Exception as e:
            print("No data available for", subject, category)
            return

        if subject:
            pass
        else:
            subject = "All"

        title = "".join(["All Districts" ,
                 "\nMean Funding = \$",
                 str(round(y.mean(),2)),
                 " $\pm$ ",
                str(round(y.std(), 2)),
                         " (1sd)"
                "\nMean Percent = ",
                str(round(x.mean(),2)),
                 " $\pm$ ",
                str(round(x.std(), 2)),
                " % (1sd)",
                "\nSubject: ",
                 subject,
                        '\n', category])

        ax1.set_title(title)
        ax1.legend()

        sns.kdeplot(x, y, shade=True, ax=ax1)

        ax2.set_title("Distribution of Residuals of Line of Best Fit")

        # Test if the residual is normally distributed to judge our LOBF
        z,pval = mstats.normaltest(residual)
        if pval < 0.05:
            text = "Errors probably not normally distributed\n(Line of best fit shows approximate correlation only)"
        else:
            # don't think this will ever happen
            text = "Errors probably normally distributed\n(Line of best fit can be used to extrapolate)"

        ax2.set_xlabel("Distance from LOBF")
        ax2.set_ylabel("Counts")
        ax2.hist(residual, bins = 20, histtype='bar', ec='black', label = text)
        ax2.legend()
    # plt.show()

# this is a lazy copy-pase reformat of my filter function. I should probably ahve
# just written a better function originally .

def course_drop2(_type, name):

    courses = list(density_plot_frame['Course Name'].unique())
    for course in courses:
        if "\n" in course:
            courses.remove(course)

    if not name:
        return courses

    filtered_course_list = []

    for course in courses:
        result = pd.DataFrame()
        y = pd.DataFrame()
        result = density_plot_frame[density_plot_frame[_type] == name].copy()
        result = result[result['Course Name'] == course].copy()
        if _type == "School Name":
            y = result['School Grade Percentage Acceptable']

        if _type == "Authority Name":
            y = result["School Grade Percentage Acceptable"].copy()

        if y.isnull().sum() >= len(y) - 2 or y.empty == True:
            continue
        else:
            # if something exists, we'll count it
            filtered_course_list.append(course)

    if len(filtered_course_list) == 0:
        # TODO: make an empty thing instead of pretending they do math
        filtered_course_list.append("Mathematics 6")
    return sorted(filtered_course_list)

categories = ['School Grade Percentage Excellent',
                     'School Grade Percentage Acceptable',
                     'School Grade Percentage Unacceptable',
                     'Provincial/Diploma Exam Percent Acceptable',
                     'Provincial Exam Percent Acceptable, Part 2',
                     'Provincial/Diploma Exam Percent Excellent',
                     'Provincial Exam Percent Excellent, Part 2',
                     "Acceptable Grade Differential",
                     "Acceptable Grade Differential, Provincial Part 2",
                     "Excellent Grade Differential",
                     "Excellent Grade Differential, Provincial Part 2",
                     "School Grade Percentage Unacceptable",
                     ]
Authority = [None] + sorted(map(str,list(density_plot_frame["Authority Name"].unique())))

auth_widget = Dropdown(options= Authority)
sub_widget = Dropdown()

def update(*args):
    sub_widget.options = course_drop2("Authority Name", auth_widget.value)

auth_widget.observe(update)

years = [None] + years

interact(make_density,
        category = categories,
        year = years,
        subject = sub_widget,
        Authority = auth_widget)

```

```python

```

Using the widget above you can look at the year to year and total performance of every school district as a function of funding in the top graph, and below is a histogram of the linear fit residuals. In the small chance those residuals are normally distributed, the line of best fit can be used for extrapolation. However, if they are not, the line of best fit -at best- represents approximate correlation between student performance and funding. The differential is defined as

\begin{equation}
\Delta \text{Score} = S_{grade} - E_{grade}
\end{equation}

where $S$ is the overall grade awarded by the school, and $E$ is the grade students achieved on the exam.

By selecting an authority you can view the performance of that district year to year against the provincial mean as well. I note that not all authorities have exam or school marks for all courses in all years. In that case, an empty plot will be created. I also note that the provincial mean and standard deviations are also overlaying the plot, as well as individual points for each school with grades recorded. This makes it far easier to judge how well a school division performed relative to the province, as well as decide if these variations from the provincial mean are necessarily meaningful.

A few interesting things to point out about the funding graph however: Excellent and acceptable scores seem to be slightly negatively correlated with funding i.e. more funding seems to be related to worse grades in some cases. That said, correlation does not depend on causation, and there are significant outliers from the actual cluster that may be over weighting the outliers. You can aggressively remove outliers by clicking the filter button which removes all points (in $x$ and $y$) that are greater than one standard deviation away from the mean of the data. I note that this feature is only available on the density plot regarding funding information.

Regardless the trend is the same, and funding doesn't seem to really matter in terms of performance. If anything, more funding seems to imply that the students do worse. However, the uncertainty is so large, and the residuals are far from normal, so at best I will cautiously state that funding amount does not seem to affect overall student performance. Surely, this is likely a good sign. It may be interesting to take into account the geographic coordinates of each school and compare performance as a function of location.

## Question Level Precision for Math 30-1 Wild Rose School Div. 66

The province also reports the per-question performance of students on diploma exams. In this case we have the data set of the Wild Rose School Division 66 Math 30-1 scores for 2018. Below we have plotted the percentage of students who got each question correct for both the province, and the students in the Wild Rose School Division. Below we have plotted the differential defined as

\begin{equation}
\Delta \text{Score (%)} = \text{Score (Wild Rose) (%) } - \text{Score (Province) (%)}
\end{equation}

based on the above definition, a positive differential implies that the students of the Wild Rose School Division out performed the province, and a negative differential implies that the students under preformed in relation to the province.

```python
mathdf = pd.read_csv("math_2018_scores.csv")
del mathdf["instl_grp_id"]

f, xarr = plt.subplots(2,1,figsize=(20, 10))

xarr[0].plot(mathdf.index, mathdf["prov_ms_correct_pct"], label = "Province")
xarr[0].plot(mathdf.index, mathdf["ms_correct_pct"], label="Wildrose")
string = "".join(["Correlation = ",
                  str(round(mathdf["ms_correct_pct"].corr(mathdf["prov_ms_correct_pct"])*100,3)),
                  " %"])

x = np.linspace(0,39,10)

up = mathdf["prov_ms_correct_pct"].mean() + mathdf["prov_ms_correct_pct"].std()
down = mathdf["prov_ms_correct_pct"].mean() - mathdf["prov_ms_correct_pct"].std()
up1 = mathdf["ms_correct_pct"].mean() + mathdf["ms_correct_pct"].std()
down1 = mathdf["ms_correct_pct"].mean() - mathdf["ms_correct_pct"].std()

xarr[0].fill_between(x, up1,down1, alpha = 0.2, label = "Wildrose 1sd Range", color='r')
xarr[0].fill_between(x, up,down, alpha = 0.2, label = "Provincial 1sd Range", color='b')

xarr[0].set_xlim(0,39)
xarr[0].text(1,20, string,size=16)
xarr[0].legend()
xarr[0].set_xlabel("Question", size =20)
xarr[0].set_ylabel("Correct (%)", size =20)

up3 = (mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"]).mean() + (mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"]).std()

down3 = (mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"]).mean()-(mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"]).std()

xarr[1].fill_between(x, up3,down3, alpha = 0.4, label = "1sd Range")

xarr[1].plot(mathdf.index, mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"])

xarr[1].set_xlabel("Question", size =20)
xarr[1].set_ylabel("Correct Differential (%)", size =20)
xarr[1].legend()
xarr[1].set_xlim(0,39)

xarr[0].legend()

plt.show()

```

From the plot above we see the per-question performance of Wild Rose School Division No 66. as compared to to the per-question performance of the province on the Mathematics 30-1 Diploma exam. I note that without access to the non-aggregated provincial score data it is impossible to tell if any of the variations on performance between the school division and province are statistically significant or not. In attempt to estimate this, the range defined by the first standard deviation of these data are plotted as translucent bars on these data to help identify potential outliers, and if any variations between the school division and the province are significant. However, with this small set of data, I would be hard pressed to believe that there's any variations of particular significance. The only data point that jumps out is the poor performance on question 12 by the division. Certainly however, the province did poorly on that question as well.

What can be stated with certainty is that based on the correlation between  Wild Rose School Division and the province is that students in the division seemed to have trouble/do well on similar questions to the rest of the province. Beyond that however, besides providing some insights into the performance by students on each question, I'm not convinced there's any broad sweeping conclusions that can be made from this data set alone.

```python

```

## Performance By Question Type

This can also be broken down by question type, in this case the key to the $x$ axis of each plot is as follows

| Symbol  | C | P | PS | RF | TRIG | PCBT |
|---------|---|---|----|----|------|------|
| **Meaning** | Conceptual  |  Procedural |  Problem Solving  | Relations and Functions   |  Trigonometry   |    Permutations, Combinations and Binomial Theorem   |

Below shows the performance of the province, the wild rose school district, and the differntial of performance between the Wild Rose School District and the province on each question type.

```python
f, xarr = plt.subplots(2,3,figsize=(20, 10))

sns.boxplot(x=mathdf["Cognitive Level"],y=mathdf["ms_correct_pct"],ax=xarr[0,0])
sns.swarmplot(x=mathdf["Cognitive Level"],y=mathdf["ms_correct_pct"],ax=xarr[0,0],color=".25")
xarr[0,0].set_ylim(0,100)

sns.boxplot(x=mathdf["Cognitive Level"],y=mathdf["prov_ms_correct_pct"], ax=xarr[0,1])
sns.swarmplot(x=mathdf["Cognitive Level"],y=mathdf["prov_ms_correct_pct"], ax=xarr[0,1],color=".25")
xarr[0,1].set_ylim(0,100)

sns.boxplot(x=mathdf["Cognitive Level"],y=mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"], ax=xarr[0,2])
sns.swarmplot(x=mathdf["Cognitive Level"],y=mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"], ax=xarr[0,2],color=".25")
xarr[0,2].set_ylim(-40,40)

sns.boxplot(x=mathdf["Topic"],y=mathdf["ms_correct_pct"],ax=xarr[1,0])
sns.swarmplot(x=mathdf["Topic"],y=mathdf["ms_correct_pct"],ax=xarr[1,0],color=".25")
xarr[1,0].set_ylim(0,100)

sns.boxplot(x=mathdf["Topic"],y=mathdf["prov_ms_correct_pct"], ax=xarr[1,1])
sns.swarmplot(x=mathdf["Topic"],y=mathdf["prov_ms_correct_pct"], ax=xarr[1,1],color=".25")
xarr[1,1].set_ylim(0,100)

sns.boxplot(x=mathdf["Topic"],y=mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"], ax=xarr[1,2])
sns.swarmplot(x=mathdf["Topic"],y=mathdf["ms_correct_pct"]-mathdf["prov_ms_correct_pct"], ax=xarr[1,2],color=".25")
xarr[1,2].set_ylim(-40,40)

xarr[0,0].set_xlabel("Cognitive Level", fontsize=16)
xarr[0,1].set_xlabel("Cognitive Level", fontsize=16)
xarr[0,2].set_xlabel("Cognitive Level", fontsize=16)

xarr[0,0].set_ylabel("Provincial Question Score (%)", fontsize=16)
xarr[0,1].set_ylabel("District Question Score (%)", fontsize=16)
xarr[0,2].set_ylabel("Differential Question Score (%)", fontsize=16)

xarr[1,0].set_xlabel("Topic", fontsize=16)
xarr[1,1].set_xlabel("Topic", fontsize=16)
xarr[1,2].set_xlabel("Topic", fontsize=16)

xarr[1,0].set_ylabel("Provincial Question Score (%)", fontsize=16)
xarr[1,1].set_ylabel("District Question Score (%)", fontsize=16)
xarr[1,2].set_ylabel("Differential Question Score (%)", fontsize=16)
plt.show()

```

The plots above show the performance of the province and the division based on each question type. From this there's potentially more interesting conclusions than the data set before in that the entire province, as well as the district seems to do poorly at both problem solving, and permutations combinations and binomial theorem as compared to the other categories. If there is any conclusions to take away from the above, it is that the Wild Rose School division sees its most negative differentials with relations and functions, as well as conceptual questions.

## Conclusion

Unfortunately there's not many concrete conclusions to take away from this data without specialized insight into the differences between divisions and individual schools. However, we did see that funding per-student does not seem to influence student performance in any significant manner. The only place there _may_ be a correlation between funding and test scores, is that students with more funding tend to do more poorly on exams. Beyond that, with the question-level resolution on the Math 30-1 diploma scores with the Wild Rose School Division we see that that division is, more or less, on par with the province with a few outliers in regards to test scores. However, students at the Wild Rose School Division seemed to have the greatest trouble with relations of functions, and conceptual questions.

[](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


# Jupyter notebook tricks

## Extending notebook width

```python
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:96% !important; }</style>"))    # change width according to your needs
```

## Hiding code cells

```python
from IPython.display import HTML
HTML('''
The raw code for this IPython notebook is by default hidden for easier reading.
To toggle on/off the raw code, click <a href="javascript:code_toggle()">here</a>.''')
```

## Automatically reload modules while developing them
simply add these lines before importing the module for the first time
```Python
%load_ext autoreload
%autoreload 2
```
Probably you have to restart the kernel to make it working.

## Latex definitions
Simply add your latex commands in a Markdown cell.
```
$\newcommand{point}[1]{\mathrm{#1}}$
$\newcommand{vecw}[2]{\mathbf{#1}_{#2}^{\mathrm{W}}}$
```

Hidden Latex definitions
$\newcommand{point}[1]{\mathrm{#1}}$
$\newcommand{vecw}[2]{\mathbf{#1}_{#2}^{\mathrm{W}}}$

Write some latex using your commands.
$\point{R}$ defines the reference point on the rear of a tracked object.
EM/TP always tracks $\point{R}$ as $\vecw{x}{R}$

## Mixing Markdown and output variable (working also in the exported HTML)

```python
from IPython.display import display, Markdown, Latex
from sympy import sin,symbols
alpha = symbols('\\alpha', real=True)
func  = sin(alpha)
display(Markdown(f'$f(\\alpha)$ = {func}')) # doesn't work properly
display(Markdown(f'$f(\\alpha)$ = '), func)
```

```python

```