# 20 - Plug-and-Play Estimators

So far, we've seen how to debias our data in the case where the treatment is not randomly assigned, which results in confounding bias. That helps us with the identification problem in causal inference. In other words, once the units are exchangeable, or \\( Y(0), Y(1) \perp X\\), it becomes possible to learn the treatment effect. But we are far from done.

Identification means that we can find the average treatment effect. In other words, we know how effective a treatment is on average. Of course this is useful, as it helps us to decide if we should roll out a treatment or not. But we want more than that. We want to know if there are subgroups of units that respond better or worse to the treatment. That should allow for a much better policy, one where we only treat the ones that will benefit from it.

## Problem Setup

Let's recall our setup of interest. Given the potential outcomes, we can define the individual treatment effect as the difference between the potential outcomes.

$
\tau_i = Y_i(1) − Y_i(0),
$

or, the continuous treatment case, \\(\tau_i = \partial Y(t)\\), where \\(t\\) is the treatment variable. Of course, we can never observe the individual treatment effect, because we only get to see the one of potential outcomes

$
Y^{obs}_i(t)=
\begin{cases}
Y_i(1), & \text{if } t=1\\
Y_i(0), & \text{if } t=0
\end{cases}
$

We can define the average treatment effect (ATE) as

$
\tau = E[Y_i(1) − Y_i(0)] = E[\tau_i]
$

and the conditional average treatment effect (CATE) as

$
\tau(x) = E[Y_i(1) − Y_i(0)|X] = E[\tau_i|X]
$

In Part I of this book, we've focused mostly on the ATE. Now, we are interested in the CATE. The CATE is useful for personalising a decision making process. For example, if you have a drug as the treatment \\(t\\), you want to know which type of patients are more responsive to the drug (higher CATE) and if there are some types of patient with a negative response (CATE < 0).

We've seen how to estimate the CATE using a linear regression with interactions between the treatment and the features

$
y_i = \beta_0 + \beta_1 t_i + \beta_2 X_i + \beta_3 t_i X_i + e_i.
$

If we estimate this model, we can get estimates for \\(\tau(x)\\)

$
\hat{\tau}(x) = \hat{\beta}_1 + \hat{\beta}_3 t_i X_i
$

Still, the linear models have some drawbacks. The main one being the linearity assumption on \\(X\\). Notice that you don't even care about \\(\beta_2\\) on this model. But if the features \\(X\\) don't have a linear relationship with the outcome, your estimates of the causal parameters \\(\beta_1\\) and \\(\beta_3\\) will be off.

It would be great if we could replace the linear model by a more flexible machine learning model. We could even plug the treatment as a feature to a ML model, like boosted trees or a neural network

$
y_i = M(X_i, T_i) + e_i
$

but from there, it is not clear how we can get treatment effect estimates, since this model will output \\(\hat{y}\\) predictions, not \\(\hat{\tau(x)}\\) predictions. Ideally, we would use a machine learning regression model that, instead of minimising the outcome MSE

$
E[(Y_i - \hat{Y}_i)^2]
$

would minimise the treatment effect MSE

$
E[(\tau(x)_i - \hat{\tau}(x)_i)^2] = E[(Y_i(1) - Y_i(0) - \hat{\tau}(x)_i)^2].
$

However, this criterion is what we call infeasible. Again, the problem here is that \\(\tau(x)_i\\) is not observable, so we can't optimize it directly. This puts us in a tough spot... Let's try to simplify it a bit and maybe we can think of something.

## Target Transformation

Suppose your treatment is binary. Let's say you are an investment firm testing the effectiveness of sending a financial education email. You hope the email will make people invest more. Also, let's say you did a randomized study where 50% of the customers got the email and the other 50% didn't.

Here is a crazy idea: let's transform the outcome variable by multiplying it with the treatment.

$
Y^*_i = 2 Y_i * T_i - 2 Y_i*(1-T_i)
$

So, if the unit was treated, you would take the outcome and multiply it by 2. If it wasn't treated, you would take the outcome and multiply it by -2. For example, if one of your customers invested BRL 2000,00 and got the email, the transformed target would be 4000. However, if he or she didn't get the email, it would be -4000.

This seems very odd, because you are saying that the effect of the email can be a negative number, but bare with me. If we do a little bit of math, we can see that, on average or in expectation, this transformed target will be the treatment effect. This is nothing short of amazing. What I'm saying is that by applying this somewhat wacky transformation, I get to estimate something that I can't even observe.

To understand that, we need a bit of math. Because of random assignment, we have that \\(T \perp Y(1), Y(1)\\), which is our old unconfoundedness friend. That implies that \\(E[T, Y(t)]=E[T]*E[Y(t)]\\), which is the definition of independence.

Also, we know that

$
Y_i*T_i = Y_(1)i*T_i \text{ and }  Y_i*(1-T_i) = Y_(0)i*T_i
$

because the treatment is what materializes one or the other potential outcomes. With that in mind, let's take the expected value of \\(Y^*_i\\) and see what we end up with.

$
\begin{align}
E[Y^*_i|X_i=x] &= E[2 Y(1)_i * T_i - 2 Y(0)_i*(1-T_i)|X_i=x] \\
&= 2E[Y(1)_i * T_i | X_i=x] - 2E[Y(0)_i*(1-T_i)|X_i=x]\\
&= 2E[Y(1)_i| X_i=x] * E[ T_i | X_i=x] - 2E[Y(0)_i| X_i=x]*E[(1-T_i)|X_i=x] \\
&= 2E[Y(1)_i| X_i=x] * 0.5 - 2E[Y(0)_i| X_i=x]*0.5 \\
&= E[Y(1)_i| X_i=x] - E[Y(0)_i| X_i=x] \\
&= \tau(x)_i
\end{align}
$

So, this apparently crazy idea ended up being an unbiased estimate of the individual treatment effect \\(\tau(x)_i\\). Now, we can replace our infeasible optimization criterion with

$
E[(Y^*_i - \hat{\tau}(x)_i)^2]
$

In simpler terms, all we have to do is use any regression machine learning model to predict \\(Y^*_i\\) and this model will output treatment effect predictions.

Now that we've solved the simple case, what about the more complicated case, where treatment is not 50% 50%, or not even randomly assigned? As it turns out, the answer is a bit more complicated, but not much. First, if we don't have random assignment, we need at least conditional independence \\(T \perp Y(1), Y(1) | X\\). That is, controlling for \\(X\\), \\(T\\) is as good as random. With that, we can generalize the transformed target to

$
Y^*_i = Y_i * \dfrac{T_i - e(X_i)}{e(X_i)(1-e(X_i))}
$

where \\(e(X_i)\\) is the propensity score. So, if the treatment is not 50% 50%, but randomized with a different probability \\(p\\), all you have to do is replace the propensity score in the above formula with \\(p\\). If the treatment is not random, then you have to use the propensity score, either stored or estimated.

If you take the expectation of this, you will see that it also matches the treatment effect. The proof is left as an exercise to the reader. Just kidding, here it is. It's a bit cumbersome, so feel free to skip it.

$
\begin{align}
E[Y^*_i|X_i=x] &= E\big[Y_i * \dfrac{T_i - e(X_i)}{e(X_i)(1-e(X_i))}|X_i=x\big] \\
&= E\big[Y_i T_i * \dfrac{T_i - e(X_i)}{e(X_i)(1-e(X_i))} + Y_i (1-T_i) * \dfrac{T_i - e(X_i)}{e(X_i)(1-e(X_i))}|X_i=x\big]\\
&= E\big[Y(1)_i * \dfrac{T_i(1 - e(X_i))}{e(X_i)(1-e(X_i))} | X_i=x\big] - E\big[Y(0)_i * \dfrac{(1-T_i)e(X_i)}{e(X_i)(1-e(X_i))}|X_i=x\big]\\
&= \dfrac{1}{e(X_i)} E[Y(1)_i * T_i|X_i=x] - \dfrac{1}{1-e(X_i)} E[Y(0)_i * (1-T_i)| X_i=x]\\
&= \dfrac{1}{e(X_i)} E[Y(1)_i|X_i=x] * E[T_i|X_i=x] - \dfrac{1}{1-e(X_i)} E[Y(0)_i|X_i=x] * E[(1-T_i)| X_i=x]\\
&= E[Y(1)_i|X_i=x] - E[Y(0)_i|X_i=x]\\
&= \tau(x)_i
\end{align}
$

As always, I think this will become much more concrete with an example. Again, consider the investment emails we've sent trying to make people invest more. The outcome variable the binary (invested vs didn't invest) `converted`.

```python
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
from nb21 import cumulative_gain, elast
```

```python
email = pd.read_csv("./data/invest_email_rnd.csv")
email.head()
```

      age
      income
      insurance
      invested
      em1
      em2
      em3
      converted

      0
      44.1
      5483.80
      6155.29
      14294.81
      0
      1
      1
      0

      1
      39.8
      2737.92
      50069.40
      7468.15
      1
      0
      0
      0

      2
      49.0
      2712.51
      5707.08
      5095.65
      1
      0
      1
      1

      3
      39.7
      2326.37
      15657.97
      6345.20
      1
      1
      1
      0

      4
      35.3
      2787.26
      27074.44
      14114.86
      1
      1
      1
      0

Our goal here is one of personalization. Let's focus on email-1. We wish to send it only to those customers who will respond better to it. In other words, we wish to estimate the conditional average treatment effect of email-1

$
E[Converted(1)_i - Converted(0)_i|X_i=x] = \tau(x)_i
$

so that we can target those customers who will have the best response to the email (higher CATE)

But first, let's break our dataset into a training and a validation set. We will estimate \\(\tau(x)_i\\) on one set and evaluate the estimates on the other.

```python
from sklearn.model_selection import train_test_split

np.random.seed(123)
train, test = train_test_split(email, test_size=0.4)
print(train.shape, test.shape)
```

    (9000, 8) (6000, 8)

Now, we will apply the target transformation we've just learned. Since the emails were randomly assigned (although not on a 50% 50% basis), we don't need to worry about the propensity score. Rather, it is constant and equal to the treatment probability.

```python
y = "converted"
T = "em1"
X = ["age", "income", "insurance", "invested"]

ps = train[T].mean()

y_star_train = train[y] * (train[T] - ps)/(ps*(1-ps))
```

With the transformed target, we can pick any ML regression algorithm to predict it. Lets use boosted trees here.

```python
from lightgbm import LGBMRegressor

np.random.seed(123)
cate_learner = LGBMRegressor(max_depth=3, min_child_samples=300, num_leaves=5)
cate_learner.fit(train[X], y_star_train);
```

This model can now estimate \\(\tau(x)_i\\). In other words, what it outputs is \\(\hat{\tau}(x)_i\\). For example, if we make predictions on the test set, we will see that some units have higher CATE than others. For example, customer 6958 has a CATE of 0.1, meaning the probability he or she will buy our investment product is predicted to increase by 0.1 if we send the email to this customer. In contrast, for customer 3903, the probability of buying the product is predicted to increase just 0.04.

