# Drive-it DQN

## Model
\begin{equation}
l_1 = relu( x W_1 + b_1) \\
l_2 = relu( x W_2 + b_2) \\
l_3 = relu( x W_3 + b_3) \\
Q(s,a) = l_1 W_o + b_o \\
\end{equation}

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import style
import seaborn as sns
style.use('ggplot')
%matplotlib inline
sns.set()
```

## Visualization
We use PCA decomposition on memory samples to visualize the $Q(s,a)$ values across the state space:

```python
def pca_plot(n=10000, alpha=1.0, size=5):
    _, samples = agent.memory.sample(n)
    states = np.array([ o[0] for o in samples ], dtype=np.float32)
    qsa = agent.brain.predict(states)[0]

    from sklearn import decomposition
    pca = decomposition.PCA(n_components=2)
    pca.fit(states)
    X = pca.transform(states)

    fig, axes = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
    ax = axes[0,0]; plt.sca(ax);ax.set_title('Action')
    plt.scatter(X[:, 0], X[:, 1], c=np.argmax(qsa, 1), alpha=alpha, s=size, cmap="rainbow")
    ax = axes[0,1]; plt.sca(ax);ax.set_title('Q(s,no-change)')
    plt.scatter(X[:, 0], X[:, 1], c=qsa[:,0], alpha=alpha, s=size, cmap="rainbow")
    ax = axes[1,0]; plt.sca(ax);ax.set_title('Q(s,left)')
    plt.scatter(X[:, 0], X[:, 1], c=qsa[:,1], alpha=alpha, s=size, cmap="rainbow")
    ax = axes[1,1]; plt.sca(ax);ax.set_title('Q(s,right)')
    plt.scatter(X[:, 0], X[:, 1], c=qsa[:,2], alpha=alpha, s=size, cmap="rainbow")
```

For a more meaningful plot, just project sample across $x_m$ and $y_m$.

```python
def slice_plot(n=10000, alpha=1.0, size=5):
    _, samples = agent.memory.sample(n)
    states = np.array([ o[0] for o in samples ], dtype=np.float32)
    qsa = agent.brain.predict(states)[0]

    fig, axes = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
    ax = axes[0,0]; plt.sca(ax);ax.set_title('Action')
    plt.scatter(states[:, 0], states[:, 1], c=np.argmax(qsa, 1), alpha=alpha, s=size, cmap="rainbow")
    ax = axes[0,1]; plt.sca(ax);ax.set_title('Q(s,no-change)')
    plt.scatter(states[:, 0], states[:, 1], c=qsa[:,0], alpha=alpha, s=size, cmap="rainbow")
    ax = axes[1,0]; plt.sca(ax);ax.set_title('Q(s,left)')
    plt.scatter(states[:, 0], states[:, 1], c=qsa[:,1], alpha=alpha, s=size, cmap="rainbow")
    ax = axes[1,1]; plt.sca(ax);ax.set_title('Q(s,right)')
    plt.scatter(states[:, 0], states[:, 1], c=qsa[:,2], alpha=alpha, s=size, cmap="rainbow")
    axes[0,0].set_ylabel('$y_m$')
    axes[1,0].set_ylabel('$y_m$')
    axes[1,0].set_xlabel('$x_m$')
    axes[1,1].set_xlabel('$x_m$')
```

## Training

```python
def run_episode(agent, render=False):
    s = env.reset()
    R = 0
    while True:
        if render: env.render()

        a = agent.act(s.astype(np.float32))
        s_, r, done, info = env.step(a)

        if done:
            s_ = None

        agent.observe((s, a, r, s_))
        s = s_
        R += r

        if done:
            agent.endEpisode()
            return R
```

```python
from DriveItGym import DriveItEnv
from agent import Agent

BATCH_SIZE = 20000

env = DriveItEnv(time_limit=10.0, throttle_limit=1.0)
stateCnt  = env.observation_space.shape[0]
actionCnt = env.action_space.n
agent = Agent(stateCnt, actionCnt)

episode_number = 0
last_batch_episode = 0
last_batch_steps = 0
episodes = []
reward_sum = 0
reward_best = 18.0

(stateCnt, actionCnt)
```

    (5, 9)

```python
while episode_number < 50000 and agent.steps < 1980000:
    episode_number += 1
    reward = run_episode(agent, render=False)
    reward_sum += reward

    if agent.steps >= last_batch_steps + BATCH_SIZE:
        reward_avg = reward_sum / (episode_number - last_batch_episode)
        last_batch_episode = episode_number
        last_batch_steps = int(agent.steps / BATCH_SIZE) * BATCH_SIZE
        episodes.append((episode_number, agent.steps, reward_avg))

        print('Episode: %d, steps: %d, epsilon: %f, average reward: %f.' \
              % (episode_number, agent.steps, agent.epsilon, reward_avg))

        if reward_avg > reward_best:
            reward_best = reward_avg
            agent.brain.model.save_model('best.mod')

        reward_sum = 0

agent.brain.model.save_model('last.mod')
print('Done.')
```

    Episode: 718, steps: 20035, epsilon: 0.972742, average reward: -1.224206.
    Episode: 1457, steps: 40017, epsilon: 0.946378, average reward: -1.251165.
    Episode: 2182, steps: 60001, epsilon: 0.920808, average reward: -1.238136.
    Episode: 2875, steps: 80012, epsilon: 0.895978, average reward: -1.200988.
    Episode: 3548, steps: 100006, epsilon: 0.871919, average reward: -1.157299.
    Episode: 4207, steps: 120032, epsilon: 0.848551, average reward: -1.132718.
    Episode: 4819, steps: 140004, epsilon: 0.825950, average reward: -1.058965.
    Episode: 5430, steps: 160005, epsilon: 0.804000, average reward: -1.062778.
    Episode: 6037, steps: 180017, epsilon: 0.782702, average reward: -1.038461.
    Episode: 6611, steps: 200012, epsilon: 0.762066, average reward: -0.992688.
    Episode: 7181, steps: 220008, epsilon: 0.742052, average reward: -0.982236.
    Episode: 7723, steps: 240044, epsilon: 0.722606, average reward: -0.923957.
    Episode: 8263, steps: 260031, epsilon: 0.703794, average reward: -0.911821.
    Episode: 8786, steps: 280017, epsilon: 0.685551, average reward: -0.870779.
    Episode: 9298, steps: 300008, epsilon: 0.667855, average reward: -0.836974.
    Episode: 9776, steps: 320036, epsilon: 0.650662, average reward: -0.758708.
    Episode: 10249, steps: 340041, epsilon: 0.634009, average reward: -0.732370.
    Episode: 10721, steps: 360090, epsilon: 0.617824, average reward: -0.736512.
    Episode: 11176, steps: 380019, epsilon: 0.602223, average reward: -0.690797.
    Episode: 11629, steps: 400128, epsilon: 0.586957, average reward: -0.666337.
    Episode: 12073, steps: 420002, epsilon: 0.572325, average reward: -0.667183.
    Episode: 12535, steps: 440022, epsilon: 0.558031, average reward: -0.720751.
    Episode: 12967, steps: 460056, epsilon: 0.544159, average reward: -0.608047.
    Episode: 13385, steps: 480028, epsilon: 0.530749, average reward: -0.556201.
    Episode: 13789, steps: 500035, epsilon: 0.517721, average reward: -0.506930.
    Episode: 14182, steps: 520081, epsilon: 0.505062, average reward: -0.470994.
    Episode: 14557, steps: 540103, epsilon: 0.492802, average reward: -0.381951.
    Episode: 14934, steps: 560001, epsilon: 0.480986, average reward: -0.387621.
    Episode: 15288, steps: 580042, epsilon: 0.469443, average reward: -0.291349.
    Episode: 15633, steps: 600007, epsilon: 0.458293, average reward: -0.263519.
    Episode: 15964, steps: 620091, epsilon: 0.447415, average reward: -0.179316.
    Episode: 16292, steps: 640007, epsilon: 0.436954, average reward: -0.146795.
    Episode: 16626, steps: 660033, epsilon: 0.426754, average reward: -0.180636.
    Episode: 16913, steps: 680091, epsilon: 0.416846, average reward: 0.155630.
    Episode: 17209, steps: 700074, epsilon: 0.407274, average reward: 0.099739.
    Episode: 17485, steps: 720044, epsilon: 0.397998, average reward: 0.239702.
    Episode: 17754, steps: 740035, epsilon: 0.388992, average reward: 0.312114.
    Episode: 18034, steps: 760041, epsilon: 0.380252, average reward: 0.178870.
    Episode: 18298, steps: 780067, epsilon: 0.371768, average reward: 0.327785.
    Episode: 18584, steps: 800076, epsilon: 0.363547, average reward: 0.176827.
    Episode: 18863, steps: 820172, epsilon: 0.355541, average reward: 0.240584.
    Episode: 19106, steps: 840011, epsilon: 0.347876, average reward: 0.524671.
    Episode: 19294, steps: 860118, epsilon: 0.340342, average reward: 1.418729.
    Episode: 19482, steps: 880082, epsilon: 0.333089, average reward: 1.440568.
    Episode: 19653, steps: 900133, epsilon: 0.326024, average reward: 1.825130.
    Episode: 19863, steps: 920022, epsilon: 0.319227, average reward: 0.954244.
    Episode: 20020, steps: 940351, epsilon: 0.312492, average reward: 2.220774.
    Episode: 20201, steps: 960042, epsilon: 0.306165, average reward: 1.462366.
    Episode: 20390, steps: 980141, epsilon: 0.299901, average reward: 1.349605.
    Episode: 20528, steps: 1000004, epsilon: 0.293898, average reward: 2.602040.
    Episode: 20682, steps: 1020065, epsilon: 0.288018, average reward: 2.161909.
    Episode: 20833, steps: 1040119, epsilon: 0.282318, average reward: 2.350221.
    Episode: 20946, steps: 1060230, epsilon: 0.276776, average reward: 3.956100.
    Episode: 21120, steps: 1080008, epsilon: 0.271489, average reward: 1.701919.
    Episode: 21264, steps: 1100103, epsilon: 0.266280, average reward: 2.632423.
    Episode: 21414, steps: 1120009, epsilon: 0.261276, average reward: 2.354048.
    Episode: 21550, steps: 1140047, epsilon: 0.256391, average reward: 2.765858.
    Episode: 21665, steps: 1160104, epsilon: 0.251649, average reward: 3.782466.
    Episode: 21789, steps: 1180039, epsilon: 0.247079, average reward: 3.386539.
    Episode: 21866, steps: 1200042, epsilon: 0.242631, average reward: 7.136366.
    Episode: 21944, steps: 1220501, epsilon: 0.238221, average reward: 6.887497.
    Episode: 22019, steps: 1240069, epsilon: 0.234131, average reward: 6.895148.
    Episode: 22091, steps: 1260590, epsilon: 0.229972, average reward: 7.997184.
    Episode: 22161, steps: 1280460, epsilon: 0.226067, average reward: 7.776642.
    Episode: 22237, steps: 1300250, epsilon: 0.222295, average reward: 7.200248.
    Episode: 22295, steps: 1320142, epsilon: 0.218617, average reward: 10.295665.
    Episode: 22361, steps: 1340565, epsilon: 0.214956, average reward: 8.899904.
    Episode: 22442, steps: 1360096, epsilon: 0.211561, average reward: 6.417967.
    Episode: 22517, steps: 1380060, epsilon: 0.208194, average reward: 7.474270.
    Episode: 22586, steps: 1400012, epsilon: 0.204930, average reward: 8.183188.
    Episode: 22643, steps: 1420371, epsilon: 0.201702, average reward: 10.896847.
    Episode: 22700, steps: 1440572, epsilon: 0.198596, average reward: 10.729821.
    Episode: 22745, steps: 1460418, epsilon: 0.195638, average reward: 14.358912.
    Episode: 22800, steps: 1480309, epsilon: 0.192762, average reward: 11.245888.
    Episode: 22852, steps: 1500002, epsilon: 0.190000, average reward: 11.856634.
    Episode: 22921, steps: 1520024, epsilon: 0.187276, average reward: 8.477440.
    Episode: 23064, steps: 1540195, epsilon: 0.184615, average reward: 2.860961.
    Episode: 23185, steps: 1560474, epsilon: 0.182021, average reward: 3.688232.
    Episode: 23246, steps: 1580136, epsilon: 0.179583, average reward: 9.714348.
    Episode: 23329, steps: 1600387, epsilon: 0.177147, average reward: 6.449905.
    Episode: 23393, steps: 1620020, epsilon: 0.174856, average reward: 8.797979.
    Episode: 23461, steps: 1640030, epsilon: 0.172592, average reward: 8.293289.
    Episode: 23532, steps: 1660199, epsilon: 0.170379, average reward: 7.849850.
    Episode: 23717, steps: 1680041, epsilon: 0.168268, average reward: 1.562792.
    Episode: 23901, steps: 1700068, epsilon: 0.166201, average reward: 1.509919.
    Episode: 24028, steps: 1720311, epsilon: 0.164175, average reward: 3.394587.
    Episode: 24171, steps: 1740097, epsilon: 0.162256, average reward: 2.581383.
    Episode: 24349, steps: 1760098, epsilon: 0.160373, average reward: 1.620501.
    Episode: 24453, steps: 1780394, epsilon: 0.158521, average reward: 4.655454.
    Episode: 24530, steps: 1800096, epsilon: 0.156778, average reward: 6.993054.
    Episode: 24574, steps: 1820009, epsilon: 0.155068, average reward: 14.689843.
    Episode: 24617, steps: 1840490, epsilon: 0.153364, average reward: 15.524565.
    Episode: 24656, steps: 1860571, epsilon: 0.151744, average reward: 17.150088.
    Episode: 24720, steps: 1880287, epsilon: 0.150202, average reward: 9.120932.
    Episode: 24780, steps: 1900189, epsilon: 0.148691, average reward: 9.953159.
    Episode: 24818, steps: 1920218, epsilon: 0.147217, average reward: 18.060605.
    Episode: 24858, steps: 1940099, epsilon: 0.145798, average reward: 16.593031.
    Episode: 24904, steps: 1960178, epsilon: 0.144408, average reward: 13.978157.
    Episode: 24953, steps: 1980114, epsilon: 0.143069, average reward: 12.740658.
    Done.

```python
plt.plot([e[1]/1000 for e in episodes], [e[2] for e in episodes])
plt.xlabel('steps x 1000');plt.ylabel('reward')
```

---

```python
while episode_number < 30000 and agent.steps < 2980000:
    episode_number += 1
    reward = run_episode(agent, render=False)
    reward_sum += reward

    if agent.steps >= last_batch_steps + BATCH_SIZE:
        reward_avg = reward_sum / (episode_number - last_batch_episode)
        last_batch_episode = episode_number
        last_batch_steps = int(agent.steps / BATCH_SIZE) * BATCH_SIZE
        episodes.append((episode_number, agent.steps, reward_avg))

        print('Episode: %d, steps: %d, epsilon: %f, average reward: %f.' \
              % (episode_number, agent.steps, agent.epsilon, reward_avg))

        if reward_avg > reward_best:
            reward_best = reward_avg
            agent.brain.model.save_model('best.mod', False)

        reward_sum = 0

