# SQL 入門

> 基本查詢：隨堂練習

郭耀仁

```python
import sqlite3
import pandas as pd
from sqlFrameCheck import checkAnsQuery
from test_queries.test_queries_02 import extract_test_queries as etq

conn_twelection = sqlite3.connect('twelection.db')
conn_nba = sqlite3.connect('nba.db')
```

## 隨堂練習：以 `||`  運算符將 `presidential2020` 表格中 `county`、`town` 與 `village` 連結誠一個新的欄位並命名為 `combined_key`，再以 `DISTINCT` 暸解台灣獨一的選舉區有哪些

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_twelection)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0201'), ans_query, conn_twelection)
caq.run_test()
```

## 隨堂練習：計算 NBA 球員的身體質量指數（BMI）並由大到小排序，選擇 `firstName`、`lastName` 與 `bmi` 這三個變數

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_nba)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0202'), ans_query, conn_nba)
caq.run_test()
```

## 隨堂練習：使用 `WHERE` 將 `presidential2020` 中臺北市的資料篩選出來

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_twelection)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0203'), ans_query, conn_twelection)
caq.run_test()
```

## 隨堂練習：使用 `WHERE` 將 `presidential2020` 表格中的六都（臺北市、新北市、桃園市、臺中市、臺南市與高雄市）的資料篩選出來

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_twelection)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0204'), ans_query, conn_twelection)
caq.run_test()
```

## 隨堂練習：查詢 `careerSummaries` 生涯場均得分 `ppg` 超過 20 分的球員 ID `personId`、場均得分 `ppg`

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_nba)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0205'), ans_query, conn_nba)
caq.run_test()
```

## 隨堂練習：從 `careerSummaries` 中找出生涯助攻失誤比（assists/turnovers）最高的前 10 個球員 ID `personId`、助攻失誤比 `ast_to_ratio`

\begin{equation}
ast\_to\_ratio = \frac{assists}{turnovers}
\end{equation}

```python
ans_query = """
-- 將查詢語法寫在這裡
"""
# 試跑看看結果
pd.read_sql(ans_query, conn_nba)
```

## 測資比對

```python
caq = checkAnsQuery(etq('0206'), ans_query, conn_nba)
caq.run_test()
```


# Phosphofructokinase (PFK) Model Construction

Based on Chapter 14 of <cite data-cite="Pal11">Systems Biology: Simulation of Dynamic Network States</cite>

To construct the phosphofructokinase module, first we import **MASSpy** and other essential packages. Constants used throughout the notebook are also defined.

```python
from operator import attrgetter
from os import path

import matplotlib.pyplot as plt

import numpy as np

from scipy import optimize

import sympy as sym

from cobra import DictList

from mass import MassConfiguration, MassMetabolite, Simulation, UnitDefinition
from mass.enzyme_modules import EnzymeModule
from mass.io import json, sbml
from mass.example_data import create_example_model
from mass.util.expressions import Keq2k, k2Keq, strip_time
from mass.util.matrix import matrix_rank
from mass.util.qcqa import qcqa_model

mass_config = MassConfiguration()

mass_config.irreversible_Keq = float("inf")
```

Note that the total enzyme concentration of PFK is $33 nM = 0.033 \mu M = 0.000033 mM$.

For the construction of the `EnzymeModule` for PFK, the following assumptions were made:

1. The enzyme is a homotetramer.
2. The enzyme binding and catalyzation of substrates occurs in an ordered sequential mechanism.
3. The mechanism of allosteric regulation is based on the Monod-Wyman-Changeux (MWC) model for allosteric transitions of homoproteins.

## Module Construction
The first step of creating the PFK module is to define the `EnzymeModule`. The `EnzymeModule` is an extension of the `MassModel`, with additional enzyme-specific attributes that aid in the construction, validation, and utilization of the module.

__Note:__ All `EnzymeModule` specific attributes start will start the prefix "enzyme" or "enzyme_module".

```python
PFK = EnzymeModule("PFK", name="Phosphofructokinase",
                   subsystem="Glycolysis")
```

    Set parameter Username

### Metabolites
#### Ligands
The next step is to define all of the metabolites using the `MassMetabolite` object. For `EnzymeModule` objects, the `MassMetabolite` objects will be refered to as ligands, for these `MassMetabolite` form a complex with the enzyme to serve some biological purpose. Some considerations for this step include the following:

1. It is important to use a clear and consistent format for identifiers and names when defining the `MassMetabolite` objects for various reasons, some of which include improvements to model clarity and utility, assurance of unique identifiers (required to add metabolites to the model), and consistency when collaborating and communicating with others.

2. In order to ensure our model is physiologically accurate, it is important to provide the `formula` argument with a string representing the chemical formula for each metabolite, and the `charge` argument with an integer representing the metabolite's ionic charge (Note that neutrally charged metabolites are provided with 0). These attributes can always be set later if necessary using the `formula` and `charge` attribute setter methods.

3. To indicate that the cytosol is the cellular compartment in which the reactions occur, the string "c" is provided to the `compartment` argument.

This model will be created using identifiers and names found in the [BiGG Database](http://bigg.ucsd.edu/).

The ligands correspond to the activators, inhibitors, cofactors, substrates, and products involved in the enzyme catalyzed reaction. In this model, there are 6 species which must be considered.

```python
f6p_c = MassMetabolite(
    "f6p_c",
    name="D-Fructose 6-phosphate",
    formula="C6H11O9P",
    charge=-2,
    compartment="c")
fdp_c = MassMetabolite(
    "fdp_c",
    name="D-Fructose 1,6-bisphosphate",
    formula="C6H10O12P2",
    charge=-4,
    compartment="c")
atp_c = MassMetabolite(
    "atp_c",
    name="ATP",
    formula="C10H12N5O13P3",
    charge=-4,
    compartment="c")
adp_c = MassMetabolite(
    "adp_c",
    name="ADP",
    formula="C10H12N5O10P2",
    charge=-3,
    compartment="c")
amp_c = MassMetabolite(
    "amp_c",
    name="AMP",
    formula="C10H12N5O7P",
    charge=-2,
    compartment="c")
h_c = MassMetabolite(
    "h_c",
    name="H+",
    formula="H",
    charge=1,
    compartment="c")
```

After generating the ligands, they are added to the `EnzymeModule` through the `add_metabolites` method. The ligands of the `EnzymeModule` can be viewed as a `DictList` through the `enzyme_module_ligands` attribute.

```python
# Add the metabolites to the EnzymeModule
PFK.add_metabolites([f6p_c, fdp_c, atp_c, adp_c, amp_c, h_c])
# Access DictList of ligands and print
print("All {0} Ligands: {1}".format(
    PFK.id, "; ".join([m.id for m in PFK.enzyme_module_ligands])))
```

    All PFK Ligands: f6p_c; fdp_c; atp_c; adp_c; amp_c; h_c

The `enzyme_module_ligands_categorized` attribute can be used to assign metabolites to groups of user-defined categories by providing a dictionary where keys are the categories and values are the metabolites. Note that any metabolite can be placed in more than one category.

```python
PFK.enzyme_module_ligands_categorized =  {
    "Substrates": f6p_c,
    "Cofactors": atp_c,
    "Activators": amp_c,
    "Inhibitors": atp_c,
    "Products": [fdp_c, adp_c, h_c]}

# Access DictList of ligands and print
print("All {0} ligands ({1} total):\n{2}\n".format(
    PFK.id, len(PFK.enzyme_module_ligands),
    str([m.id for m in PFK.enzyme_module_ligands])))

# Access categorized attribute for ligands and print
for group in PFK.enzyme_module_ligands_categorized:
    print("{0}: {1}".format(
        group.id, str([m.id for m in group.members])))
```

    All PFK ligands (6 total):
    ['f6p_c', 'fdp_c', 'atp_c', 'adp_c', 'amp_c', 'h_c']

    Substrates: ['f6p_c']
    Cofactors: ['atp_c']
    Activators: ['amp_c']
    Inhibitors: ['atp_c']
    Products: ['fdp_c', 'adp_c', 'h_c']

#### EnzymeModuleForms

The next step is to define the various states of the enzyme and enzyme-ligand complexes. These states can be represented through an `EnzymeModuleForm` object. Just like how `EnzymeModule` objects extend `MassModels`, the `EnzymeModuleForm` objects extend `MassMetabolite` objects, giving them the same functionality as a `MassMetabolite`. However, there are two important additional attrubutes that are specific to the `EnzymeModuleForm`.

* The first attribute is the `enzyme_module_id`. It is meant to hold the identifier or name of the corresponding `EnzymeModule`.
* The second attribute is the `bound_metabolites` attribute, designed to contain metabolites bound to the enzymatic site(s).
* Automatic generation of the `name`, `formula`, and `charge` attributes attributes utilize the `bound_metabolites` attribute, which can aid in identification of `EnzymeModuleForm` and mass and charge balancing of the reactions.

The most convenient way to make an `EnzymeModuleForm` is through the `EnzymeModule.make_enzyme_module_form` method. There are several reasons to use this method to generate the `EnzymeModuleForm` objects:

1. The only requirement to creating an `EnzymeModuleForm` is the identifier.
2. A string can optionally be provided for the `name` argument to set the corresponding `name` attribute, or it can automatically be generated and set by setting the string "Automatic" (case sensitve).
3. The `enzyme_module_id`, `formula` and `charge` attributes are set based on the identifier of the EnzymeModule and the MassMetabolite objects found in `bound_metabolites`.
4. Just like the `enzyme_module_ligands_categorized` attribute, there is the `enzyme_module_forms_categorized` attribute that behaves in a similar manner. Categories can be set at the time of construction by providing a string or a list of strings to the `categories` argument.
5. `EnzymeModuleForm` objects are automatically added to the `EnzymeModule` once created.

For this module, there are 20 `EnzymeModuleForm` objects that must be created. Because of the assumptions made for this module, a loop can be used to help automate the construction of the `EnzymeModuleForm` objects.

```python
# Number of identical subunits
n_subunits = 4

for i in range(n_subunits + 1):
    # Make enzyme module forms per number of bound activators (Up to 4 Total)
    PFK.make_enzyme_module_form(
        "pfk_R{0:d}_c".format(i),
        name="Automatic",
        categories=("Relaxed", "Free_Catalytic"),
        bound_metabolites={amp_c: i},
        compartment="c");

    PFK.make_enzyme_module_form(
        "pfk_R{0:d}_A_c".format(i),
        name="Automatic",
        categories=("Relaxed", "Complexed_ATP"),
        bound_metabolites={atp_c: 1, amp_c: i},
        compartment="c");

    PFK.make_enzyme_module_form(
        "pfk_R{0:d}_AF_c".format(i),
        name="Automatic",
        categories=("Relaxed", "Complexed_ATP_F6P"),
        bound_metabolites={atp_c: 1, f6p_c: 1, amp_c: i},
        compartment="c");

    # Make enzyme module forms per number of bound inhibitors (Up to 4 Total)
    PFK.make_enzyme_module_form(
        "pfk_T{0:d}_c".format(i),
        name="Automatic",
        categories="Tense",
        bound_metabolites={atp_c: i},
        compartment="c");

# Access DictList of enzyme module forms and print
print("All {0} enzyme module forms ({1} total):\n{2}\n".format(
    PFK.id, len(PFK.enzyme_module_forms),
    str([m.id for m in PFK.enzyme_module_forms])))

# Access categorized attribute for enzyme module forms and print
for group in PFK.enzyme_module_forms_categorized:
    print("{0}: {1}\n".format(
        group.id, str(sorted([m.id for m in group.members]))))
