# Problem Set 2 - Naive Bayes, Decision Trees with ensemble methods
## CSCI 4622 - Fall 2021
***
**Name**: Willem Scott
***

This assignment is due on Canvas by **1:50PM on October 6th**.

Submit only this Jupyter notebook to Canvas. Do not compress it using tar, rar, zip, etc.
Your solutions to analysis questions should be done in Markdown directly below the associated question.

Remember that you are encouraged to discuss the problems with your classmates and instructors,
but **you must write all code and solutions on your own**, and list any people or sources consulted.
The only exception to this rule is that you may copy code directly from your own solution to homework 1.
***

## Overview

Your task for this homework is to build a naive Bayes and a decision tree classifiers in the first 2 problems.
The last problem is about ensemble methods using scikit-learn decision tree as a weak learner.
We'll explore bagging, boosting (AdaBoost) and Random Forest.

```python
import numpy as np
import matplotlib.pylab as plt
import pickle
from sklearn.metrics import precision_score
from sklearn.tree import DecisionTreeClassifier
import pandas as pd
from time import time
%matplotlib inline
```

### Problem 1 - Naive Bayes [25 points]
***
Consider the problem of predicting whether a person has a college degree based on age, salary, and Colorado residency.
The dataset looks like the following.

|Age|Salary|Colorado Residency| College degree|
|:------:|:-----------:| :----------:|--:|
| 27 | 41,000 | Yes | Yes |
| 61 | 52,000 | No | No |
| 23 | 24,000 | Yes | No |
| 29 | 77,000 | Yes | Yes |
| 32 | 48,000 | No | Yes |
| 57 | 120,000 | Yes | Yes |
| 22 | 38,000 | Yes | Yes |
| 41 | 45,000 | Yes | No |
| 53 | 26,000 | No | No |
| 48 | 65,000 | Yes | Yes |

```python
features = np.array([[27 , 41000 , 1],
              [61 , 52000 , 0],
              [23 , 24000 , 1],
              [29 , 77000 , 1],
              [32 , 48000 , 0],
              [57 , 120000 , 1],
              [22 , 38000 , 1],
              [41 , 45000 , 1],
              [53 , 26000 , 0],
              [48 , 65000 , 1]])
labels = np.array([1, 0, 0, 1, 1, 1, 1, 0, 0, 1])
```

1.1 What is our expected accuracy for the baseline case where we predict one label for all rows? (*2 points*)

#BEGIN Workspace 1.1

#TODO: accuracy of the baseline using no features

60%

#END Workspace 1.1

First, we have to find a way to deal with the continuous features. For now, let's put them into binary bins based on threshold arguments to our classifier - so we can treat this as a tuning parameter.

1.2 Complete `threshold_features` to convert age and salary features to binary ones using the threshold arguments. (*3 points*)

```python
def threshold_features(features, age_threshold, salary_threshold):
    binary_X = features * 1 #This row just creates a "hard copy" of the X array so we can manipulate it as needed

    #BEGIN Workspace 1.2
    #TODO: Threshold the corresponding features
    binary_X[:, 0] = binary_X[:, 0] >= age_threshold
    binary_X[:, 1] = binary_X[:, 1] >= salary_threshold

    #END Workspace 1.2

    return binary_X
```

As seen during the class, given a row $(x_1, x_2, x_3)$, the naive Bayes classifier should assign the label $y$ that
maximizes:

\begin{align}
\log [p(y) \prod_i p(x_i | y)] = \log p(y) + \sum_{i} \log p(x_i | y)
\end{align}

$p(y)$ and $p(x_i | y)$ are computed using the training set (during `fit` call).

We have defined $p(x_i | y)$ as :
\begin{align}
p(x_i | y) = \frac{N_{y,i}}{N_y}
\end{align}
where $N_{y,i}$ is the number of rows where $y$ and $x_i$ occur together, and $N_y = \sum_i N_{y,i}$.

1.3 Complete the `fit` call by computing the counts and joint counts. Hint: Use `features_counts` to store the contingency
table $N_{y,i}$ for each feature $i$ and then use them to compute $\log p(x_i | y)$ (*10 points*)

1.4 Complete the `predict` call (*5 points*)

```python
class NaiveBayes(object):
    """
    NaiveBayes classifier for binary features and binary labels
    """

    def __init__(self, alpha=0.0):
        self.alpha = alpha
        self.classes_counts = None
        self.features_counts = []
        self.classes_log_probabilities = None
        self.features_log_probabilities = [] # same structure as features_count

    def fit(self, X, y):
        """
        Parameters
        ----------
        X: binary np.array of shape (n_samples, n_features)
        y: corresponding labels of shape (n_samples,)
        Returns
        -------
        Trained classifier
        """

        #BEGIN Workspace 1.3
        #TODO: Compute the counts and joint counts

        #P(A | B) = P(B | A)P(A)/P(B)
        # P of y given X_i = P(X_i | y)P(y)/P(X_i) for X_1 = 0 or 1
        self.classes_counts = np.array([len(y) - y.sum(), y.sum()])
        self.classes_log_probabilities = np.log(self.classes_counts/len(y))

        print(self.classes_counts)
        self.features_counts = np.zeros((X.shape[1], 2))
        self.features_log_probabilities = np.zeros((X.shape[1], 2))
        for i in range(X.shape[1]):
            self.features_counts[i, :] = ((X[:, i] == y[i]).sum(), (X[:, i] != y[i]).sum())
            self.features_log_probabilities[i, :] = np.log(
                (
                    (X[:, i] == y[i]).sum()/self.classes_counts[0],
                    (X[:, i] != y[i]).sum()/self.classes_counts[1]
                )
            )

        #END Workspace 1.3

        return self

    def predict(self, x_test):
        joint_log_likelihood = np.zeros((x_test.shape[0], self.classes_counts.shape[0]))
        y_hat = []
        #BEGIN Workspace 1.4
        #TODO: Find the corresponding labels using Naive bayes logic

        #END Workspace 1.4

        # log𝑝(𝑦)+∑𝑖log𝑝(𝑥𝑖|𝑦)

        for row in x_test:
            y_hat.append(
                max(
                    range(2),
                    key=lambda label: self.classes_log_probabilities[label] +
                    sum(
                        map(lambda a: a[0][a[1]],
                            zip(self.features_log_probabilities, row))
                    )
                )
            )

        return np.array(y_hat)

```

1.5 Using age 30 and salary 40,000 as thresholds, transform the features and evaluate (accuracy) the NaiveBayes classifier
on the training data. (*5 points*)

```python
clf = NaiveBayes()
#BEGIN Workspace 1.5
#TODO: Transform features to binary features, fit the classifier, report the accuracy
clf.fit(threshold_features(features, 30, 40000), labels)
pred = clf.predict(threshold_features(features, 30, 40000))
print("Prediction:", pred)
print("Actual:", labels)
print("Accuracy:", (pred == labels).sum())

#END Workspace 1.5
```

    [4 6]
    Prediction: [1 1 1 1 1 1 1 1 1 1]
    Actual: [1 0 0 1 1 1 1 0 0 1]
    Accuracy: 6

**Bonus question** 1.6 Use the attribute `alpha` of the NaiveBayes to convert it to the smoothed NaiveBayes presented during the class. (*5 points*)

### Problem 2 - Decision trees [25 points]
***
The goal of this problem is to implement the core elements of the Decision Tree classifier.
We do not expect a highly efficient implementation of the functions since the ensemble methods will use the implementation
from scikit-learn.

We start by considering the variable *Colorado residency*.

The leaf nodes of a decision tree act in the same way as in question (1.1) where no feature is used.

2.1 Complete `get_error_in_leaf` to return the count of misclassified instances. (*3 points*)

```python
def get_error_in_leaf(y, indices):
    """
    :param y: all labels
    :param indices: the subset of indexes in the leaf node
    :return: Returns the number of errors in a leaf node of a decision tree.
    """

    error_count = 0
    #BEGIN Workspace 2.1
    #TODO: Compute the number of errors in the leaf node (no feature is used)

    #END Workspace 2.1
    return error_count

def value_split_binary_feature(x, y, feature_index, root, criteria_func):
    """Will be used later to evaluate the criteria gain"""
    left_child = [i for i in root if x[i, feature_index] == 0]
    right_child = [i for i in root if x[i, feature_index] == 1]
    return criteria_func(y, root, left_child, right_child)

```

We will use information gain criteria to decide how to split the root node of our decision tree.

2.2 Complete the `entropy` function. (*5 points*)

2.3 Complete the `information_gain_criteria` to compute the information gained by splitting the root node.
 Print the gain value for splitting based on *Colorado residency* (*5 points*)

```python
def entropy(y, indices):
    """
    :param y: all labels
    :param indices: the indices of data points
    :return: Returns the entropy in the labels for the data points in indices.
    """

    entropy_value = 0
    if len(indices) == 0: # deal with corner case when there is no data point.
        return entropy_value
    else:
        #BEGIN Workspace 2.2
        #TODO: Compute the entropy of the labels from indices
        pass
        #END Workspace 2.2
    return entropy_value

def information_gain_criteria(y, root, left_child, right_child):
    """
    :param y: all labels
    :param root: indices of all the data points in the root
    :param left_child: the subset of indices in the left child
    :param right_child: the subset of indices in the right child
    :return: information gain of the split
    """
    information_gain = 0
    #BEGIN Workspace 2.3.a
    #TODO: Compute the information gain of the split

    #END Workspace 2.3.a
    return information_gain
```

```python
feature_id = 2
info_gain = 0
#BEGIN Workspace 2.3.b
#TODO: report the information gain of the split based on Colorado Residency

#END Workspace 2.3.b
```

Now we have to deal with continuous features for the decision tree.
One way to deal with continuous (or ordinal) data is to define binary features based on thresholding as we've done
for NaiveBayes. But we have to find the optimal threshold based on the criteria we're using.

2.4 Complete the `value_split_continuous_feature` by trying different possible threshold values of feature
of index `feature_index` and return the best criteria value and threshold. (*5 points*)

```python
def value_split_continuous_feature(x, y, feature_index, root, criteria_func=information_gain_criteria):
    """
    :param x: all feature values
    :param y: all labels
    :param feature_index: feature id to split the tree based on
    :param root: indexes of all the data points in the root
    :param criteria_func: the splitting criteria function
    :return: Return the best value and its corresponding threshold by splitting based on a continuous feature.
    """

    best_value, best_thres = 0, 0

    #BEGIN Workspace 2.4
    #TODO: Complete the function as detailed in the question and function description

    #END Workspace 2.4

    return best_value, best_thres
```

2.5 Find the best thresholds for age and salary. Print their corresponding information gains. (*5 points*)

```python
root = list(range(len(labels))) # root includes all data points
#BEGIN Workspace 2.5
#TODO: Report the best thresholds for age and salary and their split information gains

#END Workspace 2.5
```

2.6 Based on the obtained information gains, if we build a decision stump (decision tree with depth 1) greedily,
which feature should we choose? Why? What's the resulting accuracy? (*2 points*)

#BEGIN Workspace 2.6.a

#TODO: Which feature should we pick for the decision stump? Why?

#END Workspace 2.6.a

```python
#BEGIN Workspace 2.6.b
#TODO: Split based on the chosen feature and compute the accuracy (use get_error_in_leaf)

#BEGIN Workspace 2.6.b
```

**Bonus Question**

2.7 You now have all the ingredients to build a decision tree recursively.
You can build a decision tree of depth two and report its classification error on the training data and the tree.(*5 points*)