```python
test_pred = test.assign(cate=cate_learner.predict(test[X]))
test_pred.head()
```

      age
      income
      insurance
      invested
      em1
      em2
      em3
      converted
      cate

      6958
      40.9
      4486.14
      37320.33
      12559.25
      0
      0
      1
      0
      0.105665

      7534
      42.6
      6386.19
      13270.47
      29114.42
      0
      0
      1
      0
      0.121922

      2975
      47.6
      1900.26
      25588.72
      2420.39
      0
      0
      1
      0
      0.034161

      3903
      41.0
      5802.19
      57087.37
      20182.20
      1
      0
      1
      1
      0.046805

      8437
      49.1
      2202.96
      5050.81
      9245.88
      1
      0
      1
      0
      -0.009099

To evaluate how good this model is, we can show the cumulative gain curves, for both training and testing sets.

```python
gain_curve_test = cumulative_gain(test_pred, "cate", y="converted", t="em1")
gain_curve_train = cumulative_gain(train.assign(cate=cate_learner.predict(train[X])), "cate", y="converted", t="em1")
plt.plot(gain_curve_test, color="C0", label="Test")
plt.plot(gain_curve_train, color="C1", label="Train")
plt.plot([0, 100], [0, elast(test, "converted", "em1")], linestyle="--", color="black", label="Baseline")
plt.legend();
```

As we can see, this plug and play estimator is better than random on the test set. Still, it looks like it is overfitting a lot, since the performance on the training set is much better than that of the test set.

That is actually one of the biggest downsides of this target transformation technique. With this target transformation, you do get a lot of simplicity, since you can just transform the target and use any ML estimator to predict heterogeneous treatment effects. The cost of it is that you get a lot of variance. That's because the transformed target is a very noisy estimate of the individual treatment effect and that variance gets transferred to your estimation. This is a huge problem if you don't have a lot of data, but it should be less of a problem in big data applications, where you are dealing with more than 1MM samples.

## The Continuous Treatment Case

Another obvious downside of the target transformation method is that it only works for discrete or binary treatments. This is something you see a lot in the causal inference literature. Most of the research is done for the binary treatment case, but you don't find a lot about continuous treatments. That bothered me a lot, because in the industry, continuous treatments are everywhere, mostly in the form of prices you need to optimize. So, even though I couldn't find anything regarding target transformations for continuous treatment, I came up with something that works in practice. Just keep in mind that I don't have a super solid econometric research around it.

To motivate it, let's go back to the ice cream sales example. There, we were tasked with the problem of estimating demand elasticity to price so that we can better set the ice cream prices to optimize our revenues. Recall that the event sample in the dataset is a day and we wish to know when people are less sensitive to price increases. Also, recall that prices are randomly assigned in this dataset, which means we don't need to worry about confounding bias.

```python
prices_rnd = pd.read_csv("./data/ice_cream_sales_rnd.csv")
prices_rnd.head()
```

      temp
      weekday
      cost
      price
      sales

      0
      25.8
      1
      0.3
      7
      230

      1
      22.7
      3
      0.5
      4
      190

      2
      33.7
      7
      1.0
      5
      237

      3
      23.0
      4
      0.5
      5
      193

      4
      24.4
      1
      1.0
      3
      252

As before, let's start by separating our data into training and a testing set.

```python
np.random.seed(123)
train, test = train_test_split(prices_rnd, test_size=0.3)
train.shape, test.shape
```

    ((3500, 5), (1500, 5))

Now is where we need a little bit of creativity. For the discrete case, the conditional average treatment effect is given by how much the outcome changes when we go from untreated to treated, conditioned on unit characteristics \\(X\\).

$
\tau(x) = E[Y_i(1) − Y_i(0)|X] = E[\tau_i|X]
$

In plain english, this is estimating the impact of the treatment on different unit profiles, where profiles are defined using the features \\(X\\). For the continuous case, we don't have that on-off switch. Units are not treated or untreated. Rather, they are all treated, but with different intensities. Therefore, we can't talk about the effect of giving the treatment. Rather, we need to speak in terms of increasing the treatment. In other words, we wish to know how the outcome would change if we increase the treatment by some amount. This is like estimating the partial derivative of the outcome function \\(Y\\) on the treatment \\(t\\). And because we wish to know that for each group (the CATE, not the ATE), we condition on the features \\(X\\)

$
\tau(x) = E[\partial Y_i(t)|X] = E[\tau_i|X]
$

How can we estimate that? First, let's consider the easy case, where the outcome is linear on the treatment. Suppose you have two types of days: hot days (yellow) and cold days (blue). On cold days people are more sensitive to price increases. Also, as price increases, demand falls linearly.

In this case, the CATE will be the slope of each demand line. These slopes will tell us how much demand will fall if we increase price by any amount. If this relationship is indeed linear, we can estimate those elasticities with the coefficient of a simple linear regression estimate on hot days and on cold days separately.

$$
\hat{\tau(x)} = Cov(Y_i, T_i)/Var(T_i) = \dfrac{\sum(Y_i- \bar{Y})(T_i - \bar{T})}{\sum (T_i - \bar{T})^2}
$$

We can be inspired by this estimator and think about what it would be like for an individual unit. In other words, what if we have that same thing up there, defined for each day. In my head, it would be something like this:

$
Y^*_i = (Y_i- \bar{Y})\dfrac{(T_i - \bar{T})}{\sigma^2_T}
$

In plain English, we would transform the original target by subtracting the mean from it, then we would multiply it by the treatment, from which we've also subtracted the mean from. Finally, we would divide it by the treatment variance. Alas, we have a target transformation for the continuous case.

The question now is: does it work? As a matter of fact it does and we can go over a similar proof for why it works, just like we did in the binary case. First, lets call

$
V_i = \dfrac{(T_i - \bar{T})}{\sigma^2_T}
$

notice that \\(E[V_i|X_i=x]=0\\) because under random assignment \\(E[T_i|X_i=x]=\bar{T}\\). In other words, for every region of X, \\(E[T_i]=\bar{T}\\). Also \\(E[T_i V_i | X_i=x]=1\\) because \\(E[T_i(T_i - \bar{T})|X_i=x] = E[(T_i - \bar{T})^2|X_i=x]\\), which is the treatment variance. Finally, under conditional independence (which we get for free in the random treatment assignment case), \\(E[T_i e_i | X_i=x] = E[T_i | X_i=x] E[e_i | X_i=x]\\).

To show that this target transformation works, we need to remember that we are estimating the parameter for a local linear model

$
Y_i = \alpha + \beta T_i + e_i | X_i=x
$

In our example, those would be the linear models for the hot and cold days. Here, we are interested in the \\(\beta\\) parameter, which is our conditional elasticity or CATE. With all that, we can prove that

$
\begin{align}
E[Y^*_i|X_i=X] &= E[(Y_i-\bar{Y})V_i | X_i=x] \\
&= E[(\alpha + \beta T_i + e_i - \bar{Y})V_i | X_i=x] \\
&= \alpha E[V_i | X_i=x] + \beta E[T_i V_i | X_i=x] + E[e_i V_i | X_i=x] \\
&= \beta + E[e_i V_i | X_i=x] \\
&= \beta = \tau(x)
\end{align}
$

Bare in mind that this only works when the treatment is randomized. For non randomized treatment, we have to replace \\(\bar{T}\\) by \\(M(X_i)\\), where \\(M\\) is a model that estimates \\(E[T_i|X_i=x]\\).

$
Y^*_i = (Y_i- \bar{Y})\dfrac{(T_i - M(T_i))}{(T_i - M(T_i))^2}
$

This will make sure that the term \\(\alpha E[V_i | X_i=x]\\) in the third line vanishes to zero and that the term \\(E[T_i V_i | X_i=x]\\) goes to 1. Notice that you don't actually need \\(E[T_i V_i | X_i=x]\\) to go to 1 if you just want to order units in terms of treatment effect. In other words, if you just want to know in which days demand is more sensitive to price increases but you don't need to know by how much, it doesn't matter if the \\(\beta\\) estimates are scaled up or down. If that is the case, you can omit the denominator.

$
Y^*_i = (Y_i- \bar{Y})(T_i - M(T_i))
$

If all that math seems tiresome, don't worry. The code is actually very simple. Once again, we transform our training target with the formulas seen above. Here, we have random treatment assignments, so we don't need to build a model that predicts prices. I'm also omitting the denominator, because here I only care about ordering the treatment effect.

```python
y_star_cont = (train["price"] - train["price"].mean()
               *train["sales"] - train["sales"].mean())
```

Then, just like before, we fit a regression ML model to predict that target.

```python
cate_learner = LGBMRegressor(max_depth=3, min_child_samples=300, num_leaves=5)

np.random.seed(123)
cate_learner.fit(train[["temp", "weekday", "cost"]], y_star_cont)

cate_test_transf_y = cate_learner.predict(test[["temp", "weekday", "cost"]])

test_pred = test.assign(cate=cate_test_transf_y)
test_pred.sample(5)
```

      temp
      weekday
      cost
      price
      sales
      cate

      2815
      15.7
      4
      1.5
      3
      187
      -1395.956278

      257
      29.4
      3
      1.0
      3
      209
      -1607.400415

      2585
      24.6
      6
      1.0
      10
      197
      -1497.197402

      3260
      20.2
      1
      0.5
      4
      246
      -1629.798111

      1999
      10.0
      4
      0.5
      10
      139
      -1333.690544

This time, the CATE's interpretation is non intuitive. Since we've removed the denominator from the target transformation, this CATE we are seeing is scaled by \\(Var(X)\\). However, this prediction should still order the treatment effect pretty well. To see that, we can use the cumulative gain curve, just like we did before.

```python
gain_curve_test = cumulative_gain(test.assign(cate=cate_test_transf_y),
                                "cate", y="sales", t="price")

gain_curve_train = cumulative_gain(train.assign(cate=cate_learner.predict(train[["temp", "weekday", "cost"]])),
                                   "cate", y="sales", t="price")

plt.plot(gain_curve_test, label="Test")
plt.plot(gain_curve_train, label="Train")
plt.plot([0, 100], [0, elast(test, "sales", "price")], linestyle="--", color="black", label="Taseline")
plt.legend();
```

For this data, it looks like the model with transformed target is way better than random. Not only that, train and test results are pretty close, so variance is not an issue here. But this is just a characteristic of this dataset. If you recall, this was not the case when we explored the binary treatment case. There, the model performed not so great.

### Non Linear Treatment Effects

Having talked about the continuous case, there is still an elephant in the room we need to adress. We've assumed a linearity on the treatment effect. However, that is very rarely a reasonable assumption. Usually, treatment effects saturate in one form or another. In our example, it's reasonable to think that demand will go down faster at the first units of price increase, but then it will fall slowlier.

The problem here is that **elasticity or treatment effect changes with the treatment itself**. In our example, the treatment effect is more intense at the beginning of the curve and smaller as prices get higher. Again,suppose you have two types of days: hot days (yellow) and cold days (blue) and we want to distinguish between the two with a causal model. The thing is that causal models should predict elasticity, but in the nonlinear case, the elasticity for hot and cold days could be the same, if we look at different price points in the curve (right image).

There is no easy way out of this problem and I confess I'm still investigating what works best. For now, the thing that I do is try to think about the functional form of the treatment effect and somehow linearize it. For example, demand usually has the following functional form, where higher \\(\alpha\\)s means that demand falls faster with each price increase

$
D_i = \dfrac{1}{P_i^{\alpha}}
$

So, if I apply the log transformation to both the demand \\(Y\\) and prices \\(T\\), I should get something that is linear.