agent.brain.model.save_model('last.mod', False)
print('Done.')
```

    Episode: 806, steps: 20012, epsilon: 0.964170, average reward: -1.314449.
    Episode: 1568, steps: 40005, epsilon: 0.929668, average reward: -1.267090.
    Episode: 2316, steps: 60007, epsilon: 0.896400, average reward: -1.244711.
    Episode: 3040, steps: 80006, epsilon: 0.864339, average reward: -1.227046.
    Episode: 3743, steps: 100024, epsilon: 0.833410, average reward: -1.192490.
    Episode: 4381, steps: 120006, epsilon: 0.803653, average reward: -1.094363.
    Episode: 5019, steps: 140019, epsilon: 0.774927, average reward: -1.096916.
    Episode: 5626, steps: 160011, epsilon: 0.747270, average reward: -1.045155.
    Episode: 6252, steps: 180022, epsilon: 0.720588, average reward: -1.066209.
    Episode: 6861, steps: 200021, epsilon: 0.694886, average reward: -1.040312.
    Episode: 7435, steps: 220058, epsilon: 0.670068, average reward: -0.981508.
    Episode: 7956, steps: 240078, epsilon: 0.646169, average reward: -0.860671.
    Episode: 8452, steps: 260060, epsilon: 0.623179, average reward: -0.797081.
    Episode: 8938, steps: 280012, epsilon: 0.601052, average reward: -0.773937.
    Episode: 9422, steps: 300011, epsilon: 0.579674, average reward: -0.749323.
    Episode: 9896, steps: 320020, epsilon: 0.559059, average reward: -0.733105.
    Episode: 10341, steps: 340046, epsilon: 0.539174, average reward: -0.630904.
    Episode: 10776, steps: 360038, epsilon: 0.520041, average reward: -0.615549.
    Episode: 11182, steps: 380024, epsilon: 0.501605, average reward: -0.499242.
    Episode: 11568, steps: 400020, epsilon: 0.483826, average reward: -0.393527.
    Episode: 11928, steps: 420049, epsilon: 0.466663, average reward: -0.274655.
    Episode: 12286, steps: 440055, epsilon: 0.450140, average reward: -0.281907.
    Episode: 12622, steps: 460048, epsilon: 0.434226, average reward: -0.133299.
    Episode: 12945, steps: 480031, epsilon: 0.418894, average reward: -0.087263.
    Episode: 13255, steps: 500120, epsilon: 0.404039, average reward: 0.006544.
    Episode: 13520, steps: 520101, epsilon: 0.389800, average reward: 0.391778.
    Episode: 13776, steps: 540037, epsilon: 0.376105, average reward: 0.473756.
    Episode: 14019, steps: 560011, epsilon: 0.362879, average reward: 0.636351.
    Episode: 14234, steps: 580016, epsilon: 0.350112, average reward: 0.975623.
    Episode: 14452, steps: 600022, epsilon: 0.337807, average reward: 0.954006.
    Episode: 14654, steps: 620064, epsilon: 0.325925, average reward: 1.251441.
    Episode: 14845, steps: 640087, epsilon: 0.314485, average reward: 1.421935.
    Episode: 15027, steps: 660045, epsilon: 0.303494, average reward: 1.566506.
    Episode: 15210, steps: 680033, epsilon: 0.292884, average reward: 1.542400.
    Episode: 15374, steps: 700256, epsilon: 0.282540, average reward: 2.052173.
    Episode: 15517, steps: 720029, epsilon: 0.272792, average reward: 2.586585.
    Episode: 15672, steps: 740073, epsilon: 0.263266, average reward: 2.282690.
    Episode: 15797, steps: 760040, epsilon: 0.254120, average reward: 3.382139.
    Episode: 15923, steps: 780111, epsilon: 0.245259, average reward: 3.388498.
    Episode: 16053, steps: 800055, epsilon: 0.236773, average reward: 3.166681.
    Episode: 16162, steps: 820040, epsilon: 0.228576, average reward: 4.191718.
    Episode: 16284, steps: 840054, epsilon: 0.220665, average reward: 3.485517.
    Episode: 16404, steps: 860085, epsilon: 0.213033, average reward: 3.547644.
    Episode: 16501, steps: 880105, epsilon: 0.205682, average reward: 4.981430.
    Episode: 16614, steps: 900052, epsilon: 0.198623, average reward: 3.881476.
    Episode: 16704, steps: 920023, epsilon: 0.191810, average reward: 5.576601.
    Episode: 16799, steps: 940390, epsilon: 0.185115, average reward: 5.215137.
    Episode: 16887, steps: 960284, epsilon: 0.178814, average reward: 5.675337.
    Episode: 16963, steps: 980489, epsilon: 0.172646, average reward: 7.176975.
    Episode: 17026, steps: 1000490, epsilon: 0.166763, average reward: 9.227968.
    Episode: 17085, steps: 1020379, epsilon: 0.161123, average reward: 9.916854.
    Episode: 17146, steps: 1040091, epsilon: 0.155735, average reward: 9.408133.
    Episode: 17211, steps: 1060313, epsilon: 0.150406, average reward: 8.969985.
    Episode: 17270, steps: 1080478, epsilon: 0.145286, average reward: 9.953835.
    Episode: 17328, steps: 1100024, epsilon: 0.140502, average reward: 10.276583.
    Episode: 17379, steps: 1120422, epsilon: 0.135689, average reward: 12.669610.
    Episode: 17430, steps: 1140314, epsilon: 0.131167, average reward: 12.270737.
    Episode: 17482, steps: 1160085, epsilon: 0.126833, average reward: 11.671696.
    Episode: 17535, steps: 1180014, epsilon: 0.122622, average reward: 11.975995.
    Episode: 17581, steps: 1200383, epsilon: 0.118475, average reward: 14.133583.
    Episode: 17626, steps: 1220071, epsilon: 0.114611, average reward: 14.210104.
    Episode: 17670, steps: 1240216, epsilon: 0.110800, average reward: 15.101350.
    Episode: 17711, steps: 1260307, epsilon: 0.107138, average reward: 16.540284.
    Episode: 17751, steps: 1280553, epsilon: 0.103582, average reward: 17.141953.
    Episode: 17793, steps: 1300380, epsilon: 0.100226, average reward: 15.744951.
    Episode: 17830, steps: 1320375, epsilon: 0.096963, average reward: 18.829577.
    Episode: 17867, steps: 1340306, epsilon: 0.093828, average reward: 18.602951.
    Episode: 17903, steps: 1360409, epsilon: 0.090781, average reward: 19.541556.
    Episode: 17942, steps: 1380253, epsilon: 0.087881, average reward: 17.475310.
    Episode: 17983, steps: 1400095, epsilon: 0.085086, average reward: 16.419630.
    Episode: 18019, steps: 1420422, epsilon: 0.082327, average reward: 20.030583.
    Episode: 18055, steps: 1440499, epsilon: 0.079701, average reward: 19.754224.
    Episode: 18092, steps: 1460318, epsilon: 0.077202, average reward: 18.923814.
    Episode: 18128, steps: 1480265, epsilon: 0.074777, average reward: 19.340988.
    Episode: 18165, steps: 1500297, epsilon: 0.072431, average reward: 19.053294.
    Episode: 18199, steps: 1520225, epsilon: 0.070180, average reward: 20.868038.
    Episode: 18234, steps: 1540020, epsilon: 0.068026, average reward: 19.991926.
    Episode: 18271, steps: 1560569, epsilon: 0.065870, average reward: 19.540105.
    Episode: 18305, steps: 1580557, epsilon: 0.063850, average reward: 21.160380.
    Episode: 18339, steps: 1600015, epsilon: 0.061955, average reward: 20.350771.
    Episode: 18373, steps: 1620415, epsilon: 0.060038, average reward: 21.605614.
    Episode: 18409, steps: 1640582, epsilon: 0.058214, average reward: 19.655399.
    Episode: 18444, steps: 1660067, epsilon: 0.056514, average reward: 19.807977.
    Episode: 18478, steps: 1680290, epsilon: 0.054813, average reward: 21.169176.
    Episode: 18512, steps: 1700283, epsilon: 0.053193, average reward: 21.019891.
    Episode: 18547, steps: 1720473, epsilon: 0.051616, average reward: 20.724094.
    Episode: 18583, steps: 1740471, epsilon: 0.050111, average reward: 19.759975.
    Episode: 18616, steps: 1760271, epsilon: 0.048674, average reward: 21.368242.
    Episode: 18651, steps: 1780228, epsilon: 0.047278, average reward: 20.304424.
    Episode: 18686, steps: 1800406, epsilon: 0.045918, average reward: 20.739795.
    Episode: 18722, steps: 1820562, epsilon: 0.044609, average reward: 19.962188.
    Episode: 18755, steps: 1840160, epsilon: 0.043382, average reward: 21.648067.
    Episode: 18789, steps: 1860060, epsilon: 0.042180, average reward: 21.196176.
    Episode: 18823, steps: 1880460, epsilon: 0.040993, average reward: 21.870012.
    Episode: 18857, steps: 1900471, epsilon: 0.039872, average reward: 20.845313.
    Episode: 18891, steps: 1920341, epsilon: 0.038798, average reward: 20.792393.
    Episode: 18925, steps: 1940172, epsilon: 0.037765, average reward: 20.612579.
    Episode: 18959, steps: 1960572, epsilon: 0.036741, average reward: 21.402128.
    Episode: 18994, steps: 1980475, epsilon: 0.035778, average reward: 20.463964.
    Episode: 19027, steps: 2000275, epsilon: 0.034855, average reward: 21.461197.
    Episode: 19060, steps: 2020075, epsilon: 0.033965, average reward: 21.361877.
    Episode: 19094, steps: 2040475, epsilon: 0.033081, average reward: 21.201151.
    Episode: 19129, steps: 2060466, epsilon: 0.032247, average reward: 19.997824.
    Episode: 19162, steps: 2080266, epsilon: 0.031450, average reward: 21.078323.
    Episode: 19195, steps: 2100066, epsilon: 0.030682, average reward: 21.623385.
    Episode: 19229, steps: 2120466, epsilon: 0.029919, average reward: 21.369890.
    Episode: 19262, steps: 2140266, epsilon: 0.029205, average reward: 21.502574.
    Episode: 19295, steps: 2160066, epsilon: 0.028518, average reward: 21.541886.
    Episode: 19329, steps: 2180466, epsilon: 0.027835, average reward: 21.110788.
    Episode: 19362, steps: 2200266, epsilon: 0.027196, average reward: 21.301970.
    Episode: 19396, steps: 2220137, epsilon: 0.026578, average reward: 20.318614.
    Episode: 19430, steps: 2240537, epsilon: 0.025966, average reward: 21.972949.
    Episode: 19463, steps: 2260337, epsilon: 0.025395, average reward: 21.822823.
    Episode: 19497, steps: 2280207, epsilon: 0.024841, average reward: 21.130402.
    Episode: 19532, steps: 2300078, epsilon: 0.024308, average reward: 19.986867.
    Episode: 19566, steps: 2320478, epsilon: 0.023780, average reward: 21.988445.
    Episode: 19599, steps: 2340278, epsilon: 0.023287, average reward: 21.976736.
    Episode: 19633, steps: 2360095, epsilon: 0.022810, average reward: 21.372406.
    Episode: 19667, steps: 2380495, epsilon: 0.022338, average reward: 21.668707.
    Episode: 19700, steps: 2400295, epsilon: 0.021896, average reward: 20.524818.
    Episode: 19733, steps: 2420095, epsilon: 0.021470, average reward: 21.343318.
    Episode: 19767, steps: 2440495, epsilon: 0.021047, average reward: 21.967237.
    Episode: 19801, steps: 2460442, epsilon: 0.020648, average reward: 21.211278.
    Episode: 19834, steps: 2480242, epsilon: 0.020267, average reward: 20.513254.
    Episode: 19868, steps: 2500113, epsilon: 0.019898, average reward: 21.083357.
    Episode: 19903, steps: 2520583, epsilon: 0.019532, average reward: 20.038888.
    Episode: 19937, steps: 2540582, epsilon: 0.019187, average reward: 21.413617.
    Episode: 19972, steps: 2560533, epsilon: 0.018855, average reward: 19.901994.
    Episode: 20006, steps: 2580436, epsilon: 0.018537, average reward: 21.236916.
    Episode: 20039, steps: 2600236, epsilon: 0.018231, average reward: 20.967074.
    Episode: 20073, steps: 2620053, epsilon: 0.017936, average reward: 21.078987.
    Episode: 20108, steps: 2640524, epsilon: 0.017642, average reward: 20.687864.
    Episode: 20141, steps: 2660324, epsilon: 0.017368, average reward: 20.861041.
    Episode: 20174, steps: 2680124, epsilon: 0.017105, average reward: 21.678740.
    Episode: 20208, steps: 2700524, epsilon: 0.016843, average reward: 21.549884.
    Episode: 20241, steps: 2720324, epsilon: 0.016597, average reward: 21.531291.
    Episode: 20276, steps: 2740289, epsilon: 0.016359, average reward: 19.984779.
    Episode: 20309, steps: 2760089, epsilon: 0.016131, average reward: 20.775554.
    Episode: 20344, steps: 2780586, epsilon: 0.015904, average reward: 20.928459.
    Episode: 20377, steps: 2800258, epsilon: 0.015694, average reward: 20.921902.
    Episode: 20411, steps: 2820130, epsilon: 0.015489, average reward: 20.441248.
    Episode: 20445, steps: 2840530, epsilon: 0.015287, average reward: 21.524364.
    Episode: 20478, steps: 2860330, epsilon: 0.015098, average reward: 21.496326.
    Episode: 20512, steps: 2880229, epsilon: 0.014914, average reward: 20.891398.
    Episode: 20545, steps: 2900029, epsilon: 0.014738, average reward: 21.723245.
    Episode: 20579, steps: 2920429, epsilon: 0.014563, average reward: 21.604473.
    Episode: 20612, steps: 2940229, epsilon: 0.014400, average reward: 21.203717.
    Episode: 20645, steps: 2960029, epsilon: 0.014242, average reward: 21.506767.
    Episode: 20679, steps: 2980429, epsilon: 0.014086, average reward: 20.863255.
    Done.

```python
plt.plot([e[1]/1000 for e in episodes], [e[2] for e in episodes])
plt.xlabel('steps x 1000');plt.ylabel('reward')
plt.savefig('learning.png', dpi=300)
```

```python
slice_plot(n=20000, size=5, alpha=0.5)
plt.savefig('qslice.png', dpi=300)
```

```python
pca_plot(n=20000, size=5, alpha=0.5)
plt.savefig('pca.png', dpi=300)
```

### Exploration - exploitation trade-off

Note initiall $\epsilon$ is set to 1 which implies we are enitrely exploraing but as steps increase we reduce exploration and start leveraging the learnt space to collect rewards (a.k.a exploitation) as well.

```python
def epsilon(steps):
    return MIN_EPSILON + (MAX_EPSILON - MIN_EPSILON) * np.exp(-LAMBDA * steps)