```python
#BEGIN Workspace 2.7
#TODO: Build a Decision Tree of Depth 2 using age, salary and the previously computed thresholds

#END Workspace 2.7
```

Problem 3  - Decision Tree Ensembles: Bagging and (BONUS: Boosting) [50 points]
---

We are going to predict house price levels using decision tree ensembles.

In this classification problem, we compare Decision trees and it's ensembles - Bagging and Boosting on House Price prediction [dataset](https://www.kaggle.com/c/house-prices-advanced-regression-techniques/data)

Our *weak learner* for this problem is the DecisionTreeClassifier from scikit-learn with `max_depth=10`.

We start first by loading preprocessed data that we'll use. Since the original data is for regression, we have first to transform
`y_train` and `y_test` to discrete values reflecting price level.

|Price range| Label|
|:----------:|--:|
| $ P < $125000|0|
|125000$\leq P < $ 160000| 1 |
|160000$ \leq P < $ 200000| 2 |
|200000$ \leq P $ | 3 |

3.1 Start by transforming`y_train` and `y_test` to discrete values using the provided ranges. (*3 points*)

```python
X_train, X_test, y_train, y_test = pickle.load(open('./data/test_train.pkl','rb'))
#BEGIN Workspace 3.1
#TODO: Discretize y_train and y_test

y_train = np.array(list(map(lambda p: 0 if p < 125000 else (1 if p < 160000 else (2 if p < 200000 else 3)), y_train)))
y_test = np.array(list(map(lambda p: 0 if p < 125000 else (1 if p < 160000 else (2 if p < 200000 else 3)), y_test)))

#END Workspace 3.1
print(np.unique(y_train), X_train.shape)
print(np.unique(y_test), X_test.shape)
```

    [0 1 2 3] (1166, 79)
    [0 1 2 3] (292, 79)

3.2 Complete the `ensemble_test` class to `fit` the model received as parameter and store the metrics and running time. (*5 points*)

3.3 Complete `plot_metric` to show and compare different statistics of each model in a bar chart. (*5 points*)

Later we will also use `ensemble_test` class to plot score, metric and time taken to fit the data.

```python
def get_weak_learner():
    """Return a new instance of out chosen weak learner"""
    return DecisionTreeClassifier(max_depth=10)

class EnsembleTest:
    """
        Test multiple model performance
    """

    def __init__(self, x_train, y_train, x_test, y_test):
        """
        initialize data partitions
        """
        self.scores = {}
        self.execution_time = {}
        self.metric = {}
        self.x_train = X_train
        self.y_train = y_train
        self.x_test = X_test
        self.y_test = y_test
        self.score_name ='Mean accuracy'
        self.metric_name = 'Precision(micro)'

    def fit_model(self, model, name):
        """
        Fit the model on train data.
        predict on test and store score and execution time for each fit.
        :param model: model
        :param name: name of model
        """
        start = time()
        #BEGIN Workspace 3.2
        #TODO: Fit the model and get the predictions
        #       train and test data are treated as global variables

        #Hint: self.scores[name] = precision_score(?, average="micro") # in multi-class, micro implies treating it as binary precision
        #Hint self.metric[name] = Accuracy

        model.fit(self.x_train, self.y_train)
        pred = model.predict(self.x_test)
        self.scores[name] = precision_score(self.y_test, pred, average="micro")
        self.metric[name] = np.array([x.mean() for x in (pred == self.y_test)]).mean()

        #END Workspace 3.2
        self.execution_time[name] = time() - start

    def print_result(self):
        """
            print results for all models trained and tested.
        """
        models_cross = pd.DataFrame({
            'Model'         : list(self.metric.keys()),
             self.score_name     : list(self.scores.values()),
             self.metric_name    : list(self.metric.values()),
            'Execution time': list(self.execution_time.values())})
        print(models_cross.sort_values(by=self.score_name, ascending=False))

    def plot_metric(self):
        #BEGIN Workspace 3.3
        #TODO: plot bar chart for each metric : time, metric, score
        plt.bar(self.metric.keys(), self.metric.values())
        plt.bar(self.scores.keys(), self.scores.values())
        plt.bar(self.execution_time.keys(), self.execution_time.values())
        #END Workspace 3.3
```

3.4 Test `EnsembleTest` using our weak learner returned by `get_weak_learner` (*2 points*)

```python
# create a handler for ensemble_test, use the created handler for fitting different models.
ensemble_handler = EnsembleTest(X_train,y_train,X_test,y_test)
#BEGIN Workspace 3.4
#TODO: Initialize weak learner and fit ensemble_handler

#END Workspace 3.4
ensemble_handler.print_result()
```

    Empty DataFrame
    Columns: [Model, Mean accuracy, Precision(micro), Execution time]
    Index: []

**Bagging:**

Bagging consists of training a set of weak learners using random subsets of the train data.

3.5 First, complete `sample_data` to return a random sample of size `sample_ratio * len(X_train)` of features and labels (*2 points*)

3.6 Complete `fit` by building `n_estimators` of DecisionTreeClassifier, each trained on random sample of the data (*5 points*)

3.7 Complete `predict` method to return the most likely label by combining different estimators predictions.
Use a simple majority / plurality vote system similar to the one used in your KNNClassifier in Problem Set 1. However, in this case, to break a tie you should use `predict_log_proba` or `predict_proba` method of DecisionTreeClassifier:
[Documentation](https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier.predict_proba) (*2 points*)

```python
class BaggingEnsemble(object):

    def __init__(self, n_estimators, sample_ratio):
        self.n_estimators = n_estimators
        self.sample_ratio = sample_ratio
        self.estimators = []

    def sample_data(self, x_train, y_train):
        x_sample, y_sample = None, None
        #BEGIN Workspace 3.5
        #TODO: sample random subset of size sample_ratio * len(X_train)
        opts = np.random.choice(range(x_train.shape[0]), size=int(self.sample_ratio * x_train.shape[0]), replace=False)
        x_sample = x_train[opts]
        y_sample = y_train[opts]

        #END Workspace 3.5
        return x_sample, y_sample

    def fit(self, x_train, y_train):
        for _ in range(self.n_estimators):
            #BEGIN Workspace 3.6
            #TODO: sample data and create new weak learner trained on the sample
            test_x, test_y = self.sample_data(x_train, y_train)
            learner = get_weak_learner()
            learner.fit(test_x, test_y)
            self.estimators.append(learner)

            #END Workspace 3.6

    def predict(self, X_test):
        predicted_proba = None
        answer = 0
        #BEGIN Workspace 3.7
        #TODO: go through the trained estimators and acummualte their predicted_proba to get the mostly likely label
        for est in self.estimators:
            if predicted_proba is None:
                predicted_proba = est.predict(X_test)
            else:
                predicted_proba += est.predict(X_test)
        #END Workspace 3.7
        return (predicted_proba / self.n_estimators).round()

```

```python
# This cell should run without errors
ensemble_handler.fit_model(BaggingEnsemble(10, 0.9),'Bagging')
ensemble_handler.print_result()

```

         Model  Mean accuracy  Precision(micro)  Execution time
    0  Bagging       0.726027          0.726027        0.141042

**Random Forest**
Random Forest has an additional layer of randomness compared to Bagging: we also sample a random subset of the features.

3.8 First, complete `sample_data` to return a random sample of size `sample_ratio * len(X_train)` of labels and `feature_ratio * num_features` of features (*2 points*)

3.9 Complete `fit` by building `n_estimators` of DecisionTreeClassifier, each trained on random sample of the data.
Make sure to keep track of the sampled features for each estimator to use them in the prediction step. (*5 points*)

3.10 Complete `predict` method to return the most likely label by combining different estimators predictions.
Use a simple majority / plurality vote system similar to the one used in your KNNClassifier in Problem Set 1. However, in this case, to break a tie you should use `predict_log_proba` or `predict_proba`  method of DecisionTreeClassifier:
[Documentation](https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier.predict_proba) (*3 points*)

```python
class RandomForest(object):

    def __init__(self, n_estimators, sample_ratio, features_ratio):
        self.n_estimators = n_estimators
        self.sample_ratio = sample_ratio
        self.features_ratio = features_ratio
        self.estimators = []
        self.features_indices = []

    def sample_data(self, x_train, y_train):
        X_sample, y_sample, features_indices = None, None, None
        #BEGIN Workspace 3.8
        #TODO: sample random subset of size sample_ratio * len(X_train) and subset of features of size
        #         features_ratio * num_features

        opts = np.random.choice(range(len(x_train)), size=int(self.sample_ratio * x_train.shape[0]), replace=False)
        features_indices = np.random.choice(range(x_train.shape[1]), size=int(self.features_ratio * x_train.shape[1]), replace=False)

        x_sample = x_train[opts][:, features_indices]
        y_sample = y_train[opts]

        #END Workspace 3.8
        return x_sample, y_sample, features_indices

    def fit(self, x_train, y_train):
        for _ in range(self.n_estimators):
            #BEGIN Workspace 3.9
            #TODO: sample data with random subset of rows and features using sample_data
            #Hint: keep track of the features indices in features_indices to use in predict
            (test_x, test_y, indices) = self.sample_data(x_train, y_train)
            learner = get_weak_learner()
            learner.fit(test_x, test_y)
            self.estimators.append(learner)
            self.features_indices.append(indices)
            #END Workspace 3.9

    def predict(self, X_test):
        predicted_proba = 0
        answer = 0
        #BEGIN Workspace 3.10
        #TODO: compute cumulative sum of predict proba from estimators and return the labels with highest likelihood
        for ind, est in zip(self.features_indices, self.estimators):
            if predicted_proba is None:
                predicted_proba = est.predict(X_test[:, ind])
            else:
                predicted_proba += est.predict(X_test[:, ind])
        #END Workspace 3.7
        return (predicted_proba / self.n_estimators).round()
```

```python
# This cell should run without errors
ensemble_handler.fit_model(RandomForest(20, sample_ratio=0.9, features_ratio=0.8), 'RandomForest')
ensemble_handler.print_result()
```

              Model  Mean accuracy  Precision(micro)  Execution time
    1  RandomForest       0.767123          0.767123        0.236771
    0       Bagging       0.726027          0.726027        0.141042

**Boosting (BONUS 10 Points)**