$
\begin{align}
log(D)_i &= log\bigg(\dfrac{1}{P_i^{\alpha}}\bigg) \\
&= log(1) - log(P_i^{\alpha}) \\
&= log(1) - log(P_i^{\alpha}) \\
&= - \alpha * log(P_i) \\
\end{align}
$

Linearization is not so easy to do, as it involves some thinking. But you can also try stuff out and see what works best. Often, things like logs and square roots help.

## Key Ideas

We are now moving in the direction of estimating conditional average treatment effects using machine learning models. The biggest challenge when doing so is adapting a predictive model to one that estimates causal effects. Another way of thinking about it is that predictive models focus on estimating the outcome Y as a function of features X and possibly treatment T \\(Y = M(X, T) \\) while causal models need to estimate the partial derivative of this output function on the treatment \\( \partial Y = \partial M(X, T) \\). This is far from trivial, because while we do observe the outcome Y, we can't observe \\(\partial Y\\), at least not on an individual level. As a consequence, we need to be creative when designing an objective function for our models.

Here, we saw a very simple technique of target transformation. The idea is to combine the original target Y with the treatment T to form a transformed target which is, in expectation, equal to the CATE. With that new target, we can plug any predictive ML model to estimate it and then the model predictions will be CATE estimates. As a side note, besides target transformation, this method also goes by the name of **F-Learner**.

With all that simplicity, there is also a price to pay. The transformed target is a very noisy estimate of the individual treatment effect and that noise will be transferred to the model estimates in the form of variance. This makes target transformation better suited for big data applications, where variance is less of a problem due to sheer sample sizes. Another downside of the target transformation method is that it is only defined for binary or categorical treatments. We did our best to come up with a continuous version of the approach and even ended up with something that seemed to work, but up until now, there is no solid theoretical framework to back it up.

Finally, we've ended with a discussion on non linear treatment effects and the challenges that come with it. Namely, when the treatment effect changes with the treatment itself, we might mistakenly think units have the same treatment response curve because they have the same responsiveness to the treatment, but actually they are just receiving different treatment amounts.

## References

The things I've written here are mostly stuff from my head. I've learned them through experience. This means that they have **not** passed the academic scrutiny that good science often goes through. Instead, notice how I'm talking about things that work in practice, but I don't spend too much time explaining why that is the case. It's a sort of science from the streets, if you will. However, I am putting this up for public scrutiny, so, by all means, if you find something preposterous, open an issue and I'll address it to the best of my efforts.

Most of this chapter draws from Susan Atheys' and Guido W. Imbens' paper, *Machine Learning Methods for Estimating Heterogeneous Causal Effects*. Some material about target transformation can also be found on Pierre Gutierrez' and Jean-Yves G´erardy's paper, *Causal Inference and Uplift Modeling: A review of the literature*. Note that these papers only cover the binary treatment case. Another review of causal models for CATE estimation that references the F-Learner is *Meta-learners for Estimating Heterogeneous Treatment Effects using Machine Learning*, by K¨unzel et al, 2019.

## Contribute