```

    All PFK enzyme module forms (20 total):
    ['pfk_R0_c', 'pfk_R0_A_c', 'pfk_R0_AF_c', 'pfk_T0_c', 'pfk_R1_c', 'pfk_R1_A_c', 'pfk_R1_AF_c', 'pfk_T1_c', 'pfk_R2_c', 'pfk_R2_A_c', 'pfk_R2_AF_c', 'pfk_T2_c', 'pfk_R3_c', 'pfk_R3_A_c', 'pfk_R3_AF_c', 'pfk_T3_c', 'pfk_R4_c', 'pfk_R4_A_c', 'pfk_R4_AF_c', 'pfk_T4_c']

    Relaxed: ['pfk_R0_AF_c', 'pfk_R0_A_c', 'pfk_R0_c', 'pfk_R1_AF_c', 'pfk_R1_A_c', 'pfk_R1_c', 'pfk_R2_AF_c', 'pfk_R2_A_c', 'pfk_R2_c', 'pfk_R3_AF_c', 'pfk_R3_A_c', 'pfk_R3_c', 'pfk_R4_AF_c', 'pfk_R4_A_c', 'pfk_R4_c']

    Free_Catalytic: ['pfk_R0_c', 'pfk_R1_c', 'pfk_R2_c', 'pfk_R3_c', 'pfk_R4_c']

    Complexed_ATP: ['pfk_R0_A_c', 'pfk_R1_A_c', 'pfk_R2_A_c', 'pfk_R3_A_c', 'pfk_R4_A_c']

    Complexed_ATP_F6P: ['pfk_R0_AF_c', 'pfk_R1_AF_c', 'pfk_R2_AF_c', 'pfk_R3_AF_c', 'pfk_R4_AF_c']

    Tense: ['pfk_T0_c', 'pfk_T1_c', 'pfk_T2_c', 'pfk_T3_c', 'pfk_T4_c']

## Reactions
### EnzymeModuleReactions
Once all of the `MassMetabolite` and `EnzymeModuleForm` objects have been created, the next step is to define all of the enzyme-ligand binding reactions and conformation trasitions that occur in its mechanism.

These reactions can be represented through an `EnzymeModuleReaction` object. As with the previous enzyme objects,  `EnzymeModuleReactions` extend `MassReaction` objects to maintain the same functionality. However, as with the `EnzymeModuleForm`, the `EnzymeModuleReaction` has additional enzyme-specific attributes, such as the `enzyme_module_id`.

The most conveient way to make an `EnzymeModuleReaction` is through the `EnzymeModule.make_enzyme_module_reaction` method. There are several reasons to use this method to generate the EnzymeModuleReactions:

1. The only requirement to creating an `EnzymeModuleReaction` is an identifier.
2. A string can optionally be provided for the `name` argument to set the corresponding `name` attribute, or it can automatically be generated and set by setting the string "Automatic" (case sensitve).
3. There is an `enzyme_module_reactions_categorized` attribute that behaves in a similar manner as the previous categorized attributes. Categories can be set at the time of construction by providing a string or a list of strings to the `categories` argument.
4. `MassMetabolite` and `EnzymeModuleForm` objects that already exist in the `EnzymeModule` can be directly added to the newly created `EnzymeModuleReaction` by providing a dictionary to the optional `metabolites_to_add` argument using string identifiers (or the objects) as keys and their stoichiometric coefficients as the values.
5. `EnzymeModuleReactions` are automatically added to the `EnzymeModule` once created.

For this module, there are 24 `EnzymeModuleReactions` that must be created. Because of the assumptions made for this module, a loop can be used to help automate the construction of the `EnzymeModuleReactions`.

```python
for i in range(n_subunits + 1):
    # Make reactions for enzyme-ligand binding and catalytzation per number of bound activators (Up to 4 Total)
    PFK.make_enzyme_module_reaction(
        "PFK_R{0:d}1".format(i),
        name="Automatic",
        subsystem="Glycolysis",
        reversible=True,
        categories="atp_c_binding",
        metabolites_to_add={
            "pfk_R{0:d}_c".format(i): -1,
            "atp_c": -1,
            "pfk_R{0:d}_A_c".format(i): 1})

    PFK.make_enzyme_module_reaction(
        "PFK_R{0:d}2".format(i),
        name="Automatic",
        subsystem="Glycolysis",
        reversible=True,
        categories="f6p_c_binding",
        metabolites_to_add={
            "pfk_R{0:d}_A_c".format(i): -1,
            "f6p_c": -1,
            "pfk_R{0:d}_AF_c".format(i): 1})

    PFK.make_enzyme_module_reaction(
        "PFK_R{0:d}3".format(i),
        name="Automatic",
        subsystem="Glycolysis",
        reversible=False,
        categories="catalyzation",
        metabolites_to_add={
            "pfk_R{0:d}_AF_c".format(i): -1,
            "pfk_R{0:d}_c".format(i): 1,
            "adp_c": 1,
            "fdp_c": 1,
            "h_c": 1})

    if i < n_subunits:
        # Make enzyme reactions for enzyme-activator binding
        PFK.make_enzyme_module_reaction(
            "PFK_R{0:d}0".format(i + 1),
            name="Automatic",
            subsystem="Glycolysis",
            reversible=True,
            categories="amp_c_activation",
            metabolites_to_add={
                "pfk_R{0:d}_c".format(i): -1,
                "amp_c": -1,
                "pfk_R{0:d}_c".format(i + 1): 1})

        # Make enzyme reactions for enzyme-inhibitor binding
        PFK.make_enzyme_module_reaction(
            "PFK_T{0:d}".format(i + 1),
            name="Automatic",
            subsystem="Glycolysis",
            reversible=True,
            categories="atp_c_inhibition",
            metabolites_to_add={
                "pfk_T{0:d}_c".format(i): -1,
                "atp_c": -1,
                "pfk_T{0:d}_c".format(i + 1): 1})

# Make reaction representing enzyme transition from R to T state
PFK.make_enzyme_module_reaction(
    "PFK_L",
    name="Automatic",
    subsystem="Glycolysis",
    reversible=True,
    categories="RT_transition",
    metabolites_to_add={
        "pfk_R0_c": -1,
        "pfk_T0_c": 1})

# Access DictList of enzyme module reactions and print
print("All {0} enzyme module reactions ({1} total):\n{2}\n".format(
    PFK.id, len(PFK.enzyme_module_reactions),
    str([m.name for m in PFK.enzyme_module_reactions])))

# Access categorized attribute for enzyme module reactions and print
for group in PFK.enzyme_module_reactions_categorized:
    print("{0}: {1}\n".format(
        group.id, str(sorted([m.id for m in group.members]))))
```

    All PFK enzyme module reactions (24 total):
    ['pfk_R0-atp binding', 'pfk_R0_A-f6p binding', 'pfk_R0_AF catalyzation', 'pfk_R0-amp binding', 'pfk_T0-atp binding', 'pfk_R1-atp binding', 'pfk_R1_A-f6p binding', 'pfk_R1_AF catalyzation', 'pfk_R1-amp binding', 'pfk_T1-atp binding', 'pfk_R2-atp binding', 'pfk_R2_A-f6p binding', 'pfk_R2_AF catalyzation', 'pfk_R2-amp binding', 'pfk_T2-atp binding', 'pfk_R3-atp binding', 'pfk_R3_A-f6p binding', 'pfk_R3_AF catalyzation', 'pfk_R3-amp binding', 'pfk_T3-atp binding', 'pfk_R4-atp binding', 'pfk_R4_A-f6p binding', 'pfk_R4_AF catalyzation', 'pfk_R0-pfk_T0 transition']

    atp_c_binding: ['PFK_R01', 'PFK_R11', 'PFK_R21', 'PFK_R31', 'PFK_R41']

    f6p_c_binding: ['PFK_R02', 'PFK_R12', 'PFK_R22', 'PFK_R32', 'PFK_R42']

    catalyzation: ['PFK_R03', 'PFK_R13', 'PFK_R23', 'PFK_R33', 'PFK_R43']

    amp_c_activation: ['PFK_R10', 'PFK_R20', 'PFK_R30', 'PFK_R40']

    atp_c_inhibition: ['PFK_T1', 'PFK_T2', 'PFK_T3', 'PFK_T4']

    RT_transition: ['PFK_L']

### Create and  Unify Rate Parameters
The next step is to unify rate parameters of binding steps that are not unique, allowing for those parameter values to be defined once and stored in the same place. Therefore, custom rate laws with custom parameters are used to reduce the number of parameters that need to be defined and better represent the module.

The rate law parameters can be unified using the `EnzymeModule.unify_rate_parameters` class method. This method requires a list of reactions whose rate laws that should be identical, along with a string representation of the new identifier to use on the unified parameters. There is also the optional `prefix ` argument, which if set to True, will ensure the new parameter identifiers are prefixed with the `EnzymeModule` identifier. This can be used to help prevent custom parameters from being replaced when multiple models are merged.

#### Allosteric Transitions: Symmetry Model

Once rate parameters are unified, the allosteric regulation of this enzyme must be accounted for. Because this module is to be based on the (Monod-Wyman-Changeux) MWC model for ligand binding and allosteric regulation, the rate laws of the allosteric binding reactions must be adjusted to reflect the symmetry in the module using the number of identical binding sites to help determine the scalars for the parameters.

For this module, PFK is considered a homotetramer, meaning it has four identical subunits $\nu = 4$. Each subunit can be allosterically activated by AMP or inhibited by ATP. The helper functions `k2Keq`, `Keq2k`, and `strip_time` from the `mass.util` submodule will be used to help facilitate the rate law changes in this example so that the final rate laws are dependent on the forward rate (kf) and equilibrium (Keq) constants.

```python
abbreviations = ["A", "F", "I", "ACT"]
ligands = [atp_c, f6p_c, atp_c, amp_c]

for met, unified_id in zip(ligands, abbreviations):
    category = {"A": "binding",
                "F": "binding",
                "I": "inhibition",
                "ACT": "activation"}[unified_id]
    group = PFK.enzyme_module_reactions_categorized.get_by_id(
        "_".join((met.id, category)))
    reactions = sorted(group.members, key=attrgetter("id"))
    PFK.unify_rate_parameters(reactions, unified_id,
                              rate_type=2, enzyme_prefix=True)
    # Add the coefficients to make symmetry model rate laws for activation and inhibition
    if unified_id in ["I", "ACT"]:
        for i, reaction in enumerate(reactions):
            custom_rate = str(strip_time((reaction.rate)))
            custom_rate = custom_rate.replace(
                "kf_", "{0:d}*kf_".format(n_subunits - i))
            custom_rate = custom_rate.replace(
                "kr_", "{0:d}*kr_".format(i + 1))
            PFK.add_custom_rate(reaction, custom_rate)

PFK.unify_rate_parameters(
    PFK.enzyme_module_reactions_categorized.get_by_id("catalyzation").members,
    "PFK")
# Update rate laws to be in terms of kf and Keq
PFK.custom_rates.update(k2Keq(PFK.custom_rates))

# Access categorized attribute for enzyme module reactions and print
for group in PFK.enzyme_module_reactions_categorized:
    header = "Category: " + group.id
    print("\n" + header + "\n" + "-" * len(header))
    for reaction in sorted(group.members, key=attrgetter("id")):
        print(reaction.id + ": " + str(reaction.rate))