r = range(0,EXPLORATION_STOP,int(EXPLORATION_STOP/100))
plt.plot(r, [min(epsilon(x),1) for x in r], 'r')
#plt.plot(r, [min(epsilon(x),1)**EPSILON_TRAIN_FACTOR for x in r], 'b')
plt.xlabel('step');plt.ylabel('$\epsilon$')
```

## Discounted Reward

We tune $\gamma$ to look ahead only a short timespan, in which the current action is of significant importance.

```python
r = range(0,600)
plt.plot([t/60.0 for t in r], [GAMMA ** x for x in r], 'r')
plt.xlabel('time [s]');plt.ylabel('discount')
GAMMA
```

```python

```


# Introduction to compressible flow

This module will introduce the fundamentals of compressible fluid flows, which can behave a bit differently than
what you have learned for incompressible flows.

```python
%matplotlib inline
from matplotlib import pyplot as plt

import numpy as np

# Pint gives us some helpful unit conversion
from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity # We will use this to construct quantities (value + unit)
```

```python
# these lines are only for helping improve the display
import matplotlib_inline.backend_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('pdf', 'png')
plt.rcParams['figure.dpi']= 150
plt.rcParams['savefig.dpi'] = 150
```

First, let's consider how a disturbance passes through an elastic medium (solid, liquid, or gas).
If we consider the medium to be composed of molecules, then a disturbance on one side will compress
the molecules, and this information will be passed along to neighboring molecules, and so on,
forming a **traveling wave** that moves at a particular **wave speed**.
The stronger the traveling wave, the faster it will propagate.

Shock waves
: Large amplitude waves, that involve a large change in pressure and density of the fluid. Speed depends on strength of the wave.

Sound waves
: Small amplitude waves, where the speed only depends on the medium and its state. Moves at the **sonic velocity**.

Sonic velocity
: The velocity by which waves travel in an elastic medium (like a fluid) to propagate information about an object's presence.

## Sound wave propagation

Consider a piston inside a cylinder, where everything is initially at rest. The fluid properties are
velocity $V = 0$, pressure $p$, and density $\rho$.

Then, instantaneously, the piston starts moving with velocity $dV$ to the left. This compresses the adjecent
fluid molecules a small amount.
Continued motion will compress more molecules, and a wave front will propagate to the left at the sonic velocity
with magnitude $a$.

However, analyzing this problem is hard, because it is *unsteady*. Instead, we can change our frame of reference
to create a stationary, standing wave by adding a constant velocity of $+a$ (pointing to the right).
Then, we'll do a control volume analysis of the one-dimensional steady flow around the wave.

First, we can apply the continuity equation (conservation of mass), recognizing that area is constant for our control volume:

$$
\dot{m} = \rho A V = \text{constant} \\
\rho V = \text{constant} \\
\rho a = (\rho + d\rho)(a - dV) \\
\rho a = \rho a - \rho \, dV + a \, d\rho - d\rho \, dV \\
\rho \, dV = a \, d\rho \\
\therefore dV = \frac{a \, d\rho}{\rho} \;,
$$ (eq:continuity)

where $d\rho \, dV = 0$ because it is the product of two differential terms.

Next, we can apply conservation of momentum, where we can neglect any shear stresses due to the
control volume being infinitesimally small:

$$
\sum F_x = \dot{m} \left( V_{\text{out},x} - V_{\text{in}, x} \right) \\
p A - (p + dP) A = \rho A a \left[ (a - dV) - a \right] \\
-A \, dP = -\rho A a \, dV \\
\therefore dV = \frac{dp}{\rho a} \;.
$$ (eq:momentum)

We can then combine Equations {eq}`eq:continuity` and {eq}`eq:momentum`:

$$
\frac{dp}{\rho a} = \frac{a \, d\rho}{\rho} \\
\therefore a^2 = \frac{dp}{d\rho} \;.
$$

However, this result is process-dependent. Let's consider what we can assume/approximate for the current process.
If we assume negligible losses, by assuming the wave is relatively weak, then the process is reversible.
We can also assume there is negligible heat transfer in such a small region, so the process is adiabatic.
With both of these assumptions, the process is isentropic:

$$
a^2 = \left( \frac{\partial p}{\partial \rho} \right)_s \;.
$$ (eq:sonic-velocity-general)

This result applies in general to wave propagation through any medium.

We can take it further if we assume we are dealing wtih a perfect (i.e., ideal) gas.
In that case, recall our polytropic relations for an isentropic process:

$$
p v^{\gamma} = \text{constant} \rightarrow p = \rho^{\gamma} \cdot \text{constant} \\
\left( \frac{\partial p}{\partial \rho} \right)_s = \gamma \rho^{\gamma - 1} \cdot \text{constant} \\
 = \gamma \rho^{\gamma - 1} \left( \frac{p}{\rho^{\gamma}} \right) = \gamma \frac{p}{\rho} \\
\left( \frac{\partial p}{\partial rho} \right)_s = \gamma R T
\therefore a^2 = \gamma R T \\
a = \sqrt{\gamma R T} \;.
$$ (eq:sonic-velocity)

So, we see that for a perfect (i.e., ideal) gas, the sonic velocity is a function of
the gas and its local temperature only.

:::{admonition} Example: air at room temperature
:class: tip

We can calculate the sonic velocity for air at room temperature:

$$
a = \sqrt{1.4 \cdot 287 \frac{\text{N m}}{\text{kg K}} \cdot 293 \, \text{K} } \\
a = 343.1 \, \text{m}/\text{s} \;.
$$

:::

```python
# Sonic velocity for air at room temperature
gamma = 1.4
gas_constant = Q_(287, 'N*m/(kg K)')
temperature = Q_(293, 'K')