Causal Inference for the Brave and True is an open-source material on causal inference, the statistics of science. It uses only free software, based in Python. Its goal is to be accessible monetarily and intellectually.
If you found this book valuable and you want to support it, please go to [Patreon](https://www.patreon.com/causal_inference_for_the_brave_and_true). If you are not ready to contribute financially, you can also help by fixing typos, suggesting edits or giving feedback on passages you didn't understand. Just go to the book's repository and [open an issue](https://github.com/matheusfacure/python-causality-handbook/issues). Finally, if you liked this content, please share it with others who might find it useful and give it a [star on GitHub](https://github.com/matheusfacure/python-causality-handbook/stargazers).

```python

```


# Tarea Nro. 3 - LinAlg + Sympy

- Nombre y apellido: Ivo Andrés Astudillo
- Fecha:             26 de Noviembre de 2020

### Producto punto

```python
#from IPython.display import Image
#Image(filename='img/Tabla9.4.png')
```

5. La capacidad calorífica Cp de un gas se puede modelar con la ecuación empírica

\begin{equation}
C_p = a + bT + cT^2+dT^3
\end{equation}

donde a, b, c y d son constantes empíricas y T es la temperatura en grados Kelvin. El cambio en entalpía (una medida de energía) conforme el gas se caliente de T 2  es la integral de esta ecuación con respecto a T:

\begin{equation}
\bigtriangleup h = \int_{T_{1}}^{T_{2}} C_{p}dT \,dx
\end{equation}


## Small disks and geometric facility location: *a mixed integer conic quadratic tutorial*
In this tutorial we are going to:
* show examples of compact models constructed with *Fusion*,
* write a conic model with binary variables,
* apply the *Big-M* formulation,
* construct a base model and use its various extensions to solve a number of different problems.

### Introduction

We are going to work with variations of the following geometric setup. Suppose we have $n$ of points $p_1,\ldots,p_n\in\mathbb{R}^d$ and we want to find a configuration of $k$ balls with centers $x_1,\ldots,x_k\in\mathbb{R}^d$ and radii $r_1,\ldots,r_k$, respectively, which covers some/all of the points $p_i$ and satisfies additional constraints depending on the problem. For visualization purposes we will focus on the plane and disks, that is the case $d=2$, but the code and mathematical formulation remain valid in all dimensions.

For example, the next snippet displays a set of points in $\mathbb{R}^2$. Here is a sample question: how many of those points can we cover with three disks of radius at most $0.1$ each?

```python
%matplotlib inline
from mosek.fusion import *
import numpy as np

def data(n):
    np.random.seed(1236)
    return [[x,y] for x,y in np.broadcast(np.random.uniform(0.0, 1.0, n), np.random.uniform(0.0, 0.3, n))]
def gaussianData(n):
    np.random.seed(1236)
    return [[x,y] for x,y in np.broadcast(np.random.normal(0.0, 1.0, n), np.random.normal(0.0, 1.0, n))]

def display(P, R, X):
	import matplotlib.pyplot as plt
	plt.scatter(*zip(*P))
	plt.axis("equal")
	for i in range(len(R)):
		plt.gcf().gca().add_artist( plt.Circle((X[2*i],X[2*i+1]), R[i], fc="r", color="r", alpha=0.5) )
		plt.gcf().gca().plot(X[2*i],X[2*i+1], "or")
	plt.show()

display(data(20), [], [])
```

### A base model formulation in *Fusion*

Our input is a collection of points $p_1\ldots,p_n\in\mathbb{R}^d$ and an integer $k\geq 1$. What we want to compute is the centers $x_1,\ldots,x_k\in\mathbb{R}^d$ and the radii $r_1,\ldots,r_k\geq 0$ of $k$ balls $B_d(x_j;r_j)$, subject to various constraints on how the points and balls interact. The basic notion we need to model is that of which points are covered by which balls. To this end we introduce standard binary variables

\begin{equation}
s_{ij} = \left\lbrace
   \begin{array}{cl}
   1 & \text{if the ball } B_d(x_j;r_j) \text{ covers the point } p_i, \\
   0 & \text{otherwise}.
   \end{array}\right.
\end{equation}

For this to work we must ensure the implication

\begin{equation}
s_{ij}=1 \implies r_j\geq \lVert p_i-x_j \rVert_2.
\end{equation}

This can be achieved by introducing a constraint

\begin{equation}
r_j\geq \lVert p_i-x_j \rVert_2 - M(1-s_{ij})
\end{equation}

where $M$, also known as *Big-M*, is large enough to guarantee that when $s_{ij}=0$ the corresponding constraint is trivially satisfied, for example the right-hand side is surely negative. The spread of the data points $p_i$ determines a safe value of $M$ for us:

```python
def bigM(P):
    return 2*np.shape(P)[1]*max(np.amax(P,0)-np.amin(P,0))
```

Finally we get a family of conic constraints

\begin{equation}
(r_j + M(1-s_{ij}),\  p_i-x_j) \in \mathcal{Q}^{d+1}, \quad i=1,\ldots,n,\ j=1,\ldots,k
\end{equation}

and its direct implementation in *Fusion*:

```python
def basicModel(k, P, bigM):
    n, dim = len(P), len(P[0])                              #in P each row is one p_i
    M = Model("balls")
    R = M.variable(k, Domain.greaterThan(0.0))              #r_1...r_k
    X = M.variable([k,dim], Domain.unbounded())             #each row is one x_j
    S = M.variable([n,k], Domain.binary())

    RRep = Var.repeat(R, n)
    Penalty = Expr.flatten(Expr.mul(bigM, Expr.sub(1.0, S)))    #M*(1-S)
    CoordDiff = Expr.sub(np.repeat(P,k,0), Var.repeat(X, n))    #each p_i meet each x_j once in Expr.sub

    M.constraint(Expr.hstack(Expr.add(RRep,Penalty), CoordDiff), Domain.inQCone())

    return M, R, X, S
```

We can now take this as a base and solve a number of different optimization problems.

### Problem 1. Minimal enclosing ball

**Objective: find the smallest ball containing all of the points.**

This is a classical problem, see the [bounding sphere](https://en.wikipedia.org/wiki/Bounding_sphere) and [smallest-circle problem](https://en.wikipedia.org/wiki/Smallest-circle_problem) Wikipedia articles. There is an algorithm with running time $O(n)$ in any fixed dimension, and also MOSEK can solve it efficiently for a large number of points.

Since we want to include all points, the Big-M constant and the $s_{ij}$ indicators are superfluous. All we want to do is to set $k=1$ and minimize the radius of the single ball in the model.

```python
def minimalEnclosing(P):
    M, R, X, S = basicModel(1, P, 0.0)
    M.constraint(S, Domain.equalsTo(1.0))
    M.objective(ObjectiveSense.Minimize, R.index(0))
    M.writeTask('p1.task.gz')
    M.solve()
    return R.level(), X.level()

%time P=gaussianData(1000); r, x = minimalEnclosing(P); display(P, r, x)
```

### Problem 2. Maximum coverage

**Objective: find the largest subset which can be covered by a ball of fixed radius.**

This is also a well-known and multi-facet problem, see [this paper](http://www.cs.princeton.edu/~chazelle/pubs/CirclePlacement.pdf) by Chazelle and Evanston. It can be interpreted as selecting a location for a single fixed-power transmitter so that the signal reaches as many locations as possible. In the combinatorial setting it is equivalent to finding maximal cliques in unit-disk graphs. The best known algorithms for this problem achieve running time $O(n^2\log{n})$.

In this setup we still operate with $k=1$ and we bound the radius from above. Each $s_i=s_{i1}$ determines if the $i$-th point is covered, with the optimization objective equal to $\sum s_{i}$.

```python
def maxCoverage(P, rMax):
    M, R, X, S = basicModel(1, P, bigM(P))
    M.constraint(R, Domain.lessThan(rMax))
    M.objective(ObjectiveSense.Maximize, Expr.sum(S))
    M.writeTask('p2.task.gz')
    M.solve()
    return R.level(), X.level()

%time P=data(100); r, x = maxCoverage(P, 0.25); display(P, r, x)
```

### Problem 3. Smallest diameter k-circle cover

**Objective: find $k$ circles which together cover all the points and have the smallest sum of diameters.**

This is a different geometric facility location problem, where we want to serve the whole population with $k$ transmitters while minimizing their total range. See the paper [Minimum-Cost Coverage of Point Sets by Disks](http://jeffe.cs.illinois.edu/pubs/pdf/carrots.pdf) for more details. It can also be seen as a form of clustering witk $k$ clusters.

The objective here is of course to minimize $\sum r_j$. A point $p_i$ is covered if $\max_j s_{ij}=1$, a condition we can linearize as:

$$\sum_j s_{ij}\geq 1,\ \forall i=1,\ldots,n.$$

For $k=1$ we recover as a special case the minimal enclosing ball problem. It is quite frequent that certain locations are served by their own transmitter with range $r_j\approx 0$.

```python
def minDiamKCircleCover(P, k):
    M, R, X, S = basicModel(k, P, bigM(P))
    M.constraint(Expr.sum(S,1), Domain.greaterThan(1.0))       #sum inside each row, i.e. along dimension 1
    M.objective(ObjectiveSense.Minimize, Expr.sum(R))
    M.writeTask('p3.task.gz')
    M.solve()
    return R.level(), X.level()

%time P=data(20); r, x = minDiamKCircleCover(P, 3); display(P, r, x)
```

### Problem 4. Smallest area k-circle cover

**Objective: find $k$ circles which together cover all the points and have the smallest sum of areas.**

Here we have another version of the previous problem where the cost of a network is measured (roughly, up to intersections of balls) by the total area it covers, so the $\ell_2$ norm replaces the $\ell_1$ norm of the radii vector. As pointed out in [here](http://jeffe.cs.illinois.edu/pubs/pdf/carrots.pdf) the most realistic models of wireless transmission actually optimize for the $\ell_p$ norm with some $p>2$.

This problem is similar to the previous one, but with a different objective: we minimize $\sqrt{\sum_j r_j^2}$. This is expressed using a conic constraint. Optimizing the $\ell_2$ norm results in a more balanced collection of balls, as we can see below for the same dataset as in Problem 3.

```python
def minAreaKCircleCover(P, k):
    M, R, X, S = basicModel(k, P, bigM(P))
    t = M.variable(1, Domain.greaterThan(0.0))
    M.constraint(Expr.vstack(t, R), Domain.inQCone())        #t>=sqrt(sum r_i^2)
    M.constraint(Expr.sum(S,1), Domain.greaterThan(1.0))
    M.objective(ObjectiveSense.Minimize, t)
    M.writeTask('p4.task.gz')
    M.solve()
    return R.level(), X.level()

%time P=data(20); r, x = minAreaKCircleCover(P, 3); display(P, r, x)
```

### Problem 5. Maximum k-coverage

**Objective: find the largest subset which can be covered by $k$ balls of a fixed radius.**

We can combine the previous techniques to express more complicated problems, like covering the largest possible number of points with $k$ fixed-power transmitters. Here we want to maximize $\sum_i\max_j{s_{ij}}$, which is precisely the number of covered points, and we want to have an upper bound on $r_j$. The objective is equivalent to:

$$\max\sum_i t_i \quad \textrm{subject to}\quad t_i\in\{0,1\},\ \ t_i \leq \sum_js_{ij}.$$

```python
def maxKCoverage(P, k, rMax):
    M, R, X, S = basicModel(k, P, bigM(P))
    M.constraint(R, Domain.equalsTo(rMax))
    t = M.variable("t", len(P), Domain.binary())
    M.constraint(Expr.sub(t, Expr.sum(S,1)), Domain.lessThan(0.0))     #contains one inequality for each column
    M.objective(ObjectiveSense.Maximize, Expr.sum(t))
    M.setSolverParam('mioConicOuterApproximation', 'on')
    M.writeTask('p5.task.gz')
    M.solve()
    return R.level(), X.level()

%time P=data(20); r, x = maxKCoverage(P, 3, 0.1); display(P, r, x)
```

### Comments

For simplicity of the presentation we ignored various issues and hints and we leave them as an exercise:
* Checking that a solution has actually been found.
* More generally: handling exceptions.
* Symmetry breaking: in problems 3 and 4 we can achieve about $k$-fold speedup by setting $s_{11}=1$, i.e. declaring that the first point is covered by circle number $1$.
* For large instances one should perhaps terminate the solver when a solution of sufficiently high quality has been located.

This work is licensed under a Creative Commons Attribution 4.0 International License. The **MOSEK** logo and name are trademarks of Mosek ApS. The code is provided as-is. Compatibility with future release of **MOSEK** or the `Fusion API` are not guaranteed. For more information contact our [support](mailto:support@mosek.com).

```python

```


# Homework 5
## Due Date:  Tuesday, October 3rd at 11:59 PM

# Problem 1
We discussed documentation and testing in lecture and also briefly touched on code coverage.  You must write tests for your code for your final project (and in life).  There is a nice way to automate the testing process called continuous integration (CI).

This problem will walk you through the basics of CI and show you how to get up and running with some CI software.

### Continuous Integration
The idea behind continuous integration is to automate away the testing of your code.

We will be using it for our projects.

The basic workflow goes something like this:

1. You work on your part of the code in your own branch or fork
2. On every commit you make and push to GitHub, your code is automatically tested on a fresh machine on Travis CI. This ensures that there are no specific dependencies on the structure of your machine that your code needs to run and also ensures that your changes are sane
3. Now you submit a pull request to `master` in the main repo (the one you're hoping to contribute to). The repo manager creates a branch off `master`.
4. This branch is also set to run tests on Travis. If all tests pass, then the pull request is accepted and your code becomes part of master.

We use GitHub to integrate our roots library with Travis CI and Coveralls. Note that this is not the only workflow people use. Google git..github..workflow and feel free to choose another one for your group.

### Part 1:  Create a repo
Create a public GitHub repo called `cs207test` and clone it to your local machine.

**Note:** No need to do this in Jupyter.

### Part 2:  Create a roots library
Use the example from lecture 7 to create a file called `roots.py`, which contains the `quad_roots` and `linear_roots` functions (along with their documentation).

Also create a file called `test_roots.py`, which contains the tests from lecture.

All of these files should be in your newly created `cs207test` repo.  **Don't push yet!!!**

```python
%%file cs207test/roots.py
def linear_roots(a=1.0, b=0.0):
    """Returns the roots of a linear equation: ax+ b = 0.

    INPUTS
    =======
    a: float, optional, default value is 1
       Coefficient of linear term
    b: float, optional, default value is 0
       Coefficient of constant term

    RETURNS
    ========
    roots: 1-tuple of real floats
       Has the form (root) unless a = 0
       in which case a ValueError exception is raised

    EXAMPLES
    =========
    >>> linear_roots(1.0, 2.0)
    -2.0
    """
    if a == 0:
        raise ValueError("The linear coefficient is zero.  This is not a linear equation.")
    else:
        return ((-b / a))

def quad_roots(a=1.0, b=2.0, c=0.0):
    """Returns the roots of a quadratic equation: ax^2 + bx + c = 0.

    INPUTS
    =======
    a: float, optional, default value is 1
       Coefficient of quadratic term
    b: float, optional, default value is 2
       Coefficient of linear term
    c: float, optional, default value is 0
       Constant term

    RETURNS
    ========
    roots: 2-tuple of complex floats
       Has the form (root1, root2) unless a = 0
       in which case a ValueError exception is raised

    EXAMPLES
    =========
    >>> quad_roots(1.0, 1.0, -12.0)
    ((3+0j), (-4+0j))
    """
    import cmath # Can return complex numbers from square roots
    if a == 0:
        raise ValueError("The quadratic coefficient is zero.  This is not a quadratic equation.")
    else:
        sqrtdisc = cmath.sqrt(b * b - 4.0 * a * c)
        r1 = -b + sqrtdisc
        r2 = -b - sqrtdisc
        return (r1 / 2.0 / a, r2 / 2.0 / a)
```

```python
%%file cs207test/test_roots.py
import roots

def test_quadroots_result():
    assert roots.quad_roots(1.0, 1.0, -12.0) == ((3+0j), (-4+0j))

def test_quadroots_types():
    try:
        roots.quad_roots("", "green", "hi")
    except TypeError as err:
        assert(type(err) == TypeError)

def test_quadroots_zerocoeff():
    try:
        roots.quad_roots(a=0.0)
    except ValueError as err:
        assert(type(err) == ValueError)

def test_linearoots_result():
    assert roots.linear_roots(2.0, -3.0) == 1.5

def test_linearroots_types():
    try:
        roots.linear_roots("ocean", 6.0)
    except TypeError as err:
        assert(type(err) == TypeError)

def test_linearroots_zerocoeff():
    try:
        roots.linear_roots(a=0.0)
    except ValueError as err:
        assert(type(err) == ValueError)
```

### Part 3:  Create an account on Travis CI and Start Building

#### Part A:
Create an account on Travis CI and set your `cs207test` repo up for continuous integration once this repo can be seen on Travis.

#### Part B:
Create an instruction to Travis to make sure that

1. python is installed
2. its python 3.5
3. pytest is installed

The file should be called `.travis.yml` and should have the contents:
```yml
language: python
python:
    - "3.5"
before_install:
    - pip install pytest pytest-cov
script:
    - pytest
```

You should also create a configuration file called `setup.cfg`:
```cfg
[tool:pytest]
addopts = --doctest-modules --cov-report term-missing --cov roots
```

#### Part C:
Push the new changes to your `cs207test` repo.

At this point you should be able to see your build on Travis and if and how your tests pass.

```python
%%file cs207test/.travis.yml

language: python
python:
    - "3.5"
before_install:
    - pip install pytest pytest-cov
script:
    - pytest
```

```python
%%file cs207test/setup.cfg

[tool:pytest]
addopts = --doctest-modules --cov-report term-missing --cov roots

```

### Part 4:  Coveralls Integration
In class, we also discussed code coverage.  Just like Travis CI runs tests automatically for you, Coveralls automatically checks your code coverage.  One minor drawback of Coveralls is that it can only work with public GitHub accounts.  However, this isn't too big of a problem since your projects will be public.

#### Part A:
Create an account on [`Coveralls`](https://coveralls.zendesk.com/hc/en-us), connect your GitHub, and turn Coveralls integration on.

#### Part B:
Update your the `.travis.yml` file as follows:
```yml
language: python
python:
    - "3.5"
before_install:
    - pip install pytest pytest-cov
    - pip install coveralls
script:
    - py.test
after_success:
    - coveralls
```

Be sure to push the latest changes to your new repo.

```python
%%file cs207test/.travis.yml

language: python
python:
    - "3.5"
before_install:
    - pip install pytest pytest-cov
    - pip install coveralls
script:
    - py.test
after_success:
    - coveralls
```

### Part 5:  Update README.md in repo
You can have your GitHub repo reflect the build status on Travis CI and the code coverage status from Coveralls.  To do this, you should modify the `README.md` file in your repo to include some badges.  Put the following at the top of your `README.md` file:

```
[](https://travis-ci.org/dsondak/cs207testing.svg?branch=master)

[](https://coveralls.io/github/dsondak/cs207testing?branch=master)
```

Of course, you need to make sure that the links are to your repo and not mine.  You can find embed code on the Coveralls and Travis CI sites.

```python

```

---

# Problem 2
Write a Python module for reaction rate coefficients.  Your module should include functions for constant reaction rate coefficients, Arrhenius reaction rate coefficients, and modified Arrhenius reaction rate coefficients.  Here are their mathematical forms:
\begin{align}
  &k_{\textrm{const}}   = k \tag{constant} \\
  &k_{\textrm{arr}}     = A \exp\left(-\frac{E}{RT}\right) \tag{Arrhenius} \\
  &k_{\textrm{mod arr}} = A T^{b} \exp\left(-\frac{E}{RT}\right) \tag{Modified Arrhenius}
\end{align}

Test your functions with the following paramters:  $A = 10^7$, $b=0.5$, $E=10^3$.  Use $T=10^2$.

A few additional comments / suggestions:
* The Arrhenius prefactor $A$ is strictly positive
* The modified Arrhenius parameter $b$ must be real
* $R = 8.314$ is the ideal gas constant.  It should never be changed (except to convert units)
* The temperature $T$ must be positive (assuming a Kelvin scale)
* You may assume that units are consistent
* Document each function!
* You might want to check for overflows and underflows

**Recall:** A Python module is a `.py` file which is not part of the main execution script.  The module contains several functions which may be related to each other (like in this problem).  Your module will be importable via the execution script.  For example, suppose you have called your module `reaction_coeffs.py` and your execution script `kinetics.py`.  Inside of `kinetics.py` you will write something like:
```python
import reaction_coeffs
# Some code to do some things
# :
# :
# :
# Time to use a reaction rate coefficient:
reaction_coeffs.const() # Need appropriate arguments, etc
# Continue on...
# :
# :
# :
```
Be sure to include your module in the same directory as your execution script.

```python

```

```python
%%file reaction_coeffs.py

def const(k=1.0):
    """Returns the constant coefficient k.

    INPUTS
    =======
    a: float, optional, default value is 1.0
       Constant reaction coefficient

    RETURNS
    ========
    Constant k
       unless k is not an int or float
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> const(1.0)
    1.0
    """
    if type(k) != int and type(k) != float:
        raise TypeError("Input needs to be a number!")

    else:
        return k

def arr(A=10^7,T=10^2,E=10^3,R=8.314):
    """Returns the constant coefficient k.

    INPUTS
    =======
    A: float, optional, default value is 10^7
       Arhenieus constant, needs to be positive
    T: float, optional, default value is 10^2
        temperature, must be positive
    R: float, fixed value 8.314, should not change

    RETURNS
    ========
    Constant k
       unless any of A,T, and E are not numbers
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> arr(10^7,10^2,10^2)
    11.526748719357375
    """
    if A < 0:
        raise ValueError("The Arrhenius constant must be strictly positive")

    elif T < 0:
        raise ValueError("Temperature must be positive")

    elif R != 8.314:
        raise ValueError("Unless in a stargate, the universal gas constant is 8.314")

    elif type(A) != int and type(A) != float:
        raise TypeError("parameters need to be either type int or type float")

    elif type(T) !=int and type(T) != int:
        raise TypeError("parameters need to be either type int or type float")

    elif type(E) != int and type(E) != float:
        raise TypeError("parameters need to be either type int or type float")

    else:
        import numpy as np
        k=(A)*np.exp(-E/(R*T))
        return k

def mod_arr(A=10^7,b=0.5,T=10^2,E=10^3,R=8.314):
    """Returns the constant coefficient k.

    INPUTS
    =======
    A: float, optional, default value is 10^7
       Arhenieus constant, needs to be positive
    T: float, optional, default value is 10^2
        temperature, must be positive
    R: float, fixed value 8.314, should not change
    b: float, optional, default value is 0.5, must be a real number

    RETURNS
    ========
    Constant k
       unless any of A,T,b and E are not numbers
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> mod_arr()
    32.116059468138779
    """
    if A < 0:
        raise ValueError("The Arrhenius constant must be strictly positive")

    elif T < 0:
        raise ValueError("Temperature must be positive")

    elif isinstance(b,complex):
        raise ValueError("b must be a real number")
    elif R != 8.314:
        raise ValueError("Unless in a stargate, the universal gas constant is 8.314")

    elif type(A) != int and type(A) != float:
        raise TypeError("parameters need to be either type int or type float")

    elif type(T) !=int and type(T) != int:
        raise TypeError("parameters need to be either type int or type float")

    elif type(E) != int and type(E) != float:
        raise TypeError("parameters need to be either type int or type float")

    else:
        import numpy as np
        k=(A)*(T**b)*np.exp(-E/(R*T))
        return k
```

    Overwriting reaction_coeffs.py

```python
import reaction_coeffs

print(reaction_coeffs.const())

print(reaction_coeffs.arr())

print(reaction_coeffs.mod_arr())

```

    1.0
    11.3547417175
    32.1160594681

```python
!pytest
```

    ============================= test session starts ==============================
    platform darwin -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
    rootdir: /Users/filipmichalsky/cs207_filip_michalsky/homeworks/HW5, inifile:
    collected 13 items

    test_multi_progress_r.py ..
    test_multi_reaction_r.py ...
    test_single_progress_r.py ..
    cs207test/test_roots.py ......

    ========================== 13 passed in 0.24 seconds ===========================

---

# Problem 3
Write a function that returns the **progress rate** for a reaction of the following form:
\begin{align}
  \nu_{A} A + \nu_{B} B \longrightarrow \nu_{C} C.
\end{align}
Order your concentration vector so that
\begin{align}
  \mathbf{x} =
  \begin{bmatrix}
    \left[A\right] \\
    \left[B\right] \\
    \left[C\right]
  \end{bmatrix}
\end{align}

Test your function with
\begin{align}
  \nu_{i} =
  \begin{bmatrix}
    2.0 \\
    1.0 \\
    0.0
  \end{bmatrix}
  \qquad
  \mathbf{x} =
  \begin{bmatrix}
    1.0 \\
    2.0 \\
    3.0
  \end{bmatrix}
  \qquad
  k = 10.
\end{align}

You must document your function and write some tests in addition to the one suggested.  You choose the additional tests, but you must have at least one doctest in addition to a suite of unit tests.

```python
import numpy as np
```

```python
%%file single_progress_r.py

def progress_rate(v_i,x,k):
    """Returns the progress rate of a reaction

    INPUTS
    =======
    v_i: vector of stochiometric coefficients
    x: vector of concentrations (first dimension needs to match v_i),second dimension is 1
    k: float, fixed value 8.314, should not change

    RETURNS
    ========
    Progress rate of the reaction
       k*[A]^v_i[a]*[B]*v_i[b]*[C]^v_i[c]
       unless first dimension of x does not match first dimension of v_i
       in which case a ValueError exception is raised

    EXAMPLES
    =========
    >>> import numpy as np
    >>> progress_rate(np.array([2,1,0]),np.array([1,2,3]),10)
    20
    """
    #v_i=np.array([2,1,0])
    #x=np.array([1,2,3])
    #k=10
    import numpy as np
    #check that the first dimension matches
    if x.shape[0] != v_i.shape[0]:
        raise ValueError("First dimension of x and v_i need to match")

    else:
        product_X_to_vi=1
        #loop through the rows to multiply each x by its stochiometric coeffs
        for i,j in zip(v_i,x):
            product_X_to_vi=product_X_to_vi*(j**i)

        progress_rate=k*product_X_to_vi

        return progress_rate

if __name__ == "__main__":
    import doctest
    doctest.testmod()
```

    Overwriting single_progress_r.py

```python
import single_progress_r

import numpy as np

single_progress_r.progress_rate(np.array([2,1,0]),np.array([1,2,3]),10)
```

    20

```python
import single_progress_r

import numpy as np

single_progress_r.progress_rate(np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
```

```python
%%file test_single_progress_r.py

import single_progress_r
import numpy as np

#!pytest --doctest-modules

def test_progress_rate_result():
    assert single_progress_r.progress_rate(np.array([2,1,0]),np.array([1,2,3]),10) == 20

def test_progress_rate_shapes():
    try:
        single_progress_r.progress_rate(np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
    except ValueError as err:
        assert(type(err) == ValueError)

#def test_arr_result():
#       assert reaction_coeffs.arr(A=10^7,E=10^3,T=10^2) == XX
```

    Overwriting test_single_progress_r.py

```bash
%%bash
python -m pytest -v test_single_progress_r.py
```

    ============================= test session starts ==============================
    platform darwin -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /Users/filipmichalsky/anaconda/bin/python
    cachedir: .cache
    rootdir: /Users/filipmichalsky/cs207_filip_michalsky/homeworks/HW5, inifile:
    collecting ... collected 2 items

    test_single_progress_r.py::test_progress_rate_result PASSED
    test_single_progress_r.py::test_progress_rate_shapes PASSED

    =========================== 2 passed in 0.19 seconds ===========================

```bash
%%bash
python multi_progress_r.py -v
#doctest of created file
```

    Trying:
        import numpy as np
    Expecting nothing
    ok
    Trying:
        progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[0]
    Expecting:
        40.0
    ok
    Trying:
        progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[1]
    Expecting:
        10.0
    ok
    1 items had no tests:
        __main__
    1 items passed all tests:
       3 tests in __main__.progress_rate_multi
    3 tests in 2 items.
    3 passed and 0 failed.
    Test passed.

---
# Problem 4
Write a function that returns the **progress rate** for a system of reactions of the following form:
\begin{align}
  \nu_{11}^{\prime} A + \nu_{21}^{\prime} B \longrightarrow \nu_{31}^{\prime\prime} C \\
  \nu_{12}^{\prime} A + \nu_{32}^{\prime} C \longrightarrow \nu_{22}^{\prime\prime} B + \nu_{32}^{\prime\prime} C
\end{align}
Note that $\nu_{ij}^{\prime}$ represents the stoichiometric coefficient of reactant $i$ in reaction $j$ and $\nu_{ij}^{\prime\prime}$ represents the stoichiometric coefficient of product $i$ in reaction $j$.  Therefore, in this convention, I have ordered my vector of concentrations as
\begin{align}
  \mathbf{x} =
  \begin{bmatrix}
    \left[A\right] \\
    \left[B\right] \\
    \left[C\right]
  \end{bmatrix}.
\end{align}

Test your function with
\begin{align}
  \nu_{ij}^{\prime} =
  \begin{bmatrix}
    1.0 & 2.0 \\
    2.0 & 0.0 \\
    0.0 & 2.0
  \end{bmatrix}
  \qquad
  \nu_{ij}^{\prime\prime} =
  \begin{bmatrix}
    0.0 & 0.0 \\
    0.0 & 1.0 \\
    2.0 & 1.0
  \end{bmatrix}
  \qquad
  \mathbf{x} =
  \begin{bmatrix}
    1.0 \\
    2.0 \\
    1.0
  \end{bmatrix}
  \qquad
  k = 10.
\end{align}

You must document your function and write some tests in addition to the one suggested.  You choose the additional tests, but you must have at least one doctest in addition to a suite of unit tests.

Please note I have already generalized the progress rate formula, I only need to subtract now the v_ij product from v_ij reactants to get the correct v_ij's

```python
%%file multi_progress_r.py
import numpy as np
def progress_rate_multi(v_ij_reac,v_ij_proc,x,k):
    """Returns the progress rate of a set of reactions

    INPUTS
    =======
    v_ij_reac: vector of stochiometric coefficients of reactants
    v_ij_proc: vector of stochiometric coefficients of products
    x: vector of concentrations (first dimension needs to match v_i),second dimension is 1
    k: float

    RETURNS
    ========
    Progress rates of the reactions (a string based on number of reactions)
       according to the formula k*[A]^v_i[a]*[B]*v_i[b]*[C]^v_i[c]
       unless v_ij = v_ij_proc-v_ij_reac has the first dimension not matching
       the first dimension of x which returns a ValueError
       unless shape of v_ij_proc does not match the shape of x, which will return ValueError as well

    EXAMPLES
    =========
    >>> import numpy as np
    >>> progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[0]
    40.0
    >>> progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[1]
    10.0

    """
    import numpy as np
    #calculate the effective stochiometric coefficients on the reactants side

    #check length of product coeffs and reac coeffs is the same
    if v_ij_proc.shape != v_ij_reac.shape:
        raise ValueError("Dimensions of stochiometric coeffs need to match")

    #check that the first dimension matches
    elif x.shape[0] != v_ij_proc.shape[0]:
        raise ValueError("First dimension of x and v_i need to match")

    else:
        v_ij = v_ij_reac
        product_X_to_vij = 1

        #loop through the rows to multiply each x by its stochiometric coeffs
        for i,j in zip(v_ij,x):
            #print("i,j")
            #print(i,j)
            #print("j**i")
            #print(j**i)
            product_X_to_vij=product_X_to_vij*(j**i)
            #print("product")
            #print(product_X_to_vij)
        progress_rate_multi=k*product_X_to_vij

        return progress_rate_multi

if __name__ == "__main__":
    import doctest
    doctest.testmod()
```

    Overwriting multi_progress_r.py

```python
import multi_progress_r
multi_progress_r.progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)
```

    array([ 40.,  10.])

```python
a=np.array([0.25,2.0])
b=np.array([1.,1.])
a*b
```

    array([ 0.25,  2.  ])

```python
import multi_progress_r
v_ij_reac = np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]])
v_ij_proc = np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]])
x = np.array([1.0,2.0,1.0])
k = 10
assert multi_progress_r.progress_rate_multi(v_ij_reac , v_ij_proc, x , k )[0] == 40
assert multi_progress_r.progress_rate_multi(v_ij_reac , v_ij_proc, x , k )[1] == 10
```

```python

```

```python
#sample fail of the function based on wrong input
import multi_progress_r
multi_progress_r.progress_rate_multi(np.array([2,1,1]),np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
```

```python
%%file test_multi_progress_r.py

import multi_progress_r
import numpy as np
import doctest
doctest.testmod(verbose=True)

def progress_rate_result():
    #test the result og the function
    v_ij_reac = np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]])
    v_ij_proc = np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]])
    x = np.array([1.0,2.0,1.0])
    k = 10
    assert multi_progress_r.progress_rate_multi(v_ij_reac , v_ij_proc, x , k )[0] == 40.0
    assert progress_rate_multi(v_ij_reac , v_ij_proc, x , k )[1] == 10.0

def test_progress_rate_shapes_with_x():
    #test whether shape of coeffs v_i matches shape of concentrations x
    try:
        multi_progress_r.progress_rate_multi(np.array([2,1]),np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
    except ValueError as err:
        assert(type(err) == ValueError)

def test_progress_rate_shapes_coeffs():
    #test whether the shape of v_ij's matches
    try:
        multi_progress_r.progress_rate_multi(np.array([2,1,1]),np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
    except ValueError as err:
        assert(type(err) == ValueError)

```

    Overwriting test_multi_progress_r.py

```bash
%%bash
python multi_progress_r.py -v
```

    Trying:
        import numpy as np
    Expecting nothing
    ok
    Trying:
        progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[0]
    Expecting:
        40.0
    ok
    Trying:
        progress_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,0.0],[0.0,1.0],[2.0,1.0]]),np.array([1.0,2.0,1.0]),10)[1]
    Expecting:
        10.0
    ok
    1 items had no tests:
        __main__
    1 items passed all tests:
       3 tests in __main__.progress_rate_multi
    3 tests in 2 items.
    3 passed and 0 failed.
    Test passed.

```python
!pytest
```

    ============================= test session starts ==============================
    platform darwin -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
    rootdir: /Users/filipmichalsky/cs207_filip_michalsky/homeworks/HW5, inifile:
    collected 13 items

    test_multi_progress_r.py ..
    test_multi_reaction_r.py ...
    test_single_progress_r.py ..
    cs207test/test_roots.py ......

    ========================== 13 passed in 0.23 seconds ===========================

---
# Problem 5
Write a function that returns the **reaction rate** of a system of irreversible reactions of the form:
\begin{align}
  \nu_{11}^{\prime} A + \nu_{21}^{\prime} B &\longrightarrow \nu_{31}^{\prime\prime} C \\
  \nu_{32}^{\prime} C &\longrightarrow \nu_{12}^{\prime\prime} A + \nu_{22}^{\prime\prime} B
\end{align}

Once again $\nu_{ij}^{\prime}$ represents the stoichiometric coefficient of reactant $i$ in reaction $j$ and $\nu_{ij}^{\prime\prime}$ represents the stoichiometric coefficient of product $i$ in reaction $j$.  In this convention, I have ordered my vector of concentrations as
\begin{align}
  \mathbf{x} =
  \begin{bmatrix}
    \left[A\right] \\
    \left[B\right] \\
    \left[C\right]
  \end{bmatrix}
\end{align}

Test your function with
\begin{align}
  \nu_{ij}^{\prime} =
  \begin{bmatrix}
    1.0 & 0.0 \\
    2.0 & 0.0 \\
    0.0 & 2.0
  \end{bmatrix}
  \qquad
  \nu_{ij}^{\prime\prime} =
  \begin{bmatrix}
    0.0 & 1.0 \\
    0.0 & 2.0 \\
    1.0 & 0.0
  \end{bmatrix}
  \qquad
  \mathbf{x} =
  \begin{bmatrix}
    1.0 \\
    2.0 \\
    1.0
  \end{bmatrix}
  \qquad
  k = 10.
\end{align}

You must document your function and write some tests in addition to the one suggested.  You choose the additional tests, but you must have at least one doctest in addition to a suite of unit tests.

```python
#%%file multi_reaction_r.py
import numpy as np
def reaction_rate_multi(v_ij_reac,v_ij_proc,x,k):
    """Returns the rection rate for a set of species

    INPUTS
    =======
    v_ij_reac: vector of stochiometric coefficients of reactants
    v_ij_proc: vector of stochiometric coefficients of products
    x: vector of concentrations (first dimension needs to match v_i),second dimension is 1
    k: float

    RETURNS
    ========
    Reaction rates of the species (a string based on number of species)
       according to the formula k*[A]^v_i[a]*[B]*v_i[b]*[C]^v_i[c] for progress rate and
       dXi/dt = sum(v_ij_proc-v_ij_reac)*progress rate

       unless v_ij = v_ij_proc-v_ij_reac has the first dimension not matching
       the first dimension of x which returns a ValueError
       unless shape of v_ij_proc does not match the shape of x, which will return ValueError as well

    EXAMPLES
    =========
    >>> import numpy as np
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[0][0]
    -50.0
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[1][0]
    -60.0
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[2][0]
    20.0
    """
    import numpy as np
    #calculate the effective stochiometric coefficients on the reactants side

    #check length of product coeffs and reac coeffs is the same
    if v_ij_proc.shape != v_ij_reac.shape:
        raise ValueError("Dimensions of stochiometric coeffs need to match")

    #check that the first dimension matches
    elif x.shape[0] != v_ij_proc.shape[0]:
        raise ValueError("First dimension of x and v_i need to match")

    else:
        v_ij = v_ij_reac
        product_X_to_vij = 1

        #loop through the rows to multiply each x by its stochiometric coeffs
        for i,j in zip(v_ij,x):
            product_X_to_vij=product_X_to_vij*(j**i)

        product_X_to_vij=product_X_to_vij.reshape(len(product_X_to_vij),1)
        progress_rate_multi=k*product_X_to_vij

        reaction_rates=np.dot((v_ij_proc-v_ij_reac),progress_rate_multi)

        return reaction_rates

if __name__ == "__main__":
    import doctest
    doctest.testmod()
```

```python
k=np.array([[10],[10],[10]])
w_ij=np.array([[1],[2],[3]])
print(k.shape,w_ij.shape)
```

    (3, 1) (3, 1)

```python
#from multi_reaction_r import reaction_rate_multi
import multi_reaction_r

multi_reaction_r.reaction_rate_multi(np.array([[1.0,0.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),np.array([[10],[10]]))
```

    array([[-30.],
           [-60.],
           [ 20.]])

```bash
%%bash
python multi_reaction_r.py -v
```

    Trying:
        import numpy as np
    Expecting nothing
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[0][0]
    Expecting:
        -50.0
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[1][0]
    Expecting:
        -60.0
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[2][0]
    Expecting:
        20.0
    ok
    1 items had no tests:
        __main__
    1 items passed all tests:
       4 tests in __main__.reaction_rate_multi
    4 tests in 2 items.
    4 passed and 0 failed.
    Test passed.

```python
%%file test_multi_reaction_r.py

import multi_reaction_r as mr
import numpy as np
#import doctest
#doctest.testmod(verbose=True)

def test_reaction_rate_result():
    #test the result og the function
    v_ij_reac = np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]])
    v_ij_proc = np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]])
    x = np.array([1.0,2.0,1.0])
    k = 10
    assert mr.reaction_rate_multi(v_ij_reac , v_ij_proc, x , k )[0][0] == -50.0
    assert mr.reaction_rate_multi(v_ij_reac , v_ij_proc, x , k )[1][0] == -60.0
    assert mr.reaction_rate_multi(v_ij_reac , v_ij_proc, x , k )[2][0] == 20.0

def test_progress_rate_shapes_with_x():
    #test whether shape of coeffs v_i matches shape of concentrations x
    try:
        mr.reaction_rate_multi(np.array([2,1]),np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
    except ValueError as err:
        assert(type(err) == ValueError)

def test_progress_rate_shapes_coeffs():
    #test whether the shape of v_ij's matches
    try:
        mr.reaction_rate_multi(np.array([2,1,1]),np.array([2,1]),np.array([[2,1],[2,1],[2,1]]),10)
    except ValueError as err:
        assert(type(err) == ValueError)
```

    Overwriting test_multi_reaction_r.py

```bash
%%bash
python -m pytest -v test_multi_reaction_r.py
```

    ============================= test session starts ==============================
    platform darwin -- Python 3.6.1, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /Users/filipmichalsky/anaconda/bin/python
    cachedir: .cache
    rootdir: /Users/filipmichalsky/cs207_filip_michalsky/homeworks/HW5, inifile:
    collecting ... collected 3 items

    test_multi_reaction_r.py::test_reaction_rate_result PASSED
    test_multi_reaction_r.py::test_progress_rate_shapes_with_x PASSED
    test_multi_reaction_r.py::test_progress_rate_shapes_coeffs PASSED

    =========================== 3 passed in 0.22 seconds ===========================

---
# Problem 6
Put parts 3, 4, and 5 in a module called `chemkin`.

Next, pretend you're a client who needs to compute the reaction rates at three different temperatures ($T = \left\{750, 1500, 2500\right\}$) of the following system of irreversible reactions:
\begin{align}
  2H_{2} + O_{2} \longrightarrow 2OH + H_{2} \\
  OH + HO_{2} \longrightarrow H_{2}O + O_{2} \\
  H_{2}O + O_{2} \longrightarrow HO_{2} + OH
\end{align}

The client also happens to know that reaction 1 is a modified Arrhenius reaction with $A_{1} = 10^{8}$, $b_{1} = 0.5$, $E_{1} = 5\times 10^{4}$, reaction 2 has a constant reaction rate parameter $k = 10^{4}$, and reaction 3 is an Arrhenius reaction with $A_{3} = 10^{7}$ and $E_{3} = 10^{4}$.

You should write a script that imports your `chemkin` module and returns the reaction rates of the species at each temperature of interest given the following species concentrations:

\begin{align}
  \mathbf{x} =
  \begin{bmatrix}
    H_{2}  \\
    O_{2}  \\
    OH     \\
    HO_{2} \\
    H_{2}O
  \end{bmatrix} =
  \begin{bmatrix}
    2.0 \\
    1.0 \\
    0.5 \\
    1.0 \\
    1.0
  \end{bmatrix}
\end{align}

You may assume that these are elementary reactions.

```python
%%file chemkin1.py

import numpy as np

def const(k=1.0):
    """Returns the constant coefficient k.

    INPUTS
    =======
    a: float, optional, default value is 1.0
       Constant reaction coefficient

    RETURNS
    ========
    Constant k
       unless k is not an int or float
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> const(1.0)
    1.0
    """
    if type(k) != int and type(k) != float:
        raise TypeError("Input needs to be a number!")

    else:
        return k

def arr(A=10^7,T=10^2,E=10^3,R=8.314):
    """Returns the constant coefficient k.

    INPUTS
    =======
    A: float, optional, default value is 10^7
       Arhenieus constant, needs to be positive
    T: float, optional, default value is 10^2
        temperature, must be positive
    R: float, fixed value 8.314, should not change

    RETURNS
    ========
    Constant k
       unless any of A,T, and E are not numbers
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> arr(10^7,10^2,10^2)
    11.526748719357375
    """
    if A < 0:
        raise ValueError("The Arrhenius constant must be strictly positive")

    elif T < 0:
        raise ValueError("Temperature must be positive")

    elif R != 8.314:
        raise ValueError("Unless in a stargate, the universal gas constant is 8.314")

    elif type(A) != int and type(A) != float:
        raise TypeError("parameters need to be either type int or type float")

    elif type(T) !=int and type(T) != int:
        raise TypeError("parameters need to be either type int or type float")

    elif type(E) != int and type(E) != float:
        raise TypeError("parameters need to be either type int or type float")

    else:
        import numpy as np
        k=(A)*np.exp(-E/(R*T))
        return k

def mod_arr(A=10^7,b=0.5,T=10^2,E=10^3,R=8.314):
    """Returns the constant coefficient k.

    INPUTS
    =======
    A: float, optional, default value is 10^7
       Arhenieus constant, needs to be positive
    T: float, optional, default value is 10^2
        temperature, must be positive
    R: float, fixed value 8.314, should not change
    b: float, optional, default value is 0.5, must be a real number

    RETURNS
    ========
    Constant k
       unless any of A,T,b and E are not numbers
       in which case a TypeError exception is raised

    EXAMPLES
    =========
    >>> mod_arr()
    32.116059468138779
    """
    if A < 0:
        raise ValueError("The Arrhenius constant must be strictly positive")

    elif T < 0:
        raise ValueError("Temperature must be positive")

    elif isinstance(b,complex):
        raise ValueError("b must be a real number")
    elif R != 8.314:
        raise ValueError("Unless in a stargate, the universal gas constant is 8.314")

    elif type(A) != int and type(A) != float:
        raise TypeError("parameters need to be either type int or type float")

    elif type(T) !=int and type(T) != int:
        raise TypeError("parameters need to be either type int or type float")

    elif type(E) != int and type(E) != float:
        raise TypeError("parameters need to be either type int or type float")

    else:
        import numpy as np
        k=(A)*(T**b)*np.exp(-E/(R*T))
        return k

def reaction_rate_multi(v_ij_reac,v_ij_proc,x,k):
    """Returns the rection rate for a set of species

    INPUTS
    =======
    v_ij_reac: vector of stochiometric coefficients of reactants
    v_ij_proc: vector of stochiometric coefficients of products
    x: vector of concentrations (first dimension needs to match v_i),second dimension is 1
    k: float

    RETURNS
    ========
    Reaction rates of the species (a string based on number of species)
       according to the formula k*[A]^v_i[a]*[B]*v_i[b]*[C]^v_i[c] for progress rate and
       dXi/dt = sum(v_ij_proc-v_ij_reac)*progress rate

       unless v_ij = v_ij_proc-v_ij_reac has the first dimension not matching
       the first dimension of x which returns a ValueError
       unless shape of v_ij_proc does not match the shape of x, which will return ValueError as well

    EXAMPLES
    =========
    >>> import numpy as np
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[0][0]
    -50.0
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[1][0]
    -60.0
    >>> reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[2][0]
    20.0
    """
    import numpy as np
    #calculate the effective stochiometric coefficients on the reactants side

    #check length of product coeffs and reac coeffs is the same
    if v_ij_proc.shape != v_ij_reac.shape:
        raise ValueError("Dimensions of stochiometric coeffs need to match")

    #check that the first dimension matches
    elif x.shape[0] != v_ij_proc.shape[0]:
        raise ValueError("First dimension of x and v_i need to match")

    else:
        v_ij = v_ij_reac
        product_X_to_vij = 1

        #loop through the rows to multiply each x by its stochiometric coeffs
        for i,j in zip(v_ij,x):

            product_X_to_vij=product_X_to_vij*(j**i)

        product_X_to_vij=product_X_to_vij.reshape(len(product_X_to_vij),1)

        progress_rate_multi=k*product_X_to_vij

        reaction_rates=np.dot((v_ij_proc-v_ij_reac),progress_rate_multi)

        return reaction_rates

if __name__ == "__main__":
    import doctest
    doctest.testmod()
```

    Overwriting chemkin1.py

```bash
%%bash
python chemkin1.py -v
```

    Trying:
        arr(10^7,10^2,10^2)
    Expecting:
        11.526748719357375
    ok
    Trying:
        const(1.0)
    Expecting:
        1.0
    ok
    Trying:
        mod_arr()
    Expecting:
        32.116059468138779
    ok
    Trying:
        import numpy as np
    Expecting nothing
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[0][0]
    Expecting:
        -50.0
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[1][0]
    Expecting:
        -60.0
    ok
    Trying:
        reaction_rate_multi(np.array([[1.0,2.0],[2.0,0.0],[0.0,2.0]]),np.array([[0.0,1.0],[0.0,2.0],[1.0,0.0]]),np.array([1.0,2.0,1.0]),10)[2][0]
    Expecting:
        20.0
    ok
    1 items had no tests:
        __main__
    4 items passed all tests:
       1 tests in __main__.arr
       1 tests in __main__.const
       1 tests in __main__.mod_arr
       4 tests in __main__.reaction_rate_multi
    7 tests in 5 items.
    7 passed and 0 failed.
    Test passed.

```python
import chemkin1
import numpy as np

vij_reac = np.array([[2.0,0.0,0.0],[1.0,0.0,1.0],[0.0,1.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]])
vij_proc = np.array([[1.0,0.0,0.0],[0.0,1.0,0.0],[2.0,0.0,1.0],[0.0,0.0,1.0],[0.0,1.0,0.0]])
x = np.array([2.0,1.0,0.5,1.0,1.0])
species = np.array([['H2'],['O2'],['OH'],['OH2'],['H20']])
reaction_rate =[]
T = [750,1500,2500]

for temp in T:
    k1= chemkin1.mod_arr(A=10**7,b=0.5,T=temp,E=(5*(10**4)),R=8.314)
    k2= chemkin1.const(10**4)
    k3= chemkin1.arr(A=10**8,T=temp,E=5*(10**4),R=8.314)
    k=np.array([[k1],[k2],[k3]])

    this_reaction_rate = chemkin1.reaction_rate_multi(vij_reac,vij_proc,x,k)
    reaction_rate.append(this_reaction_rate)
    print("Temperature ",temp)
    print("Reaction rates of species")

    for i,j in zip(species,this_reaction_rate):
        print(i[0],': ',j[0])

```

    Temperature  750
    Reaction rates of species
    H2 :  -360707.78728
    O2 :  -388635.752574
    OH :  749343.539854
    OH2 :  27927.9652935
    H20 :  -27927.9652935
    Temperature  1500
    Reaction rates of species
    H2 :  -28111762.0765
    O2 :  -29921368.5157
    OH :  58033130.5922
    OH2 :  1809606.43925
    H20 :  -1809606.43925
    Temperature  2500
    Reaction rates of species
    H2 :  -180426142.596
    O2 :  -189442449.726
    OH :  369868592.322
    OH2 :  9016307.12982
    H20 :  -9016307.12982

---
# Problem 7
Get together with your project team, form a GitHub organization (with a descriptive team name), and give the teaching staff access.  You can have has many repositories as you like within your organization.  However, we will grade the repository called **`cs207-FinalProject`**.

Within the `cs207-FinalProject` repo, you must set up Travis CI and Coveralls.  Make sure your `README.md` file includes badges indicating how many tests are passing and the coverage of your code.


# [General examples of usage](https://docs.sympy.org/latest/modules/vector/examples.html)

## Quadrilateral problem

### The Problem

### Solution

```python
from sympy.vector import CoordSys3D
Sys = CoordSys3D('Sys')
O = Sys.origin
O
```

$\displaystyle Point\left(Sys.origin, \mathbf{\hat{0}}\right)$

```python
from sympy import symbols
a1, a2, a3 = symbols('a1 a2 a3')
A = O.locate_new('A', a1*Sys.i + a2*Sys.j + a3*Sys.k)
A
```

$\displaystyle Point\left(A, (a_{1})\mathbf{\hat{i}_{Sys}} + (a_{2})\mathbf{\hat{j}_{Sys}} + (a_{3})\mathbf{\hat{k}_{Sys}}, Point\left(Sys.origin, \mathbf{\hat{0}}\right)\right)$

```python
b1, b2, b3 = symbols('b1 b2 b3')
B = O.locate_new('B', b1*Sys.i + b2*Sys.j + b3*Sys.k)
B
```

$\displaystyle Point\left(B, (b_{1})\mathbf{\hat{i}_{Sys}} + (b_{2})\mathbf{\hat{j}_{Sys}} + (b_{3})\mathbf{\hat{k}_{Sys}}, Point\left(Sys.origin, \mathbf{\hat{0}}\right)\right)$

```python
c1, c2, c3 = symbols('c1 c2 c3')
C = O.locate_new('C', c1*Sys.i + c2*Sys.j + c3*Sys.k)
C
```

$\displaystyle Point\left(C, (c_{1})\mathbf{\hat{i}_{Sys}} + (c_{2})\mathbf{\hat{j}_{Sys}} + (c_{3})\mathbf{\hat{k}_{Sys}}, Point\left(Sys.origin, \mathbf{\hat{0}}\right)\right)$

```python
P = O.locate_new('P', A.position_wrt(O) + (O.position_wrt(A)/2))
P
```

$\displaystyle Point\left(P, (\frac{a_{1}}{2})\mathbf{\hat{i}_{Sys}} + (\frac{a_{2}}{2})\mathbf{\hat{j}_{Sys}} + (\frac{a_{3}}{2})\mathbf{\hat{k}_{Sys}}, Point\left(Sys.origin, \mathbf{\hat{0}}\right)\right)$

```python
Q = A.locate_new('Q', B.position_wrt(A)/2)
R = B.locate_new('R', C.position_wrt(B)/2)
S = O.locate_new('S', C.position_wrt(O)/2)
PQ = Q.position_wrt(P)
PQ
```

$\displaystyle (\frac{b_{1}}{2})\mathbf{\hat{i}_{Sys}} + (\frac{b_{2}}{2})\mathbf{\hat{j}_{Sys}} + (\frac{b_{3}}{2})\mathbf{\hat{k}_{Sys}}$

```python
SR = R.position_wrt(S)
SR
```

$\displaystyle (\frac{b_{1}}{2})\mathbf{\hat{i}_{Sys}} + (\frac{b_{2}}{2})\mathbf{\hat{j}_{Sys}} + (\frac{b_{3}}{2})\mathbf{\hat{k}_{Sys}}$

```python
O.locate_new('Palt', A.position_wrt(O)/2)
```

$\displaystyle Point\left(Palt, (\frac{a_{1}}{2})\mathbf{\hat{i}_{Sys}} + (\frac{a_{2}}{2})\mathbf{\hat{j}_{Sys}} + (\frac{a_{3}}{2})\mathbf{\hat{k}_{Sys}}, Point\left(Sys.origin, \mathbf{\hat{0}}\right)\right)$

```python
PQ.cross(SR)
```

$\displaystyle \mathbf{\hat{0}}$

```python
A.position_wrt(O)
```

$\displaystyle (a_{1})\mathbf{\hat{i}_{Sys}} + (a_{2})\mathbf{\hat{j}_{Sys}} + (a_{3})\mathbf{\hat{k}_{Sys}}$

```python
type(A.position_wrt(O))
```

    sympy.vector.vector.VectorAdd

```python
OA = A.position_wrt(O)
OA
```

$\displaystyle (a_{1})\mathbf{\hat{i}_{Sys}} + (a_{2})\mathbf{\hat{j}_{Sys}} + (a_{3})\mathbf{\hat{k}_{Sys}}$

```python
from sympy.vector.vector import Vector, VectorAdd
isinstance(OA, VectorAdd)
```

    True

```python
isinstance(OA, Vector)
```

    True

```python
issubclass(VectorAdd, Vector)
```

    True

```python
type(O)
```

    sympy.vector.point.Point

## Third product rule for Del operator

### Solution

```python
from sympy.vector import CoordSys3D, Del
delop = Del()
C = CoordSys3D('C')
C
```

$\displaystyle CoordSys3D\left(C, \left( \left[\begin{matrix}1 & 0 & 0\\0 & 1 & 0\\0 & 0 & 1\end{matrix}\right], \  \mathbf{\hat{0}}\right)\right)$

```python
C.origin
```

$\displaystyle Point\left(C.origin, \mathbf{\hat{0}}\right)$

```python
D = CoordSys3D('D')
```

```python
C.origin == D.origin
```

    False

```python
from sympy import symbols, Function
v1, v2, v3, f = symbols('v1 v2 v3 f', cls=Function)
```

```python
vfield = v1(C.x, C.y, C.z)*C.i + v2(C.x, C.y, C.z)*C.j + v3(C.x, C.y, C.z)*C.k
vfield
```

$\displaystyle (\operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)})\mathbf{\hat{i}_{C}} + (\operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)})\mathbf{\hat{j}_{C}} + (\operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)})\mathbf{\hat{k}_{C}}$