```

    Category: atp_c_binding
    -----------------------
    PFK_R01: kf_PFK_A*(atp_c(t)*pfk_R0_c(t) - pfk_R0_A_c(t)/Keq_PFK_A)
    PFK_R11: kf_PFK_A*(atp_c(t)*pfk_R1_c(t) - pfk_R1_A_c(t)/Keq_PFK_A)
    PFK_R21: kf_PFK_A*(atp_c(t)*pfk_R2_c(t) - pfk_R2_A_c(t)/Keq_PFK_A)
    PFK_R31: kf_PFK_A*(atp_c(t)*pfk_R3_c(t) - pfk_R3_A_c(t)/Keq_PFK_A)
    PFK_R41: kf_PFK_A*(atp_c(t)*pfk_R4_c(t) - pfk_R4_A_c(t)/Keq_PFK_A)

    Category: f6p_c_binding
    -----------------------
    PFK_R02: kf_PFK_F*(f6p_c(t)*pfk_R0_A_c(t) - pfk_R0_AF_c(t)/Keq_PFK_F)
    PFK_R12: kf_PFK_F*(f6p_c(t)*pfk_R1_A_c(t) - pfk_R1_AF_c(t)/Keq_PFK_F)
    PFK_R22: kf_PFK_F*(f6p_c(t)*pfk_R2_A_c(t) - pfk_R2_AF_c(t)/Keq_PFK_F)
    PFK_R32: kf_PFK_F*(f6p_c(t)*pfk_R3_A_c(t) - pfk_R3_AF_c(t)/Keq_PFK_F)
    PFK_R42: kf_PFK_F*(f6p_c(t)*pfk_R4_A_c(t) - pfk_R4_AF_c(t)/Keq_PFK_F)

    Category: catalyzation
    ----------------------
    PFK_R03: kf_PFK*pfk_R0_AF_c(t)
    PFK_R13: kf_PFK*pfk_R1_AF_c(t)
    PFK_R23: kf_PFK*pfk_R2_AF_c(t)
    PFK_R33: kf_PFK*pfk_R3_AF_c(t)
    PFK_R43: kf_PFK*pfk_R4_AF_c(t)

    Category: amp_c_activation
    --------------------------
    PFK_R10: kf_PFK_ACT*(4*amp_c(t)*pfk_R0_c(t) - pfk_R1_c(t)/Keq_PFK_ACT)
    PFK_R20: kf_PFK_ACT*(3*amp_c(t)*pfk_R1_c(t) - 2*pfk_R2_c(t)/Keq_PFK_ACT)
    PFK_R30: kf_PFK_ACT*(2*amp_c(t)*pfk_R2_c(t) - 3*pfk_R3_c(t)/Keq_PFK_ACT)
    PFK_R40: kf_PFK_ACT*(amp_c(t)*pfk_R3_c(t) - 4*pfk_R4_c(t)/Keq_PFK_ACT)

    Category: atp_c_inhibition
    --------------------------
    PFK_T1: kf_PFK_I*(4*atp_c(t)*pfk_T0_c(t) - pfk_T1_c(t)/Keq_PFK_I)
    PFK_T2: kf_PFK_I*(3*atp_c(t)*pfk_T1_c(t) - 2*pfk_T2_c(t)/Keq_PFK_I)
    PFK_T3: kf_PFK_I*(2*atp_c(t)*pfk_T2_c(t) - 3*pfk_T3_c(t)/Keq_PFK_I)
    PFK_T4: kf_PFK_I*(atp_c(t)*pfk_T3_c(t) - 4*pfk_T4_c(t)/Keq_PFK_I)

    Category: RT_transition
    -----------------------
    PFK_L: kf_PFK_L*(pfk_R0_c(t) - pfk_T0_c(t)/Keq_PFK_L)

## The Steady State
### Solve steady state concentrations symbolically
To determine the steady state of the enzyme, a dictionary of the ordinary differential equations as symbolic expressions for each of the `EnzymeModuleForm` objects. The ligands are first removed from the equations by assuming their values are taken into account in a lumped rate constant parameter.

For handling of all symbolic expressions, the **SymPy** package is used.

```python
# Make a dictionary of ODEs and lump ligands into rate parameters by giving them a value of 1
ode_dict = {}
lump_ligands = {sym.Symbol(met.id): 1 for met in PFK.enzyme_module_ligands}
for enzyme_module_form in PFK.enzyme_module_forms:
    symbol_key = sym.Symbol(enzyme_module_form.id)
    ode = sym.Eq(strip_time(enzyme_module_form.ode), 0)
    ode_dict[symbol_key] = ode.subs(lump_ligands)

rank = matrix_rank(PFK.S[6:])
print("Rank Deficiency: {0}".format(len(ode_dict) - rank))
```

    Rank Deficiency: 1

In order to solve the system of ODEs for the steady state concentrations, an additional equation is required due to the rank deficiency of the stoichiometric matrix. Therefore, the equation for the steady state flux through the enzyme, which will be referred to as the "enzyme net flux equation", must be defined.

To define the enzyme net flux equation, the `EnzymeModule.make_enzyme_netflux_equation` class method can be used.

* This equation is made by providing a reaction, or a list of reactions to add together.
* Passing a bool to `use_rates` argument determines whether a symbolic equation is a summation of the flux symbols returned by `EnzymeModuleReaction.flux_symbol_str`, or a summation of the rates laws for those reactions.
* The `update_enzyme` argument determines whether the new rate equation is set in the `enzyme_rate_equation` attribute.

The flux through the enzyme typically corresponds to the sum of the fluxes through the catalytic reaction steps.
Because the catalyzation reactions were assigned to the "catalyzation" cateogry, they can be accessed through the `enzyme_module_reactions_categorized` attribute to create the equation for $v_{\mathrm{PFK}}$.

```python
reactions = PFK.enzyme_module_reactions_categorized.get_by_id(
    "catalyzation").members
PFK.make_enzyme_rate_equation(
    reactions,
    use_rates=True, update_enzyme=True)
sym.pprint(PFK.enzyme_rate_equation)
```

    kf_PFK⋅(pfk_R0_AF_c(t) + pfk_R1_AF_c(t) + pfk_R2_AF_c(t) + pfk_R3_AF_c(t) + pfk_R4_AF_c(t))

The next step is to identify equations for the unknown concentrations in each reaction. These equations will need to be solved with a dependent variable before accounting for the enzyme net flux equation. The completely free form of the enzyme with no bound species will be treated as the dependent variable.

To verify that all equations are in terms of the lumped rate parameters, and the dependent variable, the solutions can be iterated through using the atoms method to identify the equation arguments. There should be no `EnzymeModuleForm` identifiers with the exception of the dependent variable.

```python
# Get enzyme module forms
enzyme_module_forms = PFK.enzyme_module_forms.copy()
# Reverse list for increased performance (due to symmetry assumption)
# by solving for the most activated/inhibitors bound first.
enzyme_module_forms.reverse()

enzyme_solutions = {}
for enzyme_module_form in enzyme_module_forms:
    # Skip dependent variable
    if "pfk_R0_c" == str(enzyme_module_form):
        continue
    enzyme_module_form = sym.Symbol(enzyme_module_form.id)
    # Susbtitute in previous solutions and solve for the enzyme module form,
    equation = ode_dict[enzyme_module_form]
    sol = sym.solveset(equation.subs(enzyme_solutions), enzyme_module_form)
    enzyme_solutions[enzyme_module_form] = list(sol)[0]
    # Update the dictionary of solutions with the solutions
    enzyme_solutions.update({
        enzyme_module_form: sol.subs(enzyme_solutions)
        for enzyme_module_form, sol in enzyme_solutions.items()})
args = set()
for sol in enzyme_solutions.values():
    args.update(sol.atoms(sym.Symbol))
print(args)
```

    {Keq_PFK_I, kf_PFK_F, Keq_PFK_F, Keq_PFK_L, Keq_PFK_ACT, pfk_R0_c, kf_PFK, kf_PFK_A, Keq_PFK_A}

The enzyme net flux equation can then be utilized as the last equation required to solve for the final unknown concentration variable in terms of the rate and equilibrium constants, allowing for all of the concentration variables to be defined in terms of the rate and equilibrium constants. Once the unknown variable has been solved for, the solution can be substituted back into the other equations. Because `sympy.solveset` function expects the input equations to be equal to 0, the `EnzymeModule.enzyme_rate_error` method with the `use_values`  argument set to `False` to get the appropriate expression.

```python
enzyme_rate_equation = strip_time(PFK.enzyme_rate_error(False))
print("Enzyme Net Flux Equation\n" + "-"*24)
sym.pprint(enzyme_rate_equation)

# Solve for last unknown concentration symbolically
sol = sym.solveset(enzyme_rate_equation.subs(enzyme_solutions), "pfk_R0_c")

# Update solution dictionary with the new solution
enzyme_solutions[sym.Symbol("pfk_R0_c")] = list(sol)[0]

# Update solutions with free variable solutions
enzyme_solutions = {
    enzyme_module_form: sym.simplify(solution.subs(enzyme_solutions))
    for enzyme_module_form, solution in enzyme_solutions.items()}

args = set()
for sol in enzyme_solutions.values():
    args.update(sol.atoms(sym.Symbol))
print("\n", args)
```

    Enzyme Net Flux Equation
    ------------------------
    -kf_PFK⋅(pfk_R0_AF_c + pfk_R1_AF_c + pfk_R2_AF_c + pfk_R3_AF_c + pfk_R4_AF_c) + v_PFK

     {Keq_PFK_I, kf_PFK_F, Keq_PFK_L, kf_PFK, Keq_PFK_ACT, v_PFK, Keq_PFK_F, kf_PFK_A, Keq_PFK_A}

#### Numerical Values
At this point, numerical values are defined for the dissociation constants and the concentrations of the substrates, cofactors, activators, and inhibitors. Providing these numerical values will speed up the subsequent calculations.

To do this, experimental data is used to define the dissociations constants for the different binding steps under the QEA. The concentrations of the non-enzyme species are taken from the glycolysis model. Experimental data gives the following for the dissociation constants:

$$K_i=0.1 mM,\
K_a=0.033 mM,\
K_A=0.068 mM,\
K_F=0.1 mM$$

and an allosteric constant of $K_L = 0.0011$.

__Note:__ The $K_i$ binding constant for ATP as an inhibitor was increased by a factor of ten since magnesium complexing of ATP is not considered here.

```python
numerical_values = {}

# Get ligand IDs and parameter IDs
ligand_ids = sorted([str(ligand) for ligand in PFK.enzyme_module_ligands])
parameter_ids = ["_".join((PFK.id, abbrev)) for abbrev in abbreviations + ["L"]]
print("Ligand IDs: " + str(ligand_ids))
print("Parameter IDs: " + str(parameter_ids))

# Load the glycolysis model to extract steady state values
glycolysis = create_example_model("SB2_Glycolysis")

# Get the steady state flux value and add to numerical values
PFK.enzyme_rate = glycolysis.reactions.get_by_id(PFK.id).steady_state_flux
numerical_values.update({PFK.enzyme_flux_symbol_str: PFK.enzyme_rate})

# Get the steady state concentration values and add to numerical values
initial_conditions = {
    str(ligand): glycolysis.initial_conditions[glycolysis.metabolites.get_by_id(ligand)]
    for ligand in ligand_ids}

# Define parameter values and add to numerical values
# Because of the QEA, invert dissociation constants for Keq
parameter_values = {
    "Keq_" + parameter_id: value
    for parameter_id, value in zip(parameter_ids, [1/0.068, 1/0.1, 1/0.1, 1/0.033, 0.0011])}

# Display numerical values
print("\nNumerical Values\n----------------")
for k, v in numerical_values.items():
    print("{0} = {1}".format(k, v))
```

    Ligand IDs: ['adp_c', 'amp_c', 'atp_c', 'f6p_c', 'fdp_c', 'h_c']
    Parameter IDs: ['PFK_A', 'PFK_F', 'PFK_I', 'PFK_ACT', 'PFK_L']

    Numerical Values
    ----------------
    v_PFK = 1.12

The next step is to define the numerical values, $K_i=0.1/1.6$, $K_a=0.033/0.0867$, $K_A=0.068/1.6$, $K_F=0.1/0.0198$, $v_{PFK}=1.12 \text{mM/hr}$, and $K_L=1/0.0011$ using the dissociation constant values and the steady state concentrations of the ligands and introduce them into the solution to get the steady state concentrations of the enzyme module forms in terms of the rate constants. The values of the equilirbium constants and initial conditions are also stored for later use.

```python
# Match abbreviations to their corresponding ligands
abbreviation_dict = {"PFK_A": "atp_c", "PFK_F": "f6p_c", "PFK_ACT": "amp_c", "PFK_I": "atp_c", "PFK_L": ""}