speed_sound = np.sqrt(gamma * gas_constant * temperature)
print(f'Speed of sound: {speed_sound.to("m/s"): .2f}')
```

    Speed of sound: 343.11 meter / second

:::{important}
The sonic velocity is *always* a property of the fluid and varies with the local state.
:::

### Mach number

With the speed of sound defined, we can now introduce a useful nondimensional quantity, the **Mach number**:

$$
M = \frac{V}{a} \;,
$$

with different regimes:

\begin{align}
M &< 1 : \text{ subsonic} \\
M &= 1 : \text{ sonic} \\
M &> 1 : \text{ supersonic}
\end{align}

## Equations for perfect gases

By taking our new relationships for Mach number ($ V = M a $),
the speed of sound ($ a = \sqrt{\gamma R T} $),
and the ideal gas law ($ p = \rho R T $), we can get convenient equations that apply
to ideal/perfect gases.

Continuity equation:

$$
\dot{m} = p A M \sqrt{\frac{\gamma}{RT}} = \text{constant}
$$

Stagnation relationships:

\begin{align}
h_t &= h \left( 1 + \frac{\gamma - 1}{2} M^2 \right) \\
T_t &= T \left( 1 + \frac{\gamma - 1}{2} M^2 \right) \\
p_t &= p \left( 1 + \frac{\gamma - 1}{2} M^2 \right)^{\frac{\gamma}{\gamma - 1}} \;,
\end{align}

where the subscript $t$ indicates the stagnation or "total" property.

Stagnation-pressure energy equation:

$$
\frac{dp_t}{p_t} + \frac{ds_e}{R} \left( 1 - \frac{T}{T_t} \right) + \frac{ds_i}{R} + \frac{\delta w_s}{R T_t} = 0
$$

For adiabatic, no-work flows:

$$
\frac{p_{t2}}{p_{t1}} = e^{-\Delta s/R}
$$

:::{important}
This last relation shows that, for a no-work adiabatic process, stagnation pressure $p_t$ is constant if there are no losses (i.e., $ \Delta s = 0 $), or drops if there are any losses/irreversibilities in the system (i.e., $ \Delta s > 0 $).
:::


# Using NRPy+ to Construct SymPy expressions for Weyl scalars and invariants in Cartesian coordinates

## Author: Zach Etienne
### Formatting improvements courtesy Brandon Clark

## This module adopts the prescription of [Baker, Campanelli, and Lousto. PRD 65, 044001 (2002)](https://arxiv.org/abs/gr-qc/0104063) (henceforth, the "BCL paper") to construct the Weyl scalars $\psi_0$, $\psi_1$, $\psi_2$, $\psi_3$, and $\psi_4$ from an approximate Kinnersley tetrad. It also constructs the corresponding Weyl invariants adopting the same approach as Einstein Toolkit's [Kranc-generated](http://kranccode.org/) [WeylScal4 diagnostic module](https://bitbucket.org/einsteintoolkit/einsteinanalysis/src). We will also follow that thorn's approach for other parts of this code.

**The numerical implementation of expressions constructed in this module have been validated against a trusted code (the WeylScal4 Einstein Toolkit thorn).**

### NRPy+ Source Code for this module:
* [WeylScal4NRPy/WeylScalarInvariants_Cartesian.py](../edit/WeylScal4NRPy/WeylScalarInvariants_Cartesian.py)
* [WeylScal4NRPy/WeylScalars_Cartesian.py](../edit/WeylScal4NRPy/WeylScalars_Cartesian.py)

## Introduction:
As this module is meant for Cartesian coordinates, all quantities are already rescaled. Further, we assume physical (as opposed to conformal) quantities including the 3-metric $\gamma_{ij}$ and 3-extrinsic curvature $K_{ij}$ are provided as input gridfunctions.

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

This module is organized as follows

1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids and reference metric
1. [Step 2](#levi_civita): Defining the Levi-Civita Symbol
1. [Step 3](#qktetrad): Defining the Approximate Quasi-Kinnersley Tetrad
1. [Step 4](#tensor): Building the Riemann and extrinsic curvature tensors
1. [Step 5](#psi4): Putting it all together and calculating $\psi_4$
    1. [Step 5.a](#code_validation1): Code Validation against WeylScal4NRPy/WeylScalars_Cartesian.py NRPy+ Module
1. [Step 6](#invariant_scalars): The Invariant Scalars
    1. [Step 6.a](#code_validation2): Code Validation against WeylScal4NRPy/WeylScalarInvariants_Cartesian.py NRPy+ Module
1. [Step 7](#latex_pdf_output): Output this module to $\LaTeX$-formatted PDF file

# Step 1: Set core NRPy+ parameters for numerical grids and reference metric \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

```python
# Step 1: import all needed modules from NRPy+:
import NRPy_param_funcs as par
import indexedexp as ixp
import grid as gri
import finite_difference as fin
from outputC import *
import sympy as sp
```

```python
# Step 2: Initialize WeylScalar parameters
thismodule = __name__
# Currently only one option: Approx_QuasiKinnersley = choice made in Baker, Campanelli, and Lousto. PRD 65, 044001 (2002)
par.initialize_param(par.glb_param("char", thismodule, "TetradChoice", "Approx_QuasiKinnersley"))
# The default value will output all psis
par.initialize_param(par.glb_param("bool", thismodule, "output_all_psis", False))
```

# Step 2: Defining the Levi-Civita Symbol \[Back to [top](#toc)\]
$$\label{levi_civita}$$

We will find the rank-3 Levi-Civita symbol, $\epsilon_{ijk}$ will be necessary to construct the Weyl scalars and invariants. Here we apply the algorithm from the [Wikipedia article on the Levi-Civita symbol](https://en.wikipedia.org/w/index.php?title=Levi-Civita_symbol&oldid=856527510#Generalization_to_n_dimensions), but exploiting properties of computer integer arithmetic as outlined [here](https://codegolf.stackexchange.com/questions/160359/levi-civita-symbol).

In short, the Levi-Civita symbol $\epsilon_{ijk}$ is 1 for even permutations of the indices, -1 for odd permutations, or zero if any indices are repeated.

```python
# Step 3: Define the rank-3 version of the Levi-Civita symbol. Amongst
#         other uses, this is needed for the construction of the approximate
#         quasi-Kinnersley tetrad.
def define_LeviCivitaSymbol_rank3(DIM=-1):
    if DIM == -1:
        DIM = par.parval_from_str("DIM")

    LeviCivitaSymbol = ixp.zerorank3()

    for i in range(DIM):
        for j in range(DIM):
            for k in range(DIM):
                # From https://codegolf.stackexchange.com/questions/160359/levi-civita-symbol :
                LeviCivitaSymbol[i][j][k] = (i - j) * (j - k) * (k - i) / 2
    return LeviCivitaSymbol
```

Additionally, we need to turn off verbose output in the outputC functions. This is an important step in this case because our final expressions are very large. Verbose output, when enabled, will print (in comments) the input SymPy expressions to the top of the file $\it{without}$ CSE, resulting here in an *enormous* output file.

We will also declare the additional gridfunctions we need for this thorn:
* the physical metric $\gamma_{ij}$,
* the extrinsic curvature $K_{ij}$,
* the Cartesian coordinates $(x,y,z)$,
* the real and imaginary components of $\psi_4$, and
* the Weyl curvature invariants

```python
# Step 4.a: Set spatial dimension (must be 3 for BSSN)
DIM = 3
par.set_parval_from_str("grid::DIM",DIM)

# Step 4.b: declare the additional gridfunctions (i.e., functions whose values are declared
#          at every grid point, either inside or outside of our SymPy expressions) needed
#         for this thorn:
#           * the physical metric $\gamma_{ij}$,
#           * the extrinsic curvature $K_{ij}$,
#           * the real and imaginary components of $\psi_4$, and
#           * the Weyl curvature invariants:
gammaDD = ixp.register_gridfunctions_for_single_rank2("AUX","gammaDD", "sym01") # The AUX or EVOL designation is *not*
                                                                                # used in diagnostic modules.
kDD = ixp.register_gridfunctions_for_single_rank2("AUX","kDD", "sym01")
x,y,z = gri.register_gridfunctions("AUX",["x","y","z"])
psi4r,psi4i,psi3r,psi3i,psi2r,psi2i,psi1r,psi1i,psi0r,psi0i = gri.register_gridfunctions("AUX",["psi4r","psi4i",\
                                                                                "psi3r","psi3i",\
                                                                                "psi2r","psi2i",\
                                                                                "psi1r","psi1i",\
                                                                                "psi0r","psi0i"])
curvIr,curvIi,curvJr,curvJi,J1curv,J2curv,J3curv,J4curv = gri.register_gridfunctions("AUX",["curvIr","curvIi",\
                                                                                "curvJr","curvJi",\
                                                                                "J1curv","J2curv",\
                                                                                "J3curv","J4curv"])