```python
ffield = f(C.x, C.y, C.z)
ffield
```

$\displaystyle f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}$

```python
lhs = (delop.dot(ffield * vfield)).doit()
lhs
```

$\displaystyle f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}$

```python
rhs = ((vfield.dot(delop(ffield)))) + (ffield * (delop.dot(vfield))).doit()
rhs
```

$\displaystyle \left(\frac{\partial}{\partial \mathbf{{x}_{C}}} \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \frac{\partial}{\partial \mathbf{{y}_{C}}} \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \frac{\partial}{\partial \mathbf{{z}_{C}}} \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}\right) f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}$

```python
lhs.expand().simplify() == rhs.expand().doit().simplify()
```

    True

```python
lhs.expand()
```

$\displaystyle f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}$

```python
lhs.expand().simplify()
```

$\displaystyle f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{1}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{x}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{2}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{y}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} + \operatorname{v_{3}}{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)} \frac{\partial}{\partial \mathbf{{z}_{C}}} f{\left(\mathbf{{x}_{C}},\mathbf{{y}_{C}},\mathbf{{z}_{C}} \right)}$


# AS-AD model
Consider the following **AS-AD model**. The **goods market equilibrium** is given by

$$ y_{t} = -\alpha r_{t} + v_{t} $$

where $y_{t}$ is the **output gap**, $r_{t}$ is the **ex ante real interest** and $v_{t}$ is a **demand disturbance**.