k2K = {sym.Symbol("kr_" + p): sym.Symbol("kf_" + p)*sym.Symbol("K_" + p) for p in abbreviation_dict.keys()}
enzyme_solutions = {met: sym.simplify(Keq2k(solution).subs(enzyme_solutions).subs(k2K))
                    for met, solution in enzyme_solutions.items()}
K_values = dict(zip(["K_" + p for p in abbreviation_dict], [0.068, 0.1, 0.033, 0.1, 0.0011]))

for abbrev, ligand_id in abbreviation_dict.items():
    K_str = "K_" + abbrev
    if ligand_id:
        numerical_value = K_values[K_str]/initial_conditions[ligand_id]
    else:
        numerical_value = 1/K_values[K_str]
    numerical_values[sym.Symbol(K_str)] = numerical_value

enzyme_solutions = {met: sym.simplify(solution.subs(numerical_values))
                    for met, solution in enzyme_solutions.items()}

# Display numerical values
print("\nNumerical Values\n----------------")
for k, v in numerical_values.items():
    print("{0} = {1}".format(k, v))
```

    Numerical Values
    ----------------
    v_PFK = 1.12
    K_PFK_A = 0.0425
    K_PFK_F = 5.05050505050505
    K_PFK_ACT = 0.3804995151513754
    K_PFK_I = 0.0625
    K_PFK_L = 909.090909090909

The last part of this step is to simplify the solutions for the enzyme module forms and, as a QA check, ensure that only rate constants are the only symbolic arguments in the solutions.

```python
# Substitute values into equations
enzyme_solutions = {
    enzyme_module_form: sym.simplify(solution.subs(numerical_values))
    for enzyme_module_form, solution in enzyme_solutions.items()}

args = set()
for sol in enzyme_solutions.values():
    args.update(sol.atoms(sym.Symbol))
print(args)
```

    {kf_PFK_F, kf_PFK, kf_PFK_A}

### Determine rate constants
#### Total Enzyme Concentration and $r_{T}$
After solving for the enzyme module forms, the next step is to define equations for the total enzyme concentration and for the fraction of the enzyme in the T state. These two equations can be used as constraints for determining the rate parameters. To view the equation for the total enzyme concentration, we can use the `EnzymeModule.enzyme_concentration_total_equation` property.

```python
sym.pprint(PFK.enzyme_concentration_total_equation)
```

    pfk_R0_AF_c(t) + pfk_R0_A_c(t) + pfk_R0_c(t) + pfk_R1_AF_c(t) + pfk_R1_A_c(t) + pfk_R1_c(t) + pfk_R2_AF_
    c(t) + pfk_R2_A_c(t) + pfk_R2_c(t) + pfk_R3_AF_c(t) + pfk_R3_A_c(t) + pfk_R3_c(t) + pfk_R4_AF_c(t) + pfk
    _R4_A_c(t) + pfk_R4_c(t) + pfk_T0_c(t) + pfk_T1_c(t) + pfk_T2_c(t) + pfk_T3_c(t) + pfk_T4_c(t)

The total concentration of PFK is 33 nM (=0.000033 mM). The `EnzymeModule.enzyme_concentration_total` atrribute can be used to set and store this concentration.

```python
PFK.enzyme_concentration_total = 33e-6
print(PFK.enzyme_concentration_total)
```

    3.3e-05

To determine the rate constants, an optimization problem where the objective function is to minimize the error between the measured and calculated total enzyme concentrations. To create the objective function, the `EnzymeModule.enzyme_concentration_total_error` method with the `use_values` argument set as False to get the symbolic expression of the constraint.

```python
enzyme_total_constraint = abs(strip_time(PFK.enzyme_concentration_total_error(use_values=False)))
sym.pprint(enzyme_total_constraint)
```

    │-PFK_Total + pfk_R0_AF_c + pfk_R0_A_c + pfk_R0_c + pfk_R1_AF_c + pfk_R1_A_c + pfk_R1_c + pfk_R2_AF_c +
    pfk_R2_A_c + pfk_R2_c + pfk_R3_AF_c + pfk_R3_A_c + pfk_R3_c + pfk_R4_AF_c + pfk_R4_A_c + pfk_R4_c + pfk_
    T0_c + pfk_T1_c + pfk_T2_c + pfk_T3_c + pfk_T4_c│

Substitute the solutions for the enzyme forms to get an equation for the error in the enzyme total concentration in terms of the rate constants.

```python
# Substitute value for enzyme concentration total
enzyme_total_constraint = enzyme_total_constraint.subs({PFK.enzyme_total_symbol_str: PFK.enzyme_concentration_total})
# Substitute solutions into constraint and simplify
enzyme_total_constraint = sym.simplify(enzyme_total_constraint.subs(enzyme_solutions))
sym.pprint(enzyme_total_constraint)
```

    │          1.19283868483391   1.71385140785683   7.14443780219149│
    │-3.3e-5 + ──────────────── + ──────────────── + ────────────────│
    │              kf_PFK_F           kf_PFK_A            kf_PFK     │

To create the objective function in a format suitable for the minimization method from the `scipy.optimize` submodule, the `sympy.lambdify` function can be used to convert the symbolic expression into a lambda function with the rate constants as the arguments. This lambda function can then be used to generate the objective function for the `optimize.minimize` method.

```python
# Create a sorted tuple of the arguments to ensure the input format does not change
args = tuple(sorted([str(arg) for arg in list(args)]))
# Create the objective function as a lambda function
objective_function = lambda x: sym.lambdify(args, enzyme_total_constraint)(*x)
```

Another constraint can be set on the amount of inhibited enzyme in the steady state of the system using the T fraction (denoted as $r_{T}$). This fraction is simply the amount of inhibited enzyme over the total amount of enzyme. The enzyme is inhibited between 10-15% under physiological conditions (Ponce et al. Biochimica et Biophysica Acta 1971 250(1):63-74)

To make the fraction as a symbolic expression, we can use the `EnzymeModule.make_enzyme_fraction` method. This method is designed to assist in making fractions and ratios by passing to the function:
1. A string to the `categorized_attr` argument identifying which categorized attribute (either "forms" for the `EnzymeModule.enzyme_module_forms_categorized` or "reactions" for the `EnzymeModule.enzyme_module_reactions_categorized`).
2. A string for the `top` argument and a string for the `bottom` argument identifying the categories to sum and use in the numerator and the denominator, respectively.
3. A bool to the `use_values` argument indicating whether to substitute numerical values into the expression to return a float or to keep the ratio as a **SymPy** expression.

__Note:__ The string "Equation" can be passed to either the `top` or `bottom` arguments to utilize the equation stored either in `enzyme_concentration_total_equation` (for `categorized_attr`="forms"), or   `enzyme_rate_equation` (for `categorized_attr`="reactions").

```python
# Set the values for the constraint bounds
r_T_lb, r_T_ub = (0.10, 0.15)
# Make a symbolic expression for enzyme fraction.
r_T_expr = PFK.make_enzyme_fraction(
    categorized_attr="forms", top="Tense", bottom="Equation",
    use_values=False)
# Substitute solutions into the expression to make
# solely dependent on the rate constants
r_T_expr = sym.simplify(strip_time(r_T_expr).subs(enzyme_solutions))

# Make lambda functions for the T fraction constraint
r_T_lb_constraint = lambda x: sym.lambdify(args, r_T_expr - r_T_lb)(*x)
r_T_ub_constraint = lambda x: sym.lambdify(args, r_T_ub - r_T_expr)(*x)
```

Lastly, we place lower and upper bounds on the rate constants to ensure that the values are non-negative and are within physiological limits, and then we solve the optmization problem. Once the optimization has finished, we check whether it was successful, and if so, what the optimality and errors are associated with this particular solution instance.

```python
print("Ordered Args: {0}\n".format(str(args)))
# Set arguments for minimization
kf_bounds = ((1e2, 1e8), (1e2, 1e8), (1e2, 1e8))
initial_guess = [
    3.07e5,
    2e5,
    1e6,]

# Find a feasible solution
sol = optimize.minimize(
    objective_function, x0=initial_guess,
    method="trust-constr",
    bounds=kf_bounds,
    options={"gtol": 1e-20, "xtol": 1e-20, "maxiter": 1e4, "disp": True})

# Check whether optimzation was successful
print("\nOptimization Success: {0}".format(sol.success))
if sol.success:
    # Update the paramter values dictionary with the feasible solution
    parameter_values.update(dict(zip(args, [round(x) for x in sol.x])))
    print("Optimization Optimality: {0:.4e}".format(sol.optimality))
    print("Parameter Solutions: {:}".format(str({arg: parameter_values[arg] for arg in args})))
    # Plug solutions back into constraints for validation
    print("Optimization Error: {0:.4e}".format(enzyme_total_constraint.subs(parameter_values)))
```

    Ordered Args: ('kf_PFK', 'kf_PFK_A', 'kf_PFK_F')

    `xtol` termination condition is satisfied.
    Number of iterations: 104, function evaluations: 224, CG iterations: 116, optimality: 3.60e-11, constraint violation: 0.00e+00, execution time: 0.67 s.

    Optimization Success: True
    Optimization Optimality: 3.6029e-11
    Parameter Solutions: {'kf_PFK': 307263, 'kf_PFK_A': 200325, 'kf_PFK_F': 1000059}
    Optimization Error: 1.2079e-11

With a successful optimization, the module is updated with the parameter values.  The inhibition and activation reactions are set to have a high forward rate constant and the allosteric transition even higher, limiting the amount of unbound enzyme and ensuring that the dynamics are determined by the dissociation and allosteric constants.

__Note:__ This assumption for the rate constants can be made because none of the enzyme concentrations are dependendent on the activation, inhibition, and allosteric rate constants.

```python
# Add the activation, inhibition, and allosteric rate constants
for abbrev, value in zip(["I", "ACT", "L"], [1e6, 1e6, 1e6**2]):
    # Account for the enzyme prefix if used in the previous function
    to_join = ("kf", PFK.id, abbrev)
    param = "_".join(to_join)
    parameter_values.update({param: value})

# Display numerical values
for k, v in parameter_values.items():
    print("{0} = {1}".format(k, v))
```

    Keq_PFK_A = 14.705882352941176
    Keq_PFK_F = 10.0
    Keq_PFK_I = 10.0
    Keq_PFK_ACT = 30.3030303030303
    Keq_PFK_L = 0.0011
    kf_PFK = 307263
    kf_PFK_A = 200325
    kf_PFK_F = 1000059
    kf_PFK_I = 1000000.0
    kf_PFK_ACT = 1000000.0
    kf_PFK_L = 1000000000000.0

### Solve steady state concentrations numerically

Once the rate constants have been defined, the steady state concentrations of the enzyme can be determined.

```python
# Substitute values into equations
initial_conditions.update({
    str(enzyme_module_form): float(sym.simplify(solution.subs(parameter_values)))
    for enzyme_module_form, solution in enzyme_solutions.items()})

for header, dictlist in zip(["Ligand", "\nEnzyme"], [PFK.enzyme_module_ligands, PFK.enzyme_module_forms]):
    header += " Concentrations"
    print("\n".join([header, "-" * len(header)]))
    for form in dictlist:
        ic = initial_conditions[form.id]
        print("{0} = {1}".format(form.id, ic))