There are different methods of boosting, but we'll focus in this problem on Adaptive Boosting (AdaBoost).
The logic of AdaBoost is to "push" the new learner to give more importance to previously misclassified data. We present
below the multiclass variant of AdaBoost [SAMME](https://web.stanford.edu/~hastie/Papers/samme.pdf). We denote $K$ the number of classes.

AdaBosst is performed by increasing the weights of misclassified simple after each iteration:
- Start with equal weights $W_1 = (w_i), $ where   $w_i = \frac{1}{\texttt{n_samples}}$
- at step j:
    - Train estimator $h_j$ using weights $W_j$
    - Find the weighted error rate $\epsilon_j$ using $W_j$: $\epsilon_j=\frac{\sum_i w_i \delta(\hat{y}_i, y_i)}{\sum_i w_i}$
    - Choose $\alpha_j = \log \frac{1-\epsilon_j}{\epsilon_j} + \log(K-1)$
    - Update $W_j$ using: $w_i \leftarrow w_i \exp(\alpha_j \delta(\hat{y_i}, y_i)) $
    - Normalize $W_j$ to have sum 1
- Global estimator is $H = \sum_j \alpha_j h_j$

$\hat{y}$ are the predicted labels, and $\delta$ the Kronecker function, equal to 1 when the two argument are equal, 0 otherwise.

Bonus.1 Complete `fit` by building `n_estimators` of DecisionTreeClassifier, each trained on the same data but with different
samples weights as detailed in the algorithm. Keep track of $(\alpha_i)$

Bonus.2 Complete `predict` method to return the predicted label using the global estimator $H$. Hint:
use one hot encoding of the predicted labels from the weak learners and cumulate the prediction with weights $\alpha_j$

Notice that if the weak learner is consistent (0 error rate on the training set), AdaBoost $\alpha_j$ are no longer defined.

```python
class AdaBoost(object):

    def __init__(self, n_estimators, num_classes=4):
        self.n_estimators = n_estimators
        self.num_classes = num_classes
        self.estimators = []
        self.alphas = []
        self.weights = None

    def fit(self, X_train, y_train):
        #BEGIN Workspace 3.11
        #TODO: Implement Multiclass Adaboost and keep track of the alpha_j

        #END Workspace 3.11

    def predict(self, X_test):
        answer = 0
        #BEGIN Workspace 3.12
        #TODO: get the labels returned by the global estimator defined as H
        #Hint: Use one-hot format to get the labels using the global estimator
        #Hint: We don't need predict_proba for this one

        #END Workspace 3.12
        return answer
```

```python
# This cell should run without errors
ensemble_handler.fit_model(AdaBoost(10), 'AdaBoost')
ensemble_handler.print_result()
```

**Comparison**

Bonus.3 Add different ensemble methods to the handler (try different parameters), plot, show, and compare them.

```python
# create a handler for ensemble_test, use the created handler for fitting different models.
ensemble_handler = EnsembleTest(X_train, y_train, X_test, y_test)
decision = get_weak_learner()
ensemble_handler.fit_model(decision,'decision_tree')
#BEGIN Workspace 3.13.a
#TODO Add multiple instances of the ensemble methods. Plot and compare their performance

#END Workspace 3.13.a
```

#BEGIN Workspace 3.13.b
#TODO Comparison write-up
#END Workspace 3.13.b


# C code generation of GR HD Equations

## Authors: Phil Chang & Zach Etienne
### Formatting improvements courtesy Brandon Clark

$\newcommand{\be}{\begin{equation}}$
$\newcommand{\ee}{\end{equation}}$
$\newcommand{\grad}{{\boldsymbol{\nabla}}}$
$\newcommand{\vel}{{\boldsymbol{v}}}$
$\newcommand{\mom}{{\boldsymbol{p}}}$
$\newcommand{\ddt}[1]{{\frac{\partial #1}{\partial t}}}$
$\newcommand{\ddx}[1]{{\frac{\partial #1}{\partial x}}}$
$\newcommand{\state}{{\boldsymbol{\mathcal{U}}}}$
$\newcommand{\charge}{{\boldsymbol{U}}}$
$\newcommand{\psicharge}{{\boldsymbol{\psi}}}$
$\newcommand{\lapse}{\alpha}$
$\newcommand{\shift}{\boldsymbol{\beta}}$
$\newcommand{\rhostar}{{\rho_*}}$
$\newcommand{\tautilde}{{\tilde{\tau}}}$
$\newcommand{\Svectilde}{{\tilde{\boldsymbol{S}}}}$
$\newcommand{\rtgamma}{{\sqrt{\gamma}}}$
$\newcommand{\T}[2]{{T^{#1 #2}}}$
$\newcommand{\uvec}{{\boldsymbol{u}}}$
$\newcommand{\Vvec}{{\boldsymbol{\mathcal{V}}}}$
$\newcommand{\vfluid}{{\boldsymbol{v}_{\rm f}}}$
$\newcommand{\vVal}{{\tilde{\boldsymbol{v}}}}$

$\newcommand{\flux}{{\boldsymbol{\mathcal{F}}}}$
$\newcommand{\fluxV}{{\boldsymbol{F}}}$
$\newcommand{\source}{{\boldsymbol{\mathcal{S}}}}$
$\newcommand{\sourceV}{{\boldsymbol{S}}}$

$\newcommand{\area}{{\boldsymbol{A}}}$
$\newcommand{\normal}{{\hat{\boldsymbol{n}}}}$
$\newcommand{\pt}{{\boldsymbol{p}}}$
$\newcommand{\nb}{{\boldsymbol{n}}}$
$\newcommand{\meshv}{{\boldsymbol{w}}}$
$\newcommand{\facev}{{\boldsymbol{\tilde{w}}_{ij}}}$
$\newcommand{\facer}{{\boldsymbol{\tilde{r}}_{ij}}}$
$\newcommand{\meshr}{{\boldsymbol{r}}}$
$\newcommand{\cmr}{{\boldsymbol{c}}}$

## Introduction:
We start out with the ** GRHD ** equations in conservative form with the state vector $\state=(\rhostar, \Svectilde, \tautilde)$:
\begin{equation}
\ddt{\state} + \grad\cdot\flux = \source,
\end{equation}
where $\rhostar = \lapse\rho\rtgamma u^0$, $\Svectilde = \rhostar h \uvec$, $\tautilde = \lapse^2\rtgamma \T00 - \rhostar$. The associated set of primitive variables are $(\rho, \vel, \epsilon)$, which are the rest mass density, fluid 3-velocity, and internal energy (measured in the rest frame).

The flux, $\flux$ is given by
\begin{equation}
 \flux=(\rhostar \vel, \lapse\rtgamma\T{j}{\beta}g_{\beta i}, \lapse^2\rtgamma\T0j - \rhostar\vel
\end{equation}
where $\vel$ is the 3-velocity, and $\source = (0, \frac 1 2 \lapse\rtgamma \T{\lapse}{\beta}g_{\lapse\beta,i}, s)$ is the source function, and
\begin{equation}
s = \lapse\rtgamma\left[\left(\T00\beta^i\beta^j + 2\T0i\beta^j\right)K_{ij} - \left(\T00\beta^i + \T0i\right)\partial_i\lapse\right]
\end{equation}
The stress energy tensor for a perfect fluid is written as
\begin{equation}
\T{\mu}{\nu} = \rho h u^{\mu} u^{\nu} + P g^{\mu\nu},
\end{equation}
where $h = 1 + \epsilon + P/\rho$ is the specific enthalpy and $u^{\mu}$ are the respective components of the four velocity.

Noting that the mass $\flux$ is defined in terms of $\rhostar$ and $\vel$, we need to first find a mapping between $\vel$ and $u$.

### Alternative formulation

The Athena++ folks have an alternative formulations that might be superior.
Begin with the continuity equation
\begin{equation}
\grad_{\mu}\rho u^{\mu} = 0,
\end{equation}
where $\grad$ is the covariant derivative.  This can be mapped directly to
\begin{equation}
\partial_{0} \sqrt{-g}\rho u^0 + \partial_i\sqrt{-g} \rho u^0 v^i = 0
\end{equation}
which we can identify with $\rhostar = \alpha\rtgamma \rho u^0$ because $\sqrt{-g} = \alpha\rtgamma$.

Now the second equation is conservation of energy-momentum which we write as
\begin{equation}
\grad_{\nu}T^{\nu}_{\mu} = 0
\end{equation}
writing this out we have
\begin{equation}
\partial_0 g_{\mu\alpha}T^{\alpha 0} + \partial_i g_{\mu\alpha}T^{\alpha i} - \Gamma_{\mu\alpha}^{\gamma} g_{\gamma\beta}T^{\alpha\beta} = 0
\end{equation}
Noting that
\begin{equation}
\Gamma^{\alpha}_{\beta\gamma} = \frac 1 2 g^{\alpha\delta}\left(\partial_{\gamma}g_{\beta\delta} + \partial_{\beta}g_{\gamma\delta} - \partial_{\delta}g_{\beta\gamma}\right)
\end{equation}
Writing this all out, we note the last term is
\begin{equation}
\Gamma_{\mu\alpha}^{\gamma} g_{\gamma\beta}T^{\alpha\beta} =
\frac 1 2 g^{\gamma\delta}\left(\partial_{\alpha}g_{\mu\delta} + \partial_{\mu}g_{\alpha \delta} - \partial_{\delta}g_{\mu \alpha}\right) T_{\gamma}^{\alpha} =
\frac 1 2 \left(\partial_{\alpha}g_{\mu\delta} + \partial_{\mu}g_{\alpha \delta} - \partial_{\delta}g_{\mu \alpha}\right)
T^{\alpha\delta}
\end{equation}
We sum over $\alpha$ and $\delta$, but noting that we are antisymmetric in first and last terms in $\alpha$ and $\delta$ in the () but symmetric in $T_{\alpha\delta}$ so we have
\begin{equation}
\Gamma_{\mu\alpha}^{\gamma} g_{\gamma\beta}T^{\alpha\beta} = \frac 1 2 \partial_{\mu}g_{\alpha \delta} T^{\alpha\delta}
\end{equation}

Thus we have
\begin{equation}
\partial_0 T^{0}_{\mu} + \partial_i T^{i}_{\mu} = \frac 1 2 \partial_{\mu}g_{\alpha \delta} T^{\alpha\delta}
\end{equation}
The $\mu = (1,2,3)$, we almost get back the equations in the standard formulation
\begin{equation}
\partial_0 \rho h u^0 u_i + \partial_j T^j_i = \frac 1 2 \partial_{i}g_{\alpha \delta} T^{\alpha\delta},
\end{equation}
which modulo a factors of $\lapse\rtgamma$ in front is the same as the "standard" equations.

The $T^0_0$ term is more interesting. Here we have
\begin{equation}
\partial_0 (\rho h u^0 u_0 + + \partial_j T^j_i = \frac 1 2 \partial_{0}g_{\alpha \delta} T^{\alpha\delta},
\end{equation}

However the disadvantage is that we need the time derivative of the metric.

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

This notebook is organized as follows

1. [Step 1](#mapping): Primitive to Conservative Mapping
1. [Step 2](#zach): Compute $u^0$ from the Valencia 3-velocity (Zach step)
1. [Step 3](#flux): Compute the flux
1. [Step 4](#source): Source Terms
1. [Step 5](#rotation): Rotation
1. [Step 6](#solver): Conservative to Primitive Solver
1. [Step 7](#lorentz): Lorentz Boosts
1. [Step 8](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Primitive to Conservative Mapping
$$\label{mapping}$$

We want to make a mapping from the primitives to conserved variables following Zach notebook:
\begin{equation}
(\rho, \vel, \epsilon) \rightarrow (\rhostar = \lapse\rho\rtgamma u^0, \Svectilde = \rhostar h \uvec, \tautilde = \lapse^2\rtgamma \T00 - \rhostar).
\end{equation}

```python
import GRHD.equations as Ge
import indexedexp as ixp
import sympy as sp
from outputC import *

# declare gammaDD
gammaDD = ixp.zerorank2()
components = ["xx", "xy", "xz", "yy", "yz", "zz"]
names = ""
for comp in components :
    names = names + "mi.gamDD{0} ".format(comp)

gxx, gxy, gxz, gyy, gyz, gzz = sp.symbols( names)

gammaDD[0][0] = gxx
gammaDD[0][1] = gxy
gammaDD[0][2] = gxz
gammaDD[1][0] = gxy
gammaDD[1][1] = gyy
gammaDD[1][2] = gyz
gammaDD[2][0] = gxz
gammaDD[2][1] = gyz
gammaDD[2][2] = gzz

#declare alpha
alpha = sp.symbols( "mi.alpha")

#declare beta
betaU = ixp.zerorank1()
for i, comp in enumerate(["X", "Y", "Z"]) :
    betaU[i] = sp.symbols( "mi.beta{0}".format(comp), real=True)

#now get the primitives
rho_b, epsilon, P = sp.symbols("rho ie p")

#get the 3-velocities
vU = ixp.zerorank1()
for i, comp in enumerate( ["vx", "vy", "vz"]) :
    vU[i] = sp.symbols("{0}".format(comp))

Ge.u4U_in_terms_of_vU__rescale_vU_by_applying_speed_limit(alpha,betaU,gammaDD, vU)

u4U = Ge.u4U_ito_vU
# Zach says: Probably want to adopt speed-limited vU[i], Ge.rescaledvU[i], here, a la:
# for i in range(3):
# ... vU[i] = Ge.rescaledvU[i]

# First compute stress-energy tensor T4UU and T4UD:
Ge.compute_T4UU(gammaDD,betaU,alpha, rho_b,P,epsilon,u4U)
Ge.compute_T4UD(gammaDD,betaU,alpha, Ge.T4UU)

# Next sqrt(gamma)
Ge.compute_sqrtgammaDET(gammaDD)

# Compute conservative variables in terms of primitive variables
Ge.compute_rho_star( alpha, Ge.sqrtgammaDET, rho_b, u4U)
Ge.compute_tau_tilde(alpha, Ge.sqrtgammaDET, Ge.T4UU,Ge.rho_star)
Ge.compute_S_tildeD( alpha, Ge.sqrtgammaDET, Ge.T4UD)

# Zach says: Why only output u^x? Debugging reasons?
outputC([u4U[1],     Ge.rho_star, Ge.S_tildeD[0], Ge.S_tildeD[1], Ge.S_tildeD[2], Ge.tau_tilde],
        ["u4U1", "con[iRhoStar]",     "con[iSx]",     "con[iSy]",     "con[iSz]",  "con[iTau]"],
        filename="NRPY+prim2Con.h", params="outCverbose=False")
!cat NRPY+prim2Con.h

outputC([Ge.sqrtgammaDET*alpha], ["detg"], filename="NRPY+detg.h")
!cat NRPY+detg.h

gammaUU, gammabarDet = ixp.symm_matrix_inverter3x3(gammaDD)
outputC([gammaUU[0][0],gammaUU[0][1],gammaUU[0][2],gammaUU[1][1],gammaUU[1][2],gammaUU[2][2]],
        [    "gamUUxx",    "gamUUxy",    "gamUUxz",    "gamUUyy",    "gamUUyz",    "gamUUzz"],
        filename="NRPY+gamUU.h")
!cat NRPY+gamUU.h
```

    Wrote to file "NRPY+prim2Con.h"
    {
       const double tmp0 = (1.0/((GAMMA_SPEED_LIMIT)*(GAMMA_SPEED_LIMIT)));
       const double tmp1 = (1.0/2.0)*tmp0;
       const double tmp2 = ((mi.alpha)*(mi.alpha));
       const double tmp3 = (1.0/(tmp2));
       const double tmp4 = mi.betaX + vx;
       const double tmp5 = mi.gamDDxx*tmp3*((tmp4)*(tmp4));
       const double tmp6 = (1.0/2.0)*tmp5;
       const double tmp7 = mi.betaY + vy;
       const double tmp8 = mi.gamDDyy*tmp3*((tmp7)*(tmp7));
       const double tmp9 = (1.0/2.0)*tmp8;
       const double tmp10 = mi.betaZ + vz;
       const double tmp11 = mi.gamDDzz*((tmp10)*(tmp10))*tmp3;
       const double tmp12 = (1.0/2.0)*tmp11;
       const double tmp13 = tmp3*tmp4;
       const double tmp14 = mi.gamDDxy*tmp13*tmp7;
       const double tmp15 = mi.gamDDxz*tmp10*tmp13;
       const double tmp16 = mi.gamDDyz*tmp10*tmp3*tmp7;
       const double tmp17 = 2*tmp14;
       const double tmp18 = 2*tmp15;
       const double tmp19 = 2*tmp16;
       const double tmp20 = (1.0/2.0)*fabs(-tmp0 - tmp11 - tmp17 - tmp18 - tmp19 - tmp5 - tmp8 + 1);
       const double tmp21 = tmp1 - tmp12 - tmp14 - tmp15 - tmp16 + tmp20 - tmp6 - tmp9 + 1.0/2.0;
       const double tmp22 = (1.0/sqrt(tmp21));
       const double tmp23 = sqrt((-tmp1 + tmp12 + tmp14 + tmp15 + tmp16 - tmp20 + tmp6 + tmp9 + 1.0/2.0)/(TINYDOUBLE + tmp11 + tmp17 + tmp18 + tmp19 + tmp5 + tmp8));
       const double tmp24 = -mi.betaX + tmp23*tmp4;
       const double tmp25 = sqrt(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*((mi.gamDDyz)*(mi.gamDDyz)) - ((mi.gamDDxy)*(mi.gamDDxy))*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - ((mi.gamDDxz)*(mi.gamDDxz))*mi.gamDDyy);
       const double tmp26 = rho*tmp22*tmp25;
       const double tmp27 = p*tmp3;
       const double tmp28 = rho*tmp3*(ie + p/rho + 1)/tmp21;
       const double tmp29 = -tmp27 + tmp28;
       const double tmp30 = mi.betaX*tmp27 + tmp24*tmp28;
       const double tmp31 = mi.betaY*tmp27 + tmp28*(-mi.betaY + tmp23*tmp7);
       const double tmp32 = mi.betaZ*tmp27 + tmp28*(-mi.betaZ + tmp10*tmp23);
       const double tmp33 = mi.alpha*tmp25;
       u4U1 = tmp22*tmp24/mi.alpha;
       con[iRhoStar] = tmp26;
       con[iSx] = tmp33*(mi.gamDDxx*tmp30 + mi.gamDDxy*tmp31 + mi.gamDDxz*tmp32 + tmp29*(mi.betaX*mi.gamDDxx + mi.betaY*mi.gamDDxy + mi.betaZ*mi.gamDDxz));
       con[iSy] = tmp33*(mi.gamDDxy*tmp30 + mi.gamDDyy*tmp31 + mi.gamDDyz*tmp32 + tmp29*(mi.betaX*mi.gamDDxy + mi.betaY*mi.gamDDyy + mi.betaZ*mi.gamDDyz));
       con[iSz] = tmp33*(mi.gamDDxz*tmp30 + mi.gamDDyz*tmp31 + mi.gamDDzz*tmp32 + tmp29*(mi.betaX*mi.gamDDxz + mi.betaY*mi.gamDDyz + mi.betaZ*mi.gamDDzz));
       con[iTau] = tmp2*tmp25*tmp29 - tmp26;
    }
    Wrote to file "NRPY+detg.h"
    /*
     *  Original SymPy expression:
     *  "detg = mi.alpha*sqrt(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy)"
     */
    {
       detg = mi.alpha*sqrt(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*((mi.gamDDyz)*(mi.gamDDyz)) - ((mi.gamDDxy)*(mi.gamDDxy))*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - ((mi.gamDDxz)*(mi.gamDDxz))*mi.gamDDyy);
    }
    Wrote to file "NRPY+gamUU.h"
    /*
     *  Original SymPy expressions:
     *  "[gamUUxx = (mi.gamDDyy*mi.gamDDzz - mi.gamDDyz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy),
     *    gamUUxy = (-mi.gamDDxy*mi.gamDDzz + mi.gamDDxz*mi.gamDDyz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy),
     *    gamUUxz = (mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy),
     *    gamUUyy = (mi.gamDDxx*mi.gamDDzz - mi.gamDDxz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy),
     *    gamUUyz = (-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy),
     *    gamUUzz = (mi.gamDDxx*mi.gamDDyy - mi.gamDDxy**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy)]"
     */
    {
       const double tmp0 = mi.gamDDyy*mi.gamDDzz;
       const double tmp1 = ((mi.gamDDyz)*(mi.gamDDyz));
       const double tmp2 = mi.gamDDxz*mi.gamDDyz;
       const double tmp3 = ((mi.gamDDxy)*(mi.gamDDxy));
       const double tmp4 = ((mi.gamDDxz)*(mi.gamDDxz));
       const double tmp5 = (1.0/(mi.gamDDxx*tmp0 - mi.gamDDxx*tmp1 + 2*mi.gamDDxy*tmp2 - mi.gamDDyy*tmp4 - mi.gamDDzz*tmp3));
       gamUUxx = tmp5*(tmp0 - tmp1);
       gamUUxy = tmp5*(-mi.gamDDxy*mi.gamDDzz + tmp2);
       gamUUxz = tmp5*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy);
       gamUUyy = tmp5*(mi.gamDDxx*mi.gamDDzz - tmp4);
       gamUUyz = tmp5*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz);
       gamUUzz = tmp5*(mi.gamDDxx*mi.gamDDyy - tmp3);
    }

# Step 3: Compute the flux
$$\label{flux}$$

The fluxes are as follows
\begin{equation}
\frac{\partial}{\partial t}
\begin{pmatrix}
\rhostar\\
\Svectilde\\
\tautilde
\end{pmatrix} + \frac{\partial}{\partial x^j}\begin{pmatrix} \rhostar v^j\\
\lapse\rtgamma T^j_i\\ \lapse^2\rtgamma T^{0j} - \rhostar v^j
\end{pmatrix}  = \begin{pmatrix} 0 \\ \frac 1 2 \lapse\rtgamma T^{\alpha\beta}g_{\alpha\beta,i} \\ s \end{pmatrix}
\end{equation}
so the flux is
\begin{equation}
\mathcal{F} = \begin{pmatrix} \rhostar v^i \\ \lapse\rtgamma T^i_k \\ \lapse^2\rtgamma T^{0i} - \rhostar v^i
\end{pmatrix}
\end{equation}
In the moving-mesh formalism, the flux is just taken along the x directions so we have
\begin{equation}
\mathcal{F} = \begin{pmatrix} \rhostar v^1 \\ \lapse\rtgamma T^1_k \\ \lapse^2\rtgamma T^{01} - \rhostar v^1
\end{pmatrix}
\end{equation}
Note that we will need to rotate $T^{\mu\nu}$ and $g_{\mu\nu}$ to get the right orientation.
In order to do this, we must first compute the stress energy tensor:
\begin{equation}
T^{\mu\nu} = \rho h u^{\mu}u^{\nu} + Pg^{\mu\nu} = \rho h (u^0)^2v^iv^j + P g^{\mu\nu}
\end{equation}

```python
# Next compute fluxes of conservative variables
Ge.compute_rho_star_fluxU(                            vU,        Ge.rho_star)
Ge.compute_tau_tilde_fluxU(alpha, Ge.sqrtgammaDET,    vU,Ge.T4UU,Ge.rho_star)
Ge.compute_S_tilde_fluxUD( alpha, Ge.sqrtgammaDET,       Ge.T4UD)

normD = ixp.zerorank1()
normD[0], normD[1], normD[2] = sp.symbols("norm[0] norm[1] norm[2]", real=True)

faceVelU = ixp.zerorank1()
faceVelU[0], faceVelU[1], faceVelU[2] = sp.symbols("faceVelocity[0] faceVelocity[1] faceVelocity[2]", real=True)
# Zach says: don't forget to limit the velocities after they are computed!

faceVelNorm = sp.sympify(0)
for i in range(3) :
    faceVelNorm += normD[i]*faceVelU[i]

exprArray = []
nameArray = []
exprArray.append( Ge.rho_star)
nameArray.append( "temp_rho_star")
exprArray.append( Ge.T4UU[0][1])
nameArray.append( "temp_T4UU01")

rho_star_flux = sp.sympify(0)
for i in range(3) :
    rho_star_flux += Ge.rho_star_fluxU[i]*normD[i]
rho_star_flux -= Ge.rho_star*faceVelNorm

exprArray.append( rho_star_flux)
nameArray.append( "flux[iRhoStar]")

tau_tilde_flux = sp.sympify(0)
for i in range(3) :
    tau_tilde_flux += Ge.tau_tilde_fluxU[i]*normD[i]

tau_tilde_flux -= Ge.tau_tilde*faceVelNorm

S_tilde_fluxD = ixp.zerorank1()

for i in range(3) :
    S_tilde_fluxD[i] -= Ge.S_tildeD[i]*faceVelNorm
    for j in range(3) :
        S_tilde_fluxD[i] += Ge.S_tilde_fluxUD[j][i]*normD[j]

for j, comp in enumerate(["x","y", "z"]) :
    exprArray.append( S_tilde_fluxD[j])
    nameArray.append( "flux[iS{0}]".format(comp))

exprArray.append( tau_tilde_flux)
nameArray.append( "flux[iTau]")

#for expr, name in zip( exprArray, nameArray) :
#    print( name)
outputC(exprArray, nameArray, filename="NRPY+calFlux.h", params="outCverbose=False")

!cat NRPY+calFlux.h
```

    Wrote to file "NRPY+calFlux.h"
    {
       const double tmp0 = mi.gamDDyy*mi.gamDDzz;
       const double tmp1 = mi.gamDDxz*mi.gamDDyz;
       const double tmp2 = 2*mi.gamDDxy;
       const double tmp3 = ((mi.gamDDyz)*(mi.gamDDyz));
       const double tmp4 = ((mi.gamDDxy)*(mi.gamDDxy));
       const double tmp5 = ((mi.gamDDxz)*(mi.gamDDxz));
       const double tmp6 = mi.gamDDxx*tmp0 - mi.gamDDxx*tmp3 - mi.gamDDyy*tmp5 - mi.gamDDzz*tmp4 + tmp1*tmp2;
       const double tmp7 = sqrt(tmp6);
       const double tmp8 = (1.0/((GAMMA_SPEED_LIMIT)*(GAMMA_SPEED_LIMIT)));
       const double tmp9 = (1.0/2.0)*tmp8;
       const double tmp10 = ((mi.alpha)*(mi.alpha));
       const double tmp11 = (1.0/(tmp10));
       const double tmp12 = mi.betaX + vx;
       const double tmp13 = mi.gamDDxx*tmp11*((tmp12)*(tmp12));
       const double tmp14 = (1.0/2.0)*tmp13;
       const double tmp15 = mi.betaY + vy;
       const double tmp16 = mi.gamDDyy*tmp11*((tmp15)*(tmp15));
       const double tmp17 = (1.0/2.0)*tmp16;
       const double tmp18 = mi.betaZ + vz;
       const double tmp19 = mi.gamDDzz*tmp11*((tmp18)*(tmp18));
       const double tmp20 = (1.0/2.0)*tmp19;
       const double tmp21 = tmp11*tmp12;
       const double tmp22 = tmp15*tmp21;
       const double tmp23 = mi.gamDDxy*tmp22;
       const double tmp24 = mi.gamDDxz*tmp18*tmp21;
       const double tmp25 = mi.gamDDyz*tmp11*tmp15*tmp18;
       const double tmp26 = tmp2*tmp22;
       const double tmp27 = 2*tmp24;
       const double tmp28 = 2*tmp25;
       const double tmp29 = (1.0/2.0)*fabs(-tmp13 - tmp16 - tmp19 - tmp26 - tmp27 - tmp28 - tmp8 + 1);
       const double tmp30 = -tmp14 - tmp17 - tmp20 - tmp23 - tmp24 - tmp25 + tmp29 + tmp9 + 1.0/2.0;
       const double tmp31 = rho*tmp7/sqrt(tmp30);
       const double tmp32 = p*tmp11;
       const double tmp33 = sqrt((tmp14 + tmp17 + tmp20 + tmp23 + tmp24 + tmp25 - tmp29 - tmp9 + 1.0/2.0)/(TINYDOUBLE + tmp13 + tmp16 + tmp19 + tmp26 + tmp27 + tmp28));
       const double tmp34 = -mi.betaX + tmp12*tmp33;
       const double tmp35 = rho*tmp11*(ie + p/rho + 1)/tmp30;
       const double tmp36 = tmp34*tmp35;
       const double tmp37 = mi.betaX*tmp32 + tmp36;
       const double tmp38 = tmp31*vx;
       const double tmp39 = tmp31*vy;
       const double tmp40 = tmp31*vz;
       const double tmp41 = faceVelocity[0]*norm[0] + faceVelocity[1]*norm[1] + faceVelocity[2]*norm[2];
       const double tmp42 = mi.betaX*mi.gamDDxx + mi.betaY*mi.gamDDxy + mi.betaZ*mi.gamDDxz;
       const double tmp43 = -tmp32 + tmp35;
       const double tmp44 = -mi.betaY + tmp15*tmp33;
       const double tmp45 = tmp35*tmp44;
       const double tmp46 = mi.betaY*tmp32 + tmp45;
       const double tmp47 = -mi.betaZ + tmp18*tmp33;
       const double tmp48 = mi.betaZ*tmp32 + tmp35*tmp47;
       const double tmp49 = mi.alpha*tmp7;
       const double tmp50 = tmp41*tmp49;
       const double tmp51 = (1.0/(tmp6));
       const double tmp52 = p*(-((mi.betaX)*(mi.betaX))*tmp11 + tmp51*(tmp0 - tmp3)) + ((tmp34)*(tmp34))*tmp35;
       const double tmp53 = mi.betaX*tmp11;
       const double tmp54 = p*(-mi.betaY*tmp53 + tmp51*(-mi.gamDDxy*mi.gamDDzz + tmp1)) + tmp36*tmp44;
       const double tmp55 = mi.gamDDxy*tmp54;
       const double tmp56 = p*(-mi.betaZ*tmp53 + tmp51*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy)) + tmp36*tmp47;
       const double tmp57 = mi.gamDDxz*tmp56;
       const double tmp58 = norm[0]*tmp49;
       const double tmp59 = p*(-((mi.betaY)*(mi.betaY))*tmp11 + tmp51*(mi.gamDDxx*mi.gamDDzz - tmp5)) + tmp35*((tmp44)*(tmp44));
       const double tmp60 = p*(-mi.betaY*mi.betaZ*tmp11 + tmp51*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz)) + tmp45*tmp47;
       const double tmp61 = norm[1]*tmp49;
       const double tmp62 = p*(-((mi.betaZ)*(mi.betaZ))*tmp11 + tmp51*(mi.gamDDxx*mi.gamDDyy - tmp4)) + tmp35*((tmp47)*(tmp47));
       const double tmp63 = norm[2]*tmp49;
       const double tmp64 = mi.betaX*mi.gamDDxy + mi.betaY*mi.gamDDyy + mi.betaZ*mi.gamDDyz;
       const double tmp65 = mi.gamDDyz*tmp60;
       const double tmp66 = mi.betaX*mi.gamDDxz + mi.betaY*mi.gamDDyz + mi.betaZ*mi.gamDDzz;
       const double tmp67 = tmp10*tmp7;
       temp_rho_star = tmp31;
       temp_T4UU01 = tmp37;
       flux[iRhoStar] = norm[0]*tmp38 + norm[1]*tmp39 + norm[2]*tmp40 - tmp31*tmp41;
       flux[iSx] = -tmp50*(mi.gamDDxx*tmp37 + mi.gamDDxy*tmp46 + mi.gamDDxz*tmp48 + tmp42*tmp43) + tmp58*(mi.gamDDxx*tmp52 + tmp37*tmp42 + tmp55 + tmp57) + tmp61*(mi.gamDDxx*tmp54 + mi.gamDDxy*tmp59 + mi.gamDDxz*tmp60 + tmp42*tmp46) + tmp63*(mi.gamDDxx*tmp56 + mi.gamDDxy*tmp60 + mi.gamDDxz*tmp62 + tmp42*tmp48);
       flux[iSy] = -tmp50*(mi.gamDDxy*tmp37 + mi.gamDDyy*tmp46 + mi.gamDDyz*tmp48 + tmp43*tmp64) + tmp58*(mi.gamDDxy*tmp52 + mi.gamDDyy*tmp54 + mi.gamDDyz*tmp56 + tmp37*tmp64) + tmp61*(mi.gamDDyy*tmp59 + tmp46*tmp64 + tmp55 + tmp65) + tmp63*(mi.gamDDxy*tmp56 + mi.gamDDyy*tmp60 + mi.gamDDyz*tmp62 + tmp48*tmp64);
       flux[iSz] = -tmp50*(mi.gamDDxz*tmp37 + mi.gamDDyz*tmp46 + mi.gamDDzz*tmp48 + tmp43*tmp66) + tmp58*(mi.gamDDxz*tmp52 + mi.gamDDyz*tmp54 + mi.gamDDzz*tmp56 + tmp37*tmp66) + tmp61*(mi.gamDDxz*tmp54 + mi.gamDDyz*tmp59 + mi.gamDDzz*tmp60 + tmp46*tmp66) + tmp63*(mi.gamDDzz*tmp62 + tmp48*tmp66 + tmp57 + tmp65);
       flux[iTau] = norm[0]*(tmp37*tmp67 - tmp38) + norm[1]*(-tmp39 + tmp46*tmp67) + norm[2]*(-tmp40 + tmp48*tmp67) - tmp41*(-tmp31 + tmp43*tmp67);
    }

# Step 4: Source Terms
$$\label{source}$$

The sources terms are for mass, momentum and energy are:
\begin{equation}
\source = (0, \frac 1 2 \lapse\rtgamma \T{\alpha}{\beta}g_{\alpha\beta,i}, s),
\end{equation}
For a time stationary metric $s\neq 0$, so we will ignore this until the next section.  As for the rest, we need to define derivatives of the metric.  Suppose I have done this already.  Then the code for the source terms is:

```python
# FIXME: Assume static spacetime with KDD = betaU = betaU_dD = 0
KDD = ixp.zerorank2()
betaU = ixp.zerorank1()
betaU_dD = ixp.zerorank2()

# Set+evaluate derivatives of alpha, performing 2nd-order finite difference
alpha_dD = ixp.zerorank1()
h = sp.symbols("h")
for i in range(3) :
    alpha_plus, alpha_minus = sp.symbols("mi_plus[{0}].alpha mi_minus[{0}].alpha".format(i))
    alpha_dD[i] = (alpha_plus - alpha_minus)/(2*h)

# Set+evaluate derivatives of gamma_{ij}, performing 2nd-order finite difference
gammaDD_dD = ixp.zerorank3()
components = ["xx", "xy", "xz", "yy", "yz", "zz"]
for i in range(3) :
    names_plus = ""
    names_minus = ""
    for comp in components :
        names_plus  = names_plus  +  "mi_plus[{0}].gamDD{1} ".format(i, comp)
        names_minus = names_minus + "mi_minus[{0}].gamDD{1} ".format(i, comp)

    gxx_plus, gxy_plus, gxz_plus, gyy_plus, gyz_plus, gzz_plus = sp.symbols( names_plus)
    gxx_minus, gxy_minus, gxz_minus, gyy_minus, gyz_minus, gzz_minus = sp.symbols( names_minus)

    gammaDD_dD[0][0][i] = (gxx_plus - gxx_minus)/(2*h)
    gammaDD_dD[0][1][i] = (gxy_plus - gxy_minus)/(2*h)
    gammaDD_dD[0][2][i] = (gxz_plus - gxz_minus)/(2*h)
    gammaDD_dD[1][0][i] = (gxy_plus - gxy_minus)/(2*h)
    gammaDD_dD[1][1][i] = (gyy_plus - gyy_minus)/(2*h)
    gammaDD_dD[1][2][i] = (gyz_plus - gyz_minus)/(2*h)
    gammaDD_dD[2][0][i] = (gxz_plus - gxz_minus)/(2*h)
    gammaDD_dD[2][1][i] = (gyz_plus - gyz_minus)/(2*h)
    gammaDD_dD[2][2][i] = (gzz_plus - gzz_minus)/(2*h)

# Compute g_{mu nu, i} based on ADM quantities & derivatives defined above
Ge.compute_g4DD_zerotimederiv_dD(gammaDD,betaU,alpha, gammaDD_dD,betaU_dD,alpha_dD)

# Compute source terms for tau tilde & S tilde:
Ge.compute_s_source_term(KDD,betaU,alpha, Ge.sqrtgammaDET,alpha_dD,  Ge.T4UU)
Ge.compute_S_tilde_source_termD(   alpha, Ge.sqrtgammaDET,Ge.g4DD_zerotimederiv_dD, Ge.T4UU)

exprArray = []
nameArray = []

#momentum terms
for i in range(3) :
    exprArray.append( Ge.S_tilde_source_termD[i])
    nameArray.append( "vSource[{0}]".format(i))

#tau term
exprArray.append( Ge.s_source_term)
nameArray.append( "eSource")
outputC( exprArray, nameArray, filename="NRPY+calSources.h", params="outCverbose=False")

!cat NRPY+calSources.h
```

    Wrote to file "NRPY+calSources.h"
    {
       const double tmp0 = (1.0/(h));
       const double tmp1 = (1.0/2.0)*tmp0;
       const double tmp2 = tmp1*(-mi_minus[0].alpha + mi_plus[0].alpha);
       const double tmp3 = mi.gamDDyy*mi.gamDDzz;
       const double tmp4 = mi.gamDDxz*mi.gamDDyz;
       const double tmp5 = 2*mi.gamDDxy;
       const double tmp6 = ((mi.gamDDyz)*(mi.gamDDyz));
       const double tmp7 = ((mi.gamDDxy)*(mi.gamDDxy));
       const double tmp8 = ((mi.gamDDxz)*(mi.gamDDxz));
       const double tmp9 = mi.gamDDxx*tmp3 - mi.gamDDxx*tmp6 - mi.gamDDyy*tmp8 - mi.gamDDzz*tmp7 + tmp4*tmp5;
       const double tmp10 = sqrt(tmp9);
       const double tmp11 = ((mi.alpha)*(mi.alpha));
       const double tmp12 = (1.0/(tmp11));
       const double tmp13 = p*tmp12;
       const double tmp14 = (1.0/((GAMMA_SPEED_LIMIT)*(GAMMA_SPEED_LIMIT)));
       const double tmp15 = (1.0/2.0)*tmp14;
       const double tmp16 = mi.betaX + vx;
       const double tmp17 = mi.gamDDxx*tmp12*((tmp16)*(tmp16));
       const double tmp18 = (1.0/2.0)*tmp17;
       const double tmp19 = mi.betaY + vy;
       const double tmp20 = mi.gamDDyy*tmp12*((tmp19)*(tmp19));
       const double tmp21 = (1.0/2.0)*tmp20;
       const double tmp22 = mi.betaZ + vz;
       const double tmp23 = mi.gamDDzz*tmp12*((tmp22)*(tmp22));
       const double tmp24 = (1.0/2.0)*tmp23;
       const double tmp25 = tmp12*tmp16;
       const double tmp26 = tmp19*tmp25;
       const double tmp27 = mi.gamDDxy*tmp26;
       const double tmp28 = mi.gamDDxz*tmp22*tmp25;
       const double tmp29 = mi.gamDDyz*tmp12*tmp19*tmp22;
       const double tmp30 = tmp26*tmp5;
       const double tmp31 = 2*tmp28;
       const double tmp32 = 2*tmp29;
       const double tmp33 = (1.0/2.0)*fabs(-tmp14 - tmp17 - tmp20 - tmp23 - tmp30 - tmp31 - tmp32 + 1);
       const double tmp34 = rho*tmp12*(ie + p/rho + 1)/(tmp15 - tmp18 - tmp21 - tmp24 - tmp27 - tmp28 - tmp29 + tmp33 + 1.0/2.0);
       const double tmp35 = tmp10*tmp11*(-tmp13 + tmp34);
       const double tmp36 = (1.0/(tmp9));
       const double tmp37 = sqrt((-tmp15 + tmp18 + tmp21 + tmp24 + tmp27 + tmp28 + tmp29 - tmp33 + 1.0/2.0)/(TINYDOUBLE + tmp17 + tmp20 + tmp23 + tmp30 + tmp31 + tmp32));
       const double tmp38 = -mi.betaX + tmp16*tmp37;
       const double tmp39 = mi.alpha*tmp10;
       const double tmp40 = (1.0/4.0)*tmp0*tmp39;
       const double tmp41 = tmp40*(p*(-((mi.betaX)*(mi.betaX))*tmp12 + tmp36*(tmp3 - tmp6)) + tmp34*((tmp38)*(tmp38)));
       const double tmp42 = -mi.betaY + tmp19*tmp37;
       const double tmp43 = tmp40*(p*(-((mi.betaY)*(mi.betaY))*tmp12 + tmp36*(mi.gamDDxx*mi.gamDDzz - tmp8)) + tmp34*((tmp42)*(tmp42)));
       const double tmp44 = -mi.betaZ + tmp22*tmp37;
       const double tmp45 = tmp40*(p*(-((mi.betaZ)*(mi.betaZ))*tmp12 + tmp36*(mi.gamDDxx*mi.gamDDyy - tmp7)) + tmp34*((tmp44)*(tmp44)));
       const double tmp46 = mi.betaX*tmp12;
       const double tmp47 = tmp34*tmp38;
       const double tmp48 = tmp1*tmp39;
       const double tmp49 = tmp48*(p*(-mi.betaY*tmp46 + tmp36*(-mi.gamDDxy*mi.gamDDzz + tmp4)) + tmp42*tmp47);
       const double tmp50 = tmp48*(p*(-mi.betaZ*tmp46 + tmp36*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy)) + tmp44*tmp47);
       const double tmp51 = tmp34*tmp42;
       const double tmp52 = tmp48*(p*(-mi.betaY*mi.betaZ*tmp12 + tmp36*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz)) + tmp44*tmp51);
       const double tmp53 = tmp1*(-mi_minus[1].alpha + mi_plus[1].alpha);
       const double tmp54 = tmp1*(-mi_minus[2].alpha + mi_plus[2].alpha);
       vSource[0] = -tmp2*tmp35 + tmp41*(-mi_minus[0].gamDDxx + mi_plus[0].gamDDxx) + tmp43*(-mi_minus[0].gamDDyy + mi_plus[0].gamDDyy) + tmp45*(-mi_minus[0].gamDDzz + mi_plus[0].gamDDzz) + tmp49*(-mi_minus[0].gamDDxy + mi_plus[0].gamDDxy) + tmp50*(-mi_minus[0].gamDDxz + mi_plus[0].gamDDxz) + tmp52*(-mi_minus[0].gamDDyz + mi_plus[0].gamDDyz);
       vSource[1] = -tmp35*tmp53 + tmp41*(-mi_minus[1].gamDDxx + mi_plus[1].gamDDxx) + tmp43*(-mi_minus[1].gamDDyy + mi_plus[1].gamDDyy) + tmp45*(-mi_minus[1].gamDDzz + mi_plus[1].gamDDzz) + tmp49*(-mi_minus[1].gamDDxy + mi_plus[1].gamDDxy) + tmp50*(-mi_minus[1].gamDDxz + mi_plus[1].gamDDxz) + tmp52*(-mi_minus[1].gamDDyz + mi_plus[1].gamDDyz);
       vSource[2] = -tmp35*tmp54 + tmp41*(-mi_minus[2].gamDDxx + mi_plus[2].gamDDxx) + tmp43*(-mi_minus[2].gamDDyy + mi_plus[2].gamDDyy) + tmp45*(-mi_minus[2].gamDDzz + mi_plus[2].gamDDzz) + tmp49*(-mi_minus[2].gamDDxy + mi_plus[2].gamDDxy) + tmp50*(-mi_minus[2].gamDDxz + mi_plus[2].gamDDxz) + tmp52*(-mi_minus[2].gamDDyz + mi_plus[2].gamDDyz);
       eSource = tmp39*(tmp2*(-mi.betaX*tmp13 - tmp47) + tmp53*(-mi.betaY*tmp13 - tmp51) + tmp54*(-mi.betaZ*tmp13 - tmp34*tmp44));
    }

# Step 6: Conservative to Primitive Solver
$$\label{solver}$$

We now discuss the reverse mapping from conservative to primitive variables.
Given the lapse, shift vector and $\rtgamma$, the mapping between primitive and conserved variable is straightforward.  However, the reverse is not as simple.  In GRMHD, the conservative to primitive solver is amplified by the inclusion of the magnetic field, leading to rather sophisticated root finding strategies.  The failure rates of these algorithms are low (??), but since this algorithm may be executed several times per timestep for every gridpoint, even a low failure can give unacceptable collective failure rates.  However, for purely polytropic equations of state, e.g., $P\propto\rho^{\Gamma_1}$, the convervative to primitive variable solver is greatly simplified.

To construct the conservative-to-primitive variable solver, we restrict ourselves to polytropic equations of states
\begin{equation}
P = P_0\left(\frac{\rho}{\rho_0}\right)^{\Gamma_1} \quad\textrm{and}\quad \epsilon = \epsilon_0\left(\frac{\rho}{\rho_0}\right)^{\Gamma_1-1},
\end{equation}
where $P_0$, $\rho_0$, and $\epsilon_0$ are the fiducial pressure, density, and internal energy, and we have used the relation $P = (\Gamma_1 - 1)\rho\epsilon$.

For such a polytropic equation of state, the energy equation is redundant and effectively we are only concerned with the continuity and momentum equations. The conservative variables of concern are $\rhostar$ and $\Svectilde$.  Noting that the shift, $\alpha$, and $\rtgamma$ are provided by the Einsteins field equation solver, we can write
\begin{equation}
u^0 = \frac{\rhostar}{\alpha\rtgamma\rho} = u^0(\rho)  \quad\textrm{and}\quad \uvec = \frac{\Svectilde}{\alpha\rtgamma\rho h} = \uvec(\rho).
\end{equation}
Noting that the four velocity $u^2 = g_{\mu\nu}u^{\mu}u^{\nu} = g^{00}u^0u^0 + 2g^{0i}u^0\uvec^i + g_{ij}\uvec^i\uvec^j = -1$, we have
\begin{equation}
 0 = f(\rho)\equiv \alpha^2\gamma\rho^2h^2 + \left(-\lapse^2 + \shift\cdot\shift\right)\rhostar^2h^2 + 2h\rhostar\shift\cdot\Svectilde + \Svectilde\cdot\Svectilde,
\end{equation}
which is an implicit equation of either $\rho$ or $u^0$, where $h(\rho = \rhostar/(\alpha\rtgamma u^0)) = 1 + \gamma_1 \epsilon$ which can be inverted by standard nonlinear root finding algorithms, e.g., Newton-raphson.

We put this all together to define a function, $f(\rho)$, whose root is zero that we will find via Newton-raphson.

Several checks must be performed:

1. $\rhostar > 0$ : This check is performed at the very beginning

2. $\rho > \rho_{\rm min}$ : This check is performed after the fact

3. $u_0 < \alpha^{-1}\Gamma_{\rm max}$ : This check is performed after the fact as well

```python
DIM = 3
# Declare rank-1 contravariant ("v") vector
vU = ixp.declarerank1("vU")
shiftU = ixp.zerorank1()
rho, gamma1 = sp.symbols("rho gamma")
Sx, Sy, Sz = sp.symbols("con[iSx] con[iSy] con[iSz]")
p0, rho0, rhostar = sp.symbols("p_0 rho_0 rhostar")
# Declare rank-2 covariant gmunu
#gammaDD = ixp.declarerank2("gammaDD","sym01")
StildeD = ixp.declarerank1("StildeD")
lapse, rtgamma, beta_x, beta_y, beta_z = sp.symbols( "mi.alpha mi.rtDetGamma mi.betaX mi.betaY mi.betaZ")

shiftU[0] = beta_x
shiftU[1] = beta_y
shiftU[2] = beta_z

StildeD[0] = Sx
StildeD[1] = Sy
StildeD[2] = Sz

# gamma = rtgamma*rtgamma <- unused
lapse2 = lapse*lapse
uU0 = rhostar/(lapse*rtgamma*rho)
epsilon = p0/rho0*(rho/rho0)**(gamma1 - 1)/(gamma1 - 1)
h = 1 + gamma1*epsilon

beta2 = 0.

for i in range(DIM) :
    for j in range(DIM) :
        beta2 += gammaDD[i][j] * shiftU[i]*shiftU[j]

betaDotStilde = 0
for i in range(DIM) :
    betaDotStilde += shiftU[i]*StildeD[i]

# Note that this is |Stilde|^2, where the absolute value denotes
#    that this is not a proper contraction of Stilde_i, as
#    Stilde^i is NOT equal to gamma^{ij} Stilde_j (to understand
#    why this is, notice that Stilde_i is proportional to the
#    *4D* stress-energy tensor.)
Stilde2 = 0
for i in range(DIM) :
    for j in range(DIM) :
        Stilde2 += gammaUU[i][j] * StildeD[i]*StildeD[j]

f = rhostar**2*h**2 + (-lapse2 + beta2)*rhostar**2.*h**2.*uU0**2 + 2.*h*rhostar*betaDotStilde*uU0 + Stilde2

outputC(f,"rootRho",filename="NRPY+rhoRoot.h")
outputC(Stilde2, "Stilde2", filename="NRPY+Stilde2.h")
!cat NRPY+rhoRoot.h
!cat NRPY+Stilde2.h
```

    Wrote to file "NRPY+rhoRoot.h"
    Wrote to file "NRPY+Stilde2.h"
    /*
     *  Original SymPy expression:
     *  "rootRho = con[iSx]**2*(mi.gamDDyy*mi.gamDDzz - mi.gamDDyz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSx]*con[iSy]*(-mi.gamDDxy*mi.gamDDzz + mi.gamDDxz*mi.gamDDyz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSx]*con[iSz]*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + con[iSy]**2*(mi.gamDDxx*mi.gamDDzz - mi.gamDDxz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSy]*con[iSz]*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + con[iSz]**2*(mi.gamDDxx*mi.gamDDyy - mi.gamDDxy**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + rhostar**2*(gamma*p_0*(rho/rho_0)**(gamma - 1)/(rho_0*(gamma - 1)) + 1)**2 + rhostar**2*(2.0*gamma*p_0*(rho/rho_0)**(gamma - 1)/(rho_0*(gamma - 1)) + 2.0)*(con[iSx]*mi.betaX + con[iSy]*mi.betaY + con[iSz]*mi.betaZ)/(mi.alpha*mi.rtDetGamma*rho) + rhostar**4.0*(gamma*p_0*(rho/rho_0)**(gamma - 1)/(rho_0*(gamma - 1)) + 1)**2.0*(-mi.alpha**2 + mi.betaX**2*mi.gamDDxx + 2*mi.betaX*mi.betaY*mi.gamDDxy + 2*mi.betaX*mi.betaZ*mi.gamDDxz + mi.betaY**2*mi.gamDDyy + 2*mi.betaY*mi.betaZ*mi.gamDDyz + mi.betaZ**2*mi.gamDDzz)/(mi.alpha**2*mi.rtDetGamma**2*rho**2)"
     */
    {
       const double tmp0 = ((rhostar)*(rhostar));
       const double tmp1 = (1.0/(rho_0));
       const double tmp2 = gamma - 1;
       const double tmp3 = gamma*p_0*tmp1*pow(rho*tmp1, tmp2)/tmp2;
       const double tmp4 = tmp3 + 1;
       const double tmp5 = mi.gamDDxz*mi.gamDDyz;
       const double tmp6 = mi.gamDDyy*mi.gamDDzz;
       const double tmp7 = 2*mi.gamDDxy;
       const double tmp8 = ((mi.gamDDyz)*(mi.gamDDyz));
       const double tmp9 = ((mi.gamDDxy)*(mi.gamDDxy));
       const double tmp10 = ((mi.gamDDxz)*(mi.gamDDxz));
       const double tmp11 = (1.0/(mi.gamDDxx*tmp6 - mi.gamDDxx*tmp8 - mi.gamDDyy*tmp10 - mi.gamDDzz*tmp9 + tmp5*tmp7));
       const double tmp12 = 2*con[iSx]*tmp11;
       const double tmp13 = ((mi.alpha)*(mi.alpha));
       const double tmp14 = 2*mi.betaZ;
       rootRho = ((con[iSx])*(con[iSx]))*tmp11*(tmp6 - tmp8) + ((con[iSy])*(con[iSy]))*tmp11*(mi.gamDDxx*mi.gamDDzz - tmp10) + 2*con[iSy]*con[iSz]*tmp11*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz) + con[iSy]*tmp12*(-mi.gamDDxy*mi.gamDDzz + tmp5) + ((con[iSz])*(con[iSz]))*tmp11*(mi.gamDDxx*mi.gamDDyy - tmp9) + con[iSz]*tmp12*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy) + tmp0*((tmp4)*(tmp4)) + ((rhostar)*(rhostar)*(rhostar)*(rhostar))*((tmp4)*(tmp4))*(((mi.betaX)*(mi.betaX))*mi.gamDDxx + mi.betaX*mi.betaY*tmp7 + mi.betaX*mi.gamDDxz*tmp14 + ((mi.betaY)*(mi.betaY))*mi.gamDDyy + mi.betaY*mi.gamDDyz*tmp14 + ((mi.betaZ)*(mi.betaZ))*mi.gamDDzz - tmp13)/(((mi.rtDetGamma)*(mi.rtDetGamma))*((rho)*(rho))*tmp13) + tmp0*(2.0*tmp3 + 2.0)*(con[iSx]*mi.betaX + con[iSy]*mi.betaY + con[iSz]*mi.betaZ)/(mi.alpha*mi.rtDetGamma*rho);
    }
    /*
     *  Original SymPy expression:
     *  "Stilde2 = con[iSx]**2*(mi.gamDDyy*mi.gamDDzz - mi.gamDDyz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSx]*con[iSy]*(-mi.gamDDxy*mi.gamDDzz + mi.gamDDxz*mi.gamDDyz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSx]*con[iSz]*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + con[iSy]**2*(mi.gamDDxx*mi.gamDDzz - mi.gamDDxz**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + 2*con[iSy]*con[iSz]*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy) + con[iSz]**2*(mi.gamDDxx*mi.gamDDyy - mi.gamDDxy**2)/(mi.gamDDxx*mi.gamDDyy*mi.gamDDzz - mi.gamDDxx*mi.gamDDyz**2 - mi.gamDDxy**2*mi.gamDDzz + 2*mi.gamDDxy*mi.gamDDxz*mi.gamDDyz - mi.gamDDxz**2*mi.gamDDyy)"
     */
    {
       const double tmp0 = mi.gamDDxz*mi.gamDDyz;
       const double tmp1 = mi.gamDDyy*mi.gamDDzz;
       const double tmp2 = ((mi.gamDDyz)*(mi.gamDDyz));
       const double tmp3 = ((mi.gamDDxy)*(mi.gamDDxy));
       const double tmp4 = ((mi.gamDDxz)*(mi.gamDDxz));
       const double tmp5 = (1.0/(mi.gamDDxx*tmp1 - mi.gamDDxx*tmp2 + 2*mi.gamDDxy*tmp0 - mi.gamDDyy*tmp4 - mi.gamDDzz*tmp3));
       const double tmp6 = 2*con[iSx]*tmp5;
       Stilde2 = ((con[iSx])*(con[iSx]))*tmp5*(tmp1 - tmp2) + ((con[iSy])*(con[iSy]))*tmp5*(mi.gamDDxx*mi.gamDDzz - tmp4) + 2*con[iSy]*con[iSz]*tmp5*(-mi.gamDDxx*mi.gamDDyz + mi.gamDDxy*mi.gamDDxz) + con[iSy]*tmp6*(-mi.gamDDxy*mi.gamDDzz + tmp0) + ((con[iSz])*(con[iSz]))*tmp5*(mi.gamDDxx*mi.gamDDyy - tmp3) + con[iSz]*tmp6*(mi.gamDDxy*mi.gamDDyz - mi.gamDDxz*mi.gamDDyy);
    }

The root solve above finds $\rho$, which then allows us to get
\begin{equation}
u^0 = \frac{\rhostar}{\alpha\rtgamma\rho}\quad\textrm{and}\quad \vel = \frac{\uvec}{u^0} = \frac{\Svectilde}{\rhostar h(\rho)}.
\end{equation}
and thus we can find the rest of the primitives.

```python
#rhostar = sp.symbols("rhostar")
#StildeU = ixp.declarerank1("StildeU")
velU = ixp.zerorank1()

#lapse, rtgamma, rho, gamma1, c = sp.symbols("lapse rtgamma rho gamma1 c")
rho, rhostar = sp.symbols("testPrim[iRho] con[iRhoStar]")

u0 = rhostar/(lapse*rtgamma*rho)
epsilon = p0/rho0*(rho/rho0)**(gamma1 - 1)/(gamma1 - 1)

h = 1. + gamma1*epsilon

for i in range(DIM) :
    for j in range(DIM) :
        velU[i] += gammaUU[i][j]*StildeD[j]/(rhostar * h)/u0

outputC([h,u0,velU[0],velU[1],velU[2]], ["h", "u0","testPrim[ivx]", "testPrim[ivy]", "testPrim[ivz]"],filename="NRPY+getv.h")
```

    Wrote to file "NRPY+getv.h"

# Step 7: Lorentz Boosts
$$\label{lorentz}$$

We need to boost to the frame of the moving face.  The boost is
\begin{equation}
B(\beta) =\begin{pmatrix}
\gamma & -\beta\gamma n_x & -\beta\gamma n_y & -\beta\gamma n_z  \\
-\beta\gamma n_x  & 1 + (\gamma-1)n_x^2 & (\gamma-1)n_x n_y & (\gamma-1)n_x n_z\\
-\beta\gamma n_x  & (\gamma-1)n_y n_x & 1 + (\gamma-1)n_y^2 & (\gamma-1)n_y n_z\\
-\beta\gamma n_x & (\gamma-1) n_z n_x & (\gamma-1)n_z n_x & 1 + (\gamma-1)n_z^2
\end{pmatrix}
\end{equation}
And the boost is $X' = B(\beta) X$, where $X'$ and $X$ are four vectors.

So the rest of this is straightforward.

# Step 8: 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-GRHD_Equations-Cartesian-c-code.pdf](Tutorial-GRHD_Equations-Cartesian-c-code.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 --log-level='WARN' Tutorial-GRHD_Equations-Cartesian-c-code.ipynb
!pdflatex -interaction=batchmode Tutorial-GRHD_Equations-Cartesian-c-code.tex
!pdflatex -interaction=batchmode Tutorial-GRHD_Equations-Cartesian-c-code.tex
!pdflatex -interaction=batchmode Tutorial-GRHD_Equations-Cartesian-c-code.tex
!rm -f Tut*.out Tut*.aux Tut*.log
```

    [NbConvertApp] Converting notebook Tutorial-GRHD_Equations-Cartesian-c-code.ipynb to latex
    [NbConvertApp] Writing 87879 bytes to Tutorial-GRHD_Equations-Cartesian-c-code.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
import sympy

e1, e2, d1, d2, y1, y2, d3 = sympy.symbols("e1 e2 d1 d2 y1 y2 d3")
c11, c22, c33, c44, c55, c66 = sympy.symbols("c11 c22 c33 c44 c55 c66")
c12, c13, c23 = sympy.symbols("c12 c13 c23")

e2 = (c11 - c33)/(2*c33)
d2 = ((c13 + c55)**2 - (c33 - c55)**2) / (2*c33*(c33 - c55))
y2 = (c66 - c44) / (2*c44)

e1 = (c22 - c33)/(2*c33)
d1 = ((c23 + c44)**2 - (c33 - c44)**2) / (2*c33*(c33 - c44))
y2 = (c66 - c55) / (2*c55)

S = sympy.solve([e2, d2, y2, e1, d1, y1], [c12])
print(S)
print('Done Solve')
```

    []
    Done Solve


| |Pierre Proulx, ing, professeur|
|:---|:---|
|Département de génie chimique et de génie biotechnologique |** GCH200-Phénomènes d'échanges I **|

### Section18.4, Réacteur chimique homogène

Voir les hypothèses dans le livre de Transport Phenomena.

```python
#
# Pierre Proulx
#
#
# Préparation de l'affichage et des outils de calcul symbolique
#
import sympy as sp
from IPython.display import *
sp.init_printing(use_latex=True)
%matplotlib inline
```

```python
# Paramètres, variables et fonctions

z,L,k_1,C_A0,D_AB,S,delta_z=sp.symbols('z,L,k_1,C_A0,D_AB,S,delta_z')
N_A=sp.symbols('N_A')
C_A=sp.Function('C_A')(z)
```

```python
#
# Bilan sur le réacteur, divisé tout de suite par le volume différentiel
#
eq=(S*N_A(z)-S*N_A(z+delta_z)-k_1*C_A*S*delta_z)/(S*delta_z)
display(eq)
```

```python
eq=sp.limit(eq,delta_z,0).doit()
display(eq)
```

```python
fick=-D_AB*sp.diff(C_A,z)
eq=eq.subs(N_A(z),fick)
display(eq)
```

```python
C_A=sp.dsolve(eq,C_A)
display(C_A)
C=C_A                    # je garde une copie de l'expression pour plus tard
```

```python
C_A=C_A.rhs
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
condition_1=sp.Eq(C_A.diff(z).subs(z,L ),0)
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A1=C_A.subs(constantes)
display(C_A1.simplify())
```

On peut maintenant utiliser la solution pour calculer , par exemple, la quantité de la substance A qui réagit dans le réacteur. Elle est obtenue en multipliant le flux molaire en z=0 par la surface, donc:

$ W_A = S * N_A $ en z=0 donc $ W_A = S * (-D_{AB}) \frac {dC_A}{dz}_{z=0} $

```python
W_A=S*C_A1.diff(z).subs(z,0)
display(W_A)
```

____

# Reprenons l'exemple précédent , en modifiant la condition à la limite z=L

____

## Le problème développé à la section 18.4 est identique au problème d'ailette traité à la section 10.7.

### 1. Au lieu de considérer la fond du réacteur comme imperméable, on pourrait considérer qu'il contient un catalyseur rapide. La condition en z=L deviendrait que $C_{AL}=0$

```python
#
# Reprenons C, la copie de C_A qu'on a fait avant d'y substituer les conditions aux limites
#
C_A=C.rhs
display(C_A)
```

```python
#
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
condition_1=sp.Eq(C_A.subs(z,L ),0)
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A2=C_A.subs(constantes)
display(C_A2.simplify())
```

____

### 2. Au lieu de considérer la fond du réacteur comme imperméable, on pourrait considérer qu'il contient un catalyseur réel. La condition en z=L deviendrait que la concentration est déterminée par la vitesse de réaction de surface donc:
### $ -D_{AB} \frac {dC_A}{dz} = k_s C_A $ en z=L

```python
#
# Reprenons encore C, la copie de C_A avant d'y substituer les conditions aux limites
#
C_A=C.rhs
display(C_A)
```

```python
#
# Pose et solution des 2 équations de conditions aux limites pour C1 et C2
k_s=sp.symbols('k_s')
condition_1=sp.Eq(-D_AB*C_A.diff(z).subs(z,L)-k_s*C_A.subs(z,L))
condition_2=sp.Eq(C_A.subs(z,0)-C_A0,0)
constantes=sp.solve([condition_1,condition_2],sp.symbols('C1,C2') )
display(constantes)
```

```python
C_A3=C_A.subs(constantes)
display(C_A3.simplify())
```

```python
#
# On peut maintenant tracer, en donnant des valeurs pour les paramètres.
#
dico={'D_AB':1.e-10,'C_A0':1,'L':0.1,'k_1':1.e-7,'k_s':1e-8}
C_A1p=C_A1.subs(dico).evalf()
C_A2p=C_A2.subs(dico).evalf()
C_A3p=C_A3.subs(dico).evalf()
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize']=6,5
p=sp.plot((C_A1p,(z,0,L.subs(dico))),(C_A2p,(z,0,L.subs(dico))),(C_A3p,(z,0,L.subs(dico))),
        title='Concentration vs z',ylabel='C_A',show=False,legend=True)
p[0].line_color = 'blue'
p[0].label='Fond du réacteur imperméable'
p[1].line_color = 'black'
p[1].label='Fond du réacteur catalyseur rapide'
p[2].line_color = 'red'
p[2].label='Fond du réacteur catalyseur réel'
p.show()
```


# Region Proposal Network

## Anchor Box
In `configuration.py` we have
```python
aspect = lambda s, r: (s * 1 / r ** 0.5, s * r ** 0.5)
self.rpn_base_aspect_ratios = [
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
        ]

self.rpn_base_sizes = [8, 16, 32, 64]
self.rpn_scales = [2, 4, 8, 16]
```
In which `s, r` means scale and ratio, each element in `rpn_base_apsect_ratios` means (height, width) of one anchor box. 4 array means 4 layers, so there are 3 different anchor boxes for each of the 4 feature maps generated from FPN, i.e
$$
\begin{align}
height & = scale \times (\frac{1}{\sqrt{ratio}}) \\
width  & = scale \times (\sqrt{ratio})
\end{align}
$$

So
$$\frac{width}{height} = ratio$$

And we have 2 scales $(1, \sqrt{2})$ and 3 aspects $(1:1, 1:2, 2:1)$, each feature map correspinding to 3 different anchor boxes.

1. The actual proposal size is controlled by `rpn_base_sizes`, different feature maps have different receptive field, small feature map has big receptive field, so we assign big base size of anchor box for small feature maps.

2. The corresponding scale ratio from image to feature map is recorded in `rpn_scales`, when using RoiAlign, we need to know which feature map to pool from given proposals by measuring the proposals' sizes. When feature map is set, we need these scales to get the actual sizes to crop from the feature maps.

The figure above explains the parameters used in `configuration.py`

## RPN's output

1. FPN generate 4 feature maps from a image
2. For one feature map say p3(64\*64), take each pixel(of the feature map) as the centre of an anchor box.
3. There are a total of $(64*64)*3*2 = 24576$ proposals arranged in shape $(3, 2, 64, 64)$, in which 2 means the score for foreground/background. Flatten it on the pixels' dimension gives the tensor $(B, N, 2)$, in which `B` is batch size, `N` will be calculated:
4. All 4 feature maps have a total of $(16*16*3 + 32*32*3 + 64*64*3 + 128*128*3) = 65280$ anchor boxes for foreground and background, so the final return size for RPN layer is $(B, 65280, 2)$. Let's check it out:

```python
import sys
sys.path.append("..")
import torch
from configuration import Configuration
from net.layer.rpn.rpn_head import RpnMultiHead
```

```python
cfg = Configuration()
# set anchor boxes to our example above
aspect = lambda s, r: (s * 1 / r ** 0.5, s * r ** 0.5)
cfg.rpn_base_aspect_ratios = [
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
           [(1,1), aspect(2**0.5,2), aspect(2**0.5,0.5),],
        ]

# input features, the batch size is set to 5, channels of each feature map is 256
p2 = torch.randn(5, 256, 128, 128)
p3 = torch.randn(5, 256, 64, 64)
p4 = torch.randn(5, 256, 32, 32)
p5 = torch.randn(5, 256, 16, 16)
fs = [p2, p3, p4, p5]
# init RPN head
net = RpnMultiHead(cfg, 256)
```

```python
logits_flat, deltas_flat = net(fs)
print(logits_flat.size(), deltas_flat.size())
```

    torch.Size([5, 65280, 2]) torch.Size([5, 65280, 2, 4])

RPN layer has 2 branch, one gives the score for foreground/background for each anchor box, another gives the box regression parameters, which is used to reshape each anchor box to fit the instance better. There are 4 parameters to refine the anchor box, so we have output tensor of size $(B, 65280, 2, 4)$

So this is the base idea behind RPN layer, which is probably the most complicated layer in mask rcnn.

## RPN Train/Test Process

1. Get features from FPN
2. Get rpn output: logits and deltas
3. Make targets for training from `rpn_target.py`
4. Do nms to get rpn proposals, which is in `nms.py`

## RCNN Train/Test Process

The train/test process for rcnn is not much different from RPN except for:

1. RCNN use cropped features by roi-align layer. The crop size is decided by rpn proposals
2. RCNN proposals for training are re-sampled, with balanced positive/negative samples
3. RCNN predict class label for each proposal

## MASK Train/Test Process

The train/test process for mask head is not much different from RPN and RCNN except for:

1. Mask head predicts per-pixel probability mask for each class, so it's output is (B, mask_size, mask_size)
2. The target for mask size is cropped from truth box and truch instance, which may not the same size as mask_size, so we need to resize the cropped instance into mask_size.

```python

```