The central bank's **Taylor rule** is

$$ i_{t} = \pi_{t+1}^{e} + h \pi_{t} + b y_{t}$$

where $i_{t}$ is the **nominal interest rate**, $\pi_{t}$ is the **inflation gap**, and $\pi_{t+1}^{e}$ is the **expected inflation gap**.

The **ex ante real interest rate** is given by

$$ r_{t} = i_{t} - \pi_{t+1}^{e} $$

Together, the above implies that the **AD-curve** is

$$ \pi_{t} = \frac{1}{h\alpha}\left[v_{t} - (1+b\alpha)y_{t}\right]$$

Further, assume that the **short-run supply curve (SRAS)** is given by

$$ \pi_{t} = \pi_{t}^{e} + \gamma y_{t} + s_{t}$$

where $s_t$ is a **supply disturbance**.

**Inflation expectations are adaptive** and given by

$$ \pi_{t}^{e} = \phi\pi_{t-1}^{e} + (1-\phi)\pi_{t-1}$$

Together, this implies that the **SRAS-curve** can also be written as

$$ \pi_{t} = \pi_{t-1} + \gamma y_{t} - \phi\gamma y_{t-1} + s_{t} - \phi s_{t-1} $$

The **parameters** of the model are:

```python
import sympy as sm
import numpy as np
import matplotlib.pyplot as plt
sm.init_printing(use_unicode=True)

#Defining symbols
pi_t = sm.symbols('pi_t')
pi_t2 = sm.symbols('pi_t-1')
y_t = sm.symbols('y_t')
y_t2 = sm.symbols('y_t-1')
v_t = sm.symbols('v_t')
s_t = sm.symbols('s_t')
s_t2 = sm.symbols('s_t-1')
alpha = sm.symbols('alpha')
gamma = sm.symbols('gamma')
phi = sm.symbols('phi')
h = sm.symbols('h')
b = sm.symbols('b')

```