```

    Ligand Concentrations
    ---------------------
    f6p_c = 0.0198
    fdp_c = 0.0146
    atp_c = 1.6
    adp_c = 0.29
    amp_c = 0.0867281
    h_c = 8.99757e-05

    Enzyme Concentrations
    ----------------------
    pfk_R0_c = 3.705684451779081e-08
    pfk_R0_A_c = 1.1270977736701491e-07
    pfk_R0_AF_c = 2.1036774576199985e-08
    pfk_T0_c = 4.0762528969569896e-11
    pfk_R1_c = 3.895599656998077e-07
    pfk_R1_A_c = 1.1848611930259641e-06
    pfk_R1_AF_c = 2.2114902898450058e-07
    pfk_T1_c = 2.6088018540524733e-09
    pfk_R2_c = 1.5357179846004314e-06
    pfk_R2_A_c = 4.670943637948869e-06
    pfk_R2_AF_c = 8.71810686394121e-07
    pfk_T2_c = 6.261124449725935e-08
    pfk_R3_c = 2.690705109903528e-06
    pfk_R3_A_c = 8.183880139927137e-06
    pfk_R3_AF_c = 1.5274845331446054e-06
    pfk_T3_c = 6.678532746374332e-07
    pfk_R4_c = 1.7678768321380616e-06
    pfk_R4_A_c = 5.377063448209202e-06
    pfk_R4_AF_c = 1.0036047828713532e-06
    pfk_T4_c = 2.6714130985497327e-06

#### Set Initial Conditions and Parameters
Once the steady state concentrations have been determined, the initial conditions and parameters are added to the module. All custom parameter are added to the custom_parameter attribute. The allosteric transition uses the standard parameter identifiers (returned by `kf_str` and `Keq_str` properties of the `EnzymeModuleReaction`), so they are popped out of the custom parameters and set through their respective attribute setter methods.

```python
# Set initial conditions
for met, concentration in initial_conditions.items():
    PFK.metabolites.get_by_id(str(met)).ic = concentration

# Add the custom parameters and values for kf and Keq to model
PFK.custom_parameters.update(parameter_values)
# PFK_L uses standard reaction parameters and not custom parameters
PFK_L = PFK.enzyme_module_reactions.PFK_L
PFK_L.kf = PFK.custom_parameters.pop(PFK_L.kf_str)
PFK_L.Keq = PFK.custom_parameters.pop(PFK_L.Keq_str)

# Set parameter values in reaction fields
for group in PFK.enzyme_module_reactions_categorized:
    if group.id == "atp_c_binding":
        param_id = "PFK_A"
    elif group.id == "f6p_c_binding":
        param_id = "PFK_F"
    elif group.id == "catalyzation":
        param_id = "PFK"
    elif group.id == "atp_c_inhibition":
        param_id = "PFK_I"
    elif group.id == "amp_c_activation":
        param_id = "PFK_ACT"
    else:
        continue
    for reaction in group.members:
        kf, Keq = ("kf_" + param_id, "Keq_" + param_id)
        if kf in PFK.custom_parameters:
            reaction.kf = PFK.custom_parameters[kf]
        if Keq in PFK.custom_parameters:
            reaction.Keq = PFK.custom_parameters[Keq]
```

#### Ordering of internal species and reactions

Sometimes, it is also desirable to reorder the metabolite and reaction objects inside the model to follow the physiology. To reorder the internal objects, one can use `cobra.DictList` containers and the `DictList.get_by_any` method with the list of object identifiers in the desirable order. To ensure all objects are still present and not forgotten in the model, a small QA check is also performed.

```python
new_metabolite_order = ['f6p_c', 'fdp_c', 'amp_c', 'adp_c', 'atp_c', 'h_c',
                        'pfk_R0_c', 'pfk_R0_A_c', 'pfk_R0_AF_c',
                        'pfk_R1_c', 'pfk_R1_A_c', 'pfk_R1_AF_c',
                        'pfk_R2_c', 'pfk_R2_A_c', 'pfk_R2_AF_c',
                        'pfk_R3_c', 'pfk_R3_A_c', 'pfk_R3_AF_c',
                        'pfk_R4_c', 'pfk_R4_A_c', 'pfk_R4_AF_c',
                        'pfk_T0_c','pfk_T1_c', 'pfk_T2_c', 'pfk_T3_c', 'pfk_T4_c']

if len(glycolysis.metabolites) == len(new_metabolite_order):
    PFK.metabolites = DictList(
        PFK.metabolites.get_by_any(new_metabolite_order))

if len(PFK.metabolites) == len(new_metabolite_order):
    PFK.metabolites = DictList(PFK.metabolites.get_by_any(new_metabolite_order))

new_reaction_order = ["PFK_R01", 'PFK_R02', "PFK_R03", "PFK_R10",
                      "PFK_R11", "PFK_R12", "PFK_R13", "PFK_R20",
                      "PFK_R21", "PFK_R22", "PFK_R23", "PFK_R30",
                      "PFK_R31", "PFK_R32", "PFK_R33", "PFK_R40",
                      "PFK_R41", "PFK_R42", "PFK_R43", "PFK_L",
                      "PFK_T1", "PFK_T2", "PFK_T3", "PFK_T4"]

if len(PFK.reactions) == len(new_reaction_order):
    PFK.reactions = DictList(
        PFK.reactions.get_by_any(new_reaction_order))

PFK.update_S(array_type="DataFrame", dtype=int)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>PFK_R01</th>
      <th>PFK_R02</th>
      <th>PFK_R03</th>
      <th>PFK_R10</th>
      <th>PFK_R11</th>
      <th>PFK_R12</th>
      <th>PFK_R13</th>
      <th>PFK_R20</th>
      <th>PFK_R21</th>
      <th>PFK_R22</th>
      <th>...</th>
      <th>PFK_R33</th>
      <th>PFK_R40</th>
      <th>PFK_R41</th>
      <th>PFK_R42</th>
      <th>PFK_R43</th>
      <th>PFK_L</th>
      <th>PFK_T1</th>
      <th>PFK_T2</th>
      <th>PFK_T3</th>
      <th>PFK_T4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>f6p_c</th>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>fdp_c</th>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>amp_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>adp_c</th>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>atp_c</th>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>-1</td>
      <td>-1</td>
      <td>-1</td>
    </tr>
    <tr>
      <th>h_c</th>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R0_c</th>
      <td>-1</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R0_A_c</th>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R0_AF_c</th>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R1_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R1_A_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R1_AF_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R2_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R2_A_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R2_AF_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R3_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R3_A_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R3_AF_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R4_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R4_A_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_R4_AF_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_T0_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_T1_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_T2_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
      <td>0</td>
    </tr>
    <tr>
      <th>pfk_T3_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
    </tr>
    <tr>
      <th>pfk_T4_c</th>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>...</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>0</td>
      <td>1</td>
    </tr>
  </tbody>
</table>
<p>26 rows × 24 columns</p>
</div>

## Module Validation
### QC/QA model
Before saving the module, it is important to ensure that the module is elementally balanced, and that the module can be integrated into a larger network for simulation. Therefore, the `qcqa_model` function from `mass.util.qcqa` is used to provide a report on the module quality and and indicate whether simulation is possible and if not, what parameters and/or initial conditions are missing.

```python
qcqa_model(PFK, parameters=True, concentrations=True,
           fluxes=False, superfluous=True, elemental=True)
```

    ╒══════════════════════════════════════════╕
    │ MODEL ID: PFK                            │
    │ SIMULATABLE: True                        │
    │ PARAMETERS NUMERICALY CONSISTENT: True   │
    ╞══════════════════════════════════════════╡
    ╘══════════════════════════════════════════╛

### Constraint Satisfaction and Error Values
Another QA check we perform is to substitute the steady state numerical values back into the constraints used in determining the rate constants in order to ensure that the constraints remain satisified, and that errors are small.

```python
t_fraction = PFK.make_enzyme_fraction("forms", top="Tense",
                                      bottom="Equation", use_values=True)
print("Enzyme T-fraction: {:.4f}".format(t_fraction))

print("Concentration Absolute Error: {0:.4e}".format(
    abs(PFK.enzyme_concentration_total_error(use_values=True))))
print("Flux Absolute Error: {0:.4e}".format(
    abs(PFK.enzyme_rate_error(use_values=True))))
```

    Enzyme T-fraction: 0.1032
    Concentration Absolute Error: 1.2079e-11
    Flux Absolute Error: 2.2204e-16

### Add Enzyme to MassModel
In order to determine whether the module can be successfully integrated into a model, another model can be loaded, merged with the module, and simulated. To validate this module, it will be merged with a glycolysis model.

To integrate the `EnzymeModule` into the `MassModel`, the reaction that the EnzymeModule will be replacing is first removed. The `MassModel.merge` method can then be utilized to add the `EnzymeModule` to the `MassModel`.

When merging an `EnzymeModule` and a `MassModel`, the `EnzymeModule` should always be merged into the `MassModel`.

```python
# Load and merge glycolysis with PFK model
glycolysis = create_example_model("SB2_Glycolysis.json")
# Remove the PFK MassReaction, then merge the EnzymeModule into the MassModel
glycolysis.remove_reactions([glycolysis.reactions.get_by_id("PFK")])
glycolysis_PFK = glycolysis.merge(PFK)
glycolysis_PFK
```

<table>
    <tr>
        <td><strong>Name</strong></td><td>Glycolysis</td>
    </tr><tr>
        <td><strong>Memory address</strong></td><td>0x07ff5e94eb7f0</td>
    </tr><tr>
        <td><strong>Stoichiometric Matrix</strong></td>
        <td>40x44</td>
    </tr><tr>
        <td><strong>Matrix Rank</strong></td>
        <td>37</td>
    </tr><tr>
        <td><strong>Number of metabolites</strong></td>
        <td>40</td>
    </tr><tr>
        <td><strong>Initial conditions defined</strong></td>
        <td>40/40</td>
    </tr><tr>
        <td><strong>Number of reactions</strong></td>
        <td>44</td>
    </tr><tr>
        <td><strong>Number of genes</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Number of enzyme modules</strong></td>
        <td>1</td>
    </tr><tr>
        <td><strong>Number of groups</strong></td>
        <td>16</td>
    </tr><tr>
        <td><strong>Objective expression</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Compartments</strong></td>
        <td>Cytosol</td>
    </tr>
</table>

Using `MassModel.merge` class method enables the `EnzymeModule` and `MassModel` to be merged like as if they were both `MassModel` objects. However, all attributes specific to the `EnzymeModule` (e.g the categorized attributes) are condensed into a speciailzed container called an `EnzymeModuleDict`.

The `EnzymeModuleDict` behaves like an ordered dictionary, but is unique in that its contents can be accessed as if they were attributes. These attributes can be viewed using `EnzymeModuleDict.keys` method. All `EnzymeModuleDicts` associated with a `MassModel` can be accessed via `MassModel.enzyme_modules` attribute.

```python
print(str(glycolysis_PFK.enzyme_modules) + "\n")
print("Attribute Accessors:\n-------------------\n" + "\n".join(list(
    glycolysis_PFK.enzyme_modules.PFK.keys())) + "\n")
glycolysis_PFK.enzyme_modules.PFK
```

    [<EnzymeModuleDict PFK at 0x7ff5f8aabcc0>]

    Attribute Accessors:
    -------------------
    id
    name
    subsystem
    enzyme_module_ligands
    enzyme_module_forms
    enzyme_module_reactions
    enzyme_module_ligands_categorized
    enzyme_module_forms_categorized
    enzyme_module_reactions_categorized
    enzyme_concentration_total
    enzyme_rate
    enzyme_concentration_total_equation
    enzyme_rate_equation
    S
    model