```

# Step 3: Defining the Approximate Quasi-Kinnersley Tetrad \[Back to [top](#toc)\]
$$\label{qktetrad}$$

To define the Weyl scalars, first a tetrad must be chosen. Below, for compatibility with the [WeylScal4 diagnostic module](https://bitbucket.org/einsteintoolkit/einsteinanalysis/src), we implement the approximate  quasi-Kinnersley tetrad of the [BCL paper](https://arxiv.org/abs/gr-qc/0104063).

We begin with the vectors given in eqs. 5.6 and 5.7 of the [BCL paper](https://arxiv.org/abs/gr-qc/0104063),
\begin{align}
    v_1^a &= [-y,x,0] \\
    v_2^a &= [x,y,z] \\
    v_3^a &= {\rm det}(g)^{1/2} g^{ad} \epsilon_{dbc} v_1^b v_2^c,
\end{align}
and carry out the Gram-Schmidt orthonormalization process. Note that these vectors are initially orthogonal to each other; one is in the $\phi$ direction, one is in $r$, and the third is the cross product of the first two. The vectors $w_i^a$ are placeholders in the code; the final product of the orthonormalization is the vectors $e_i^a$. So,
\begin{align}
e_1^a &= \frac{v_1^a}{\sqrt{\omega_{11}}} \\
e_2^a &= \frac{v_2^a - \omega_{12} e_1^a}{\sqrt{\omega_{22}}} \\
e_3^a &= \frac{v_3^a - \omega_{13} e_1^a - \omega_{23} e_2^a}{\sqrt{\omega_{33}}}, \\
\end{align}
where $\omega_{ij} = v_i^a v_j^b \gamma_{ab}$ needs to be updated between steps (to save resources, we can get away with only calculating components as needed), and uses $e_i^a$ instead of $v_i^a$ if it has been calculated. Recall that $\gamma_{ab}$ was declared as a gridfunction above.

Once we have orthogonal, normalized vectors, we can contruct the tetrad itself, again drawing on eqs. 5.6. We could draw on SymPy's built-in tools for complex numbers to build the complex vectors $m^a$ and $(m^*)^a$; however, the final expressions for the Weyl scalars are complex enough that $\text{sp.re()}$ and $\text{sp.im()}$ are prohibitively time consuming. To get around this, we will define the real and imaginary components of $\overset{*}{m}{}^a$, and do the complex algebra by hand. Thus,
\begin{align}
    l^a &= \frac{1}{\sqrt{2}} e_2^a \\
    n^a &= -\frac{1}{\sqrt{2}} e_2^a \\
    m^a &= \frac{1}{\sqrt{2}} (e_3^a + i e_1^a) \\
    \overset{*}{m}{}^a &= \frac{1}{\sqrt{2}} (e_3^a - i e_1^a)
\end{align}

In coding this procedure, we will follow the code from [WeylScal4](https://bitbucket.org/einsteintoolkit/einsteinanalysis/src) very closely. We will also assume that $l^0 = n^0 = \frac{1}{\sqrt{2}}$ and that $m^0 = \overset{*}{m}{}^0 = 0$ (again, following the example of the Kranc-generated WeylScal4). This last assumption in particular will significantly reduce the terms needed to find $\psi_4$.

```python
# Step 5: Set which tetrad is used; at the moment, only one supported option

# The tetrad depends in general on the inverse 3-metric gammaUU[i][j]=\gamma^{ij}
#          and the determinant of the 3-metric (detgamma), which are defined in
#          the following line of code from gammaDD[i][j]=\gamma_{ij}.
gammaUU, detgamma = ixp.symm_matrix_inverter3x3(gammaDD)

if par.parval_from_str("TetradChoice") == "Approx_QuasiKinnersley":
    # Eqs 5.6 in https://arxiv.org/pdf/gr-qc/0104063.pdf
    xmoved = x# - xorig
    ymoved = y# - yorig
    zmoved = z# - zorig

    # Step 5.a: Choose 3 orthogonal vectors. Here, we choose one in the azimuthal
    #          direction, one in the radial direction, and the cross product of the two.
    # Eqs 5.7
    v1U = ixp.zerorank1()
    v2U = ixp.zerorank1()
    v3U = ixp.zerorank1()
    v1U[0] = -ymoved
    v1U[1] = xmoved# + offset
    v1U[2] = sp.sympify(0)
    v2U[0] = xmoved# + offset
    v2U[1] = ymoved
    v2U[2] = zmoved
    LeviCivitaSymbol_rank3 = define_LeviCivitaSymbol_rank3()
    for a in range(DIM):
        for b in range(DIM):
            for c in range(DIM):
                for d in range(DIM):
                    v3U[a] += sp.sqrt(detgamma) * gammaUU[a][d] * LeviCivitaSymbol_rank3[d][b][c] * v1U[b] *v2U[c]

    # Step 5.b: Gram-Schmidt orthonormalization of the vectors.
    # The w_i^a vectors here are used to temporarily hold values on the way to the final vectors e_i^a

    w1U = ixp.zerorank1()
    for a in range(DIM):
        w1U[a] = v1U[a]
    omega11 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega11 += w1U[a] * w1U[b] * gammaDD[a][b]
    e1U = ixp.zerorank1()
    for a in range(DIM):
        e1U[a] = w1U[a] / sp.sqrt(omega11)

    omega12 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega12 += e1U[a] * v2U[b] * gammaDD[a][b]
    w2U = ixp.zerorank1()
    for a in range(DIM):
        w2U[a] = v2U[a] - omega12*e1U[a]
    omega22 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega22 += w2U[a] * w2U[b] *gammaDD[a][b]
    e2U = ixp.zerorank1()
    for a in range(DIM):
        e2U[a] = w2U[a] / sp.sqrt(omega22)

    omega13 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega13 += e1U[a] * v3U[b] * gammaDD[a][b]
    omega23 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega23 += e2U[a] * v3U[b] * gammaDD[a][b]
    w3U = ixp.zerorank1()
    for a in range(DIM):
        w3U[a] = v3U[a] - omega13*e1U[a] - omega23*e2U[a]
    omega33 = sp.sympify(0)
    for a in range(DIM):
        for b in range(DIM):
            omega33 += w3U[a] * w3U[b] * gammaDD[a][b]
    e3U = ixp.zerorank1()
    for a in range(DIM):
        e3U[a] = w3U[a] / sp.sqrt(omega33)

    # Step 3c: Construct the tetrad itself.
    # Eqs. 5.6
    isqrt2 = 1/sp.sqrt(2)
    ltetU = ixp.zerorank1()
    ntetU = ixp.zerorank1()
    #mtetU = ixp.zerorank1()
    #mtetccU = ixp.zerorank1()
    remtetU = ixp.zerorank1() # SymPy did not like trying to take the real/imaginary parts of such a
    immtetU = ixp.zerorank1() # complicated expression, so we do it ourselves.
    for i in range(DIM):
        ltetU[i] = isqrt2 * e2U[i]
        ntetU[i] = -isqrt2 * e2U[i]
        remtetU[i] = isqrt2 * e3U[i]
        immtetU[i] = isqrt2 * e1U[i]
    nn = isqrt2

else:
    print("Error: TetradChoice == "+par.parval_from_str("TetradChoice")+" unsupported!")
    exit(1)
```

# Step 4: Building the Riemann and extrinsic curvature tensors \[Back to [top](#toc)\]
$$\label{tensor}$$

Now that we have the tetrad in place, we can contract it with the Weyl tensor to obtain the Weyl scalars. Naturally, we must first construct the Weyl tensor to do that. We will first build the Christoffel symbols,
\begin{align}
\Gamma^i_{kl} &= \frac{1}{2} \gamma^{im} (\gamma_{mk,l} + \gamma_{ml,k} - \gamma_{kl,m}), \\
\end{align}

```python
#Step 6: Declare and construct the second derivative of the metric.
#gammabarDD_dDD = ixp.zerorank4()
#for i in range(DIM):
#    for j in range(DIM):
#        for k in range(DIM):
#            for l in range(DIM):
#                gammabarDD_dDD[i][j][k][l] = bssn.hDD_dDD[i][j][k][l]*rfm.ReDD[i][j] + \
#                                             bssn.hDD_dD[i][j][k]*rfm.ReDDdD[i][j][l] + \
#                                             bssn.hDD_dD[i][j][l]*rfm.ReDDdD[i][j][k] + \
#                                             bssn.hDD[i][j]*rfm.ReDDdDD[i][j][k][l] + \
#                                             rfm.ghatDDdDD[i][j][k][l]
gammaDD_dD = ixp.declarerank3("gammaDD_dD","sym01")

# Define the Christoffel symbols
GammaUDD = ixp.zerorank3(DIM)
for i in range(DIM):
    for k in range(DIM):
        for l in range(DIM):
            for m in range(DIM):
                GammaUDD[i][k][l] += (sp.Rational(1,2))*gammaUU[i][m]*\
                                     (gammaDD_dD[m][k][l] + gammaDD_dD[m][l][k] - gammaDD_dD[k][l][m])

```

since we will contract them with the extrinsic curvature, and the Riemann curvature tensor,
\begin{align}
R_{abcd} = \frac{1}{2} (\gamma_{ad,cb}+\gamma_{bc,da}-\gamma_{ac,bd}-\gamma_{bd,ac}) + \gamma_{je} \Gamma^{j}_{bc}\Gamma^{e}_{ad} - \gamma_{je} \Gamma^{j}_{bd} \Gamma^{e}_{ac},
\end{align}
since several terms in our expression for $\psi_4$ are contractions of this tensor.
To do this, we need second derivatives of the metric tensor, $\gamma_{ab,cd}$, using the finite differencing functionality in NRPy+.

```python
# Step 6.a: Declare and construct the Riemann curvature tensor:
gammaDD_dDD = ixp.declarerank4("gammaDD_dDD","sym01_sym23")
RiemannDDDD = ixp.zerorank4()
for a in range(DIM):
    for b in range(DIM):
        for c in range(DIM):
            for d in range(DIM):
                RiemannDDDD[a][b][c][d] = (gammaDD_dDD[a][d][c][b] + \
                                           gammaDD_dDD[b][c][d][a] - \
                                           gammaDD_dDD[a][c][b][d] - \
                                           gammaDD_dDD[b][d][a][c]) / 2
                for e in range(DIM):
                    for j in range(DIM):
                        RiemannDDDD[a][b][c][d] +=  gammaDD[j][e] * GammaUDD[j][b][c] * GammaUDD[e][a][d] - \
                                                    gammaDD[j][e] * GammaUDD[j][b][d] * GammaUDD[e][a][c]

```

We also need the extrinsic curvature tensor $K_{ij}$; in the Cartesian case, we simply declared $K_{ij}$ as a gridfunction above. We will need the trace, which can be computed as usual: $K = \gamma^{ij} K_{ij}$.

```python
# Step 6.b: We also need the extrinsic curvature tensor $K_{ij}$. This can be built from quantities from BSSN_RHSs
#           For Cartesian coordinates, we already made this a gridfunction.
#extrinsicKDD = ixp.zerorank2()
#for i in range(DIM):
#    for j in range(DIM):
#        extrinsicKDD[i][j] = (bssn.AbarDD[i][j] + sp.Rational(1,3)*gammaDD[i][j]*bssn.trK)/bssn.exp_m4phi
# We will, however, need to calculate the trace of K seperately:
trK = sp.sympify(0)
for i in range(DIM):
    for j in range(DIM):
        trK += gammaUU[i][j] * kDD[i][j]