## **Question 1:** Use the ``sympy`` module to solve for the equilibrium values of output, $y_t$, and inflation, $\pi_t$, (where AD = SRAS) given the parameters ($\alpha$, $h$, $b$, $\alpha$, $\gamma$) and $y_{t-1}$ , $\pi_{t-1}$, $v_t$, $s_t$, and $s_{t-1}$.

```python
#Defining AD-curve
AD = sm.Eq(pi_t,(1/(h*alpha))*(v_t-(1+b*alpha)*y_t))
#Defining SRAS-curve
SRAS = sm.Eq(pi_t,pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
#Setting AD equal to SRAS and solving for equilibrium values of output, y*.
AD_SRAS=sm.Eq((1/(h*alpha))*(v_t-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y = sm.solve(AD_SRAS,y_t)
print('The equilibrium for output y is:')
eq_y
```

```python
#Solving equilibrium values of pi
#Substituting equilibrium of y_t* into the AD-function
eq_pi=1/(h*alpha)*(v_t-(1+b*alpha)*eq_y[0])
print('The equilibrium for output pi is:')
eq_pi
```

Now, one an just set the paramter values to find the equilibrium given the paramter values

```python
#Setting parameter values
alpha = 5.76
h = 0.5
b = 0.5
phi = 0
gamma = 0.075
#Resolving for equilibrium
AD_SRAS_val=sm.Eq((1/(h*alpha))*(v_t-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_val = sm.solve(AD_SRAS_val,y_t)
print('The equilibrium for output y given parameter values is:')
eq_y_val

```