<table>
    <tr>
        <td><strong>Name</strong></td><td>PFK</td>
    </tr><tr>
        <td><strong>Memory address</strong></td><td>0x07ff5f8aabcc0</td>
    </tr><tr>
        <td><strong>Stoichiometric Matrix</strong></td>
        <td>26x24</td>
    </tr><tr>
        <td><strong>Matrix Rank</strong></td>
        <td>20</td>
    </tr><tr>
        <td><strong>Subsystem</strong></td>
        <td>Glycolysis</td>
    </tr><tr>
        <td><strong>Number of Ligands</strong></td>
        <td>6</td>
    </tr><tr>
        <td><strong>Number of EnzymeForms</strong></td>
        <td>20</td>
    </tr><tr>
        <td><strong>Number of EnzymeModuleReactions</strong></td>
        <td>24</td>
    </tr><tr>
        <td><strong>Enzyme Concentration Total</strong></td>
        <td>3.3e-05</td>
    </tr><tr>
        <td><strong>Enzyme Net Flux</strong></td>
        <td>1.12</td>
    </tr>
</table>

### Validate Steady State

To find the steady state of the model and perform simulations, the model must first be loaded into a `Simulation`. In order to load a model into a `Simulation`, the model must be simulatable, meaning there are no missing numerical values that would prevent the integration of the ODEs that comprise the model. The `verbose` argument can be used while loading a model to produce a message indicating the successful loading of a model, or why a model could not load.

Once loaded into a `Simulation`, the `find_steady_state` method can be used with the `update_values` argument in order to update the initial conditions and fluxes of the model to a steady state. The model can then be simulated using the `simulate` method by passing the model to simulate, and a tuple containing the start time and the end time. The number of time points can also be included, but is optional.

After a successful simulation, two `MassSolution` objects are returned. The first `MassSolution` contains the concentration results of the simulation, and the second contains the flux results of the simulation.

To visually validate the steady state of the model, concentration and flux solutions can be plotted using the `plot_time_profile` function from `mass.visualization`. Alternatively, the `MassSolution.view_time_profile` property can be used to quickly generate a time profile for the results.

```python
# Setup simulation object, ensure model is at steady state
sim = Simulation(glycolysis_PFK, verbose=True)
sim.find_steady_state(glycolysis_PFK, strategy="simulate",
                      update_values=True, verbose=True,
                      tfinal=1e4, steps=1e6)

# Simulate from 0 to 1000 with 10001 points in the output
conc_sol, flux_sol = sim.simulate(
    glycolysis_PFK,time=(0, 1e3))
# Quickly render and display time profiles
conc_sol.view_time_profile()
```

### Storing information and references
#### Compartment
Because the character "c" represents the cytosol compartment, it is recommended to define and set the compartment in the `EnzymeModule.compartments` attribute.

```python
PFK.compartments = {"c": "Cytosol"}
print(PFK.compartments)
```

    {'c': 'Cytosol'}

#### Units
All of the units for the numerical values used in this model are "Millimoles" for amount and "Liters" for volume (giving a concentration unit of 'Millimolar'), and "Hours" for time. In order to ensure that future users understand the numerical values for model, it is important to define the `MassModel.units` attribute.

The `MassModel.units` is a `cobra.DictList` that contains only `UnitDefinition` objects from the `mass.core.unit` submodule. Each `UnitDefinition` is created from `Unit` objects representing the base units that comprise the `UnitDefinition`. These `Units` are stored in the `list_of_units` attribute. Pre-built units can be viewed using the `print_defined_unit_values` function from the `mass.core.unit` submodule. Alternatively, custom units can also be created using the `UnitDefinition.create_unit` method. For more information about units, please see the module docstring for `mass.core.unit` submodule.

__Note:__ It is important to note that this attribute will NOT track units, but instead acts as a reference for the user and others so that they can perform necessary unit conversions.

```python
# Using pre-build units to define UnitDefinitions
concentration = UnitDefinition("mM", name="Millimolar",
                               list_of_units=["millimole", "per_litre"])
time = UnitDefinition("hr", name="hour", list_of_units=["hour"])

# Add units to model
PFK.add_units([concentration, time])
print(PFK.units)
```

    [<UnitDefinition Millimolar "mM" at 0x7ff5b8b8e130>, <UnitDefinition hour "hr" at 0x7ff5b8b8e940>]

## Export

After validation, the model is ready to be saved. The model can either be exported as a ".json" file or as an ".sbml" (".xml") file using their repsective submodules in `mass.io`.

To export the model, only the path to the directory and the model object itself need to be specified.

### Export using SBML

```python
sbml.write_sbml_model(mass_model=PFK, filename="SB2_" + PFK.id + ".xml")
```

### Export using JSON

```python
json.save_json_model(mass_model=PFK, filename="SB2_" + PFK.id + ".json")
```


# Calculating Thermodynamics Observables with a quantum computer

```python
# imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from functools import partial

from qiskit.utils import QuantumInstance
from qiskit import Aer
from qiskit.algorithms import NumPyMinimumEigensolver, VQE

from qiskit_nature.drivers import UnitsType, Molecule
from qiskit_nature.drivers.second_quantization import (
    ElectronicStructureDriverType,
    ElectronicStructureMoleculeDriver,
)
from qiskit_nature.problems.second_quantization import ElectronicStructureProblem
from qiskit_nature.converters.second_quantization import QubitConverter
from qiskit_nature.mappers.second_quantization import JordanWignerMapper
from qiskit_nature.algorithms import GroundStateEigensolver
import qiskit_nature.constants as const
from qiskit_nature.algorithms.pes_samplers import BOPESSampler, EnergySurface1DSpline
from thermodynamics_utils.thermodynamics import constant_volume_heat_capacity
from thermodynamics_utils.vibrational_structure_fd import VibrationalStructure1DFD
from thermodynamics_utils.partition_function import DiatomicPartitionFunction
from thermodynamics_utils.thermodynamics import Thermodynamics

import warnings

warnings.simplefilter("ignore", np.RankWarning)
```

A preliminary draft with more information related to this tutorial can be found in preprint: Stober et al, arXiv 2003.02303 (2020)

### Calculation of the Born Oppenheimer Potential Energy Surface (BOPES)

To compute thermodynamic observables we begin with single point energy calculation which calculates the wavefunction and charge density and therefore  the energy of a particular arrangement of nuclei. Here we  compute the Born-Oppenheimer potential energy surface of a hydrogen molecule, as an example, which is simply the electronic energy as a function of bond length.

```python
qubit_converter = QubitConverter(mapper=JordanWignerMapper())
quantum_instance = QuantumInstance(backend=Aer.get_backend("aer_simulator_statevector"))
solver = VQE(quantum_instance=quantum_instance)

me_gss = GroundStateEigensolver(qubit_converter, solver)
```

```python
stretch1 = partial(Molecule.absolute_distance, atom_pair=(1, 0))
mol = Molecule(
    geometry=[("H", [0.0, 0.0, 0.0]), ("H", [0.0, 0.0, 0.2])],
    degrees_of_freedom=[stretch1],
    masses=[1.6735328e-27, 1.6735328e-27],
)

# pass molecule to PSYCF driver
driver = ElectronicStructureMoleculeDriver(mol, driver_type=ElectronicStructureDriverType.PYSCF)

es_problem = ElectronicStructureProblem(driver)
```

```python
# BOPES sampler testing
bs = BOPESSampler(me_gss, bootstrap=True)
points = np.linspace(0.45, 5, 50)
res = bs.sample(es_problem, points)
```

```python
energies = []
bs_res_full = res.raw_results
for point in points:
    energy = bs_res_full[point].computed_energies + bs_res_full[point].nuclear_repulsion_energy
    energies.append(energy)
```

```python
fig = plt.figure()
plt.plot(points, energies)
plt.title("Dissociation profile")
plt.xlabel("Interatomic distance")
plt.ylabel("Energy")
```

```python
energy_surface = EnergySurface1DSpline()

xdata = res.points
ydata = res.energies
energy_surface.fit(xdata=xdata, ydata=ydata)
```

```python
plt.plot(xdata, ydata, "kx")
x = np.arange(min(xdata) - 0.25, max(xdata) + 0.25, 0.05)
plt.plot(x, energy_surface.eval(x), "r-")
plt.xlabel(r"distance, $\AA$")
plt.ylabel("energy, Hartree")
dist = max(ydata) - min(ydata)
plt.ylim(min(ydata) - 0.1 * dist, max(ydata) + 0.1 * dist)
```

### Calculation of the molecular Vibrational Energy levels

The Born-Oppeheimer approximation removes internuclear vibrations from the molecular Hamiltonian and the energy computed from quantum mechanical ground-state energy calculations using this approximation contain only the electronic energy. Since even at absolute zero internuclear vibrations still occur, a correction is required to obtain the true zero-temperature energy of a molecule. This correction is called the zero-point vibrational energy (ZPE), which is computed by summing the contribution from internuclear vibrational modes. Therefore, the next step in computing thermodynamic observables is determining the vibrational energy levels. This can be done by constructing the Hessian matrix based on computed single point energies close to the equilibrium bond length. The eigenvalues of the Hessian matrix can then be used to determine the vibrational energy levels and the zero-point vibrational energy
\begin{equation}
{\rm ZPE} = \frac{1}{2}\, \sum_i ^M \nu_i \, ,
\end{equation}
with $\nu_i$ being the vibrational frequencies, $M = 3N − 6$ or $M = 3N − 5$ for non-linear or linear molecules, respectively, and $N$ is number of the particles.

Here we fit a "full" energy surface using a 1D spline potential and use it to evaluate molecular vibrational energy levels.

```python
vibrational_structure = VibrationalStructure1DFD(mol, energy_surface)

plt.plot(xdata, ydata, "kx")
x = np.arange(min(xdata) - 0.25, max(xdata) + 0.25, 0.05)
plt.plot(x, energy_surface.eval(x), "r-")
plt.xlabel(r"distance, $\AA$")
plt.ylabel("energy, Hartree")
dist = max(ydata) - min(ydata)
plt.ylim(min(ydata) - 0.1 * dist, max(ydata) + 0.1 * dist)
for N in range(15):
    on = np.ones(x.shape)
    on *= energy_surface.eval(
        energy_surface.get_equilibrium_geometry()
    ) + vibrational_structure.vibrational_energy_level(N)
    plt.plot(x, on, "g:")
on = np.ones(x.shape)

plt.show()
```

### Create a partition function for the calculation of heat capacity

The partition function for a molecule is the product of contributions from translational, rotational, vibrational, electronic, and nuclear degrees of freedom. Having the vibrational frequencies, now we can obtain the vibrational partition function  $q_{\rm vibration}$ to compute the whole molecular partition function
\begin{equation}
q_{\rm vibration} = \prod_{i=1} ^M \frac{\exp\,(-\Theta_{\nu_i}/2T)}{1-\exp\,(-\Theta_{\nu_i}/2T)} \, .
\end{equation}
Here $\Theta_{\nu_i}= h\nu_i/k_B$, $T$ is the temperature and $k_B$ is the Boltzmann constant.

The single-point energy calculations and the resulting partition function can be used to calculate the (constant volume or constant pressure) heat capacity of the molecules. The constant volume heat capacity, for example, is given by

\begin{equation}
C_v = \left.\frac{\partial U}{\partial T}\right|_{N,V}\, ,
\qquad
{\rm with} \quad
U=k_B T^2 \left.\frac{\partial {\rm ln} Q}{\partial T}\right|_{N,V} .
\end{equation}

$U$ is the internal energy, $V$ is the volume and $Q$ is the partition function.

Here we illustrate the simplest usage of the partition function, namely creating a Thermodynamics object to compute properties like the constant pressure heat capacity defined above.