```

# Step 5: Putting it all together and calculating $\psi_4$ \[Back to [top](#toc)\]
$$\label{psi4}$$

We do not not need to explicitly build the Weyl tensor itself, because the [BCL paper](https://arxiv.org/abs/gr-qc/0104063) shows that, for the Weyl tensor $C_{ijkl}$,
\begin{align}
\psi_4 =&\  C_{ijkl} n^i \overset{*}{m}{}^j n^k \overset{*}{m}{}^l\\
=&\ (R_{ijkl} + 2K_{i[k}K_{l]j}) n^i \overset{*}{m}{}^j n^k \overset{*}{m}{}^l \\
&- 8 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}) n^{[0} \overset{*}{m}{}^{j]} n^k \overset{*}{m}{}^l \\
&+ 4 (R_{jl} - K_{jp} K^p_l + KK_{jl}) n^{[0} \overset{*}{m}{}^{j]} n^{[0} \overset{*}{m}{}^{l]}.
\end{align}

Note here the brackets around pairs of indices. This indicates the antisymmetric part of a tensor; that is, for some arbitrary tensor $A_{ij}$, $A_{[ij]} = \frac{1}{2}(A_{ij}-A_{ji})$. This applies identically for indices belonging to separate tensors as well as superscripts in place of subscripts.

The other Weyl scalars from the [BCL paper](https://arxiv.org/abs/gr-qc/0104063), appendix A, that we may want to consider are
\begin{align}
\psi_3 =&\ (R_{ijkl} + 2K_{i[k}K_{l]j}) l^i n^j \overset{*}{m}{}^k n^l \\
&- 4 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}) (l^{[0} n^{j]} \overset{*}{m}{}^k n^l + l^k n^j\overset{*}{m}{}^{[0} n^{l]}) \\
&+ 4 (R_{jl} - K_{jp} K^p_l + KK_{jl}) l^{[0} n^{j]} \overset{*}{m}{}^{[0} n^{l]} \\
\psi_2 =&\ (R_{ijkl} + 2K_{i[k}K_{l]j}) l^i m^j \overset{*}{m}{}^k n^l \\
&- 4 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}) (l^{[0} m^{j]} \overset{*}{m}{}^k n^l + l^k m^l \overset{*}{m}{}^{[0} n^{j]}) \\
&+ 4 (R_{jl} - K_{jp} K^p_l + KK_{jl}) l^{[0} m^{j]} \overset{*}{m}{}^{[0} n^{l]} \\
\psi_1 =&\ (R_{ijkl} + 2K_{i[k}K_{l]j}) n^i l^j m^k l^l \\
&- 4 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}) (n^{[0} l^{j]} m^k l^l + n^k l^l m^{[0} l^{j]}) \\
&+ 4 (R_{jl} - K_{jp} K^p_l + KK_{jl}) n^{[0} l^{j]} m^{[0} l^{l]} \\
\psi_0 =&\ (R_{ijkl} + 2K_{i[k}K_{l]j}) l^i m^j l^k m^l \\
&- 8 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}) l^{[0} m^{j]} l^k m^l \\
&+ 4 (R_{jl} - K_{jp} K^p_l + KK_{jl}) l^{[0} m^{j]} l^{[0} m^{l]}. \\
\end{align}

To make it easier to track the construction of this expression, we will break it down into three parts, by first defining each of the parenthetical terms above separately. This is effectively identical to the procedure used in the Mathematica notebook that generates the original [WeylScal4](https://bitbucket.org/einsteintoolkit/einsteinanalysis/src). That is, let
\begin{align}
\text{GaussDDDD[i][j][k][l]} =& R_{ijkl} + 2K_{i[k}K_{l]j},
\end{align}

```python
# Step 7: Build the formula for \psi_4.
# Gauss equation: involving the Riemann tensor and extrinsic curvature.
GaussDDDD = ixp.zerorank4()
for i in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            for l in range(DIM):
                GaussDDDD[i][j][k][l] = RiemannDDDD[i][j][k][l] + kDD[i][k]*kDD[l][j] - kDD[i][l]*kDD[k][j]
```

\begin{align}
\text{CodazziDDD[j][k][l]} =& -2 (K_{j[k,l]} + \Gamma^p_{j[k} K_{l]p}),
\end{align}

```python
# Codazzi equation: involving partial derivatives of the extrinsic curvature.
# We will first need to declare derivatives of kDD
kDD_dD = ixp.declarerank3("kDD_dD","sym01")
CodazziDDD = ixp.zerorank3()
for j in range(DIM):
    for k in range(DIM):
        for l in range(DIM):
            CodazziDDD[j][k][l] = kDD_dD[j][l][k] - kDD_dD[j][k][l]
            for p in range(DIM):
                CodazziDDD[j][k][l] += GammaUDD[p][j][l]*kDD[k][p] - GammaUDD[p][j][k]*kDD[l][p]
```

and
\begin{align}
\text{RojoDD[j][l]} = & R_{jl} - K_{jp} K^p_l + KK_{jl} \\
= & \gamma^{pd} R_{jpld} - K_{jp} K^p_l + KK_{jl},
\end{align}

```python
# Another piece. While not associated with any particular equation,
# this is still useful for organizational purposes.
RojoDD = ixp.zerorank2()
for j in range(DIM):
    for l in range(DIM):
        RojoDD[j][l] = trK*kDD[j][l]
        for p in range(DIM):
            for d in range(DIM):
                RojoDD[j][l] += gammaUU[p][d]*RiemannDDDD[j][p][l][d] - kDD[j][p]*gammaUU[p][d]*kDD[d][l]
