# Building a Spam Filter with Naive Bayes

In this project, we're going to build a spam filter for SMS messages using the multinomial Naive Bayes algorithm. Our goal is to write a program that classifies new messages with an accuracy greater than 80% — so we expect that more than 80% of the new messages will be classified correctly as spam or ham (non-spam).

To train the algorithm, we'll use a dataset of 5,572 SMS messages that are already classified by humans. The dataset was put together by Tiago A. Almeida and José María Gómez Hidalgo, and it can be downloaded from the The [UCI Machine Learning Repository](https://archive.ics.uci.edu/ml/datasets/sms+spam+collection). The data collection process is described in more details on [this page](http://www.dt.fee.unicamp.br/~tiago/smsspamcollection/#composition), where you can also find some of the papers authored by Tiago A. Almeida and José María Gómez Hidalgo.

## Exploring the Dataset

```python
import pandas as pd

sms_collection = pd.read_csv('SMSSpamCollection',
                             sep = '\t',
                             header = None,
                             names = ['Label','SMS'])
```

```python
sms_collection.head()
```

      Label
      SMS

      0
      ham
      Go until jurong point, crazy.. Available only ...

      1
      ham
      Ok lar... Joking wif u oni...

      2
      spam
      Free entry in 2 a wkly comp to win FA Cup fina...

      3
      ham
      U dun say so early hor... U c already then say...

      4
      ham
      Nah I don't think he goes to usf, he lives aro...

```python
sms_collection.shape
```

    (5572, 2)

As we can see that there are 5572 rows and only 2 columns which are LABEL and SMS column.

```python
sms_collection['Label'].value_counts(normalize=True)* 100
```

    ham     86.593683
    spam    13.406317
    Name: Label, dtype: float64

There are about 13.4 % of spam messages in the dataset and about 86.6 % of non spam sms which are labeled as ham.

## Training and Test set
e're going to keep 80% of our dataset for training, and 20% for testing (we want to train the algorithm on as much data as possible, but we also want to have enough test data). The dataset has 5,572 messages, which means that:

* The training set will have 4,458 messages (about 80% of the dataset).
* The test set will have 1,114 messages (about 20% of the dataset).

To better understand the purpose of putting a test set aside, let's begin by observing that all 1,114 messages in our test set are already classified by a human. When the spam filter is ready, we're going to treat these messages as new and have the filter classify them. Once we have the results, we'll be able to compare the algorithm classification with that done by a human, and this way we'll see how good the spam filter really is.

```python
# randomizing the dataset
'''
We will use frac =1 parameter to randomize the entire dataset
and random_state = 1 to make our results reproducible
'''
randomized_sms = sms_collection.sample(frac = 1, random_state = 1)

#splitting the randomized dataset
# Calculate index for split
training_test_index = round(len(randomized_sms) * 0.8)

# Training/Test split
training_set = randomized_sms[:training_test_index].reset_index(drop=True)
test_set = randomized_sms[training_test_index:].reset_index(drop=True)

print(training_set.shape)
print(test_set.shape)
```

    (4458, 2)
    (1114, 2)

Now lets Find the percentage of spam and ham in both the training and the test set.

```python
training_set['Label'].value_counts(normalize = True)* 100
```

    ham     86.54105
    spam    13.45895
    Name: Label, dtype: float64

```python
test_set['Label'].value_counts(normalize = True)* 100
```

    ham     86.804309
    spam    13.195691
    Name: Label, dtype: float64

The results look good! We'll now move on to cleaning the dataset.

The results look good! We'll now move on to cleaning the dataset.

## Data Cleaning

To calculate all the probabilities required by the algorithm, we'll first need to perform a bit of data cleaning to bring the data in a format that will allow us to extract easily all the information we need.

Essentially, we want to bring data to this format:

### Letter Case and Punctuation

We'll begin with removing all the punctuation and bringing every letter to lower case.

```python
# Before cleaning data
training_set.head()
```

      Label
      SMS

      0
      ham
      Yep, by the pretty sculpture

      1
      ham
      Yes, princess. Are you going to make me moan?

      2
      ham
      Welp apparently he retired

      3
      ham
      Havent.

      4
      ham
      I forgot 2 ask ü all smth.. There's a card on ...

```python
# After cleaning data
training_set['SMS'] = training_set['SMS'].str.replace('\W', ' ')
training_set['SMS'] = training_set['SMS'].str.lower()
training_set.head()
```

      Label
      SMS

      0
      ham
      yep  by the pretty sculpture

      1
      ham
      yes  princess  are you going to make me moan

      2
      ham
      welp apparently he retired

      3
      ham
      havent

      4
      ham
      i forgot 2 ask ü all smth   there s a card on ...

## Creating the Vocabulary

Let's now move to creating the vocabulary, which in this context means a list with all the unique words in our training set.

```python
training_set['SMS'] = training_set['SMS'].str.split()

vocabulary = []
for sms in training_set['SMS']:
    for word in sms:
        vocabulary.append(word)

vocabulary = list(set(vocabulary))
```

```python
len(vocabulary)
```

    7783

It looks like there are 7,783 unique words in all the messages of our training set.

## The final Training Set

We're now going to use the vocabulary we just created to make the data transformation we want

```python
word_counts_per_sms = {unique_word: [0] * len(training_set['SMS']) for unique_word in vocabulary}

for index, sms in enumerate(training_set['SMS']):
    for word in sms:
        word_counts_per_sms[word][index] += 1
```

```python
#Transformimg into dataframe
word_counts = pd.DataFrame(word_counts_per_sms)
word_counts.head()
```

      0
      00
      000
      000pes
      008704050406
      0089
      01223585334
      02
      0207
      02072069400
      ...
      zindgi
      zoe
      zogtorius
      zouk
      zyada
      é
      ú1
      ü
      〨ud
      鈥

      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      1
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      2
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      4
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      2
      0
      0

5 rows × 7783 columns