```python
Q = DiatomicPartitionFunction(mol, energy_surface, vibrational_structure)

P = 101350  # Pa
temps = np.arange(10, 1050, 5)  # K

mol.spins = [1 / 2, 1 / 2]

td = Thermodynamics(Q, pressure=101350)
td.set_pressure(101350)
temps = np.arange(10, 1500, 5)
ymin = 5
ymax = 11

plt.plot(temps, td.constant_pressure_heat_capacity(temps) / const.CAL_TO_J)
plt.xlim(0, 1025)
plt.ylim(ymin, ymax)
plt.xlabel("Temperature, K")
plt.ylabel("Cp, cal mol$^{-1}$ K$^{-1}$")

plt.show()
```

Here we demonstrate how to access particular components (the rotational part) of the partition function, which in the H2 case we can further split to para-hydrogen and ortho-hydrogen   components.

```python
eq = Q.get_partition(part="rot", split="eq")
para = Q.get_partition(part="rot", split="para")
ortho = Q.get_partition(part="rot", split="ortho")
```

We will now plot the constant volume heat capacity (of the rotational part) demonstrating how we can call directly the functions in the 'thermodynamics' module, providing a callable object for the partition function (or in this case its rotational component). Note that in the plot we normalize the plot dividing by the universal gas constant R (Avogadro's number times Boltzmann's constant) and we use crossed to compare with experimental data found in literature.

```python
# REFERENCE DATA from literature
df_brink_T = [80.913535, 135.240157, 176.633783, 219.808499, 246.226899]
df_brink_Cv = [0.118605, 0.469925, 0.711510, 0.833597, 0.895701]

df_eucken_T = [
    25.120525,
    30.162485,
    36.048121,
    41.920364,
    56.195875,
    62.484934,
    72.148692,
    73.805910,
    73.804236,
    92.214423,
    180.031917,
    230.300866,
]
df_eucken_Cv = [
    0.012287,
    0.012354,
    0.008448,
    0.020478,
    0.032620,
    0.048640,
    0.048768,
    0.076678,
    0.078670,
    0.170548,
    0.667731,
    0.847681,
]

df_gia_T = [
    190.919338,
    195.951254,
    202.652107,
    204.292585,
    209.322828,
    225.300754,
    234.514217,
    243.747768,
]
df_gia_Cv = [0.711700, 0.723719, 0.749704, 0.797535, 0.811546, 0.797814, 0.833793, 0.845868]

df_parting_T = [80.101665, 86.358919, 185.914204, 239.927797]
df_parting_Cv = [0.084730, 0.138598, 0.667809, 0.891634]

df_ce_T = [
    80.669344,
    135.550569,
    145.464190,
    165.301153,
    182.144856,
    203.372528,
    237.993108,
    268.696642,
    294.095771,
    308.872014,
]
df_ce_Cv = [
    0.103048,
    0.467344,
    0.541364,
    0.647315,
    0.714078,
    0.798258,
    0.891147,
    0.944848,
    0.966618,
    0.985486,
]
```

```python
HeatCapacity = constant_volume_heat_capacity

R = const.N_A * const.KB_J_PER_K
plt.plot(temps, HeatCapacity(eq, temps) / R, "-k", label="Cv_rot Equilibrium")
plt.plot(temps, HeatCapacity(para, temps) / R, "-b", label="Cv_rot Para")
plt.plot(temps, HeatCapacity(ortho, temps) / R, "-r", label="Cv_rot Ortho")
plt.plot(
    temps,
    0.25 * HeatCapacity(para, temps) / R + 0.75 * HeatCapacity(ortho, temps) / R,
    "-g",
    label="Cv_rot 1:3 para:ortho",
)
plt.plot(df_brink_T, df_brink_Cv, "+g")
plt.plot(df_eucken_T, df_eucken_Cv, "+g")
plt.plot(df_gia_T, df_gia_Cv, "+g")
plt.plot(df_parting_T, df_parting_Cv, "+g")
plt.plot(df_ce_T, df_ce_Cv, "+g", label="experimental data")
plt.legend(loc="upper right", frameon=False)
plt.xlim(10, 400)
plt.ylim(-0.1, 2.8)
plt.xlabel("Temperature, K")
plt.ylabel("Cv (rotational)/R")
plt.tight_layout()
plt.show()
```

```python
import qiskit.tools.jupyter

%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

qiskit-terra	0.20.1

qiskit-aer	0.10.4

qiskit-ignis	0.7.0

qiskit-ibmq-provider	0.19.0

qiskit-nature	0.4.0

System information

Python version	3.8.12

Python compiler	Clang 10.0.0

Python build	default, Oct 12 2021 06:23:56

OS	Darwin

CPUs	8

Memory (Gb)	64.0

Thu Jun 09 09:01:03 2022 CEST

This code is a part of Qiskit

© Copyright IBM 2017, 2022.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.


```python
from sympy.matrices import Matrix
import sympy as sp
import numpy as np
from Exercise import Exercise, MarkdownBlock

from process_latex import process_sympy

try:
    from config import URL, TOKEN
except:
    None

# TODO: replace with supplied strings
Exercise.URL = URL
Exercise.TOKEN = TOKEN
```

```python

```

$\displaystyle \left[\begin{matrix}4\\6\end{matrix}\right]$

## Introduction
In this notebook, you are about to create some (linear algebra) exercises using the developed `Exercise` Python library aiming to facilitate authoring parameterized mathematics exercises at a high level of abstraction (i.e. access to a scripting language and the libraries available in there, including as SymPy, NumPy and Matplotlib).
Created exercises can be 'played' inline, using the web-based player developed as part of this project.
Roughly speaking this project is new combination of existing approaches: MEGUA-like parameterized text, SymPy's CAS functionality and exercise-setup as used by Grasple and SageMath for working with mathematical objects in notebooks.

The goal is to evaluate the usability of the developed library and the authoring setup (this notebook).
Note that by no means you or your skills are being tested, it is by no means a problem if exercises are left uncompleted.
Notes, comments and suggestions are very welcome, please write these either as code-comments or in the Markdown cells in the notebook.
All feedback will be reported and reflected upon anonymously.
Completing the notebook should take about 30 minutes, depending on setup time, prior knowledge about this project, familiarity with linear algebra and the supplied frameworks etc.
Please download the notebook when done and send it by email.
After completion, in a brief semi-structured interview, you can further elaborate upon your experiences.

To start creating exercises, please replace the `URL` and `TOKEN` in the block above with the strings supplied by email:
```
Exercise.URL = "<supplied_url_here>"
Exercise.TOKEN = "<supplied_token_here>"
```

Assumptions:
- Familiarity with Python, Markdown, LaTeX
- Familiarity with Jupyter-Notebook
- Familiarity with the very basics of linear algebra

Recommendations:
- Use Binder (www.mybinder.org) to edit this notebook, if you prefer local setup instead, see README.md.
- Use Firefox, the iFrame exercise player embeddings do not work in Chrome or Safari due to global cross-origin policies set by these browsers.
- Other browsers (Chrome, Safari) can still be used, however, playing exercises is only possible outside of the notebook by clicking the generated exercise links, which is rather inconvenient.

Notes:
- Documentation can for the Python library can be found in the `html` directory.
- Within Jupyter-Notebook, function documentation can be viewed by writing a `?` after the function, like so: `Exercise("What is $1 + 1$?").add_answer?`
- Within exercises, only inline math notation is supported.
- Preview-exercises are purged from the server from time to time, don't expect long-term, persistent availability of any played exercises.
- Please skip an exercise in case completing it requires more than a few minutes.

Happy coding ;)

## Exercise Basics
The most basic exercise contains a Markdown string with the exercise content and a single answer rule specifying the correct answer.
Mathematics notation can be written inline in LaTeX between dollar signs.

```python
# Create an exercise instance
e = Exercise("What is $1 + 1$?")
# Add 2 as a correct answer
e.add_answer(2, True, "Correct!")
# Verify that the exercise is working correctly
e.play()
# Note: as of now, all basic arithmatic is simplified by sp.simplify(...), there is not yet a way to control this behaviour;
# therefore writing 1 + 1 in the answer box is accepted correct
# Details on what is simplified: https://docs.sympy.org/latest/tutorial/simplification.html
```

    Published succesfully, preview at: https://www.mscthesis.nl/preview?id=1f62594b-70fc-4cd7-ad2b-b6f50c01c85c

Let's imagine the typical student mistake for this exercise is computing $1 - 1 = 0$ instead.
We add an answer rule to catch that error and provide the student with answer-specific feedback.

```python
e.add_answer(0, False, "🤔 That's not right, did you compute $1 - 1 = 0$ instead?")
# Verify that the specific feedback is shown
e.play()
```

    Published succesfully, preview at: https://www.mscthesis.nl/preview?id=59116756-efb1-4e2b-a102-b4494b2b0120

### Task 1
Create an exercise asking learners to compute $3/3$.
Provide answer-specific feedback in case learners compute $3*3$ instead.
Add default feedback (using `e.add_default_feedback(...)`) with a link pointing to a source of preference explaining (integer) devision (hint: `[link](www.example.com)`).
Feel free to embed your favorite meme or xkcd at a correct/incorrect answer (hint ``).

```python
# Task 1 user code:
```

## Templating Exercises
Exercises can be parameterized/templated (still looking for the correct terminology on this one), this allows for two things:
1. Randomization. By making part of the content random, multiple instances can be generated, allowing for repeated practice.
2. Abstraction. By utilizing the functionality of SymPy objects to be translated to LaTeX, authoring exercises remains efficient and effective.

The integer-exercise can be randomized as follows:

```python
string = """
### Integer addition

Please compute $@a + @b$
"""

params = {}
# avoid 0 + 0 instance, since 0 + 0 == 0 - 0, answer same in case our typical mistake is made
params["a"] = np.random.randint(0, 10)
params["b"] = np.random.randint(1, 10)
params["ans_correct"] = params["a"] + params["b"]
params["ans_incorrect"] = params["a"] - params["b"]

e = Exercise(MarkdownBlock(string, params))
e.add_answer(params["ans_correct"], True, "Correct!")
e.add_answer(params["ans_incorrect"], False, MarkdownBlock("Did you compute $@a - @b = @ans_incorrect$ instead?", params))

e.play()
```

    Published succesfully, preview at: https://www.mscthesis.nl/preview?id=aec6d9f1-b0d1-44d8-98fe-5d095810c2b5

```python
s = """
What is $@a^\intercal$?
"""

params = {}
params["a"] = sp.Matrix([[1, 2], [3, 4]])
params["ans"] = params["a"].T
e = Exercise(MarkdownBlock(s, params))
e.add_answer(params["ans"], True, "You are right!")
e.write("demo_transpose")
# e.play()

```

```python
s = "What is $@a^\intercal$?"

params = {}
params["a"] = sp.Matrix([[1, 2], [3, 4]])
params["ans"] = params["a"].T

e = Exercise(MarkdownBlock(s, params))
e.add_answer(params["ans"], True, "You are right!")
e.play()
```

    Published succesfully, preview at: https://www.mscthesis.nl/preview?id=aaab78fc-982f-4a11-833c-b86622e8f67d

Currently, only a single instance is generated played at a time. Support for multi-instance generation is planned.

### Working with SymPy objects to represent mathematical objects
We can work with SymPy objects to represent mathematical objects, like vectors and matrices.
An vector addition exercise can be created as follows:

```python
string = "What is $@v_1 + @v_2$?"

params["v_1"] = sp.Matrix([1, 2, 3])
params["v_2"] = sp.Matrix([4, 5, 6])
params["ans"] = params["v_1"] + params["v_2"]

e = Exercise(MarkdownBlock(string, params))
e.add_answer(params["ans"], True, "That's right!")