```python
eq_pi_val=1/(h*alpha)*(v_t-(1+b*alpha)*eq_y_val[0])
print('The equilibrium for output pi given parameter values is:')
eq_pi_val
```

## **Question 2:** Find and illustrate the equilibrium when $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$. Illustrate how the equilibrium changes when instead $v_t = 0.1$.

To find the equilibrium when $y_{t-1} = \pi_{t-1} = v_t = s_t = s_{t-1} = 0$, one needs to set the new values for the variables, and here, just re-solve the same problem as earlier.

```python
#Setting variable values - parameter is similar to previous questions
pi_t2 = 0
y_t2 = 0
v_t = 0
s_t = 0
s_t2 = 0

#Solving for equilibrium given variable values
AD_SRAS_param=sm.Eq((1/(h*alpha))*(v_t-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_param = sm.solve(AD_SRAS_param,y_t)
eq_pi_param=1/(h*alpha)*(v_t-(1+b*alpha)*eq_y_param[0])
print('The equilibrium for output y given parameter and variable values is: %8.3f' % eq_y_param[0])
print('The equilibrium for output pi given parameter and variable values is: %8.3f' % eq_pi_param)
```

    The equilibrium for output y given parameter and variable values is:    0.000
    The equilibrium for output pi given parameter and variable values is:    0.000

Now, one can investigate how the equilibrium changes when $v_t=0.1$

```python
#Solving for equilibrium given v_t=0.1
v_t2 = 0.1
AD_SRAS_param2=sm.Eq((1/(h*alpha))*(v_t2-(1+b*alpha)*y_t),pi_t2+gamma*y_t-phi*gamma*y_t2+s_t-phi*s_t2)
eq_y_param2 = sm.solve(AD_SRAS_param2,y_t)
eq_pi_param2=1/(h*alpha)*(v_t2-(1+b*alpha)*eq_y_param2[0])
print('The equilibrium for output y given parameter and variable values is: %8.3f' % eq_y_param2[0])
print('The equilibrium for output pi given parameter and variable values is: %8.3f' % eq_pi_param2)
```

    The equilibrium for output y given parameter and variable values is:    0.024
    The equilibrium for output pi given parameter and variable values is:    0.002

Now, lets illustrate the equilibrium. First, one creates a vector consisting of the equilibrium values for $y_t$ and $\pi_t$, respectively. Next step is to construct a illustration of the equilibrium values for given values of $v_t$.

```python
#Creating vector with equilibrium values for y_t and pi_t for given values of v_t
## Vector with equilibrium values for y
y_values=[]
y_values.append(eq_y_param[0])
y_values.append(eq_y_param2[0])
## Vector with equilibrium values for pi
pi_values=[]
pi_values.append(eq_pi_param)
pi_values.append(eq_pi_param2)
```

```python
#Setting up illustration
ax.grid()
ax.legend()
ax.set_ylabel('$y_t^*$')
ax.set_xlabel('$\pi_t^*$')
ax.set_title('Equilibrium values for given values of $v_t$')

#Creating illustration of the equilibrium values
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(pi_values[0], y_values[0], label='$v_t=0$')
ax.scatter(pi_values[1], y_values[1], label='$v_t=0.1$')

```