```python
training_set_clean = pd.concat([training_set, word_counts], axis=1)
training_set_clean.head()
```

      Label
      SMS
      0
      00
      000
      000pes
      008704050406
      0089
      01223585334
      02
      ...
      zindgi
      zoe
      zogtorius
      zouk
      zyada
      é
      ú1
      ü
      〨ud
      鈥

      0
      ham
      [yep, by, the, pretty, sculpture]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      1
      ham
      [yes, princess, are, you, going, to, make, me,...
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      2
      ham
      [welp, apparently, he, retired]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3
      ham
      [havent]
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      4
      ham
      [i, forgot, 2, ask, ü, all, smth, there, s, a,...
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      0
      0
      0
      0
      0
      0
      0
      2
      0
      0

5 rows × 7785 columns

## Calculating Constants First

We're now done with cleaning the training set, and we can begin creating the spam filter. The Naive Bayes algorithm will need to answer these two probability questions to be able to classify new messages:

\begin{equation}
P(Spam | w_1,w_2, ..., w_n) \propto P(Spam) \cdot \prod_{i=1}^{n}P(w_i|Spam)
\end{equation}

\begin{equation}
P(Ham | w_1,w_2, ..., w_n) \propto P(Ham) \cdot \prod_{i=1}^{n}P(w_i|Ham)
\end{equation}

Also, to calculate P(wi|Spam) and P(wi|Ham) inside the formulas above, we'll need to use these equations:

\begin{equation}
P(w_i|Spam) = \frac{N_{w_i|Spam} + \alpha}{N_{Spam} + \alpha \cdot N_{Vocabulary}}
\end{equation}

\begin{equation}
P(w_i|Ham) = \frac{N_{w_i|Ham} + \alpha}{N_{Ham} + \alpha \cdot N_{Vocabulary}}
\end{equation}

Some of the terms in the four equations above will have the same value for every new message. We can calculate the value of these terms once and avoid doing the computations again when a new messages comes in. Below, we'll use our training set to calculate:

- P(Spam) and P(Ham)
- NSpam, NHam, NVocabulary

We'll also use Laplace smoothing and set $\alpha = 1$.

```python
#isolating spam and ham messages
spam_msg = training_set_clean[training_set_clean['Label']=='spam']
ham_msg = training_set_clean[training_set_clean['Label']=='ham']
```

```python
# p(spam) and p(ham)
p_spam = len(spam_msg) / len(training_set_clean)
p_ham = len(ham_msg) / len(training_set_clean)
print('The probability of spam messages is: '+ str(p_spam))
print('The probability of non-spam messages is: '+ str(p_ham))
```

    The probability of spam messages is: 0.13458950201884254
    The probability of non-spam messages is: 0.8654104979811574

```python
# N_Spam
n_words_per_spam_message = spam_msg['SMS'].apply(len)
n_spam = n_words_per_spam_message.sum()

# N_Ham
n_words_per_ham_message = ham_msg['SMS'].apply(len)
n_ham = n_words_per_ham_message.sum()

# N_Vocabulary
n_vocabulary = len(vocabulary)

# Laplace smoothing
alpha = 1
```

## Calculating Parameters

Now that we have the constant terms calculated above, we can move on with calculating the parameters $P(w_i|Spam)$ and $P(w_i|Ham)$. Each parameter will thus be a conditional probability value associated with each word in the vocabulary.

The parameters are calculated using the formulas:

\begin{equation}
P(w_i|Spam) = \frac{N_{w_i|Spam} + \alpha}{N_{Spam} + \alpha \cdot N_{Vocabulary}}
\end{equation}

\begin{equation}
P(w_i|Ham) = \frac{N_{w_i|Ham} + \alpha}{N_{Ham} + \alpha \cdot N_{Vocabulary}}
\end{equation}

```python
# Initiate parameters
parameters_spam = {unique_word:0 for unique_word in vocabulary}
parameters_ham = {unique_word:0 for unique_word in vocabulary}

# Calculate parameters
for word in vocabulary:
    n_word_given_spam = spam_msg[word].sum()   # spam_messages already defined in a cell above
    p_word_given_spam = (n_word_given_spam + alpha) / (n_spam + alpha*n_vocabulary)
    parameters_spam[word] = p_word_given_spam

    n_word_given_ham = ham_msg[word].sum()   # ham_messages already defined in a cell above
    p_word_given_ham = (n_word_given_ham + alpha) / (n_ham + alpha*n_vocabulary)
    parameters_ham[word] = p_word_given_ham
```

## Classifying A New Message

Now that we have all our parameters calculated, we can start creating the spam filter. The spam filter can be understood as a function that:

- Takes in as input a new message (w1, w2, ..., wn).
- Calculates P(Spam|w1, w2, ..., wn) and P(Ham|w1, w2, ..., wn).
- Compares the values of P(Spam|w1, w2, ..., wn) and P(Ham|w1, w2, ..., wn), and:
    - If P(Ham|w1, w2, ..., wn) > P(Spam|w1, w2, ..., wn), then the message is classified as ham.
    - If P(Ham|w1, w2, ..., wn) < P(Spam|w1, w2, ..., wn), then the message is classified as spam.
    -  If P(Ham|w1, w2, ..., wn) = P(Spam|w1, w2, ..., wn), then the algorithm may request human help.

```python
import re

def classify(message):
    '''
    message: a string
    '''

    message = re.sub('\W', ' ', message)
    message = message.lower().split()

    p_spam_given_message = p_spam
    p_ham_given_message = p_ham

    for word in message:
        if word in parameters_spam:
            p_spam_given_message *= parameters_spam[word]

        if word in parameters_ham:
            p_ham_given_message *= parameters_ham[word]

    print('P(Spam|message):', p_spam_given_message)
    print('P(Ham|message):', p_ham_given_message)

    if p_ham_given_message > p_spam_given_message:
        print('Label: Ham')
    elif p_ham_given_message < p_spam_given_message:
        print('Label: Spam')
    else:
        print('Equal proabilities, have a human classify this!')
```

```python
print(classify('WINNER!! This is the secret code to unlock the money: C3421.'))
```

    P(Spam|message): 1.3481290211300841e-25
    P(Ham|message): 1.9368049028589875e-27
    Label: Spam
    None

```python
print(classify("Sounds good, Tom, then see u there"))
```

    P(Spam|message): 2.4372375665888117e-25
    P(Ham|message): 3.687530435009238e-21
    Label: Ham
    None

```python

```

##  Measuring the Spam Filter's Accuracy
The two results above look promising, but let's see how well the filter does on our test set, which has 1,114 messages.

We'll start by writing a function that returns classification labels instead of printing them.

```python
def classify_test_set(message):
    '''
    message: a string
    '''

    message = re.sub('\W', ' ', message)
    message = message.lower().split()

    p_spam_given_message = p_spam
    p_ham_given_message = p_ham

    for word in message:
        if word in parameters_spam:
            p_spam_given_message *= parameters_spam[word]

        if word in parameters_ham:
            p_ham_given_message *= parameters_ham[word]

    if p_ham_given_message > p_spam_given_message:
        return 'ham'
    elif p_spam_given_message > p_ham_given_message:
        return 'spam'
    else:
        return 'needs human classification'
```

Now that we have a function that returns labels instead of printing them, we can use it to create a new column in our test set.

```python
test_set['predicted'] = test_set['SMS'].apply(classify_test_set)
test_set.head()
```

      Label
      SMS
      predicted

      0
      ham
      Later i guess. I needa do mcat study too.
      ham

      1
      ham
      But i haf enuff space got like 4 mb...
      ham

      2
      spam
      Had your mobile 10 mths? Update to latest Oran...
      spam

      3
      ham
      All sounds good. Fingers . Makes it difficult ...
      ham

      4
      ham
      All done, all handed in. Don't know if mega sh...
      ham

Now, we'll write a function to measure the accuracy of our spam filter to find out how well our spam filter does.

```python
correct = 0
total = test_set.shape[0]

for row in test_set.iterrows():
    row = row[1]
    if row['Label'] == row['predicted']:
        correct += 1

print('Correct:', correct)
print('Incorrect:', total - correct)
print('Accuracy:', correct/total)
```

    Correct: 1100
    Incorrect: 14
    Accuracy: 0.9874326750448833

The accuracy is close to 98.74%, which is really good. Our spam filter looked at 1,114 messages that it hasn't seen in training, and classified 1,100 correctly.

```python

```


# Building a ME-model

```python
from __future__ import print_function

import cobrame
from cobrame.util import dogma, building
import cobrame.util.building
import cobra
import cobra.test
from collections import defaultdict

#import warnings
#warnings.filterwarnings('ignore')
```

    /home/sbrg-cjlloyd/cobrapy/cobra/io/sbml3.py:24: UserWarning: Install lxml for faster SBML I/O
      warn("Install lxml for faster SBML I/O")
    /home/sbrg-cjlloyd/cobrapy/cobra/io/__init__.py:12: UserWarning: cobra.io.sbml requires libsbml
      warn("cobra.io.sbml requires libsbml")

## Overview

COBRAme is constructed entirely over COBRApy. This means that ME-model reactions will have all of the same properties, methods, and functions as a COBRApy reaction. However, one key difference between M and ME models is that many reactions involved in gene expression are effecively templates that are constructed identically but vary based on characteristics of the gene being expressed. For example, a gene with a given nucleotide sequence is always translated following the same rules provided by the codon table for that organism.

In order to facilliate the template nature of many gene expression reactions, COBRAme reactions are constructed and their components are manipulated through the use of `ProcessData` classes. These act as information vessels for holding the information assocatied with a cellular process in simple, standard datatypes such as dictionaries and strings.

-----

This tutorial will go step-by-step through the process of creating a generic enzyme catalyzed reaction (i.e. metabolic reaction):

which requires the formation and coupling of **complex\_ab** in order to proceed.

-------

In order for this reaction to carry flux in the model we will additionally need to first add the corresponding:

 1. **Transcription reactions**
 2. **Translation reactions**
 3. **tRNA charging reactions**
 4. **Complex formation reactions**

Once these are added we will add in the synthesis of key macromolecular components (ribosome, RNA polymerase, etc.) and show how they are coupled to their respective reactions. The derived coupling coefficients will also be described. For more on the derivation of the coupling coefficients, reference the supplemental text of [O'brien et. al. 2013](https://www.ncbi.nlm.nih.gov/pubmed/24084808)

## Initializing new ME-Models

When applying some constraints in the ME-model, metabolite properties are required. For instance, to calculate the total biomass (by molecular weight) produced by a particular macromolecule, the amino acid, nucleotide, etc. molecular weights are required. To enable these calculations, all metabolites from *i*JO1366, along with their metabolite attributes are added to the newly initialized ME-model.

Further the reactions from *i*JO1366 will be added to the ME-model to demonstrate ME-model solving procedures.

```python
# create empty ME-model
me = cobrame.MEModel('test')
ijo = cobra.test.create_test_model('ecoli')
```

```python
# Add all metabolites and reactions from iJO1366 to the new ME-model
for met in ijo.metabolites:
    me.add_metabolites(met)
for rxn in ijo.reactions:
    me.add_reaction(rxn)
```

The ME-model contains a "global_info" attribute which stores information used to calculate coupling constraints, along with other functions. The specifics of each of these constraints will be discussed when they are implemented.

**Note**: k_deg will initially be set to 0. We will apply RNA degradation later in the tutorial.

```python
# "Translational capacity" of organism
me.global_info['kt'] = 4.5  # (in h-1)scott 2010, RNA-to-protein curve fit
me.global_info['r0'] = 0.087  # scott 2010, RNA-to-protein curve fit
me.global_info['k_deg'] = 1.0/5. * 60.0  # 1/5 1/min 60 min/h # h-1

# Molecular mass of RNA component of ribosome
me.global_info['m_rr'] = 1453. # in kDa

# Average molecular mass of an amino acid
me.global_info['m_aa'] = 109. / 1000.  # in kDa

# Proportion of RNA that is rRNA
me.global_info['f_rRNA'] = .86
me.global_info['m_nt'] = 324. / 1000.  # in kDa
me.global_info['f_mRNA'] = .02

# tRNA associated global information
me.global_info['m_tRNA'] = 25000. / 1000.  # in kDA
me.global_info['f_tRNA'] = .12

# Define the types of biomass that will be synthesized in the model
me.add_biomass_constraints_to_model(["protein_biomass", "mRNA_biomass", "tRNA_biomass", "rRNA_biomass",
                                     "ncRNA_biomass", "DNA_biomass", "lipid_biomass", "constituent_biomass",
                                     "prosthetic_group_biomass", "peptidoglycan_biomass"])
```

Define sequence of gene that will be expressed in tutorial

```python
sequence = ("ATG" + "TTT" * 12 + "TAT" * 12 +
            "ACG" * 12 + "GAT" * 12 + "AGT" * 12 + "TGA")
```

## Adding Reactions without utility functions
We'll first demonstrate how transcription, translation, tRNA charging, complex formation, and metabolic reactions can be added to a model without using any of the utility functions provided in `cobrame.util.building.py`. The second half of the tutorial will show how these utility functions can be used to add these reactions.

The basic workflow for adding any reaction to a ME-model using COBRAme occurs in three steps:

1. **Create the ProcessData(s) associated with the reaction and populate them with the necessary information**
2. **Create the MEReaction and link the appropriate ProcessData**
3. **Execute the MEReaction's update method**

### Add Transcription Reaction

#### Add TranscribedGene metabolite to model
Transcription reactions is unique in that they occur at a transcription unit level and can code for multiple transcript products. Therefore the nucleotide sequence of both the transcription unit and the RNA transcripts must be defined in order to correctly construct a transcription reaction.

```python
gene = cobrame.TranscribedGene('RNA_a', 'mRNA', sequence)
me.add_metabolites([gene])
```

When adding the `TranscribedGene` above, the `RNA_type` and `nucleotide_sequence` was assigned to the gene. This sequence cannot be determined from the transcription unit (TU) sequence because a single TU often contains several different RNAs.

#### Add TranscriptionData to model

```python
transcription_data = cobrame.TranscriptionData('TU_a',me,rna_products={'RNA_a'})
transcription_data.nucleotide_sequence = sequence
```

#### Add TranscriptionReaction to model
And point `TranscriptionReaction` to `TranscriptionData`

```python
transcription_rxn = cobrame.TranscriptionReaction('transcription_TU_a')
transcription_rxn.transcription_data = transcription_data
me.add_reactions([transcription_rxn])
```

#### Update TranscriptionReaction

```python
transcription_rxn.update()
print(transcription_rxn.reaction)
```

    86 atp_c + 38 ctp_c + 12 gtp_c + 50 utp_c --> RNA_a + 59.172286 mRNA_biomass + 186 ppi_c

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:813 UserWarning: RNA Polymerase () not found

**Note**: the RNA_polymerase complex is not included in the reaction. This will be added later

This reaction now produces a small amount of the a `mRNA_biomass` metabolite (constraint). This term has a coefficient corresponding to the molecular weight (in $kDA$) of the RNA being transcribed. This constraint will be implemented into a $v_{biomasss\_dilution}$ reaction with the form:
$$\mu \leq v_{biomass\_dilution} \leq \mu $$

A mathematical description of the `biomass` constraint can be found in *Biomass Dilution Constraints* in **ME-Model Fundamentals**.

**Note**: This is not a complete picture of transcription because the RNA polymerase is missing.

#### Incorporate RNA Polymerase
For the purposes of this tutorial, we'll skip the steps required to add the reactions to form the RNA_polymerase. The steps however are identical to those outlined in **add enzyme complexes** below

```python
RNAP = cobrame.RNAP('RNA_polymerase')
me.add_metabolites(RNAP)
```

Associate RNA_polymerase with all `TranscriptionData` and update

```python
for data in me.transcription_data:
    data.RNA_polymerase = RNAP.id
me.reactions.transcription_TU_a.update()

print(me.reactions.transcription_TU_a.reaction)
```

    0.00088887053605567*mu + 0.000347992814865795 RNA_polymerase + 86 atp_c + 38 ctp_c + 12 gtp_c + 50 utp_c --> RNA_a + 59.172286 mRNA_biomass + 186 ppi_c

The coefficient for RNA_polymerase is the first instance in this tutorial where a coupling constraint is imposed. In this case the constraint couples the formation of a RNA_polymerase metabolite to its transcription flux. This constraint is formulated as in [O'brien et. al. 2013](https://www.ncbi.nlm.nih.gov/pubmed/24084808), with assumption that $k_{rnap} = 3 \cdot k_{ribosome}$ based on data from [Proshkin et al. 2010](https://www.ncbi.nlm.nih.gov/pubmed/20413502):

$$
\begin{align}
v_{dilution,RNAP, j} =  \frac{l_{TU,j}}{3 c_{ribo}\kappa_{\tau}} v_{transcription,j} (\mu+r_0\kappa_{\tau}), & \forall j \in TU
\end{align}
$$

where:

 - $\kappa_{\tau}$ and $r_0$ are phenomenological parameters from [Scott et. al. 2010](https://www.ncbi.nlm.nih.gov/pubmed/21097934) that describe the linear relationship between the observed RNA/protein ratio of *E. coli* and its growth rate ($\mu$)

 - $c_{ribo} = \frac{m_{rr}}{f_{rRNA}\cdot m_{aa}}$ where: $m_{nt}$ is the mass of rRNA per ribosome. $f_{rRNA}$ is the fraction of total RNA that is rRNA $m_{aa}$ is the molecular weight of an average amino acid

 - $v_{transcription, j}$ is the rate of transcription for $TU_j$

 - $l_{TU, j}$ is number of nucleotides in $TU_j$

----
### Add Translation Reaction
#### Add TranslationData to model
In order to add a TranslationData object to a ME-model the user must additionally specifify the mRNA id and protein id of the translation reaction that will be added. This information as well as a nucleotide sequence is the only information required to add a translation reaction.

```python
data = cobrame.TranslationData('a', me, 'RNA_a', 'protein_a')
data.nucleotide_sequence = sequence
```

#### Add TranslationReaction to model
By associating the TranslationReaction with its corresponding TranslationData object and running the update function, COBRAme will create a reaction reaction for the nucleotide sequence given based on the organisms codon table and prespecified translation machinery.

```python
rxn = cobrame.TranslationReaction('translation_a')
rxn.translation_data = data
me.add_reaction(rxn)
```

#### Update TranslationReaction

```python
rxn.update()
print(rxn.reaction)
```

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:1051 UserWarning: ribosome not found
    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:1094 UserWarning: RNA_degradosome not found

    0.000498399634202103*mu + 0.000195123456790123 + 0.00598079561042524*(mu + 0.3915)/mu RNA_a + 12 asp__L_c + 0.276611796982167*(mu + 0.3915)/mu atp_c + 0.353897348367627*(mu + 0.3915)/mu mRNA_biomass + met__L_c + 12 phe__L_c + 12 ser__L_c + 12 thr__L_c + 12 tyr__L_c --> 0.276611796982167*(mu + 0.3915)/mu adp_c + 0.514348422496571*(mu + 0.3915)/mu amp_c + 0.227270233196159*(mu + 0.3915)/mu cmp_c + 0.0717695473251029*(mu + 0.3915)/mu gmp_c + 60.0 - 0.276611796982167*(mu + 0.3915)/mu h2o_c + 0.276611796982167*(mu + 0.3915)/mu h_c + 0.276611796982167*(mu + 0.3915)/mu pi_c + protein_a + 7.500606 protein_biomass + 0.299039780521262*(mu + 0.3915)/mu ump_c

In this case the constraint couples the formation of a mRNA metabolite to its translation flux. This constraint is formulated as in [O'brien et. al. 2013](https://www.ncbi.nlm.nih.gov/pubmed/24084808):

$$
\begin{align}
v_{dilution,j} = \frac{3}{\kappa_{\tau} c_{mRNA}} \cdot (\mu + \kappa_{\tau} r_0) v_{translation,j}  , & &  \forall j \in mRNA
\end{align}
$$

where:

 - $\kappa_{\tau}$ and $r_0$ are phenomenological parameters from [Scott et. al. 2010](https://www.ncbi.nlm.nih.gov/pubmed/21097934) that describe the linear relationship between the observed RNA/protein ratio of *E. coli* and its growth rate ($\mu$)

 - $c_{mRNA} = \frac{m_{nt}}{f_{mRNA}\cdot m_{aa}}$ where: $m_{nt}$ is the molecular weight of an average mRNA nucleotide. $f_{mRNA}$ is the fraction of total RNA that is mRNA $m_{aa}$ is the molecular weight of an average amino acid

 - $v_{translation, j}$ is the rate of translation for $mRNA_j$

#### Incorporate Ribosome

```python
ribosome = cobrame.Ribosome('ribosome')
me.add_metabolites([ribosome])
me.reactions.translation_a.update()
print(me.reactions.translation_a.reaction)
```

    0.000498399634202103*mu + 0.000195123456790123 + 0.00598079561042524*(mu + 0.3915)/mu RNA_a + 12 asp__L_c + 0.276611796982167*(mu + 0.3915)/mu atp_c + 0.353897348367627*(mu + 0.3915)/mu mRNA_biomass + met__L_c + 12 phe__L_c + 0.000874533914506385*mu + 0.00034238002752925 ribosome + 12 ser__L_c + 12 thr__L_c + 12 tyr__L_c --> 0.276611796982167*(mu + 0.3915)/mu adp_c + 0.514348422496571*(mu + 0.3915)/mu amp_c + 0.227270233196159*(mu + 0.3915)/mu cmp_c + 0.0717695473251029*(mu + 0.3915)/mu gmp_c + 60.0 - 0.276611796982167*(mu + 0.3915)/mu h2o_c + 0.276611796982167*(mu + 0.3915)/mu h_c + 0.276611796982167*(mu + 0.3915)/mu pi_c + protein_a + 7.500606 protein_biomass + 0.299039780521262*(mu + 0.3915)/mu ump_c

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:1094 UserWarning: RNA_degradosome not found

This imposes a new coupling constraint for the ribosome. In this case the constraint couples the formation of a ribosome to its translation flux. This constraint is formulated as in [O'brien et. al. 2013](https://www.ncbi.nlm.nih.gov/pubmed/24084808):

$$
\begin{align}
v_{dilution,ribo, j} = \frac{l_{p,j}}{c_{ribo}\kappa_{\tau}} v_{translation,j} (\mu+r_0\kappa_{\tau}) , & \forall j \in mRNA
\end{align}
$$

where:

 - $\kappa_{\tau}$ and $r_0$ are phenomenological parameters from [Scott et. al. 2010](https://www.ncbi.nlm.nih.gov/pubmed/21097934) that describe the linear relationship between the observed RNA/protein ratio of *E. coli* and its growth rate ($\mu$)

 - $c_{ribo} = \frac{m_{rr}}{f_{rRNA}\cdot m_{aa}}$ where: $m_{nt}$ is the mass of rRNA per ribosome. $f_{rRNA}$ is the fraction of total RNA that is rRNA $m_{aa}$ is the molecular weight of an average amino acid

 - $v_{translation, j}$ is the rate of translation for $mRNA_j$

 - $l_{p, j}$ is number of amino acids in peptide translated from $mRNA_j$

-------

**Note**: The above reactions do not provide a complete picture of translation in that it is missing charged tRNAs to facillitate tRNA addition.

Below, we'll correct this by adding in an tRNA charging reaction.

### Add tRNA Charging Reaction

#### Add tRNAData to model

```python
# Must add tRNA metabolite first
gene = cobrame.TranscribedGene('RNA_d', 'tRNA', sequence)
me.add_metabolites([gene])
```

```python
data = cobrame.tRNAData('tRNA_d_GUA', me, 'val__L_c', 'RNA_d', 'GUA')
```

#### Add tRNAChargingReaction to model
And point `tRNAChargingReaction` to `tRNAData`

```python
rxn = cobrame.tRNAChargingReaction('charging_tRNA_d_GUA')
me.add_reaction(rxn)
rxn.tRNA_data = data
```

#### Update tRNAChargingReaction

```python
#Setting verbose=False suppresses print statements indicating that new metabolites were created
rxn.update(verbose=False)
print(rxn.reaction)
```

    0.000116266666666667*mu + 4.55184e-5 RNA_d + 0.000116266666666667*mu + 4.55184e-5 val__L_c --> generic_tRNA_GUA_val__L_c

This reaction creates one ```generic_charged_tRNA``` equivalement that can then be used in a translation reaction

The coefficient for ```RNA_d``` and ```lys__L_c``` are defined by:

$$
\begin{align}
v_{dilution,j} \geq \frac{1}{\kappa_{\tau} c_{tRNA,j}} (\mu + \kappa_{\tau} r_0)  v_{charging,j} , & \forall j \in tRNA
\end{align}
$$

where:

 - $\kappa_{\tau}$ and $r_0$ are phenomenological parameters from [Scott et. al. 2010](https://www.ncbi.nlm.nih.gov/pubmed/21097934) that describe the linear relationship between the observed RNA/protein ratio of *E. coli* and its growth rate ($\mu$)

 - $c_{tRNA, j} = \frac{m_{tRNA}}{f_{tRNA}\cdot m_{aa}}$ where: $m_{tRNA}$ is molecular weight of an average tRNA. $f_{tRNA}$ is the fraction of total RNA that is tRNA $m_{aa}$ is the molecular weight of an average amino acid

 - $v_{charging, j}$ is the rate of charging for $tRNA_j$

**Note**: This tRNA charging reaction is still missing a tRNA synthetase which catalyzes the amino acid addition to the uncharged tRNA.

#### Incorporate tRNA Synthetases
.. autoclass:: cobrame.core.component.Complex
   :noindex:

```python
synthetase = cobrame.Complex('synthetase')
me.add_metabolites(synthetase)
```

Associate synthetase with `tRNAData` and update

```python
data.synthetase = synthetase.id
rxn.update()
print(rxn.reaction)
```

    0.000116266666666667*mu + 4.55184e-5 RNA_d + 4.27350427350427e-6*mu*(0.000116266666666667*mu + 1.0000455184) synthetase + 0.000116266666666667*mu + 4.55184e-5 val__L_c --> generic_tRNA_GUA_val__L_c

The synthetase coupling was reformulated from [O'brien et. al. 2013](https://www.ncbi.nlm.nih.gov/pubmed/24084808) enable more modularity in the ME-model. A more complete mathematical description of the tRNA synthetase coupling constraints can be found in the [tRNA.ipynb](tRNA.ipynb)

---

### Add tRNAs to Translation

Here we take advantage of an additional subclass of `ProcessData`, called a `SubreactionData` object. This class is used to lump together processeses that occur as a result of many individual reactions, including translation elongation, ribosome formation, tRNA modification, etc. Since each of these steps often involve an enzyme that requires its own coupling constraint, this process allows these processes to be lumped into one reaction while still enabling each subprocess to be modified.

`TranslationData` objects have an `subreaction_from_sequence` method that returns any subreactions that have been added to the model and are part of translation elongation (i.e. tRNA). Since no tRNA-mediated amino acid addition subreactions have been added to the model, the below call returns nothing.

```python
print(me.process_data.a.subreactions_from_sequence)
```

    {}

    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction phe_addition_at_UUU not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction tyr_addition_at_UAU not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction thr_addition_at_ACG not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction asp_addition_at_GAU not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction ser_addition_at_AGU not in model

`UserWarnings` are returned to indicate that tRNA subreactions have not been added for each codon.

Below, we add the SubreactionData (excluding enzymes) for the addition of an amino acid using information from the *E. coli* codon table. The charge tRNA does not act as an enzyme in this case because it's coupling is handled in the `tRNAChargingReaction`

#### Add Subreactions for tRNA addition to model

```python
data = cobrame.SubreactionData('asp_addition_at_GAU', me)
data.stoichiometry = {'generic_tRNA_GAU_asp__L_c': -1,
                      'gtp_c': -1, 'gdp_c': 1, 'h_c': 1,
                      'pi_c': 1}
```

Now calling `subreactions_from_sequence` returns the number of tRNA subreactions that should be added to the `TranslationData`

```python
translation_subreactions = me.process_data.a.subreactions_from_sequence
print(translation_subreactions)
```

    {'asp_addition_at_GAU': 12}

    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction phe_addition_at_UUU not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction tyr_addition_at_UAU not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction thr_addition_at_ACG not in model
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:826 UserWarning: tRNA addition subreaction ser_addition_at_AGU not in model

Updating `TranslationData.subreactions` with the tRNA subreactions incorporates this information into the `TranslationReaction`

```python
print("Before adding tRNA subreaction")
print("------------------------------")
print(me.reactions.translation_a.reaction)
print("")
# Link tranlation_data to subreactions and update
for subreaction, value in translation_subreactions.items():
    me.process_data.a.subreactions[subreaction] = value
me.reactions.translation_a.update(verbose=False)
print("After adding tRNA subreaction")
print("-----------------------------")
print(me.reactions.translation_a.reaction)
```

    Before adding tRNA subreaction
    ------------------------------
    0.000498399634202103*mu + 0.000195123456790123 + 0.00598079561042524*(mu + 0.3915)/mu RNA_a + 12 asp__L_c + 0.276611796982167*(mu + 0.3915)/mu atp_c + 0.353897348367627*(mu + 0.3915)/mu mRNA_biomass + met__L_c + 12 phe__L_c + 0.000874533914506385*mu + 0.00034238002752925 ribosome + 12 ser__L_c + 12 thr__L_c + 12 tyr__L_c --> 0.276611796982167*(mu + 0.3915)/mu adp_c + 0.514348422496571*(mu + 0.3915)/mu amp_c + 0.227270233196159*(mu + 0.3915)/mu cmp_c + 0.0717695473251029*(mu + 0.3915)/mu gmp_c + 60.0 - 0.276611796982167*(mu + 0.3915)/mu h2o_c + 0.276611796982167*(mu + 0.3915)/mu h_c + 0.276611796982167*(mu + 0.3915)/mu pi_c + protein_a + 7.500606 protein_biomass + 0.299039780521262*(mu + 0.3915)/mu ump_c

    After adding tRNA subreaction
    -----------------------------
    0.000498399634202103*mu + 0.000195123456790123 + 0.00598079561042524*(mu + 0.3915)/mu RNA_a + 12 asp__L_c + 0.276611796982167*(mu + 0.3915)/mu atp_c + 12.0 generic_tRNA_GAU_asp__L_c + 12.0 gtp_c + 0.353897348367627*(mu + 0.3915)/mu mRNA_biomass + met__L_c + 12 phe__L_c + 0.000874533914506385*mu + 0.00034238002752925 ribosome + 12 ser__L_c + 12 thr__L_c + 12 tyr__L_c --> 0.276611796982167*(mu + 0.3915)/mu adp_c + 0.514348422496571*(mu + 0.3915)/mu amp_c + 0.227270233196159*(mu + 0.3915)/mu cmp_c + 12.0 gdp_c + 0.0717695473251029*(mu + 0.3915)/mu gmp_c + 60.0 - 0.276611796982167*(mu + 0.3915)/mu h2o_c + 12.0 + 0.276611796982167*(mu + 0.3915)/mu h_c + 12.0 + 0.276611796982167*(mu + 0.3915)/mu pi_c + protein_a + 7.500606 protein_biomass + 0.299039780521262*(mu + 0.3915)/mu ump_c

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:1094 UserWarning: RNA_degradosome not found
    /home/sbrg-cjlloyd/cobrame/cobrame/core/processdata.py:229 UserWarning: No element contribution input for subreaction (asp_addition_at_GAU), calculating based on stoichiometry instead

### Add Complex Formation Reaction
#### Add ComplexData to model
For COBRAme models, the reaction gene-protein-reaction rule (GPR) is replaced with a metabolite representing the synthesis of the enzyme(s) catalyzing a reaction. This metabolite is formed explicitly in a ME model by seperate reaction to transcribe the gene(s) and translate the protein(s) the compose the complex.

```python
data = cobrame.ComplexData('complex_ab', me)
data.stoichiometry = {'protein_a': 1, 'protein_b': 1}
```

#### Add  ComplexFormation reaction to model
And point `ComplexFormation` to `ComplexData`
.. autoclass:: cobrame.core.reaction.ComplexFormation
   :noindex:

```python
rxn = cobrame.ComplexFormation('formation_complex_ab')
me.add_reaction(rxn)
rxn.complex_data_id = data.id
rxn._complex_id = data.id
```

#### Update ComplexFormation reaction

```python
rxn.update(verbose=False)
print(me.reactions.formation_complex_ab.reaction)
```

    protein_a + protein_b --> complex_ab

#### Apply modification to complex formation reaction
Many enzyme complexes in an ME-model require cofactors or prosthetic groups in order to properly function. Information about such processes are stored as ModificationData.

For instance, we can add the modification of an iron-sulfur cluster, a common prosthetic group, by doing the following:

```python
# Define the stoichiometry of the modification
mod_data = cobrame.SubreactionData('mod_2fe2s_c', me)
mod_data.stoichiometry = {'2fe2s_c': -1}
# this process can also be catalyzed by a chaperone
mod_data.enzyme = 'complex_ba'
mod_data.keff = 65.  # default value
```

Associate modification to complex and ```update()``` its formation

```python
complex_data = me.process_data.complex_ab
complex_data.subreactions['mod_2fe2s_c'] = 1
```

Update ComplexFormation reaction

```python
print('Before adding modification')
print('--------------------------')
print(me.reactions.formation_complex_ab.reaction)
me.reactions.formation_complex_ab.update()
print('\n')
print('After adding modification')
print('-------------------------')
print(me.reactions.formation_complex_ab.reaction)
```

    Before adding modification
    --------------------------
    protein_a + protein_b --> complex_ab
    Created <Complex complex_ba at 0x7f4bf69a8b38> in <ComplexFormation formation_complex_ab at 0x7f4bf5f13ef0>

    After adding modification
    -------------------------
    2fe2s_c + 4.27350427350427e-6*mu complex_ba + protein_a + protein_b --> complex_ab + 0.17582 prosthetic_group_biomass

### Add Metabolic Reaction
#### Add StoichiometricData to model
MetabolicReactions require, at a minimum, one corresponding StoichiometricData. StoichiometricData essentially holds the information contained in an M-model reaction. This includes the metabolite stoichiometry and the upper and lower bound of the reaction. As a best practice, StoichiometricData typically uses an ID equivalent to the M-model reaction ID.

So first, we will create a StoichiometricData object to define the stoichiometry of the conversion of *a* to *b*. **Only one StoichiometricData object should be created for both reversible and irreversible reactions**

```python
# unique to COBRAme, construct a stoichiometric data object with the reaction information
data = cobrame.StoichiometricData('a_to_b', me)
stoichiometry = {'a':-1, 'b': 1}
data._stoichiometry = stoichiometry
data.lower_bound = -1000
data.upper_bound = 1000
```

#### Add MetabolicReaction to model

The StoichiometricData for this reversible reaction is then assigned to two different MetabolicReactions (Due to the enzyme dilution constraint, all enzyme catalyzed reactions must be reverisble; more on this later). The MetabolicReactions require:
 - The associated StoichiometricData
 - The *reverse* flag set to True for reverse reactions, False for forward reactions
 - Enzyme $K_{eff}$ for reaction (discussed later, dafault=65)

These fields are then processed and the actual model reaction is created using the MetabolicReaction's update() function

```python
# Create a forward ME Metabolic Reaction and associate the stoichiometric data to it
rxn_fwd = cobrame.MetabolicReaction('a_to_b_FWD_complex_ab')
me.add_reaction(rxn_fwd)
rxn_fwd.stoichiometric_data = data
rxn_fwd.reverse = False
rxn_fwd.keff = 65.

# Create a reverse ME Metabolic Reaction and associate the stoichiometric data to it
rxn_rev = cobrame.MetabolicReaction('a_to_b_REV_complex_ab')
me.add_reaction(rxn_rev)
rxn_rev.stoichiometric_data = data
rxn_rev.reverse = True
rxn_rev.keff = 65.
```

#### Update MetabolicReactions

```python
rxn_fwd.update(verbose=False)
rxn_rev.update(verbose=False)
print(me.reactions.a_to_b_FWD_complex_ab.reaction)
print(me.reactions.a_to_b_REV_complex_ab.reaction)
```

    a --> b
    b --> a

**Note**: the $k_{eff}$ and `complex_ab` is not included in the reaction since no complex has been associated to it yet

#### Associate enzyme with MetabolicReaction

The ComplexData object created in the previous cell can be incorporated into the MetabolicReaction using code below.

**Note**: the update() function is required to apply the change.

```python
data = me.process_data.complex_ab
me.reactions.a_to_b_FWD_complex_ab.complex_data = data
print('Forward reaction (before update): %s' %
      (me.reactions.a_to_b_FWD_complex_ab.reaction))
me.reactions.a_to_b_FWD_complex_ab.update()
print('Forward reaction (after update): %s' %
      (me.reactions.a_to_b_FWD_complex_ab.reaction))
print('')

me.reactions.a_to_b_REV_complex_ab.complex_data = data
print('Reverse reaction (before update): %s' %
      (me.reactions.a_to_b_REV_complex_ab.reaction))
me.reactions.a_to_b_REV_complex_ab.update()
print('Reverse reaction (after update): %s' %
      (me.reactions.a_to_b_REV_complex_ab.reaction))
```

    Forward reaction (before update): a --> b
    Forward reaction (after update): a + 4.27350427350427e-6*mu complex_ab --> b

    Reverse reaction (before update): b --> a
    Reverse reaction (after update): b + 4.27350427350427e-6*mu complex_ab --> a

The coefficient for complex_ab is determined by the expression $$\frac{\mu}{k_{eff}}$$ which in its entirety represents the dilution of an enzyme following a cell doubling. The coupling constraint can be summarized as followed
$$
\begin{align}
&v_{dilution,j} = \mu \sum_{i} \left( \frac{1}{k_{eff,i}} v_{usage,i} \right), & \forall j \in Enzyme
\end{align}
$$

Where

 - $v_{usage,i}$ is the flux through the metabolic reaction
 - $k_{eff}$ is the turnover rate for the process and conveys the productivity of the enzyme complex. Physically, it can be thought of as the number of reactions the enzyme can catalyze per cell division.

By default the $k_{eff}$ for a MetabolicReaction is set to 65 but this can be changed using the code below.

#### Different Keff for forward reaction

```python
me.reactions.a_to_b_FWD_complex_ab.keff = .00001
me.reactions.a_to_b_FWD_complex_ab.update()

# The forward and reverse direction can have differing keffs
print('Forward reaction')
print('----------------')
print(me.reactions.a_to_b_FWD_complex_ab.reaction)
print('')
print('Reverse reaction')
print('----------------')
print(me.reactions.a_to_b_REV_complex_ab.reaction)
```

    Forward reaction
    ----------------
    a + 27.7777777777778*mu complex_ab --> b

    Reverse reaction
    ----------------
    b + 4.27350427350427e-6*mu complex_ab --> a

## Adding Reactions using utility functions
Add reactions using some of the utility functions provided in `cobrame.util.building.py`

### Transcription
Using the utility functions to create the TranscribedGene metabolite has the advantage of forcing the assignment of sequence, strand and RNA_type.

```python
building.create_transcribed_gene(me, 'b','tRNA', 'ATCG')
building.add_transcription_reaction(me, 'TU_b', {'b'}, sequence)
print(me.reactions.transcription_TU_b.reaction)
me.reactions.transcription_TU_b.update()
```

    86 atp_c + 38 ctp_c + 12 gtp_c + 182 h2o_c + 50 utp_c --> RNA_b + 85 amp_c + 37 cmp_c + 11 gmp_c + 182 h_c + 186 ppi_c + 1.2817349999999998 tRNA_biomass + 49 ump_c

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:813 UserWarning: RNA Polymerase () not found

### Translation
`add_translation_reaction` assumes that the RNA and protein have the same locus_id. It creates the appropriate `TranslationData` and `TranslationReaction` instance, links the two together and updates the `TranslationReaction`.

```python
building.add_translation_reaction(me, 'b', dna_sequence=sequence, update=True)
print(me.reactions.translation_b.reaction)
```

    /home/sbrg-cjlloyd/cobrame/cobrame/core/reaction.py:1094 UserWarning: RNA_degradosome not found

    0.000498399634202103*mu + 0.000195123456790123 + 0.00598079561042524*(mu + 0.3915)/mu RNA_b + 12 asp__L_c + 0.00448559670781893*(mu + 0.3915)/mu atp_c + 0.0076657950617284*(mu + 0.3915)/mu mRNA_biomass + met__L_c + 12 phe__L_c + 0.000874533914506385*mu + 0.00034238002752925 ribosome + 12 ser__L_c + 12 thr__L_c + 12 tyr__L_c --> 0.00448559670781893*(mu + 0.3915)/mu adp_c + 0.00598079561042524*(mu + 0.3915)/mu amp_c + 0.00598079561042524*(mu + 0.3915)/mu cmp_c + 0.00598079561042524*(mu + 0.3915)/mu gmp_c + 60.0 - 0.00448559670781893*(mu + 0.3915)/mu h2o_c + 0.00448559670781893*(mu + 0.3915)/mu h_c + 0.00448559670781893*(mu + 0.3915)/mu pi_c + protein_b + 7.500606 protein_biomass + 0.00598079561042524*(mu + 0.3915)/mu ump_c

### Complex Formation
Alternatively, ComplexData has a ```create_complex_formation()``` function to create the sythesis reaction following the naming conventions. It contains an ```update()``` function which incorporates changes in the ComplexData

```python
data = cobrame.ComplexData('complex_ba', me)
data.stoichiometry = {'protein_a': 1, 'protein_b': 1}
data.create_complex_formation()
print(me.reactions.formation_complex_ba.reaction)
```

    protein_a + protein_b --> complex_ba

### Metabolic Reaction

```python
stoich_data = cobrame.StoichiometricData('b_to_c', me)
stoich_data._stoichiometry = {'b': -1, 'c': 1}
stoich_data.lower_bound = 0
stoich_data.upper_bound = 1000.
building.add_metabolic_reaction_to_model(me, stoich_data.id, 'forward', complex_id='complex_ab',
                                         update=True)
print('Reaction b_to_c')
print('---------------')
print(me.reactions.b_to_c_FWD_complex_ab.reaction)
```

    Created <Metabolite c at 0x7f4bf6abf6d8> in <MetabolicReaction b_to_c_FWD_complex_ab at 0x7f4bf6abf7b8>
    Reaction b_to_c
    ---------------
    b + 4.27350427350427e-6*mu complex_ab --> c


# Rich Output

In Python, objects can declare their textual representation using the `__repr__` method.  IPython expands on this idea and allows objects to declare other, rich representations including:

* HTML
* JSON
* PNG
* JPEG
* SVG
* LaTeX

A single object can declare some or all of these representations; all are handled by IPython's **display system**. This Notebook shows how you can use this display system to incorporate a broad range of content into your Notebooks.

## Basic display imports

The `display` function is a general purpose tool for displaying different representations of objects. Think of it as `print` for these rich representations.

```python
from IPython.display import display
```

A few points:

* Calling `display` on an object will send **all** possible representations to the Notebook.
* These representations are stored in the Notebook document.
* In general the Notebook will use the richest available representation.

If you want to display a particular representation, there are specific functions for that:

```python
from IPython.display import (
    display_pretty, display_html, display_jpeg,
    display_png, display_json, display_latex, display_svg
)
```

## Images

To work with images (JPEG, PNG) use the `Image` class.

```python
from IPython.display import Image
```

```python
i = Image(filename='./ipython_logo.png')
```

Returning an `Image` object from an expression will automatically display it:

```python
i
```

Or you can pass an object with a rich representation to `display`:

```python
display(i)
```

An image can also be displayed from raw data or a URL.

```python
Image(url='http://python.org/images/python-logo.gif')
```

## HTML

Python objects can declare HTML representations that will be displayed in the Notebook. If you have some HTML you want to display, simply use the `HTML` class.

```python
from IPython.display import HTML
```

```python
s = """<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>row 1, cell 1</td>
<td>row 1, cell 2</td>
</tr>
<tr>
<td>row 2, cell 1</td>
<td>row 2, cell 2</td>
</tr>
</table>"""
```

```python
h = HTML(s)
```

```python
display(h)
```

You can also use the `%%html` cell magic to accomplish the same thing.

```python
%%html
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td>row 1, cell 1</td>
<td>row 1, cell 2</td>
</tr>
<tr>
<td>row 2, cell 1</td>
<td>row 2, cell 2</td>
</tr>
</table>
```

## JavaScript

The Notebook also enables objects to declare a JavaScript representation. At first, this may seem odd as  output is inherently visual and JavaScript is a programming language. However, this opens the door for rich output that leverages the full power of JavaScript and associated libraries such as [d3.js](http://d3js.org) for output.

```python
from IPython.display import Javascript
```

Pass a string of JavaScript source code to the `JavaScript` object and then display it.

```python
js = Javascript('alert("hi")');
```

```python
display(js)
```

The same thing can be accomplished using the `%%javascript` cell magic:

```javascript
%%javascript

alert("hi");
```

## LaTeX

The IPython display system also has builtin support for the display of mathematical expressions typeset in LaTeX, which is rendered in the browser using [MathJax](http://mathjax.org).

You can pass raw LaTeX test as a string to the `Math` object:

```python
from IPython.display import Math
Math(r'F(k) = \int_{-\infty}^{\infty} f(x) e^{2\pi i k} dx')
```

With the `Latex` class, you have to include the delimiters yourself.  This allows you to use other LaTeX modes such as `eqnarray`:

```python
from IPython.display import Latex
Latex(r"""\begin{eqnarray}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
\nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{eqnarray}""")
```

Or you can enter LaTeX directly with the `%%latex` cell magic:

```latex
%%latex
\begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
\nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{align}
```

## Audio

IPython makes it easy to work with sounds interactively. The `Audio` display class allows you to create an audio control that is embedded in the Notebook. The interface is analogous to the interface of the `Image` display class. All audio formats supported by the browser can be used. Note that no single format is presently supported in all browsers.

```python
from IPython.display import Audio
Audio(url="http://www.nch.com.au/acm/8k16bitpcm.wav")
```

A NumPy array can be auralized automatically. The `Audio` class normalizes and encodes the data and embeds the resulting audio in the Notebook.

For instance, when two sine waves with almost the same frequency are superimposed a phenomena known as [beats](https://en.wikipedia.org/wiki/Beat_%28acoustics%29) occur. This can be auralised as follows:

```python
import numpy as np
max_time = 3
f1 = 220.0
f2 = 224.0
rate = 8000.0
L = 3
times = np.linspace(0,L,rate*L)
signal = np.sin(2*np.pi*f1*times) + np.sin(2*np.pi*f2*times)

Audio(data=signal, rate=rate)
```

## Video

More exotic objects can also be displayed, as long as their representation supports the IPython display protocol.  For example, videos hosted externally on YouTube are easy to load:

```python
from IPython.display import YouTubeVideo
YouTubeVideo('sjfsUzECqK0')
```

Using the nascent video capabilities of modern browsers, you may also be able to display local
videos.  At the moment this doesn't work very well in all browsers, so it may or may not work for you;
we will continue testing this and looking for ways to make it more robust.

The following cell loads a local file called  `animation.m4v`, encodes the raw video as base64 for http
transport, and uses the HTML5 video tag to load it. On Chrome 15 it works correctly, displaying a control bar at the bottom with a play/pause button and a location slider.

```python
from IPython.display import HTML
from base64 import b64encode
video = open("./animation.m4v", "rb").read()
video_encoded = b64encode(video).decode('ascii')
video_tag = '<video controls alt="test" src="data:video/x-m4v;base64,{0}">'.format(video_encoded)
HTML(data=video_tag)
```

## External sites

You can even embed an entire page from another site in an iframe; for example this is today's Wikipedia
page for mobile users:

```python
from IPython.display import IFrame
IFrame('http://ipython.org', width='100%', height=350)
```

# Exercises

## Basic display classes

```python
from IPython.display import display, Image, HTML, Javascript
```

Try using the basic display classes in a notebook.
- Find an image on the internet and display it in a notebook using the Image class. You can either save the image in the notebook's directory or load it by its URL.

```python

```

- Use the HTML class and the %%html cell magic to display a snippet of HTML.

```python

```

```python

```

- Use the JavaScript class and the %%javascript cell magic to run a snippet of JavaScript.

```python

```

```python

```


Name: Aly Baughman and Cynthia Parks|Student ID:1923165, 2303535|Email:baugh107@mail.chapman.edu, cparks@chapman.edu|Course: CS510 Fall 2017 Assignment: Classwork 5

```python
import numpy as np    # Numeric python
import sympy as sp    # Symbolic python
import pandas as pd   # Data processing python
```

### Critiques of Aly's Code###
The organization of Aly's code was clear and done well.  They had docstrings for their functions but could have added a bit more detail.  The primes function was clear and easy to follow.  The algorithm makes sense and is a clear way to find prime numbers in a list.  Their Jupyter notebook was very well designed and clearly labeled with discussions of how the functions worked.

### Critiques of Cindy's Code ###

The organization of Cindy's code was great. It was formatted very well, with a very detailed docstrings section for the functions. I was able to follow along with the algorithm for primes. The only issue was that the generator did not run correctly. I really liked how her and her partner added error statements to the main function at the end of their primes.py code. The Jupyter notebook was very clear and easy to follow. The format was very nice and I liked how they implemented code portions in the Jupyter notebook.

```python

```