e.play()
```

### Task 2 Parameterized vector addition
Create an exercise asking learners to compute the sum of two vectors of random length (within reasonable limits), with random integer values.
Note: if you prefer NumPy for working with matrices, you are in luck! NumPy objects can be passed to the SymPy matrix constructor, e.g. `sp.Matrix(np.arange(4))`.

```python
# Task 2 user code:
```

### Task 3 - Matrix indexing
Create an exercise asking learners to identify a value at randomized indices (but within bounds) in a 5 by 5 matrix.
Please make sure all values are unique so there is only one correct answer.

```python
# Task 3 user code:
```

### Task 4 - Matrix multiplication
Create an exercise asking users to multiply two matrices.
Provide a default answer explaining the procedure in case a wrong answer is supplied.
You can use the `symbolic_matrix` and `explain_multiply` functions supplied in `helpers.py` as follows:

```python
from helpers import symbolic_matrix, explain_multiply
a = symbolic_matrix("a", 2, 2)
b = symbolic_matrix("b", 2, 2)
display(explain_multiply(a, b))

a = sp.Matrix([1,2,3])
b = sp.Matrix(np.matrix([5,6,7]).reshape(-1))
display(explain_multiply(a, b))
```

$\displaystyle \left[\begin{matrix}{a}_{1, 1} \cdot {b}_{1, 1} + {a}_{1, 2} \cdot {b}_{2, 1} & {a}_{1, 1} \cdot {b}_{1, 2} + {a}_{1, 2} \cdot {b}_{2, 2}\\{a}_{2, 1} \cdot {b}_{1, 1} + {a}_{2, 2} \cdot {b}_{2, 1} & {a}_{2, 1} \cdot {b}_{1, 2} + {a}_{2, 2} \cdot {b}_{2, 2}\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}1 \cdot 5 & 1 \cdot 6 & 1 \cdot 7\\2 \cdot 5 & 2 \cdot 6 & 2 \cdot 7\\3 \cdot 5 & 3 \cdot 6 & 3 \cdot 7\end{matrix}\right]$

```python
# Task 4 user code:
```

Hooray!
If you made it this far, you completed the notebook!
Please add any additonal comments below.
Thank you for participating!

Write any additional comments here...


---
## Archivos `.dat` + `ReadMe`

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

### Resumen

En este cuaderno se utiliza la librería `astropy.io` para leer una arcivo de datos de texto plano con extensión .dat (formato ascii) junto con un archivo adicional ReadMe que contiene los metadatos.

---

## 1. Obtención y Lectura de los Datos

Consideraremos el conjunto de datos reportado Greene and Ho [2006], el cual contiene información de 88 galaxias cercanas.

Greene, J. E. and Ho, L. C. *The MBH − σ∗ Relation in Local Active Galaxies*. ApJ 641 L21 (2006)
https://ui.adsabs.harvard.edu/abs/2006ApJ...641L..21G/abstract

El conjunto de datos está disponibele, en varios formatos en

http://vizier.cfa.harvard.edu/viz-bin/VizieR?-source=J/ApJ/641/L21.

Al hacer click en  el link ReadMe+ftp link se puede acceder a los datos via FTP.

---

### Abrir los archivos .dat+ReadMe.

Para acceder a la información del archivo plano .dat file (en formato ascii) y los metadatos en el archivo ReadMe se utilizará el comando  [astropy.io.ascii](https://docs.astropy.org/en/stable/io/ascii/)

```python
import numpy as np
from astropy.io import ascii
import matplotlib.pyplot as plt

data = ascii.read('table1.dat', readme='ReadMe')
data
```

    WARNING: UnitsWarning: '[10-7W]' did not parse as cds unit: Syntax error If this is meant to be a custom unit, define it with 'u.def_unit'. To have it recognized inside a file reader or other code, enable it with 'u.add_enabled_units'. For details, see https://docs.astropy.org/en/latest/units/combining_and_defining.html [astropy.units.core]

Table length=88

Name	z	sigma*	e_sigma*	n_sigma*	FWHM	e_FWHM	logL	e_logL	logM	E_logM	e_logM

		km / s	km / s		km / s	km / s	[10-7W]	[10-7W]	dex(Msun)	dex(Msun)	dex(Msun)

str24	float64	float64	float64	str1	int64	int64	float64	float64	float64	float64	float64

SDSS J000805.62+145023.4	0.0454	140.0	27.0	--	7610	380	41.13	0.04	7.7	--	0.1

...	...	...	...	...	...	...	...	...	...	...	...

POX 52	0.0218	36.0	5.0	--	--	--	--	--	5.2	--	0.3

NGC 4395	0.000947	30.0	5.0	--	--	--	--	--	4.9	--	0.4

La longitud de la tabla es de 88 muestras y 12 características, incluyendo 'Name', 'z', 'sigma*', etc.

---
Es posible acceder a cada una de las características,

```python
data['z']
```

<Column name='z' dtype='float64' description='Redshift' length=88>

0.0454

0.0419

0.0456

0.0772

...

0.0172

0.0163

0.0218

0.000947

```python
data['logL']
```

<MaskedColumn name='logL' dtype='float64' unit='[10-7W]' description='Log of H{alpha} luminosity in erg/s' length=88>

41.13

41.58

41.45

41.13

...

--

--

--

--

También se puede acceder a los datos completos de una muestra particular o un conjunto de muestras,

```python
data[1]
```

Row index=1

Name	z	sigma*	e_sigma*	n_sigma*	FWHM	e_FWHM	logL	e_logL	logM	E_logM	e_logM

		km / s	km / s		km / s	km / s	[10-7W]	[10-7W]	dex(Msun)	dex(Msun)	dex(Msun)

str24	float64	float64	float64	str1	int64	int64	float64	float64	float64	float64	float64

SDSS J004236.86-104921.8	0.0419	78.4	10.0	--	1960	97	41.58	0.14	6.7	--	0.1

```python
data[[0,1,3,4]]
```

Table length=4

Name	z	sigma*	e_sigma*	n_sigma*	FWHM	e_FWHM	logL	e_logL	logM	E_logM	e_logM

		km / s	km / s		km / s	km / s	[10-7W]	[10-7W]	dex(Msun)	dex(Msun)	dex(Msun)

str24	float64	float64	float64	str1	int64	int64	float64	float64	float64	float64	float64

SDSS J000805.62+145023.4	0.0454	140.0	27.0	--	7610	380	41.13	0.04	7.7	--	0.1

SDSS J004236.86-104921.8	0.0419	78.4	10.0	--	1960	97	41.58	0.14	6.7	--	0.1

SDSS J020459.25-080816.0	0.0772	121.0	9.4	a	3720	180	41.13	0.05	7.0	--	0.1

SDSS J020615.99-001729.1	0.0426	216.0	30.0	--	3860	190	41.91	0.07	7.5	--	0.1

Distribuiremos la información en un conjunto de arreglos de numpy,

```python
z = np.array(data["z"])
sigma_star = np.array(data["sigma*"])
e_sigma_star = np.array(data["e_sigma*"])
logL = np.array(data["logL"])
e_logL = np.array(data["e_logL"])
logM = np.array(data["logM"])
e_logM = np.array(data["e_logM"])
```

## 2. Visualizar la información

Veamos diferentes combinaciones de características, buscando alguna clase de patrón de interés,

```python
plt.scatter(z, logM,  color='red')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log M$')
plt.show()
```

```python
plt.figure(figsize=(10, 10))

plt.subplot(221)
plt.plot(z, logM, 'b.')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log M$')

plt.subplot(222)
plt.plot(logL, logM, 'r.')
plt.xlabel(r'$\log L$')
plt.ylabel(r'$\log M$')

plt.subplot(223)
plt.plot(z, logL, 'g.')
plt.xlabel(r'$z$')
plt.ylabel(r'$\log L$')

plt.subplot(224)
plt.plot(sigma_star, logM, 'k.')
plt.xlabel(r'$\sigma_{*}$')
plt.ylabel(r'$\log M$')

plt.show()
```

Aparecen alguna información o patrones intersantes. Por ejemplo,

- Se obsrevan dos puntos separados (outcaast) en el gráfico de $\log M$ vs $z$.
- Parece que existen dos cúmulos separados en el gráfico de $\log M$ vs $\log L$
- Parece que existen tres cúmulos separados (o dos cumulos y dos puntos separados) en el gráfico de  $\log L$ vs $z$
- Existe una tendencia de correlación en el gráfico de  $\log M$ vs $\sigma_{*}$

A pesar de que estos comportamientos parecen existir, algunos de ellos deben revisarse con cuidado. Por ejemplo, los aparentes cúmulos en las figuras 2 y 3 corresponden a una ausencia de datos, mal interpreata al convertir la información a arreglos. Al revisar la información en $\log L$ se tiene

```python
logL
```

    array([41.13, 41.58, 41.45, 41.13, 41.91, 41.24, 41.58, 40.45, 41.63,
           41.67, 40.14, 40.42, 41.17, 41.31, 41.27, 40.72, 41.14, 40.8 ,
           41.46, 41.74, 41.51, 41.65, 41.17, 41.66, 40.1 , 41.57, 41.3 ,
           40.92, 41.62, 41.86, 40.58, 40.62, 41.81, 41.55, 41.62, 41.23,
           41.29, 41.1 , 41.84, 40.73, 40.46, 41.18, 42.09, 41.4 , 41.51,
           41.41, 41.86, 41.09, 40.99, 41.92, 41.24, 41.41, 41.83, 41.21,
           41.65, 41.87, 42.57, 41.93, 42.03, 40.47, 42.14, 42.68, 42.57,
           42.35, 42.75, 41.87, 42.67, 42.19, 41.22, 42.96, 43.61,  0.  ,
            0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,
            0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ])

donde se observa una gran cantidad de valores $0.$ que en realidad correspondian a datos ausentes en el conjunto inicial. Estos valores de cero fueron incorporados al utilizar el comando `logL = np.array(data["logL"])`.

NOTA: Es importante manjear adecuadamente la ausencia de datos!

---
Por otra parte, podemos analizar con mayor detalle la aparente correlación entre  $\log M$ y $\sigma_{*}$

```python
plt.plot(sigma_star, logM,  'k.')
plt.xlabel(r'$\sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

Puede parecer una dependencia lineal entre las dos variables. Sin embargo, para tener una mejor apreciación, incluiremos las barras de error. Para ello utilizamos la función [matplotlib.pyplot.errorbar](https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.errorbar.html)

```python
plt.errorbar(sigma_star, logM, e_logM, e_sigma_star, fmt='k.')
plt.xlabel(r'$\sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

Esta visualización miestra una correlación no-lineal. Para comprobarlo, podemos modificar la gráfica incluyendo el logaritmo de la dispersion de velocidades, $\log M$ vs $\log \sigma_{*}$,con lo cual la tendencia lineal es evidente,

```python
plt.plot(np.log10(sigma_star), logM,  'k.')
plt.xlabel(r'$\log \sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

Para confirmar esta apreciación, incluimos nuevamente las incertidumbres asociadas. Para ello es necesario recordar la propagación de errores,

\begin{align}
f(x) =& \log_{10} x = \frac{\ln x}{\ln 10}\\
\Delta f = &\frac{1}{\ln 10} \frac{\Delta x}{x}
\end{align}

```python

e_log_sigma = e_sigma_star/(np.log(10.)*sigma_star)

plt.errorbar(np.log10(sigma_star), logM, e_logM, e_log_sigma, fmt='k.')
plt.xlabel(r'$\log \sigma_{*}$')
plt.ylabel(r'$\log M$')
plt.show()
```

Luego volveremos sobre este conjunto de datos para analizar mejor esta relación.

```python

```

15. Astrophysics Data/02. Archivos DAT+ReadMe/DATFiles.ipynb