```

where these quantities are so named because of their relation to the Gauss-Codazzi equations. Then, we simply contract these with the tetrad we chose earlier to arrive at an expression for $\psi_4$. The barred Christoffel symbols and barred Ricci tensor have already been calculated by [BSSN/BSSN_RHSs.py](../edit/BSSN/BSSN_RHSs.py), so we use those values. So, our expression for $\psi_4$ has become
\begin{align}
\psi_4 =&\ (\text{GaussDDDD[i][j][k][l]}) n^i \overset{*}{m}{}^j n^k \overset{*}{m}{}^l \\
&+2 (\text{CodazziDDD[j][k][l]}) n^{0} \overset{*}{m}{}^{j} n^k \overset{*}{m}{}^l \\
&+ (\text{RojoDD[j][l]}) n^{0} \overset{*}{m}{}^{j} n^{0} \overset{*}{m}{}^{l}.
\end{align}

Likewise, we can rewrite the other Weyl scalars:
\begin{align}
\psi_3 =&\ (\text{GaussDDDD[i][j][k][l]}) l^i n^j \overset{*}{m}{}^k n^l \\
&+ (\text{CodazziDDD[j][k][l]}) (l^{0} n^{j} \overset{*}{m}{}^k n^l - l^{j} n^{0} \overset{*}{m}{}^k n^l - l^k n^j\overset{*}{m}{}^l n^0) \\
&- (\text{RojoDD[j][l]}) l^{0} n^{j} \overset{*}{m}{}^l n^0 - l^{j} n^{0} \overset{*}{m}{}^l n^0 \\
\psi_2 =&\ (\text{GaussDDDD[i][j][k][l]}) l^i m^j \overset{*}{m}{}^k n^l \\
&+ (\text{CodazziDDD[j][k][l]}) (l^{0} m^{j} \overset{*}{m}{}^k n^l - l^{j} m^{0} \overset{*}{m}{}^k n^l - l^k m^l \overset{*}{m}{}^l n^0) \\
&- (\text{RojoDD[j][l]}) l^0 m^j \overset{*}{m}{}^l n^0 \\
\psi_1 =&\ (\text{GaussDDDD[i][j][k][l]}) n^i l^j m^k l^l \\
&+ (\text{CodazziDDD[j][k][l]}) (n^{0} l^{j} m^k l^l - n^{j} l^{0} m^k l^l - n^k l^l m^j l^0) \\
&- (\text{RojoDD[j][l]}) (n^{0} l^{j} m^l l^0 - n^{j} l^{0} m^l l^0) \\
\psi_0 =&\ (\text{GaussDDDD[i][j][k][l]}) l^i m^j l^k m^l \\
&+2 (\text{CodazziDDD[j][k][l]}) (l^0 m^j l^k m^l + l^k m^l l^0 m^j) \\
&+ (\text{RojoDD[j][l]}) l^0 m^j l^0 m^j. \\
\end{align}

We will start by setting the scalars to SymPy's $0$ (this is done so that Python knows that the scalars are symbolic, not numeric, avoiding some potential bugs later on) and then performing the needed contractions of $\text{RojoDD[j][l]}$. Recall that the tetrad vectors were defined above and that we just built $\text{RojoDD[j][l]}$ from the Ricci tensor and extrinsic curvature.

The relavant terms here are:
\begin{align}
\psi_4:&\ \ \ (\text{RojoDD[j][l]}) n^{0} \overset{*}{m}{}^{j} n^{0} \overset{*}{m}{}^{l} \\
\psi_3:&\ -(\text{RojoDD[j][l]}) (l^{0} n^{j} \overset{*}{m}{}^l n^0 - l^{j} n^{0} \overset{*}{m}{}^l n^0) \\
\psi_2:&\ - (\text{RojoDD[j][l]}) l^0 m^j \overset{*}{m}{}^l n^0 \\
\psi_1:&\ - (\text{RojoDD[j][l]}) (n^{0} l^{j} m^l l^0 - n^{j} l^{0} m^l l^0) \\
\psi_0:&\ \ \ (\text{RojoDD[j][l]}) l^0 m^j l^0 m^j \\
\end{align}

```python
# Now we can calculate $\psi_4$ itself!
psi4r = sp.sympify(0)
psi4i = sp.sympify(0)
psi3r = sp.sympify(0)
psi3i = sp.sympify(0)
psi2r = sp.sympify(0)
psi2i = sp.sympify(0)
psi1r = sp.sympify(0)
psi1i = sp.sympify(0)
psi0r = sp.sympify(0)
psi0i = sp.sympify(0)
for l in range(DIM):
    for j in range(DIM):
        psi4r += RojoDD[j][l] * nn * nn * (remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
        psi4i += RojoDD[j][l] * nn * nn * (-remtetU[j]*immtetU[l]-immtetU[j]*remtetU[l])
        psi3r +=-RojoDD[j][l] * nn * nn * (ntetU[j]-ltetU[j]) * remtetU[l]
        psi3i += RojoDD[j][l] * nn * nn * (ntetU[j]-ltetU[j]) * immtetU[l]
        psi2r +=-RojoDD[j][l] * nn * nn * (remtetU[l]*remtetU[j]+immtetU[j]*immtetU[l])
        psi2i +=-RojoDD[j][l] * nn * nn * (immtetU[l]*remtetU[j]-remtetU[j]*immtetU[l])
        psi1r += RojoDD[j][l] * nn * nn * (ntetU[j]*remtetU[l]-ltetU[j]*remtetU[l])
        psi1i += RojoDD[j][l] * nn * nn * (ntetU[j]*immtetU[l]-ltetU[j]*immtetU[l])
        psi0r += RojoDD[j][l] * nn * nn * (remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
        psi0i += RojoDD[j][l] * nn * nn * (remtetU[j]*immtetU[l]+immtetU[j]*remtetU[l])
```

Now, we will add the the contractions of $\text{CodazziDDD[j][k][l]}$ to the Weyl Scalars. Again, we use the null tetrad we constructed and the tensor $\text{CodazziDDD[j][k][l]}$ we constructed from the extrinsic curvature and Christoffel symbols.

The relavant terms here are:
\begin{align}
\psi_4:&\ 2 (\text{CodazziDDD[j][k][l]}) n^{0} \overset{*}{m}{}^{j} n^k \overset{*}{m}{}^l \\
\psi_3:&\ \ \ (\text{CodazziDDD[j][k][l]}) (l^{0} n^{j} \overset{*}{m}{}^k n^l - l^{j} n^{0} \overset{*}{m}{}^k n^l - l^k n^j\overset{*}{m}{}^l n^0) \\
\psi_2:&\ \ \ (\text{CodazziDDD[j][k][l]}) (l^{0} m^{j} \overset{*}{m}{}^k n^l - l^{j} m^{0} \overset{*}{m}{}^k n^l - l^k m^l \overset{*}{m}{}^l n^0) \\
\psi_1:&\ \ \ (\text{CodazziDDD[j][k][l]}) (n^{0} l^{j} m^k l^l - n^{j} l^{0} m^k l^l - n^k l^l m^j l^0) \\
\psi_0:&\ 2 (\text{CodazziDDD[j][k][l]}) (l^0 m^j l^k m^l + l^k m^l l^0 m^j) \\
\end{align}

```python
for l in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            psi4r += 2 * CodazziDDD[j][k][l] * ntetU[k] * nn * (remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
            psi4i += 2 * CodazziDDD[j][k][l] * ntetU[k] * nn * (-remtetU[j]*immtetU[l]-immtetU[j]*remtetU[l])
            psi3r += 1 * CodazziDDD[j][k][l] * nn * ((ntetU[j]-ltetU[j])*remtetU[k]*ntetU[l]-remtetU[j]*ltetU[k]*ntetU[l])
            psi3i +=-1 * CodazziDDD[j][k][l] * nn * ((ntetU[j]-ltetU[j])*immtetU[k]*ntetU[l]-immtetU[j]*ltetU[k]*ntetU[l])
            psi2r += 1 * CodazziDDD[j][k][l] * nn * (ntetU[l]*(remtetU[j]*remtetU[k]+immtetU[j]*immtetU[k])-ltetU[k]*(remtetU[j]*remtetU[l]+immtetU[j]*immtetU[l]))
            psi2i += 1 * CodazziDDD[j][k][l] * nn * (ntetU[l]*(immtetU[j]*remtetU[k]-remtetU[j]*immtetU[k])-ltetU[k]*(remtetU[j]*immtetU[l]-immtetU[j]*remtetU[l]))
            psi1r += 1 * CodazziDDD[j][k][l] * nn * (ltetU[j]*remtetU[k]*ltetU[l]-remtetU[j]*ntetU[k]*ltetU[l]-ntetU[j]*remtetU[k]*ltetU[l])
            psi1i += 1 * CodazziDDD[j][k][l] * nn * (ltetU[j]*immtetU[k]*ltetU[l]-immtetU[j]*ntetU[k]*ltetU[l]-ntetU[j]*immtetU[k]*ltetU[l])
            psi0r += 2 * CodazziDDD[j][k][l] * nn * ltetU[k]*(remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
            psi0i += 2 * CodazziDDD[j][k][l] * nn * ltetU[k]*(remtetU[j]*immtetU[l]+immtetU[j]*remtetU[l])
```

Finally, we will add the contractions of $\text{GaussDDDD[i][j][k][l]}$ (from the Riemann tensor and extrinsic curvature, above) with the null tetrad.

The relavant terms here are:
\begin{align}
\psi_4:&\ (\text{GaussDDDD[i][j][k][l]}) n^i \overset{*}{m}{}^j n^k \overset{*}{m}{}^l \\
\psi_3:&\ (\text{GaussDDDD[i][j][k][l]}) l^i n^j \overset{*}{m}{}^k n^l \\
\psi_2:&\ (\text{GaussDDDD[i][j][k][l]}) l^i m^j \overset{*}{m}{}^k n^l \\
\psi_1:&\ (\text{GaussDDDD[i][j][k][l]}) n^i l^j m^k l^l \\
\psi_0:&\ (\text{GaussDDDD[i][j][k][l]}) l^i m^j l^k m^l \\
\end{align}

```python
for l in range(DIM):
    for j in range(DIM):
        for k in range(DIM):
            for i in range(DIM):
                psi4r += GaussDDDD[i][j][k][l] * ntetU[i] * ntetU[k] * (remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
                psi4i += GaussDDDD[i][j][k][l] * ntetU[i] * ntetU[k] * (-remtetU[j]*immtetU[l]-immtetU[j]*remtetU[l])
                psi3r += GaussDDDD[i][j][k][l] * ltetU[i] * ntetU[j] * remtetU[k] * ntetU[l]
                psi3i +=-GaussDDDD[i][j][k][l] * ltetU[i] * ntetU[j] * immtetU[k] * ntetU[l]
                psi2r += GaussDDDD[i][j][k][l] * ltetU[i] * ntetU[l] * (remtetU[j]*remtetU[k]+immtetU[j]*immtetU[k])
                psi2i += GaussDDDD[i][j][k][l] * ltetU[i] * ntetU[l] * (immtetU[j]*remtetU[k]-remtetU[j]*immtetU[k])
                psi1r += GaussDDDD[i][j][k][l] * ntetU[i] * ltetU[j] * remtetU[k] * ltetU[l]
                psi1i += GaussDDDD[i][j][k][l] * ntetU[i] * ltetU[j] * immtetU[k] * ltetU[l]
                psi0r += GaussDDDD[i][j][k][l] * ltetU[i] * ltetU[k] * (remtetU[j]*remtetU[l]-immtetU[j]*immtetU[l])
                psi0i += GaussDDDD[i][j][k][l] * ltetU[i] * ltetU[k] * (remtetU[j]*immtetU[l]+immtetU[j]*remtetU[l])
```

## Step 5.a: Code Validation against WeylScal4NRPy/WeylScalars_Cartesian.py NRPy+ Module  \[Back to [top](#toc)\]
$$\label{code_validation1}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for Weyl invariants between

1. this tutorial and
2. the NRPy+ [WeylScal4NRPy/WeylScalars_Cartesian.py](../edit/WeylScal4NRPy/WeylScalars_Cartesian.py) module.

```python
#psi4rb,psi4ib,psi3rb,psi3ib,psi2rb,psi2ib,psi1rb,psi1ib,psi0rb,psi0ib = psi4r,psi4i,psi3r,psi3i,psi2r,psi2i,psi1r,psi1i,psi0r,psi0i
gri.glb_gridfcs_list = []

#print("vvv Ignore the minor warnings below. vvv")

import WeylScal4NRPy.WeylScalars_Cartesian as weyl
par.set_parval_from_str("WeylScal4NRPy.WeylScalars_Cartesian::output_scalars","all_psis")
weyl.WeylScalars_Cartesian()

#print("^^^ Ignore the minor warnings above. ^^^\n")

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

print("psi4r - weyl.psi4r = " + str(psi4r - weyl.psi4r))
exit(1)
print("psi4i - weyl.psi4i = " + str(psi4i - weyl.psi4i))
print("psi3r - weyl.psi3r = " + str(psi3r - weyl.psi3r))
print("psi3i - weyl.psi3i = " + str(psi3i - weyl.psi3i))
print("psi2r - weyl.psi2r = " + str(psi2r - weyl.psi2r))
print("psi2i - weyl.psi2i = " + str(psi2i - weyl.psi2i))
print("psi1r - weyl.psi1r = " + str(psi1r - weyl.psi1r))
print("psi1i - weyl.psi1i = " + str(psi1i - weyl.psi1i))
print("psi0r - weyl.psi0r = " + str(psi0r - weyl.psi0r))
print("psi0i - weyl.psi0i = " + str(psi0i - weyl.psi0i))
```

    Consistency check between WeylScalars_Cartesian tutorial and NRPy+ module: ALL SHOULD BE ZERO.
    psi4r - weyl.psi4r = 0
    psi4i - weyl.psi4i = 0
    psi3r - weyl.psi3r = 0
    psi3i - weyl.psi3i = 0
    psi2r - weyl.psi2r = 0
    psi2i - weyl.psi2i = 0
    psi1r - weyl.psi1r = 0
    psi1i - weyl.psi1i = 0
    psi0r - weyl.psi0r = 0
    psi0i - weyl.psi0i = 0

# Step 6: The Invariant Scalars \[Back to [top](#toc)\]
$$\label{invariant_scalars}$$

We may also wish to compute the invariant scalars, whose value does not depend on the choice of the null tetrad. While they are defined using the Weyl tensor, they can also be expressed in terms of the Weyl scalars. We will use those expressions for simplicity.

Following after the method used in the Kranc code, we will read in the already-computed values of the Weyl scalars to find the invariants instead of trying to make NRPy output a very large expression in terms of the metric and extrinsic curvature.

We will start with the invariants $I$ and $J$, as defined in equations (2.3a) and (2.3b) of [arXiv:gr-qc/0407013](https://arxiv.org/abs/gr-qc/0407013). They are
\begin{align}
I &= 3 \psi_2^2 - 4 \psi_1 \psi_3 + \psi_4 \psi_0 \\
J &=
\begin{vmatrix}
\psi_4 & \psi_3 & \psi_2 \\
\psi_3 & \psi_2 & \psi_1 \\
\psi_2 & \psi_1 & \psi_0 \\
\end{vmatrix}
\end{align}
Here, since we can work in terms of the Weyl scalars themselves, we will use SymPy's built in tools for handling complex numbers, which will not get overwhelmed as they did when computing the Weyl scalars.

```python
gri.glb_gridfcs_list = []
psi4r,psi4i,psi3r,psi3i,psi2r,psi2i,psi1r,psi1i,psi0r,psi0i = gri.register_gridfunctions("AUX",["psi4r","psi4i",\
                                                                                "psi3r","psi3i",\
                                                                                "psi2r","psi2i",\
                                                                                "psi1r","psi1i",\
                                                                                "psi0r","psi0i"])

psi4 = psi4r + sp.I * psi4i
psi3 = psi3r + sp.I * psi3i
psi2 = psi2r + sp.I * psi2i
psi1 = psi1r + sp.I * psi1i
psi0 = psi0r + sp.I * psi0i

curvIr = sp.re(3*psi2*psi2 - 4*psi1*psi3 + psi4*psi0)
curvIi = sp.im(3*psi2*psi2 - 4*psi1*psi3 + psi4*psi0)
curvJr = sp.re(psi4 * (psi2*psi0 - psi1*psi1) - \
               psi3 * (psi3*psi0 - psi1*psi2) +\
               psi2 * (psi3*psi1 - psi2*psi2) )
curvJi = sp.im(psi4 * (psi2*psi0 - psi1*psi1) - \
               psi3 * (psi3*psi0 - psi1*psi2) +\
               psi2 * (psi3*psi1 - psi2*psi2) )
```

We will now code the invariants $J_1$, $J_2$, $J_3$, and $J_4$, as found in equations B5-B8 of [arXiv:0704.1756](https://arxiv.org/abs/0704.1756). As with the other invariants, we will simply read in the values of the gridfunctions that the already calculated (that is, the Weyl scalars). These equations are based directly on those used in the Mathematica notebook that generates $\text{WeylScal4}$ (available at [this](https://bitbucket.org/einsteintoolkit/einsteinanalysis/src) repository), modified so that Python can interpret them. Those equations were generated in turn using $\text{xTensor}$ from equations B5-B8.

```python
J1curv =-16*(3*psi2i**2-3*psi2r**2-4*psi1i*psi3i+4*psi1r*psi3r+psi0i*psi4i-psi0r*psi4r)

J2curv = 96*(-3*psi2i**2*psi2r+psi2r**3+2*psi1r*psi2i*psi3i+2*psi1i*psi2r*psi3i-psi0r*psi3i**2+\
            2*psi1i*psi2i*psi3r-2*psi1r*psi2r*psi3r-2*psi0i*psi3i*psi3r+psi0r*psi3r**2-\
            2*psi1i*psi1r*psi4i+psi0r*psi2i*psi4i+psi0i*psi2r*psi4i-psi1i**2*psi4r+psi1r**2*psi4r+\
            psi0i*psi2i*psi4r-psi0r*psi2r*psi4r)

J3curv = 64*(9*psi2i**4-54*psi2i**2*psi2r**2+9*psi2r**4-24*psi1i*psi2i**2*psi3i+48*psi1r*psi2i*psi2r*psi3i+\
            24*psi1i*psi2r**2*psi3i+16*psi1i**2*psi3i**2-16*psi1r**2*psi3i**2+\
            24*psi1r*psi2i**2*psi3r+48*psi1i*psi2i*psi2r*psi3r-24*psi1r*psi2r**2*psi3r-64*psi1i*psi1r*psi3i*psi3r-\
            16*psi1i**2*psi3r**2+16*psi1r**2*psi3r**2+6*psi0i*psi2i**2*psi4i-12*psi0r*psi2i*psi2r*psi4i-\
            6*psi0i*psi2r**2*psi4i-8*psi0i*psi1i*psi3i*psi4i+8*psi0r*psi1r*psi3i*psi4i+8*psi0r*psi1i*psi3r*psi4i+\
            8*psi0i*psi1r*psi3r*psi4i+psi0i**2*psi4i**2-psi0r**2*psi4i**2-6*psi0r*psi2i**2*psi4r-\
            12*psi0i*psi2i*psi2r*psi4r+6*psi0r*psi2r**2*psi4r+8*psi0r*psi1i*psi3i*psi4r+8*psi0i*psi1r*psi3i*psi4r+\
            8*psi0i*psi1i*psi3r*psi4r-8*psi0r*psi1r*psi3r*psi4r-4*psi0i*psi0r*psi4i*psi4r-psi0i**2*psi4r**2+\
            psi0r**2*psi4r**2)

J4curv = -640*(-15*psi2i**4*psi2r+30*psi2i**2*psi2r**3-3*psi2r**5+10*psi1r*psi2i**3*psi3i+\
              30*psi1i*psi2i**2*psi2r*psi3i-30*psi1r*psi2i*psi2r**2*psi3i-10*psi1i*psi2r**3*psi3i-\
              16*psi1i*psi1r*psi2i*psi3i**2-3*psi0r*psi2i**2*psi3i**2-8*psi1i**2*psi2r*psi3i**2+\
              8*psi1r**2*psi2r*psi3i**2-6*psi0i*psi2i*psi2r*psi3i**2+3*psi0r*psi2r**2*psi3i**2+\
              4*psi0r*psi1i*psi3i**3+4*psi0i*psi1r*psi3i**3+10*psi1i*psi2i**3*psi3r-\
              30*psi1r*psi2i**2*psi2r*psi3r-30*psi1i*psi2i*psi2r**2*psi3r+10*psi1r*psi2r**3*psi3r-\
              16*psi1i**2*psi2i*psi3i*psi3r+16*psi1r**2*psi2i*psi3i*psi3r-6*psi0i*psi2i**2*psi3i*psi3r+\
              32*psi1i*psi1r*psi2r*psi3i*psi3r+12*psi0r*psi2i*psi2r*psi3i*psi3r+6*psi0i*psi2r**2*psi3i*psi3r+\
              12*psi0i*psi1i*psi3i**2*psi3r-12*psi0r*psi1r*psi3i**2*psi3r+16*psi1i*psi1r*psi2i*psi3r**2+\
              3*psi0r*psi2i**2*psi3r**2+8*psi1i**2*psi2r*psi3r**2-8*psi1r**2*psi2r*psi3r**2+\
              6*psi0i*psi2i*psi2r*psi3r**2-3*psi0r*psi2r**2*psi3r**2-12*psi0r*psi1i*psi3i*psi3r**2-\
              12*psi0i*psi1r*psi3i*psi3r**2-4*psi0i*psi1i*psi3r**3+4*psi0r*psi1r*psi3r**3-\
              6*psi1i*psi1r*psi2i**2*psi4i+2*psi0r*psi2i**3*psi4i-6*psi1i**2*psi2i*psi2r*psi4i+\
              6*psi1r**2*psi2i*psi2r*psi4i+6*psi0i*psi2i**2*psi2r*psi4i+6*psi1i*psi1r*psi2r**2*psi4i-\
              6*psi0r*psi2i*psi2r**2*psi4i-2*psi0i*psi2r**3*psi4i+12*psi1i**2*psi1r*psi3i*psi4i-\
              4*psi1r**3*psi3i*psi4i-2*psi0r*psi1i*psi2i*psi3i*psi4i-2*psi0i*psi1r*psi2i*psi3i*psi4i-\
              2*psi0i*psi1i*psi2r*psi3i*psi4i+2*psi0r*psi1r*psi2r*psi3i*psi4i-2*psi0i*psi0r*psi3i**2*psi4i+\
              4*psi1i**3*psi3r*psi4i-12*psi1i*psi1r**2*psi3r*psi4i-2*psi0i*psi1i*psi2i*psi3r*psi4i+\
              2*psi0r*psi1r*psi2i*psi3r*psi4i+2*psi0r*psi1i*psi2r*psi3r*psi4i+2*psi0i*psi1r*psi2r*psi3r*psi4i-\
              2*psi0i**2*psi3i*psi3r*psi4i+2*psi0r**2*psi3i*psi3r*psi4i+2*psi0i*psi0r*psi3r**2*psi4i-\
              psi0r*psi1i**2*psi4i**2-2*psi0i*psi1i*psi1r*psi4i**2+psi0r*psi1r**2*psi4i**2+\
              2*psi0i*psi0r*psi2i*psi4i**2+psi0i**2*psi2r*psi4i**2-psi0r**2*psi2r*psi4i**2-3*psi1i**2*psi2i**2*psi4r+\
              3*psi1r**2*psi2i**2*psi4r+2*psi0i*psi2i**3*psi4r+12*psi1i*psi1r*psi2i*psi2r*psi4r-\
              6*psi0r*psi2i**2*psi2r*psi4r+3*psi1i**2*psi2r**2*psi4r-3*psi1r**2*psi2r**2*psi4r-\
              6*psi0i*psi2i*psi2r**2*psi4r+2*psi0r*psi2r**3*psi4r+4*psi1i**3*psi3i*psi4r-12*psi1i*psi1r**2*psi3i*psi4r-\
              2*psi0i*psi1i*psi2i*psi3i*psi4r+2*psi0r*psi1r*psi2i*psi3i*psi4r+2*psi0r*psi1i*psi2r*psi3i*psi4r+\
              2*psi0i*psi1r*psi2r*psi3i*psi4r-psi0i**2*psi3i**2*psi4r+psi0r**2*psi3i**2*psi4r-\
              12*psi1i**2*psi1r*psi3r*psi4r+4*psi1r**3*psi3r*psi4r+2*psi0r*psi1i*psi2i*psi3r*psi4r+\
              2*psi0i*psi1r*psi2i*psi3r*psi4r+2*psi0i*psi1i*psi2r*psi3r*psi4r-2*psi0r*psi1r*psi2r*psi3r*psi4r+\
              4*psi0i*psi0r*psi3i*psi3r*psi4r+psi0i**2*psi3r**2*psi4r-psi0r**2*psi3r**2*psi4r-\
              2*psi0i*psi1i**2*psi4i*psi4r+4*psi0r*psi1i*psi1r*psi4i*psi4r+2*psi0i*psi1r**2*psi4i*psi4r+\
              2*psi0i**2*psi2i*psi4i*psi4r-2*psi0r**2*psi2i*psi4i*psi4r-4*psi0i*psi0r*psi2r*psi4i*psi4r+\
              psi0r*psi1i**2*psi4r**2+2*psi0i*psi1i*psi1r*psi4r**2-psi0r*psi1r**2*psi4r**2-
              2*psi0i*psi0r*psi2i*psi4r**2-psi0i**2*psi2r*psi4r**2+psi0r**2*psi2r*psi4r**2)

#cse_output = sp.cse(psi0i,sp.numbered_symbols("tmp"))
#for commonsubexpression in cse_output:
#    print("hello?",commonsubexpression)
#for commonsubexpression in cse_output[0]:
#    print((str(commonsubexpression[0])+" = "+str(commonsubexpression[1])+";").replace("**","^").replace("_d","d"))
#for i,result in enumerate(cse_output[1]):
#   print(("psi0iPy = "+str(result)+";").replace("**","^").replace("_d","d"))
# These replace commands are used to allow us to validate against Einstein Toolkit's WeylScal4 thorn in Mathematica.
# Specifically, the first changes exponentiation to Mathematica's format, and the second strips the underscores
# that have a very specific meaning in Mathematica and thus cannot be used in variable names.
```

## Step 6.a: Code Validation against WeylScal4NRPy/WeylScalarInvariants_Cartesian.py NRPy+ Module  \[Back to [top](#toc)\]
$$\label{code_validation2}$$

Here, as a code validation check, we verify agreement in the SymPy expressions for Weyl invariants between

1. this tutorial and
2. the NRPy+ [WeylScal4NRPy/WeylScalarInvariants_Cartesian.py](../edit/WeylScal4NRPy/WeylScalarInvariants_Cartesian.py) module.

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

#print("vvv Ignore the minor warnings below. vvv")

import WeylScal4NRPy.WeylScalarInvariants_Cartesian as invar
invar.WeylScalarInvariants_Cartesian()

#print("^^^ Ignore the minor warnings above. ^^^\n")
print("Consistency check between ScalarInvariants_Cartesian tutorial and NRPy+ module for invariant scalars: ALL SHOULD BE ZERO.")

print("curvIr - weyl.curvIr = " + str(curvIr - invar.curvIr))
print("curvIi - weyl.curvIi = " + str(curvIi - invar.curvIi))
print("curvJr - weyl.curvJr = " + str(curvJr - invar.curvJr))
print("curvJi - weyl.curvJi = " + str(curvJi - invar.curvJi))
print("J1curv - weyl.J1curv = " + str(J1curv - invar.J1curv))
print("J2curv - weyl.J2curv = " + str(J2curv - invar.J2curv))
print("J3curv - weyl.J3curv = " + str(J3curv - invar.J3curv))
print("J4curv - weyl.J4curv = " + str(J4curv - invar.J4curv))
```

    Consistency check between ScalarInvariants_Cartesian tutorial and NRPy+ module for invariant scalars: ALL SHOULD BE ZERO.
    curvIr - weyl.curvIr = 0
    curvIi - weyl.curvIi = 0
    curvJr - weyl.curvJr = 0
    curvJi - weyl.curvJi = 0
    J1curv - weyl.J1curv = 0
    J2curv - weyl.J2curv = 0
    J3curv - weyl.J3curv = 0
    J4curv - weyl.J4curv = 0

# Step 7: Output this module 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-WeylScalarsInvariants-Cartesian.pdf](Tutorial-WeylScalarsInvariants-Cartesian.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-WeylScalarsInvariants-Cartesian.ipynb
!pdflatex -interaction=batchmode Tutorial-WeylScalarsInvariants-Cartesian.tex
!pdflatex -interaction=batchmode Tutorial-WeylScalarsInvariants-Cartesian.tex
!pdflatex -interaction=batchmode Tutorial-WeylScalarsInvariants-Cartesian.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-WeylScalarsInvariants-Cartesian.ipynb to latex
    [NbConvertApp] Writing 125033 bytes to Tutorial-WeylScalarsInvariants-Cartesian.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