Table of Contents

1  Dimensionality Reduction

1.1  The Problem

1.1.1  Multi-Collinearity

1.2  Sparsity

2  Principle Component Analysis

2.1  Important Points:

3  Singular Value Decomposition

3.1  Measuring the Quality of the Reconstruction

3.2  Heuristic Step for How Many Dimensions to Keep

4  GLOVE

4.1  Using Spacy word2vec embeddings

4.2  Using Glove

5  Clustering Text

# Dimensionality Reduction

## The Problem

There is an interesting tradeoff between model performance and a feature's dimensionality:

>*If the amount of available training data is fixed, then overfitting occurs if we keep adding dimensions. On the other hand, if we keep adding dimensions, the amount of **training data needs to grow exponentially fast to maintain the same coverage** and to avoid overfitting* ([Computer Vision for Dummies](http://www.visiondummy.com/2014/04/curse-dimensionality-affect-classification/)).

### Multi-Collinearity

In many cases, there is a high degree of correlation between many of the features in a dataset. This multi-collinearity has the effect of drowning out the "signal" of your dataset in many cases, and amplifies "outlier" noise.

## Sparsity

- High dimensionality increases the sparsity of your features (**what NLP techniques have we used that illustrate this point?**)
- The density of the training samples decreases when dimensionality increases:
- **Distance measures (Euclidean, for instance) start losing their effectiveness**, because there isn't much difference between the max and min distances in higher dimensions.
- Many models that rely upon **assumptions of Gaussian distributions** (like OLS linear regression), Gaussian mixture models, Gaussian processes, etc. become less and less effective since their distributions become flatter and "fatter tailed".

What is the amount of data needed to maintain **20% coverage** of the feature space? For 1 dimension, it is **20% of the entire population's dataset**. For a dimensionality of $D$:

$$
X^{D} = .20
$$
$$
(X^{D})^{\frac{1}{D}} = .20^{\frac{1}{D}}
$$
$$
X = \sqrt[D]{.20}
$$
You can approximate this as
```python
def coverage_requirement(requirement, D):
    return requirement ** (1 / D)

x = []
y = []
for d in range(1,20):
    y.append(coverage_requirement(0.10, d))
    x.append(d)

import matplotlib.pyplot as plt

plt.plot(x,y)
plt.xlabel("Number of Dimensions")
plt.ylabel("Appromximate % of Population Dataset")
plt.title("% of Dataset Needed to Maintain 10% Coverage of Feature Space")
plt.show()
```

```python
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
reviews = pd.read_csv("mcdonalds-yelp-negative-reviews.csv", encoding='latin-1')
reviews = open("poor_amazon_toy_reviews.txt", encoding='latin-1')

#text = reviews["review"].values
text = reviews.readlines()

vectorizer = CountVectorizer(ngram_range=(3,3), min_df=0.01, max_df=0.75, max_features=200)
# tokenize and build vocab
vectorizer.fit(text)
vector = vectorizer.transform(text)
features = vector.toarray()
features_df = pd.DataFrame(features, columns=vectorizer.get_feature_names())

correlations = features_df.corr()
correlations_stacked = correlations.stack().reset_index()
#set column names
correlations_stacked.columns = ['Bi-Gram 1','Bi-Gram 2','Correlation']
correlations_stacked = correlations_stacked[correlations_stacked["Correlation"] < 1]
correlations_stacked = correlations_stacked.sort_values(by=['Correlation'], ascending=False)
correlations_stacked.head()
```

      Bi-Gram 1
      Bi-Gram 2
      Correlation

      43
      don waste your
      waste your money
      0.777888

      197
      waste your money
      don waste your
      0.777888

      82
      of the box
      out of the
      0.609369

      110
      out of the
      of the box
      0.609369

      123
      this for my
      my year old
      0.078176

```python
import numpy as np
import matplotlib.pyplot as plt
# visualize the correlations (install seaborn first)!
import seaborn as sns

# Generate a mask for the upper triangle
mask = np.triu(np.ones_like(correlations, dtype=np.bool))

# Set up the matplotlib figure
f, ax = plt.subplots(figsize=(11, 9))

# Generate a custom diverging colormap
cmap = sns.diverging_palette(220, 10, as_cmap=True)

# Draw the heatmap with the mask and correct aspect ratio
sns.heatmap(correlations, mask=mask, cmap=cmap, vmax=.3, center=0,
            square=True, linewidths=.5, cbar_kws={"shrink": .5})

```

# Principle Component Analysis

If you have an original matrix $Z$, you can decompose this matrix into two smaller matrices $X$ and $Q$.

## Important Points:

- Multiplying a vector by a matrix typically changes the direction of the vector. For instance:

Lazy Programmer-
        Tutorial to PCA

However, there are eigenvalues λ and eigenvectors $v$ such that

$$
\sum_{X}v = \lambda v
$$

Multiplying the eigenvectors $v$ with the eigenvalue $\lambda$ does not change the direction of the eigenvector.

Multiplying the eigenvector $v$ by the covariance matrix $\sum_{X}$ also does not change the direction of the eigenvector.

If our data $X$ is of shape $N \times D$, it turns out that we have $D$ eigenvalues and $D$ eigenvectors. This means we can arrange the eigenvalues $\lambda$ in decreasing order so that

$$
\lambda_3 > \lambda_2 > \lambda_5
$$

In this case, $\lambda_3$ is the largest eigenvalue, followed by $\lambda_2$, and then $\lambda_5$. Then, we can arrange

We can also rearrange the eigenvectors the same: $v_3$ will be the first column, $v_2$ will be the second column, and $v_5$ will be the third column.

We'll end up with two matrices $V$ and $\Lambda$:

Lazy Programmer-
        Tutorial to PCA

```python
# what is the shape of our features?
features.shape
```

    (12700, 15)

```python
from sklearn.decomposition import PCA

pca = PCA(n_components=4)

Z = pca.fit_transform(features)

# what is the shape of Z?
Z.shape
```

    (12700, 4)

```python
# what will happen if we take the correlation matrix and covariance matrix of our new reduced features?
import numpy as np
covariances = pd.DataFrame(np.cov(Z.transpose()))
plt.rcParams["figure.figsize"] = (5,5)
sns.heatmap(covariances)
```

```python
# train the model to reduce the dimensions down to 2
pca = PCA(n_components=2)
Z_two_dimensions = pca.fit_transform(features)
Z_two_dimensions
```

    array([[ 0.19595259, -0.05187769],
           [-0.03897472, -0.02642106],
           [-0.03897472, -0.02642106],
           ...,
           [-0.03897472, -0.02642106],
           [-0.03897472, -0.02642106],
           [-0.03897472, -0.02642106]])

```python
import matplotlib.pyplot as plt

plt.scatter(Z_two_dimensions[:,0], Z_two_dimensions[:, 1])
reduced_features_df = pd.DataFrame(Z_two_dimensions, columns=["x1", "x2"])
reduced_features_df["text"] = text
```

# Singular Value Decomposition

Given an input matrix $A$, we want to try to represent it instead as three smaller matrices $U$, $\sum$, and $V$. Instead of **$n$ original terms**, we want to represent each document as **$r$ concepts** (other referred to as **latent dimensions**, or **latent factors**):

        Mining of Massive Datasets - Dimensionality Reduction: Singular Value Decomposition by Leskovec, Rajaraman, and Ullman (Stanford University)

Here, **$A$ is your matrix of word vectors** - you could use any of the word vectorization techniques we have learned so far, include one-hot encoding, word count, TF-IDF.

- $\sum$ will be a **diagonal matrix** with values that are positive and sorted in decreasing order. Its value indicate the **variance (information encoded on that new dimension)**- therefore, the higher the value, the stronger that dimension is in capturing data from A, the original features. For our purposes, we can think of the rank of this $\sum$ matrix as the number of desired dimensions. Instance, if we want to reduce $A$ from shape $1020 x 300$ to $1020 x 10$, we will want to reduce the rank of $\sum$ from 300 to 10.

- $U^T U = I$ and $V^T V = I$

## Measuring the Quality of the Reconstruction

A popular metric used for measuring the quality of the reconstruction is the [Frobenius Norm](https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm). When you explain your methodology for reducing dimensions, usually managers / stakeholders will want to some way to compare multiple dimensionality techniques' ability to quantify its ability to retain information but trim dimensions:

$$
\begin{equation}
||A_{old}-A_{new}||_{F} = \sqrt{\sum_{ij}{(A^{old}_{ij}- A^{new}_{ij}}})^2
\end{equation}
$$

## Heuristic Step for How Many Dimensions to Keep

1. Sum the $\sum$ matrix's diagonal values:
$$
\begin{equation}
\sum_{i}^{m}\sigma_{i}
\end{equation}
$$

2. Define your threshold of "information" (variance) $\alpha$ to keep: usually 80% to 90%.

3. Define your cutoff point $C$: $$
\begin{equation}
C = \sum_{i}^{m}\sigma_{i} \alpha
\end{equation}
$$

4. Beginning with your largest singular value, sum your singular values $\sigma_{i}$ until it is greater than C. Retain only those dimensions.

        Mining of Massive Datasets - Dimensionality Reduction: Singular Value Decomposition by Leskovec, Rajaraman, and Ullman (Stanford University)

```python
# create sample data

import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import svd
x = np.linspace(1,20, 20) # create the first dimension
x = np.concatenate((x,x))
y = x + np.random.normal(0,1, 40) # create the second dimension
z = x + np.random.normal(0,2, 40) # create the third dimension
a = x + np.random.normal(0,4, 40) # create the fourth dimension
plt.scatter(x,y) # plot just the first two dimensions
plt.show()
```

```python
# create matrix
A = np.stack([x,y,z,a]).T
```

```python
# perform SVD
D = 1
U, s, V = svd(A)
print(f"s is {s}\n")
print(f"U is {U}\n")
print(f"V is {V}")
```

```python
# Frobenius norm

s[D:] = 0
S = np.zeros((A.shape[0], A.shape[1]))
S[:A.shape[1], :A.shape[1]] = np.diag(s)
A_reconstructed = U.dot(S.dot(V))
np.sum((A_reconstructed - A) ** 2) ** (1/2) # Frobenius norm
# reconstruct matrix
U.dot(S)
```

# GLOVE

Global vectors for word presentation:

        GloVe: Global Vectors for Word Representation

```python
!pip3 install gensim
```

    Collecting gensim
      Downloading gensim-3.8.2.tar.gz (23.4 MB)
         |████████████████████████████████| 23.4 MB 187 kB/s eta 0:00:01
    Requirement already satisfied: numpy>=1.11.3 in /usr/local/lib/python3.8/site-packages (from gensim) (1.18.2)
    Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.8/site-packages (from gensim) (1.4.1)
    Requirement already satisfied: six>=1.5.0 in /usr/local/lib/python3.8/site-packages (from gensim) (1.14.0)
    Collecting smart_open>=1.8.1
      Downloading smart_open-1.11.1.tar.gz (105 kB)
         |████████████████████████████████| 105 kB 264 kB/s eta 0:00:01
    Requirement already satisfied: requests in /usr/local/lib/python3.8/site-packages (from smart_open>=1.8.1->gensim) (2.23.0)
    Collecting boto
      Downloading boto-2.49.0-py2.py3-none-any.whl (1.4 MB)
         |████████████████████████████████| 1.4 MB 205 kB/s eta 0:00:01
    Requirement already satisfied: boto3 in /usr/local/lib/python3.8/site-packages (from smart_open>=1.8.1->gensim) (1.12.39)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.8/site-packages (from requests->smart_open>=1.8.1->gensim) (2020.4.5.1)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.8/site-packages (from requests->smart_open>=1.8.1->gensim) (2.9)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.8/site-packages (from requests->smart_open>=1.8.1->gensim) (3.0.4)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.8/site-packages (from requests->smart_open>=1.8.1->gensim) (1.25.8)
    Requirement already satisfied: botocore<1.16.0,>=1.15.39 in /usr/local/lib/python3.8/site-packages (from boto3->smart_open>=1.8.1->gensim) (1.15.39)
    Requirement already satisfied: s3transfer<0.4.0,>=0.3.0 in /usr/local/lib/python3.8/site-packages (from boto3->smart_open>=1.8.1->gensim) (0.3.3)
    Requirement already satisfied: jmespath<1.0.0,>=0.7.1 in /usr/local/lib/python3.8/site-packages (from boto3->smart_open>=1.8.1->gensim) (0.9.5)
    Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /usr/local/lib/python3.8/site-packages (from botocore<1.16.0,>=1.15.39->boto3->smart_open>=1.8.1->gensim) (2.8.1)
    Requirement already satisfied: docutils<0.16,>=0.10 in /usr/local/lib/python3.8/site-packages (from botocore<1.16.0,>=1.15.39->boto3->smart_open>=1.8.1->gensim) (0.15.2)
    Building wheels for collected packages: gensim, smart-open
      Building wheel for gensim (setup.py) ... done
      Created wheel for gensim: filename=gensim-3.8.2-cp38-cp38-macosx_10_14_x86_64.whl size=24208273 sha256=6bd62ae0639fb3236fa9f41540baf674913434b2bcb4124b691f9bdd7a76b93e
      Stored in directory: /Users/yuchen/Library/Caches/pip/wheels/91/fa/af/b14a7e3943121f028c82af23946002f99ba18853de2381b148
      Building wheel for smart-open (setup.py) ... done
      Created wheel for smart-open: filename=smart_open-1.11.1-py3-none-any.whl size=95255 sha256=a104018d08730ff5b1f115c5e2cbf0c58a0becb891a33e3e3bb9f4060fac5705
      Stored in directory: /Users/yuchen/Library/Caches/pip/wheels/90/f7/ab/7b855402a7d4bf9dececf03bb7f8c831dabad2551487b675bc
    Successfully built gensim smart-open
    Installing collected packages: boto, smart-open, gensim
    Successfully installed boto-2.49.0 gensim-3.8.2 smart-open-1.11.1

```python
# import glove embeddings into a word2vec format that is consumable by Gensim
from gensim.scripts.glove2word2vec import glove2word2vec
glove_input_file = 'glove.6B.100d.txt'
word2vec_output_file = 'glove.6B.100d.txt.word2vec'
glove2word2vec(glove_input_file, word2vec_output_file)
```

```python
from gensim.models import KeyedVectors
# load the Stanford GloVe model
filename = 'glove.6B.100d.txt.word2vec'
model = KeyedVectors.load_word2vec_format(filename, binary=False)
# calculate: (king - man) + woman = ?
result = model.most_similar(positive=['woman', 'king'], negative=['man'], topn=1)
print(result)
```

    [('queen', 0.7698541283607483)]

```python
words = ["woman", "king", "man", "queen", "puppy", "kitten", "cat",
         "quarterback", "football", "stadium", "touchdown",
         "dog", "government", "tax", "federal", "judicial", "elections",
         "avocado", "tomato", "pear", "championship", "playoffs"]

vectors = [model.wv[word] for word in words]

import pandas as pd
vector_df = pd.DataFrame(vectors)
vector_df["word"] = words
vector_df.head()
```

      0
      1
      2
      3
      4
      5
      6
      7
      8
      9
      ...
      91
      92
      93
      94
      95
      96
      97
      98
      99
      word

      0
      0.59368
      0.44825
      0.59320
      0.074134
      0.111410
      1.27930
      0.16656
      0.240700
      0.39045
      0.32766
      ...
      -0.22008
      0.824010
      -0.092207
      -0.459410
      0.465710
      -0.56018
      -0.54648
      0.151620
      -0.30754
      woman

      1
      -0.32307
      -0.87616
      0.21977
      0.252680
      0.229760
      0.73880
      -0.37954
      -0.353070
      -0.84369
      -1.11130
      ...
      -0.71320
      0.089623
      0.413940
      -1.336300
      -0.619150
      -0.33089
      -0.52881
      0.164830
      -0.98878
      king

      2
      0.37293
      0.38503
      0.71086
      -0.659110
      -0.001013
      0.92715
      0.27615
      -0.056203
      -0.24294
      0.24632
      ...
      -0.37501
      0.457420
      0.045975
      -0.246750
      0.045656
      -0.38302
      -0.93711
      0.039138
      -0.53911
      man

      3
      -0.50045
      -0.70826
      0.55388
      0.673000
      0.224860
      0.60281
      -0.26194
      0.738720
      -0.65383
      -0.21606
      ...
      -0.70584
      0.430120
      0.510510
      -0.803300
      -0.665720
      -0.63717
      -0.36032
      0.133470
      -0.56075
      queen

      4
      0.69548
      0.66222
      0.24528
      -1.130400
      -0.768530
      0.10149
      0.36038
      0.048728
      0.13055
      -0.16981
      ...
      0.34369
      0.015695
      0.099343
      -0.004544
      -0.736170
      -0.10185
      0.43978
      -0.003745
      -0.48888
      puppy

5 rows × 101 columns

## Using Spacy word2vec embeddings

```python
import en_core_web_md
import spacy
from scipy.spatial.distance import cosine
nlp = en_core_web_md.load()
```

```python
words = ["woman", "king", "man", "queen", "puppy", "kitten", "cat",
         "quarterback", "football", "stadium", "touchdown",
         "dog", "government", "tax", "federal", "judicial", "elections",
         "avocado", "tomato", "pear", "championship", "playoffs"]

```

```python
tokens = nlp(" ".join(words))
word2vec_vectors = [token.vector for token in tokens]
```

```python
np.array(word2vec_vectors).shape
```

    (22, 300)

```python
%matplotlib inline
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.decomposition import TruncatedSVD
import matplotlib.pyplot as plt
import matplotlib
dimension_model = PCA(n_components=2)
reduced_vectors = dimension_model.fit_transform(word2vec_vectors)
```

```python
matplotlib.rc('figure', figsize=(10, 10))
for i, vector in enumerate(reduced_vectors):
    x = vector[0]
    y = vector[1]
    plt.plot(x,y, 'bo')
    plt.text(x * (1 + 0.01), y * (1 + 0.01) , words[i], fontsize=12)
```

## Using Glove

```python
%matplotlib inline
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.decomposition import TruncatedSVD
import matplotlib.pyplot as plt

dimension_model = PCA(n_components=2)
reduced_vectors = dimension_model.fit_transform(vectors)

for i, vector in enumerate(reduced_vectors):
    x = vector[0]
    y = vector[1]
    plt.plot(x,y, 'bo')
    plt.text(x * (1 + 0.01), y * (1 + 0.01) , words[i], fontsize=12)

```

# Clustering Text

```python
from sklearn.cluster import KMeans

kmeans = KMeans(n_clusters=4)
cluster_assignments = kmeans.fit_predict(reduced_vectors)
for cluster_assignment, word in zip(cluster_assignments, words):
    print(f"{word} assigned to cluster {cluster_assignment}")

```

    woman assigned to cluster 1
    king assigned to cluster 1
    man assigned to cluster 1
    queen assigned to cluster 1
    puppy assigned to cluster 3
    kitten assigned to cluster 3
    cat assigned to cluster 3
    quarterback assigned to cluster 2
    football assigned to cluster 2
    stadium assigned to cluster 2
    touchdown assigned to cluster 2
    dog assigned to cluster 3
    government assigned to cluster 0
    tax assigned to cluster 0
    federal assigned to cluster 0
    judicial assigned to cluster 0
    elections assigned to cluster 0
    avocado assigned to cluster 3
    tomato assigned to cluster 3
    pear assigned to cluster 3
    championship assigned to cluster 2
    playoffs assigned to cluster 2

```python
color_map = {
    0: "r",
    1: "b",
    2: "g",
    3: "y"
}

plt.rcParams["figure.figsize"] = (10,10)

for i, vector in enumerate(reduced_vectors):
    x = vector[0]
    y = vector[1]
    plt.plot(x,y, 'bo', c=color_map[cluster_assignments[i]])
    plt.text(x * (1 + 0.01), y * (1 + 0.01) , words[i], fontsize=12)
```

week6/Dimensionality Reduction and Clustering.ipynb


# Robotic Systems

$\newcommand{\mb}[1]{\mathbf{ #1 }}$
$\newcommand{\bb}[1]{\mathbb{ #1 }}$
$\newcommand{\bs}[1]{\boldsymbol{ #1 }}$
$\newcommand{\norm}[1]{\left\Vert #1 \right\Vert}$
$\newcommand{\ip}[2]{\left\langle #1, #2 \right\rangle}$
$\newcommand{\der}[2]{\frac{\mathrm{d} #1 }{\mathrm{d} #2 }}$
$\newcommand{\derp}[2]{\frac{\partial #1 }{\partial #2 }}$

$\newcommand{\R}{\bb{R}}$

## Dynamics

Let $\mathcal{Q} \subseteq \R^n$ be a configuration space. Let $T: \mathcal{Q} \times \R^n \to \R_+$ and $U: \mathcal{Q} \to \R$ denote the kinetic and potential energies of the system, respectively.

The kinetic energy satisfies:

\begin{equation}
    T(\mb{q}, \dot{\mb{q}}) = \frac{1}{2}\dot{\mb{q}}^\top\mb{D}(\mb{q})\dot{\mb{q}},
\end{equation}

for all coordinates $\mb{q} \in \mathcal{Q}$ and coordinate rates $\dot{\mb{q}} \in \R^n$, with inertia matrix function $\mb{D}: \mathcal{Q} \to \bb{S}^n_{++}$. Here, $\bb{S}^n_{++}$ denotes the set of positive definite $n \times n$ symmetric matrices.

Let $\mathcal{L}: \mathcal{Q} \times \R^n \to \R$ denote the system Lagrangian, defined as $\mathcal{L}(\mb{q}, \dot{\mb{q}}) = T(\mb{q}, \dot{\mb{q}}) - U(\mb{q})$ for all $\mb{q} \in \mathcal{Q}$ and $\dot{\mb{q}} \in \R^n$.

The system Lagrangian and actions are related via the Euler-Lagrange equation:

\begin{equation}
    \der{}{t}\left( \derp{\mathcal{L}}{\dot{\mb{q}}} \right)^\top + \left(\derp{\mathcal{L}}{\mb{q}}\right)^\top = \mb{B}\mb{u},
\end{equation}

for all $\mb{q} \in \mathcal{Q}$, $\dot{\mb{q}} \in \R^n$, and actions $\mb{u} \in \mathcal{U}$, with full-rank static actuation matrix $\mb{B} \in \mathbb{R}^{n \times m}$.

To obtain the equations of motion of the system, we expand the terms in the Euler-Lagrange equation as:

\begin{equation}
    \der{}{t}\left(\derp{T}{\dot{\mb{q}}}\right)^\top - \left(\derp{T}{\mb{q}}\right)^\top + \left(\derp{U}{\mb{q}}\right)^\top = \der{}{t} (\mb{D}(\mb{q})\dot{\mb{q}}) - \frac{1}{2}\derp{\mb{D}}{\mb{q}}(\dot{\mb{q}}, \dot{\mb{q}}, \cdot) + \left(\derp{U}{\mb{q}}\right)^\top.
\end{equation}

Here, $\derp{\mb{D}}{\mb{q}}: \mathcal{Q} \to \R^{n \times n \times n}$ is represented as a multilinear operator-valued function, with:

\begin{equation}
    \derp{\mb{D}}{\mb{q}}(\mb{e}_i, \mb{e}_j, \mb{e}_k) = \derp{D_{ij}(\mb{q})}{q_k},
\end{equation}

for all $\mb{q} \in \mathcal{Q}$ and $i, j, k \in \{1, \dots, n\}$. Here, $\mb{e}_i, \mb{e}_j, \mb{e}_k \in \R^n$ denote elementary basis vectors.

$\derp{\mb{D}}{\mb{q}}(\dot{\mb{q}}, \dot{\mb{q}}, \cdot): \mathcal{Q} \to \R^n$ denotes a tensor contraction in the first two arguments.

Further expansion yields:

\begin{equation}
    \mb{D}(\mb{q})\ddot{\mb{q}} + \underbrace{\derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \dot{\mb{q}}) - \frac{1}{2}\derp{\mb{D}}{\mb{q}}(\dot{\mb{q}}, \dot{\mb{q}}, \cdot)}_{\mb{C}(\mb{q}, \dot{\mb{q}})\dot{\mb{q}}} + \underbrace{\left(\derp{U}{\mb{q}}\right)^\top}_{\mb{G}(\mb{q})} = \mb{B}\mb{u},
\end{equation}

with Coriolis terms $\mb{C}: \mathcal{Q} \times \R^n \to \R^{n \times n}$ and potential (typically gravity) terms $\mb{G}: \mathcal{Q} \to \R^n$.

The Coriolis terms come from:

\begin{align}
    \derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \dot{\mb{q}}) - \frac{1}{2}\derp{\mb{D}}{\mb{q}}(\dot{\mb{q}}, \dot{\mb{q}}, \cdot) &= \derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \cdot)\dot{\mb{q}} - \frac{1}{2} \left( \dot{\mb{q}}^\top \derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \cdot) \right)^\top\\
    &= \underbrace{\left( \derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \cdot) - \frac{1}{2} \left( \derp{\mb{D}}{\mb{q}}(\cdot, \dot{\mb{q}}, \cdot) \right)^\top \right)}_{\mb{C}(\mb{q}, \dot{\mb{q}})} \dot{\mb{q}}
\end{align}

The equations of motion can be expressed as:

\begin{equation}
    \mb{D}(\mb{q})\ddot{\mb{q}} + \underbrace{\mb{C}(\mb{q}, \dot{\mb{q}})\dot{\mb{q}} + \mb{G}(\mb{q})}_{\mb{H}(\mb{q}, \dot{\mb{q}})} = \mb{B}\mb{u},
\end{equation}

with nonlinear terms $\mb{H}: \mathcal{Q} \times \R^n \to \R^n$.

Alternatively, the equations can be expressed as an system of first-order equations:

\begin{equation}
    \der{}{t} \begin{bmatrix} \mb{q} \\ \dot{\mb{q}} \end{bmatrix} = \begin{bmatrix} \dot{\mb{q}} \\ -\mb{D}(\mb{q})^{-1}\mb{H}(\mb{q}, \dot{\mb{q}}) \end{bmatrix} + \begin{bmatrix} \mb{0}_{n \times m} \\ \mb{D}(\mb{q})^{-1}\mb{B} \end{bmatrix}\mb{u},
\end{equation}

with dynamics affine in the action.

## Control: Full Actuation

Assume $m = n$, in which case $\mb{B}$ is invertible. Suppose we want to drive the system to a desired configuration $\mb{q}_d \in \mathcal{Q}$ (trajectory tracking strategies more closely resemble strategies for underactuated systems). We have a number of nonlinear control techniques to employ.

We can induce linear dynamics with linearizing feedback, and then apply linear control techniques to stabilize the resulting system.

We can specify an energy-based Lyapunov function and generate an associated asymptotically-stabilizing controller.

Finally, we can specify a Lyapunov function through feedback linearization and generate an optimization-based exponentially-stabilizing controller.

## Example: Inverted Pendulum

Consider a frictionless inverted pendulum, modeled as a massless rod of length $l$ with torque input $\tau$ at a fixed base and point mass $m$ at the opposite end of the rod.

Consider inertial reference frame $\mathcal{I} = (\mb{i}_1, \mb{i}_2, \mb{i}_3)$ and body reference frame $\mathcal{B} = (\mb{b}_1, \mb{b}_2, \mb{b}_3)$, with $\mb{b}_3 = \mb{i}_3$.

Let $\mb{f}^{\mathcal{I}} = -mg \mb{e}_1$ denote the gravitational force resolved in the inertial frame and let $\mb{r}^\mathcal{B} = l \mb{e}_1$ denote the position of the mass relative to the base resolved in the body frame. Here $\mb{e}_1$ denotes the first elementary basis vector.

Suppose the angle from upright $\theta$ is measured along with the corresponding angular rate $\dot{\theta}$.

The direction cosine matrix (DCM) relating body frame coordinates to inertial frame coordinates is:

\begin{equation}
    \mb{C}^{\mathcal{B}\mathcal{I}} = \begin{bmatrix} \ip{\mb{b}_1}{\mb{i}_1} & \ip{\mb{b}_1}{\mb{i}_2} & \ip{\mb{b}_1}{\mb{i}_3} \\
    \ip{\mb{b}_2}{\mb{i}_1} & \ip{\mb{b}_2}{\mb{i}_2} & \ip{\mb{b}_2}{\mb{i}_3} \\
    \ip{\mb{b}_3}{\mb{i}_1} & \ip{\mb{b}_3}{\mb{i}_2} & \ip{\mb{b}_3}{\mb{i}_3} \end{bmatrix} = \underbrace{\begin{bmatrix} \cos{\theta} & \sin{\theta} & 0 \\ -\sin{\theta} & \cos{\theta} & 0 \\ 0 & 0 & 1
    \end{bmatrix}}_{\mb{R}(\theta)^\top},
\end{equation}

noting that $\cos{\left( \frac{\pi}{2} - \theta \right)} = \sin{\theta}$.

Because $\mb{C}^{\mathcal{B}\mathcal{I}}$ is orthogonal, $\mb{C}^{\mathcal{I}\mathcal{B}} = \left( \mb{C}^{\mathcal{B}\mathcal{I}} \right)^{-1} = \left( \mb{C}^{\mathcal{B}\mathcal{I}} \right)^\top$.

The position of the mass relative to the base is resolved in the inertial frame as:

\begin{align}
    \mb{r}^{\mathcal{I}} &= \mb{C}^{\mathcal{I}\mathcal{B}}\mb{r}^\mathcal{B}\\
    &= l \mb{R}(\theta)\mb{e}_1.
\end{align}

The corresponding velocity is resolved in the inertial frame as:

\begin{equation}
    \dot{\mb{r}}^{\mathcal{I}} = l \dot{\theta} \der{\mb{R}}{\theta} \mb{e}_1.
\end{equation}

Here:

\begin{equation}
    \der{\mb{R}}{\theta} = \begin{bmatrix} -\sin{\theta} & \cos{\theta} & 0 \\ -\cos{\theta} & -\sin{\theta} & 0 \\ 0 & 0 & 0 \end{bmatrix}.
\end{equation}

Finally, the associated kinetic energy is:

\begin{align}
    T(\theta, \dot{\theta}) &= \frac{1}{2}m \norm{\dot{\mb{r}}^\mathcal{I}}_2^2\\
    &= \frac{1}{2} ml^2 \dot{\theta}^2 (\sin^2{\theta} + \cos^2{\theta})\\
    &= \frac{1}{2} ml^2 \dot{\theta}^2.
\end{align}

The kinetic energy can be expressed as:

\begin{equation}
    T(\theta, \dot{\theta}) = \frac{1}{2} D \dot{\theta}^2,
\end{equation}

for static inertia $D = ml^2$.

Since the inertia is not a function of the angle $\theta$, there are no Coriolis terms and $C = 0$.

On the other hand, the potential energy of the system is:

\begin{align}
    U(\theta) &= -\left(\mb{f}^\mathcal{I}\right)^\top \mb{r}^{\mathcal{I}}\\
    &= mgl \mb{e}_1^\top \mb{R}(\theta) \mb{e}_1\\
    &= mgl \cos{\theta}.
\end{align}

This yields:

\begin{equation}
    G(\theta) = \der{U}{\theta} = -mgl\sin{\theta}.
\end{equation}

The system Lagrangian is therefore:

\begin{align}
    \mathcal{L}(\theta, \dot{\theta}) &= T(\theta, \dot{\theta}) - U(\theta)\\
    &= \frac{1}{2}ml\dot{\theta}^2 - mgl\cos{\theta}.
\end{align}

The equations of motion are:

\begin{equation}
    D\ddot{\theta} + G(\theta) = ml^2 \ddot{\theta} - mgl\sin{\theta} = \tau,
\end{equation}

assuming the static actuation is identity.

In first order form, the equations of motion are expressed as:

\begin{equation}
    \der{}{t} \begin{bmatrix} \theta \\ \dot{\theta} \end{bmatrix} = \begin{bmatrix} \dot{\theta} \\ \frac{g}{l}\sin{\theta} \end{bmatrix} + \begin{bmatrix} 0 \\ \frac{1}{ml^2} \end{bmatrix} \tau.
\end{equation}

```python
# %load core/systems/inverted_pendulum.py
from numpy import array, cos, sin

from core.dynamics import RoboticDynamics, SystemDynamics

class InvertedPendulum(SystemDynamics, RoboticDynamics):
    def __init__(self, m, l, g=9.81):
        SystemDynamics.__init__(self, 2, 1)
        RoboticDynamics.__init__(self, array([[1]]))
        self.params = m, l, g

    def D(self, q):
        m, l, _ = self.params
        return array([[m * (l ** 2)]])

    def C(self, q, q_dot):
        return array([[0]])

    def U(self, q):
        m, l, g = self.params
        theta, = q
        return m * g * l * cos(theta)

    def G(self, q):
        m, l, g = self.params
        theta, = q
        return array([-m * g * l * sin(theta)])
```

We can create an inverted pendulum system with a mass of $0.25$ kg and a length of $0.5$ m.

```python
m, l = 0.25, 0.5
ip = InvertedPendulum(m, l)
```

## Feedback Linearization

Let $\bs{\nu}$ denote an auxilliary action to be specified later. Consider the controller:

\begin{align}
    \mb{u} &= (\mb{D}(\mb{q})^{-1}\mb{B})^{-1}(\mb{D}(\mb{q})^{-1}\mb{H}(\mb{q}, \dot{\mb{q}}) + \bs{\nu})\\
    &= \mb{B}^{-1}(\mb{H}(\mb{q}, \dot{\mb{q}}) + \mb{D}(\mb{q})\bs{\nu}).
\end{align}

This controller induces dynamics:

\begin{equation}
    \mb{D}(\mb{q})\ddot{\mb{q}} = \mb{D}(\mb{q})\bs{\nu}.
\end{equation}

Alternatively, the induced dynamics can be represented as:

\begin{equation}
    \underbrace{\der{}{t}\begin{bmatrix} \mb{q} - \mb{q}_d \\ \dot{\mb{q}} \end{bmatrix}}_{\dot{\mb{x}}} = \begin{bmatrix} \dot{\mb{q}} \\ \bs{\nu} \end{bmatrix} = \underbrace{\begin{bmatrix} \mb{0}_{n \times n} & \mb{I}_n \\ \mb{0}_{n \times n} & \mb{0}_{n \times n} \end{bmatrix}}_{\mb{F}} \underbrace{\begin{bmatrix} \mb{q} - \mb{q}_d \\ \dot{\mb{q}} \end{bmatrix}}_{\mb{x}} + \underbrace{\begin{bmatrix} \mb{0}_{n \times m} \\ \mb{I}_m \end{bmatrix}}_{\mb{G}} \bs{\nu},
\end{equation}

which are controllable double integrator dynamics.

We can now choose any linear controller to stabilize the induced dynamics. For example, we can choose the controller minimizing the infinite-horizon Linear Quadratic Regulator cost:

\begin{equation}
    \int_{t_0}^{\infty} \frac{1}{2}\left(\mb{x}(t)^\top\mb{Q}\mb{x}(t) + \bs{\nu}(t)^\top\mb{R}\bs{\nu}(t) \right)\mathrm{d}t,
\end{equation}

for cost matrices $\mb{Q} \in \bb{S}^{2n}_{++}$ and $\mb{R} \in \bb{S}^m_{++}$, and initial time $t_0$.

The minimizing auxilliary controller is:

\begin{equation}
    \bs{\nu}^* = -\underbrace{\frac{1}{2}\mb{R}^{-1}\mb{G}^\top\mb{P}}_{\mb{K}}\mb{x}.
\end{equation}

with $\mb{P} \in \bb{S}^{2n}_{++}$ solving the continuous-time algebraic Riccati equation (CARE):

\begin{equation}
    \mb{F}^\top\mb{P} + \mb{P}\mb{F} - \mb{P}\mb{G}\mb{R}^{-1}\mb{G}^\top\mb{P} = -\mb{Q}.
\end{equation}

Importantly, this controller need not be optimal in terms of the cost of actions $\mb{u}$.

Let's create a linearizing feedback controller for the inverted pendulum with $Q = 10 \cdot \mb{I}_2$ and $R = 1$.

```python
from numpy import identity
from core.controllers import FBLinController, LQRController

Q, R = 10 * identity(2), identity(1)
lqr = LQRController.build(ip, Q, R)
fb_lin = FBLinController(ip, lqr)
```

We simulate the system for $10$ seconds with control frequency of $100$ Hz from initial conditions $\theta = 1$ rad and $\dot{\theta} = 0$ rad/sec.

```python
from numpy import linspace

x_0 = array([1, 0])
ts = linspace(0, 10, 1000 + 1)
fb_lin_data = ip.simulate(x_0, fb_lin, ts)
```

```python
from matplotlib.pyplot import figure, grid, plot, show, subplot, xlabel, ylabel
```

```python
xs, us = fb_lin_data

figure(figsize=(12, 4))

subplot(121)
plot(ts, xs[:, 0], linewidth=3)
grid()
xlabel('Time (sec)', fontsize=16); ylabel('$\\theta$', fontsize=16)

subplot(122)
plot(ts[:-1], us, linewidth=3)
grid()
xlabel('Time (sec)', fontsize=16); ylabel('$\\tau$', fontsize=16)

show()
```

## Energy-Based Lyapunov Functions

From the Euler-Lagrange equation, the time-derivative of kinetic energy satisfies:

\begin{align}
    \dot{T}(\mb{q}, \dot{\mb{q}}, \mb{u}) &= \derp{T}{\mb{q}}\dot{\mb{q}} + \derp{T}{\dot{\mb{q}}}\ddot{\mb{q}}\\
    &= \left(\der{}{t}\left(\derp{T}{\dot{\mb{q}}}\right) + \derp{U}{\mb{q}} - (\mb{B}\mb{u})^\top \right)\dot{\mb{q}} + \derp{T}{\dot{\mb{q}}}\ddot{\mb{q}}
\end{align}

Rearranging terms, we obtain:

\begin{align}
    \dot{T}(\mb{q}, \dot{\mb{q}}, \mb{u}) &= \left(\der{}{t}\left(\derp{T}{\dot{\mb{q}}}\right)\dot{\mb{q}} + \derp{T}{\dot{\mb{q}}}\ddot{\mb{q}}\right) + \dot{U}(\mb{q}, \dot{\mb{q}}) - \dot{\mb{q}}^\top\mb{B}\mb{u}\\
    &= \der{}{t}\left( \derp{T}{\dot{\mb{q}}}\dot{\mb{q}} \right) + \dot{U}(\mb{q}, \dot{\mb{q}}) - \dot{\mb{q}}^\top\mb{B}\mb{u}
\end{align}

Note that:

\begin{align}
    \der{}{t}\left( \derp{T}{\dot{\mb{q}}}\dot{\mb{q}} \right) = \der{}{t} \left( \dot{\mb{q}}^\top\mb{D}(\mb{q})\dot{\mb{q}} \right) = 2\dot{T}(\mb{q}, \dot{\mb{q}}, \mb{u}).
\end{align}

Therefore, we have:

\begin{equation}
    \dot{T}(\mb{q}, \dot{\mb{q}}, \mb{u}) + \dot{U}(\mb{q}, \dot{\mb{q}}) = \dot{\mb{q}}^\top\mb{B}\mb{u}.
\end{equation}

Defining total energy $E: \mathcal{Q} \times \R^n \to \R$ as $E(\mb{q}, \dot{\mb{q}}) = T(\mb{q}, \dot{\mb{q}}) + U(\mb{q})$ for all $\mb{q} \in \mathcal{Q}$ and $\dot{\mb{q}} \in \R^n$ yields:

\begin{equation}
    \dot{E}(\mb{q}, \dot{\mb{q}}, \mb{u}) = \dot{\mb{q}}^\top\mb{B}\mb{u}.
\end{equation}

With no control, energy is conserved and the system evolves along the level sets of $E$.

We'll use the energy of the system to motivate a Lyapunov function. Let $V: \mathcal{Q} \times \R^n \to \R_+$ be defined as:

\begin{equation}
    V(\mb{q}, \dot{\mb{q}}) = T(\mb{q}, \dot{\mb{q}}) + \frac{1}{2}(\mb{q} - \mb{q}_d)^\top\mb{K}_P(\mb{q} - \mb{q}_d),
\end{equation}

for all $\mb{q} \in \mathcal{Q}$ and $\dot{\mb{q}} \in \R^n$, with proportional gain $\mb{K}_P \in \bb{S}^n_{++}$.

Note that $V(\mb{q}, \dot{\mb{q}}) = 0$ if only if $\mb{q} = \mb{q}_d$ and $\dot{\mb{q}} = \mb{0}_n$. We will design a controller such that this set point is asymptotically stable.

The time-derivative of $V$ satisfies:

\begin{align}
    \dot{V}(\mb{q}, \dot{\mb{q}}, \mb{u}) &= \dot{T}(\mb{q}, \dot{\mb{q}}, \mb{u}) + (\mb{q} - \mb{q}_d)^\top\mb{K}_P\dot{\mb{q}}\\
    &= \dot{\mb{q}}^\top\mb{B}\mb{u} -\dot{U}(\mb{q}, \dot{\mb{q}}) + (\mb{q} - \mb{q}_d)^\top\mb{K}_P\dot{\mb{q}}\\
    &= \dot{\mb{q}}^\top\left( \mb{B}\mb{u} - \left(\derp{U}{\mb{q}}\right)^\top + \mb{K}_P(\mb{q} - \dot{\mb{q}}) \right).
\end{align}

Recall that:

\begin{equation}
    \left(\derp{U}{\mb{q}}\right)^\top = \mb{G}(\mb{q}).
\end{equation}

Therefore, we can choose the controller:

\begin{equation}
    \mb{u} = \mb{B}^{-1}(\mb{G}(\mb{q}) - \mb{K}_P(\mb{q} - \mb{q}_d) - \mb{K}_D\dot{\mb{q}}),
\end{equation}

with derivative gain $\mb{K}_D \in \bb{S}^n_{++}$.

Using this controller yields:

\begin{equation}
    \dot{V}(\mb{q}, \dot{\mb{q}}, \mb{u}) = -\dot{\mb{q}}^\top\mb{K}_D\dot{\mb{q}},
\end{equation}

which is negative for all $\mb{q} \in \mathcal{Q}$ and $\dot{\mb{q}} \in \R^n \setminus \{ \mb{0}_n \}$.

When $\dot{\mb{q}} = \mb{0}_n$, the equations of motion yield:

\begin{equation}
    \mb{D}(\mb{q})\ddot{\mb{q}} + \mb{G}(\mb{q}) = \mb{B}\mb{B}^{-1}(\mb{G}(\mb{q}) - \mb{K}_P(\mb{q} - \mb{q}_d)),
\end{equation}

or:

\begin{equation}
    \ddot{\mb{q}} = -\mb{D}(\mb{q})^{-1}\mb{K}_P(\mb{q} - \mb{q}_d).
\end{equation}

This means the only invariant set with $\dot{\mb{q}} = \mb{0}_n$ uniformly is $\{ (\mb{q}_d, \mb{0}_n) \}$, and by LaSalle's invariance principle the set point is asymptotically stable.

Let's create an energy-based controller for the inverted pendulum with $K_P = 1$ and $K_D = 1$ to track an upright angle.

```python
from core.controllers import EnergyController

K_p, K_d = identity(1), identity(1)
energy = EnergyController(ip, K_p, K_d)
```

```python
energy_data = ip.simulate(x_0, energy, ts)
```

```python
from matplotlib.pyplot import legend

figure(figsize=(12, 4))

subplot(121)
xs, _ = fb_lin_data; plot(ts, xs[:, 0], linewidth=3, label='FBL')
xs, _ = energy_data; plot(ts, xs[:, 0], linewidth=3, label='Energy')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\theta$', fontsize=16)

subplot(122)
_, us = fb_lin_data; plot(ts[:-1], us, linewidth=3, label='FBL')
_, us = energy_data; plot(ts[:-1], us, linewidth=3, label='Energy')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\tau$', fontsize=16)

show()
```

## Feeback Linearization-Based Lyapunov Functions

Recall that feedback linearization induced controllable linear dynamics:

\begin{equation}
    \dot{\mb{x}} = \mb{F}\mb{x} + \mb{G}\bs{\nu}.
\end{equation}

Additionally, solving the CARE gave a matrix $\mb{P} \in \bb{S}^{2n}_{++}$ satisfying:

\begin{equation}
    \mb{F}^\top\mb{P} + \mb{P}\mb{F} - \mb{P}\mb{G}\mb{R}^{-1}\mb{G}^\top\mb{P} = -\mb{Q},
\end{equation}

for $\mb{Q} \in \bb{S}^{2n}_{++}$ and $\mb{R} \in \bb{S}^m_{++}$.

Let $V: \R^{2n} \to \R_+$ be defined as $V(\mb{x}) = \mb{x}^\top\mb{P}\mb{x}$ for all $\mb{x} \in \R^{2n}$ and note that:

\begin{equation}
    \lambda_{2n}(\mb{P}) \norm{\mb{x}}_2^2 \leq V(\mb{x}) \leq \lambda_1(\mb{P}) \norm{\mb{x}}_2^2,
\end{equation}

with $\lambda_1(\cdot)$ and $\lambda_{2n}(\cdot)$ denoting maximum and minimum eigenvalues, respectively.

Let $\mb{u}_{LF}(\bs{\nu})$ denote the linearizing feedback controller with auxilliary control $\bs{\nu}$. The time-derivative of $V$ satisfies:

\begin{align}
    \dot{V}(\mb{x}, \mb{u}_{LF}(\bs{\nu})) &= \dot{\mb{x}}^\top\mb{P}\mb{x} + \mb{x}^\top\mb{P}\dot{\mb{x}}\\
    &= \mb{x}^\top(\mb{F}^\top\mb{P} + \mb{P}\mb{F})\mb{x} + 2\mb{x}^\top\mb{P}\mb{G}\bs{\nu}.
\end{align}

Since $\mb{P}$, $\mb{Q}$, and $\mb{R}$ solve CARE, we obtain:

\begin{equation}
    \dot{V}(\mb{x}, \mb{u}_{LF}(\bs{\nu})) = -\mb{x}^\top\mb{Q}\mb{x} + \mb{x}^\top\mb{P}\mb{G}\mb{R}^{-1}\mb{G}^\top\mb{P}\mb{x} + 2\mb{x}^\top\mb{P}\mb{G}\bs{\nu}.
\end{equation}

Therefore we can choose the auxilliary controller:

\begin{equation}
    \bs{\nu}^* = -\frac{1}{2}\mb{R}^{-1}\mb{G}^\top\mb{P}\mb{x},
\end{equation}

the same LQR solution obtained previously.

Using this controller yields:

\begin{equation}
    \dot{V}(\mb{x}, \mb{u}_{LF}(\bs{\nu}^*)) = -\mb{x}^\top\mb{Q}\mb{x},
\end{equation}

which is negative for all $\mb{x} \in \R^{2n} \setminus \{ \mb{0}_{2n} \}$.

This can be bounded further in terms of $\mb{P}$ and $\mb{Q}$ as:

\begin{align}
    \dot{V}(\mb{x}, \mb{u}_{LF}(\bs{\nu}^*)) &\leq -\lambda_{2n}(\mb{Q}) \norm{\mb{x}}_2^2\\
    &\leq -\frac{\lambda_{2n}(\mb{Q})}{\lambda_1(\mb{P})} V(\mb{x}),
\end{align}

for all $\mb{x} \in \R^{2n}$.

From the comparison principle, it follows that:

\begin{equation}
    V(\mb{x}(t)) \leq V(\mb{x}(t_0)) \exp{\left( -\frac{\lambda_{2n}(\mb{Q})}{\lambda_1(\mb{P})} (t - t_0) \right)},
\end{equation}

for all $t \geq t_0$ with initial time $t_0$.

Finally, we obtain:

\begin{equation}
    \norm{\mb{x}(t)}_2 \leq \norm{\mb{x}(t_0)}_2 \exp{\left( -\frac{\lambda_{2n}(\mb{Q})}{2\lambda_1(\mb{P})}(t - t_0) \right)},
\end{equation}

showing that the set point $\mb{0}_{2n}$, corresponding to $\mb{q} = \mb{q}_d$ and $\dot{\mb{q}} = \mb{0}_n$, is globally exponentially stable with this controller.

This controller is no different than the controller obtained through feedback linearization with LQR. However, this construction presents a bound that may be employed in provably feasible optimization-based controllers.

Let $\alpha \geq \lambda_{2n}(\mb{Q}) / \lambda_1(\mb{P})$ and consider the optimization problem:

\begin{align}
    \inf_{\mb{u} \in \R^m} ~&~ \mb{u}^\top\bs{\Lambda}\mb{u}\\
    \mathrm{s.t.} ~&~ \dot{V}(\mb{x}, \mb{u}) \leq -\alpha V(\mb{x})
\end{align}

for all $\mb{x} \in \R^{2n}$. Here $\bs{\Lambda} \in \bb{S}^m_{++}$.

$\mb{u}_{LF}(\bs{\nu})$ is always a feasible solution for this problem.

To better condition the problem, we can regularize the objective using distance from another controller.

Let $\rho \geq 0$ and, consider the optimization problem:

\begin{align}
    \inf_{\mb{u} \in \R^m} ~&~ \mb{u}^\top\bs{\Lambda}\mb{u} + \rho \norm{ \mb{u} - \mb{u}_0 }_2^2\\
    \mathrm{s.t.} ~&~ \dot{V}(\mb{x}, \mb{u}) \leq -\alpha V(\mb{x}),
\end{align}

for all $\mb{x} \in \R^{2n}$. Here, $\mb{u}_0$ is another controller.

Let's create an optimization-based controller for the inverted pendulum. We'll use the same parameters as we did with the linearizing feedback controller. Additionally, we'll use $\bs{\Lambda} = 1$.

```python
from core.controllers import QPController

qp = QPController.build_care(ip, Q, R)
```

```python
qp_data = ip.simulate(x_0, qp, ts)
```

```python
figure(figsize=(12, 4))

subplot(121)
xs, _ = fb_lin_data; plot(ts, xs[:, 0], linewidth=3, label='FBL')
xs, _ = energy_data; plot(ts, xs[:, 0], linewidth=3, label='Energy')
xs, _ = qp_data; plot(ts, xs[:, 0], linewidth=3, label='QP')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\theta$', fontsize=16)

subplot(122)
_, us = fb_lin_data; plot(ts[:-1], us, linewidth=3, label='FBL')
_, us = energy_data; plot(ts[:-1], us, linewidth=3, label='Energy')
_, us = qp_data; plot(ts[:-1], us, linewidth=3, label='QP')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\tau$', fontsize=16)

show()
```

Let's compare this to the results with $\rho = 25$ and $\rho = 250$ for regularization around the linearizing feedback controller.

```python
qp_25 = QPController.build_care(ip, Q, R)
qp_25.add_regularizer(fb_lin, 25)

qp_250 = QPController.build_care(ip, Q, R)
qp_250.add_regularizer(fb_lin, 250)
```

```python
qp_25_data = ip.simulate(x_0, qp_25, ts)
qp_250_data = ip.simulate(x_0, qp_250, ts)
```

```python
figure(figsize=(12, 4))

subplot(121)
xs, _ = qp_data; plot(ts, xs[:, 0], linewidth=3, label='$\\rho = 0$')
xs, _ = qp_25_data; plot(ts, xs[:, 0], linewidth=3, label='$\\rho = 25$')
xs, _ = qp_250_data; plot(ts, xs[:, 0], linewidth=3, label='$\\rho = 250$')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\theta$', fontsize=16)

subplot(122)
_, us = qp_data; plot(ts[:-1], us, linewidth=3, label='$\\rho = 0$')
_, us = qp_25_data; plot(ts[:-1], us, linewidth=3, label='$\\rho = 25$')
_, us = qp_250_data; plot(ts[:-1], us, linewidth=3, label='$\\rho = 250$')
grid(); legend(fontsize=16)
xlabel('Time (sec)', fontsize=16); ylabel('$\\tau$', fontsize=16)

show()
```


# Datatrack.py Tutorial
Datatracks are the bread and butter of a great deal of research computational biology. After much of the preprocessing of experimental sequencing data we are usually left with a file which in some way details how some measurement of interest varies across the genome. The simplest way of thinking about a datatrack is just as a function which can take real values. If we let $\mathbf{C}$ be the set of chromosomes then we might define a datatrack as a function:

\begin{equation}
    f: \mathbb{N}_{+}\times\mathbf{C} \to \mathbb{R}
\end{equation}

Where we input a positive integer and chromosome and we get a real value. This value could, for example, tell us about the strength of binding of a given protein to a given basepair. Or it could be the number of reads which we observe at that basepair.

### The DataTrack Class
Simple and quick manipulation of the functions which we come across for various ChIPseq datasets (or, for example, DNA methylation dataset datasets) vastly increases the speed at which different hypotheses can be tested. Being able to keep track of variables also greatly reduces mental load! The Datatrack class is a quick way to load up data from standard data sources and interrogate them to find out properties of the underlying functions which they represent.

### dtrack_utils
The Datatrack class makes heavy use of a cython module called GrapHiC.utils.dtrack_utils. Mastering this module will allow users to implement much more complicated things than in this notebook. However, the datatrack class is useful as-is for a lot of analysis. It's been implemented in cython so that many of the most common tasks such as overlapping two datatracks or collecting statistics on some group of regions can be done faster than would be possible in python.

## Importing Modules

```python
#dtrack_utils Cython Module
#from GrapHiC.utils import dtrack_utils as dtu
#Numpy is always useful for numerical manipulation
import numpy as np
import itertools
import pandas as pd
#Import our datatrack classes!
from GrapHiC.utils.Datatrack import DataTrack_rvp as dtrvp
from GrapHiC.utils.Datatrack import DataTrack_bigwig as dtbw

from GrapHiC.Datatrack_creation import evaluate_tracks_over_cooler_bins, evaluate_tracks_over_bed_dataframe
```

# Basic Datatrack creation over cooler bins

One of the basic things to do with datatracks is to collect statistics for those tracks over hi-C data for each bin. We can do this using the evaluate_tracks_over_cooler_bins function:

```python
contact_path = 'tutorial_data/WT_contacts.cool'
nanog_peaks = "tutorial_data/GSEXXXXXHendrich20161026_Nanog_ESC_peaks.bed"
nanog_bigwig = "tutorial_data/GSE71932_Nanog_mESCs_treat_pileup_filter.bw"
```

```python
df = evaluate_tracks_over_cooler_bins(contact_path,
                             paths = [nanog_peaks,nanog_bigwig],
                             names = ['nanog_bigwig','nanog_peaks'],
                             stats_types = ['mean','max','std'],
                             allowed_chroms = ['chr1','chr2','chrX'],
                                      value_col = 4
                            )
df
```

    nanog_bigwig tutorial_data/GSEXXXXXHendrich20161026_Nanog_ESC_peaks.bed
    	mean
    	max
    	std
    nanog_peaks tutorial_data/GSE71932_Nanog_mESCs_treat_pileup_filter.bw
    	mean
    	max
    	std

      nanog_bigwig_mean
      nanog_bigwig_max
      nanog_bigwig_std
      nanog_peaks_mean
      nanog_peaks_max
      nanog_peaks_std

      0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      1
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      2
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      3
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      4
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

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

      26346
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

      26347
      0.0
      0.0
      0.0
      0.10049
      6.0
      0.586188

      26348
      0.0
      0.0
      0.0
      0.092287
      6.0
      0.501517

      26349
      0.0
      0.0
      0.0
      0.147928
      3.0
      0.489231

      26350
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0

5488 rows × 6 columns

## Evaluation over arbitrary regions
alternatively, we can evaluate datatracks over arbitrary regions specificed by a bed file. Let's take a look at some regions of active promoters:

```python
df = pd.read_table("tutorial_data/active_promoters.csv")

colnames, arr = evaluate_tracks_over_bed_dataframe(df,
                             paths = [nanog_peaks,nanog_bigwig],
                             names = ['nanog_bigwig','nanog_peaks'],
                             stats_types = ['mean','max','std'],
                             value_col = 4
                            )
pd.DataFrame(data = arr, columns = colnames)
```

    nanog_bigwig tutorial_data/GSEXXXXXHendrich20161026_Nanog_ESC_peaks.bed
    	mean
    	max
    	std
    nanog_peaks tutorial_data/GSE71932_Nanog_mESCs_treat_pileup_filter.bw
    	mean
    	max
    	std

      nanog_bigwig_mean
      nanog_bigwig_max
      nanog_bigwig_std
      nanog_peaks_mean
      nanog_peaks_max
      nanog_peaks_std

      0
      0.00000
      0.0
      0.000000
      7.225386
      37.0
      7.559524

      1
      0.00000
      0.0
      0.000000
      4.174326
      13.0
      2.608505

      2
      0.00000
      0.0
      0.000000
      3.006454
      9.0
      1.850368

      3
      0.00000
      0.0
      0.000000
      3.074620
      8.0
      1.882457

      4
      7.74675
      99.0
      26.587894
      4.073532
      16.0
      3.523583

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

      10208
      0.00000
      0.0
      0.000000
      2.200084
      6.0
      1.574169

      10209
      0.00000
      0.0
      0.000000
      1.917015
      8.0
      1.600306

      10210
      0.00000
      0.0
      0.000000
      2.287324
      18.0
      2.841367

      10211
      0.00000
      0.0
      0.000000
      1.609052
      5.0
      1.164586

      10212
      0.00000
      0.0
      0.000000
      2.955551
      14.0
      3.159173

10213 rows × 6 columns

# Region-Value Pair Datatracks
## BED format files
One of the most common formats for storing datatracks is in region-value-pair format. That is, on each chromosome we simple detail the regions where the function $f$ takes nonzero values and we specify the value that the function takes in that region. Below is an example of bed file created for a Nanog ChIPseq from naive embryonic stem cells:

```python
!head tutorial_data/GSEXXXXXHendrich20161026_Nanog_ESC_peaks.bed
```

    chr1	3062626	3063246	GSEXXXXXHendrich20161026_Nanog_ESC_peak_1	1091
    chr1	3445421	3446366	GSEXXXXXHendrich20161026_Nanog_ESC_peak_2	658
    chr1	3681365	3681811	GSEXXXXXHendrich20161026_Nanog_ESC_peak_3	48
    chr1	3947036	3947306	GSEXXXXXHendrich20161026_Nanog_ESC_peak_4	55
    chr1	4150783	4151277	GSEXXXXXHendrich20161026_Nanog_ESC_peak_5	1017
    chr1	4447859	4448346	GSEXXXXXHendrich20161026_Nanog_ESC_peak_6	329
    chr1	4611937	4612228	GSEXXXXXHendrich20161026_Nanog_ESC_peak_7	104
    chr1	4660171	4660590	GSEXXXXXHendrich20161026_Nanog_ESC_peak_8	195
    chr1	4802495	4802928	GSEXXXXXHendrich20161026_Nanog_ESC_peak_9	424
    chr1	4819456	4819933	GSEXXXXXHendrich20161026_Nanog_ESC_peak_10	346

We can see that there is chromosome information in collumn zero, then a region start collumn, a region end collumn, some unique IDs for each peak and then a value collumn detailing the size of the function at each peak. Lets go ahead and load this information into a DataTrack_rvp object (the rvp stands for region-value-pairs) using the .from_bed() method:

```python
#In order to initialise our Datatrack all we need to do is specify a name. So lets create datatrack object
#with name nanog and save it in the variable nanog
nanog = dtrvp("Nanog")

#Lets now construct the datatrack from the bed file
nanog.from_bed(nanog_peaks,
               chrom_col = 0, #What collumn is our chromosome information in?
               region_cols = (1,2), #What collumns are our region start and region end?
               value_col = 4, #What collumns are our values in?
               ID_col = 3) #What collumns are our region IDs in?)
```

    <GrapHiC.utils.Datatrack.DataTrack_rvp at 0x7fd6ed2ae510>

Note that we could also ignore the ID and value information and simply put all values as a constant:

```python
nanog = dtrvp("Nanog").from_bed(nanog_peaks,
                                chrom_col = 0, #What collumn is our chromosome information in?
                                region_cols = (1,2), #What collumns are our region start and region end?
                                value_col = None, #What collumns are our values in?
                                value_fill = 1,
                                ID_col = None)
```

Now we can take a look at the data which is in our datatrack object:

```python
#Lets take a look at our regions first
nanog.regions['chr1']
```

    array([[  3062626,   3063246],
           [  3445421,   3446366],
           [  3681365,   3681811],
           ...,
           [194354695, 194354978],
           [194472371, 194472801],
           [194890314, 194890780]], dtype=int32)

```python
#Now our values in chromosome 2
nanog.values['chr2']
```

    array([[1.],
           [1.],
           [1.],
           ...,
           [1.],
           [1.],
           [1.]])

```python
#We can also look at the name and type of our datatrack if we forget
print("'nanog' is a datatrack of type: {}".format(nanog.dtrack_type))
print("It has name: {}".format(nanog.name))
```

    'nanog' is a datatrack of type: region-value-pairs
    It has name: Nanog

## Interrogating the datatrack function
Lets now look at functions for investigating region-value-pair functions.

### Chromosome Limits
One of the first things we might be interested in is what the lengths of our chromosomes actually are! The Datatrack class doesn't assume that the chromosomes have any particular length and leaves these as functions which are free to be input by the user. However, we can estimate the chromosome limits from the maximum and minimum values of the regions we observe on each chromosome:

```python
nanog.chrlims_from_regions()
nanog.chr_lims
```

    {'chr1': [3062626, 194890780],
     'chr10': [3302560, 130420449],
     'chr11': [3145328, 121964594],
     'chr12': [3124034, 119777974],
     'chr13': [3712816, 119334473],
     'chr14': [7809188, 124446400],
     'chr15': [3748080, 103383326],
     'chr16': [3531957, 97936334],
     'chr17': [3505534, 94866952],
     'chr18': [3006850, 90581368],
     'chr19': [3532657, 61321440],
     'chr2': [3100032, 181773870],
     'chr3': [3370820, 159615381],
     'chr4': [3368156, 156194661],
     'chr5': [3293540, 151645339],
     'chr6': [4118617, 149357512],
     'chr7': [3507008, 145139802],
     'chr8': [3100430, 128538866],
     'chr9': [3376306, 123731738],
     'chrX': [6279324, 169906459],
     'chrY': [4829304, 4829578]}

### Datatrack function evaluation

Now that we have estimated our chromosome limits let's look at interrogating our function. Looking at the function for all values in a given region is simple and just uses the .func method. We just provide a midpoint and a windowsize (or buffer) around that midpoint where we want to evaluate the function:

```python
#Here is the start of a peak in our Nanog datatrack on chromosome 1
nanog.func('1',[3062586], buffer = 100)
```

    array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0., 0., 0., 0., 0.]])

Note that we can provide multiple midpoints and we'll get multiple rows in our output array:

```python
#Here is the start of a peak in our Nanog datatrack on chromosome 1
nanog.func('1',[3062686,194354695], buffer = 10)
```

    array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0.],
           [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
            0., 0., 0., 0.]])

This comes in use when we might want to evaluate a function across an entire set of genomic regions. For example active promoters:

```python
x = pd.read_table('tutorial_data/active_promoters.csv')
x = {k1: np.concatenate([item[None,1:3] for item in list(g1)],
                                       axis = 0).astype('int32') for k1,g1 in itertools.groupby(sorted(x.values,
                                                                                key = lambda x:x[0]),
                                                                                lambda x: x[0])}
x['chr1']
```

    array([[ 57993971,  57997971],
           [ 11412112,  11416112],
           [136129183, 136133183],
           [ 91296338,  91300338],
           [ 43443751,  43447751],
           [193033698, 193037698],
           [ 91364430,  91368430],
           [ 75140786,  75144786],
           [ 64120282,  64124282],
           [151258914, 151262914],
           [ 75208778,  75212778],
           [181838568, 181842568],
           [ 52117438,  52121438],
           [155081266, 155085266],
           [152764351, 152768351],
           [161967186, 161971186],
           [ 63174825,  63178825],
           [166000960, 166004960],
           [ 86524736,  86528736],
           [ 52628705,  52632705],
           [ 53295018,  53299018],
           [ 88006551,  88010551],
           [156717001, 156721001],
           [ 45793166,  45797166],
           [ 72581251,  72585251],
           [178403881, 178407881],
           [ 80338480,  80342480],
           [166378493, 166382493],
           [131960967, 131964967],
           [152764542, 152768542],
           [177794511, 177798511],
           [163401641, 163405641],
           [  9745648,   9749648],
           [ 56969491,  56973491],
           [ 93340854,  93344854],
           [ 75217253,  75221253],
           [ 60178599,  60182599],
           [143775278, 143779278],
           [ 92639985,  92643985],
           [157410595, 157414595],
           [191715834, 191719834],
           [134492660, 134496660],
           [ 53311624,  53315624],
           [ 36305733,  36309733],
           [ 67036872,  67040872],
           [191819444, 191823444],
           [182515495, 182519495],
           [ 55129231,  55133231],
           [ 37888411,  37892411],
           [151753374, 151757374],
           [153897926, 153901926],
           [171293024, 171297024],
           [ 97660074,  97664074],
           [186703992, 186707992],
           [171358798, 171362798],
           [ 93633722,  93637722],
           [ 13060019,  13064019],
           [134359999, 134363999],
           [192134898, 192138898],
           [ 20949626,  20953626],
           [131795381, 131799381],
           [121085405, 121089405],
           [195129570, 195133570],
           [ 74282738,  74286738],
           [182122737, 182126737],
           [175690776, 175694776],
           [182407172, 182411172],
           [135718061, 135722061],
           [ 53295095,  53299095],
           [ 92904989,  92908989],
           [119911208, 119915208],
           [134453531, 134457531],
           [177256203, 177260203],
           [170172957, 170176957],
           [ 63196031,  63200031],
           [ 57405101,  57409101],
           [172026444, 172030444],
           [180811776, 180815776],
           [164247046, 164251046],
           [171248421, 171252421],
           [135144824, 135148824],
           [130715327, 130719327],
           [162546551, 162550551],
           [130460576, 130464576],
           [ 63175983,  63179983],
           [130460744, 130464744],
           [156034480, 156038480],
           [136413519, 136417519],
           [193271188, 193275188],
           [ 16655552,  16659552],
           [169529464, 169533464],
           [183386981, 183390981],
           [133608411, 133612411],
           [ 75433930,  75437930],
           [164501307, 164505307],
           [136694211, 136698211],
           [157456580, 157460580],
           [ 75505077,  75509077],
           [ 58503148,  58507148],
           [172519044, 172523044],
           [184031998, 184035998],
           [171111918, 171115918],
           [ 74586246,  74590246],
           [ 71650837,  71654837],
           [180933022, 180937022],
           [ 33755794,  33759794],
           [ 34431199,  34435199],
           [111862909, 111866909],
           [ 79774143,  79778143],
           [191181340, 191185340],
           [ 86301221,  86305221],
           [119524359, 119528359],
           [ 65049149,  65053149],
           [176812660, 176816660],
           [ 64530804,  64534804],
           [132005607, 132009607],
           [191023350, 191027350],
           [135106961, 135110961],
           [ 85892281,  85896281],
           [ 43096622,  43100622],
           [153747426, 153751426],
           [ 57375504,  57379504],
           [120072074, 120076074],
           [172374531, 172378531],
           [171343646, 171347646],
           [ 64735750,  64739750],
           [171435535, 171439535],
           [ 55235177,  55239177],
           [120119078, 120123078],
           [189920363, 189924363],
           [ 74282930,  74286930],
           [136622901, 136626901],
           [ 46852046,  46856046],
           [ 24676544,  24680544],
           [155415415, 155419415],
           [193505530, 193509530],
           [160904418, 160908418],
           [136344104, 136348104],
           [  9545948,   9549948],
           [ 65184750,  65188750],
           [ 34847976,  34851976],
           [136413271, 136417271],
           [191573763, 191577763],
           [ 63176677,  63180677],
           [189726268, 189730268],
           [194974375, 194978375],
           [ 36066400,  36070400],
           [179666210, 179670210],
           [ 13372083,  13376083],
           [ 63728318,  63732318],
           [ 16617489,  16621489],
           [ 74853029,  74857029],
           [125433727, 125437727],
           [160790938, 160794938],
           [132189436, 132193436],
           [  7395801,   7399801],
           [ 83036448,  83040448],
           [184844451, 184848451],
           [179958472, 179962472],
           [ 23920317,  23924317],
           [ 63443891,  63447891],
           [ 74303622,  74307622],
           [161874661, 161878661],
           [ 62701285,  62705285],
           [ 16518112,  16522112],
           [ 33905816,  33909816],
           [191395041, 191399041],
           [ 14753454,  14757454],
           [170883065, 170887065],
           [132201803, 132205803],
           [ 92908783,  92912783],
           [ 86580904,  86584904],
           [ 16654865,  16658865],
           [ 82722890,  82726890],
           [ 86580629,  86584629],
           [ 37863299,  37867299],
           [ 54247208,  54251208],
           [ 38834090,  38838090],
           [  6213292,   6217292],
           [131865224, 131869224],
           [ 54436971,  54440971],
           [ 75189923,  75193923],
           [ 84837841,  84841841],
           [132196088, 132200088],
           [125559016, 125563016],
           [136232280, 136236280],
           [143775068, 143779068],
           [165192433, 165196433],
           [171327145, 171331145],
           [187213508, 187217508],
           [ 59911006,  59915006],
           [143776928, 143780928],
           [153898208, 153902208],
           [165458465, 165462465],
           [ 82314452,  82318452],
           [  6212590,   6216590],
           [140244216, 140248216],
           [172630974, 172634974],
           [105988406, 105992406],
           [170213397, 170217397],
           [135582773, 135586773],
           [ 75315637,  75319637],
           [ 93510079,  93514079],
           [119420248, 119424248],
           [127772164, 127776164],
           [161129511, 161133511],
           [ 93303915,  93307915],
           [ 72210307,  72214307],
           [130729976, 130733976],
           [ 21077229,  21081229],
           [ 20888622,  20892622],
           [ 36556349,  36560349],
           [165234943, 165238943],
           [ 54248683,  54252683],
           [187213465, 187217465],
           [156937626, 156941626],
           [171435318, 171439318],
           [ 23254406,  23258406],
           [182407020, 182411020],
           [ 36545201,  36549201],
           [156419159, 156423159],
           [ 59480147,  59484147],
           [ 92932376,  92936376],
           [ 44549511,  44553511],
           [ 74504058,  74508058],
           [ 93341482,  93345482],
           [174499825, 174503825],
           [138617696, 138621696],
           [ 74586289,  74590289],
           [118457265, 118461265],
           [  9698209,   9702209],
           [ 75178392,  75182392],
           [ 59118096,  59122096],
           [170974547, 170978547],
           [ 89931290,  89935290],
           [ 75477471,  75481471],
           [192769213, 192773213],
           [167306670, 167310670],
           [181851189, 181855189],
           [177441270, 177445270],
           [170212860, 170216860],
           [ 74848895,  74852895],
           [171268328, 171272328],
           [ 60407619,  60411619],
           [ 14308200,  14312200],
           [180902274, 180906274],
           [ 23381175,  23385175],
           [ 36556359,  36560359],
           [154078199, 154082199],
           [151569186, 151573186],
           [ 16663207,  16667207],
           [181148931, 181152931],
           [186965416, 186969416],
           [ 37869738,  37873738],
           [ 72234465,  72238465],
           [ 44116808,  44120808],
           [167347025, 167351025],
           [190909770, 190913770],
           [ 37428103,  37432103],
           [ 90601425,  90605425],
           [171279186, 171283186],
           [ 72234946,  72238946],
           [132415453, 132419453],
           [119647000, 119651000],
           [ 51914071,  51918071],
           [ 66698831,  66702831],
           [136622650, 136626650],
           [ 91538565,  91542565],
           [ 72242213,  72246213],
           [135256472, 135260472],
           [ 93801965,  93805965],
           [ 34437851,  34441851],
           [159230326, 159234326],
           [ 43825800,  43829800],
           [155156703, 155160703],
           [ 96870221,  96874221],
           [171220514, 171224514],
           [ 86152780,  86156780],
           [ 63177004,  63181004],
           [ 75504452,  75508452],
           [ 37995346,  37999346],
           [ 34437672,  34441672],
           [ 66861277,  66865277],
           [180891108, 180895108],
           [ 36681183,  36685183],
           [ 63198313,  63202313],
           [113962134, 113966134],
           [132389281, 132393281],
           [ 91492668,  91496668],
           [ 97768172,  97772172],
           [ 74584972,  74588972],
           [ 39517533,  39521533],
           [177442661, 177446661],
           [169967241, 169971241],
           [133688108, 133692108],
           [ 37888477,  37892477],
           [167288125, 167292125],
           [136695374, 136699374],
           [ 44116957,  44120957],
           [ 63212543,  63216543],
           [135282084, 135286084],
           [178316549, 178320549],
           [133129782, 133133782],
           [ 59762279,  59766279],
           [ 11412105,  11416105],
           [119051619, 119055619],
           [169967409, 169971409],
           [ 91412038,  91416038],
           [132314126, 132318126],
           [165459037, 165463037],
           [179801376, 179805376],
           [171148711, 171152711],
           [ 72282314,  72286314],
           [ 57772162,  57776162],
           [ 64688455,  64692455],
           [178527125, 178531125],
           [ 88700221,  88704221],
           [ 39649182,  39653182],
           [ 24098341,  24102341],
           [ 58971522,  58975522],
           [ 53350752,  53354752],
           [191988138, 191992138],
           [ 33667843,  33671843],
           [ 57375620,  57379620],
           [ 38985814,  38989814],
           [132475045, 132479045],
           [ 51476399,  51480399],
           [ 42849233,  42853233],
           [ 93752838,  93756838],
           [ 92908764,  92912764],
           [ 82837446,  82841446],
           [170883540, 170887540],
           [136129389, 136133389],
           [  9746382,   9750382],
           [ 57404328,  57408328],
           [ 54555684,  54559684],
           [191060932, 191064932],
           [ 86357574,  86361574],
           [131525903, 131529903],
           [ 85791441,  85795441],
           [143737029, 143741029],
           [155810883, 155814883],
           [ 21216575,  21220575],
           [185360865, 185364865],
           [172498047, 172502047],
           [128242181, 128246181],
           [106169752, 106173752],
           [ 59117833,  59121833],
           [ 24003505,  24007505],
           [179544370, 179548370],
           [ 37863298,  37867298],
           [191904767, 191908767],
           [ 95311628,  95315628],
           [ 82723117,  82727117],
           [185330149, 185334149],
           [135686105, 135690105],
           [ 58027931,  58031931],
           [ 52815688,  52819688],
           [ 89928234,  89932234],
           [161032849, 161036849],
           [171501163, 171505163],
           [152384688, 152388688],
           [155097636, 155101636],
           [ 69683960,  69687960],
           [ 16103774,  16107774],
           [ 88275661,  88279661],
           [171501478, 171505478],
           [165761746, 165765746],
           [170866645, 170870645],
           [171343670, 171347670],
           [167347991, 167351991],
           [171502111, 171506111],
           [151342736, 151346736],
           [132282447, 132286447],
           [ 89068415,  89072415],
           [119502794, 119506794],
           [192853752, 192857752],
           [185453568, 185457568],
           [ 36509867,  36513867],
           [ 74659745,  74663745],
           [ 55050770,  55054770],
           [171285991, 171289991],
           [131151181, 131155181],
           [  4855814,   4859814],
           [ 43090588,  43094588],
           [184997549, 185001549],
           [178274639, 178278639],
           [170587861, 170591861],
           [119524425, 119528425],
           [ 97768411,  97772411],
           [ 87851287,  87855287],
           [ 90201980,  90205980],
           [133043667, 133047667],
           [134960565, 134964565],
           [172479788, 172483788],
           [ 20888473,  20892473],
           [167391841, 167395841],
           [157410352, 157414352],
           [186747396, 186751396],
           [138961436, 138965436],
           [182280804, 182284804],
           [ 58584384,  58588384],
           [135583355, 135587355],
           [133422377, 133426377],
           [ 54924387,  54928387],
           [135764119, 135768119],
           [133361572, 133365572],
           [ 64954824,  64958824],
           [138854854, 138858854],
           [191222474, 191226474],
           [ 60096247,  60100247],
           [192136010, 192140010],
           [134077120, 134081120],
           [192852928, 192856928],
           [171245122, 171249122],
           [ 43096710,  43100710],
           [191061012, 191065012],
           [ 93683694,  93687694],
           [194974959, 194978959],
           [ 98093646,  98097646],
           [ 58584323,  58588323],
           [ 53783224,  53787224],
           [ 74389509,  74393509],
           [ 89452811,  89456811],
           [ 36443271,  36447271],
           [ 33717882,  33721882],
           [ 86701804,  86705804],
           [118456747, 118460747],
           [ 75477532,  75481532],
           [ 58971430,  58975430],
           [184881218, 184885218],
           [ 44100441,  44104441],
           [143700893, 143704893],
           [  5081173,   5085173],
           [ 24003656,  24007656],
           [127202986, 127206986],
           [ 75519529,  75523529],
           [ 72282369,  72286369],
           [185452848, 185456848],
           [119524160, 119528160],
           [ 55086132,  55090132],
           [ 64615242,  64619242],
           [ 44145847,  44149847],
           [167283127, 167287127],
           [ 74392310,  74396310],
           [186115310, 186119310],
           [180940344, 180944344],
           [ 64688659,  64692659],
           [155556120, 155560120],
           [ 43932002,  43936002],
           [189005840, 189009840],
           [ 52231232,  52235232],
           [ 91396783,  91400783],
           [191905527, 191909527],
           [ 36689487,  36693487],
           [155970890, 155974890],
           [171409313, 171413313],
           [163400283, 163404283],
           [192743571, 192747571],
           [ 93823240,  93827240],
           [153184447, 153188447],
           [ 12989177,  12993177],
           [ 66815562,  66819562],
           [ 75166646,  75170646],
           [183219529, 183223529],
           [ 75140368,  75144368],
           [166377097, 166381097],
           [ 52498679,  52502679],
           [  9543408,   9547408],
           [ 60041087,  60045087],
           [ 14753966,  14757966],
           [ 91248311,  91252311],
           [ 75207731,  75211731],
           [ 75190151,  75194151],
           [ 74599445,  74603445],
           [152088320, 152092320],
           [193507803, 193511803],
           [132137685, 132141685],
           [ 75208838,  75212838],
           [ 96870171,  96874171],
           [161068767, 161072767],
           [ 74599397,  74603397],
           [ 59235231,  59239231],
           [163992889, 163996889],
           [ 39648176,  39652176],
           [163777583, 163781583],
           [120119187, 120123187],
           [131825493, 131829493],
           [106794728, 106798728],
           [ 75234465,  75238465]], dtype=int32)

These are the active promoter regions on chromosome 1 in naive ES cells. Lets look at nanog signal within +/-1kb of the active promoter centres on chromosome 1:

```python
buffer = 1000
promoter_nanog_signal = np.zeros(2*buffer)
numproms = 0
for chrom in nanog.chromosomes:
    try:
        mids = np.mean(x[chrom], axis = 1).astype('int')
    except:
        print("couldn't find active promoter info for chromosome {}".format(chrom))
        continue

    promoter_nanog_signal += np.sum(nanog.func(chrom, mids, buffer = buffer), axis = 0)
    numproms += len(mids)

promoter_nanog_signal /= numproms
```

    couldn't find active promoter info for chromosome chrY

```python
import matplotlib.pyplot as plt

fig = plt.figure(figsize = (12,10))
plot = plt.plot(np.arange(len(promoter_nanog_signal)), promoter_nanog_signal)
xticks = plt.xticks([0,500,1000,1500,2000], [-1000, -500, 0, 500,1000])
plt.tick_params(labelsize = 15)
plt.ylabel("Average Signal", size = 20)
plt.xlabel("Distance from active promoter centre", size = 20)
plt.title("Active promoter nanog signal\nNaive mESCs", size = 20)
```

We might also just be interested in looking at coverage over certain regions. We can therefore replace the datatrack values with constants if we want to when evaluating the function:

```python
buffer = 1000
promoter_nanog_signal = np.zeros(2*buffer)
numproms = 0
for chrom in nanog.chromosomes:
    try:
        mids = np.mean(x[chrom], axis = 1).astype('int')
    except:
        print("couldn't find active promoter info for chromosome {}".format(chrom))
        continue

    promoter_nanog_signal += np.sum(nanog.func(chrom, mids, buffer = buffer, use_constant_val = 1), axis = 0)
    numproms += len(mids)

promoter_nanog_signal /= numproms
fig = plt.figure(figsize = (12,10))
plot = plt.plot(np.arange(len(promoter_nanog_signal)), promoter_nanog_signal)
xticks = plt.xticks([0,500,1000,1500,2000], [-1000, -500, 0, 500,1000])
plt.tick_params(labelsize = 15)
plt.ylabel("Average Coverage", size = 20)
plt.xlabel("Distance from active promoter centre", size = 20)
plt.title("Chromosome 1 active promoter nanog signal\nNaive mESCs", size = 20)
```

### Raw data within an interval
We might instead want to get the actual raw region-value-pair information which overlaps with a region:

```python
nanog.data_in_interval('chr1', [3062586, 3062686])
```

    {'regions': array([[3062626, 3063246]], dtype=int32), 'values': array([[1.]])}

### Binning Data within an interval
One more thing we might like to do is bin all of our information within a given region. For example, we might want to bin the entirety of chromosome 19 at a bin-size of 1000000 basepairs per bin and we're interested in the average Nanog signal per basepair in each bin:

#### mean per-basepair signal

```python
nanog.bin_single_interval('chr19',
                          int(1e6),
                          interval = nanog.chr_lims['chr19'],
                          stats_type = 'mean')[1]
```

    array([0.00303 , 0.006552, 0.001425, 0.000669, 0.00132 , 0.00296 ,
           0.002537, 0.003173, 0.001201, 0.002482, 0.001221, 0.00664 ,
           0.002168, 0.00283 , 0.000415, 0.004411, 0.000977, 0.001374,
           0.003797, 0.007565, 0.007639, 0.00335 , 0.001578, 0.002382,
           0.007922, 0.005926, 0.003652, 0.001017, 0.002473, 0.003697,
           0.00318 , 0.004725, 0.002021, 0.002326, 0.008272, 0.001042,
           0.004696, 0.003917, 0.004359, 0.002566, 0.003806, 0.002973,
           0.00468 , 0.003173, 0.001299, 0.001785, 0.001107, 0.001866,
           0.000896, 0.003227, 0.001479, 0.003052, 0.00401 , 0.003168,
           0.003457, 0.002755, 0.004034, 0.001409])

We could also look at the coverage (in basepairs) within each bin:

#### Coverage

```python
nanog.bin_single_interval('chr19',
                          int(1e6),
                          interval = nanog.chr_lims['chr19'],
                          stats_type = 'coverage')[1]
```

    array([3030., 6552., 1425.,  669., 1320., 2960., 2537., 3173., 1201.,
           2482., 1221., 6640., 2168., 2830.,  415., 4411.,  977., 1374.,
           3797., 7565., 7639., 3350., 1578., 2382., 7922., 5926., 3652.,
           1017., 2473., 3697., 3180., 4725., 2021., 2326., 8272., 1042.,
           4696., 3917., 4359., 2566., 3806., 2973., 4680., 3173., 1299.,
           1785., 1107., 1866.,  896., 3227., 1479., 3052., 4010., 3168.,
           3457., 2755., 4034., 1409.])

We may also be interested in the mean signal within basepairs where we observe any signal at all:

#### Mean per-region signal

```python
nanog.bin_single_interval('chr19',
                          int(1e6),
                          interval = nanog.chr_lims['chr19'],
                          stats_type = 'per_region_mean')[1]
```

    array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
           1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
           1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
           1., 1., 1., 1., 1., 1., 1.])

Finally we might be interested in the sum of all basepair signals in our data over each bin
#### Sum of all signals

```python
nanog.bin_single_interval('chr19',
                          int(1e6),
                          interval = nanog.chr_lims['chr19'],
                          stats_type = 'sum')[1]
```

    array([3030., 6552., 1425.,  669., 1320., 2960., 2537., 3173., 1201.,
           2482., 1221., 6640., 2168., 2830.,  415., 4411.,  977., 1374.,
           3797., 7565., 7639., 3350., 1578., 2382., 7922., 5926., 3652.,
           1017., 2473., 3697., 3180., 4725., 2021., 2326., 8272., 1042.,
           4696., 3917., 4359., 2566., 3806., 2973., 4680., 3173., 1299.,
           1785., 1107., 1866.,  896., 3227., 1479., 3052., 4010., 3168.,
           3457., 2755., 4034., 1409.])

## Statistics over an interval
We can also just input random intervals and get statistics across these intervals. Here we can use the stats function to pass an interval as well as a statistic of interest (stats_type) for regions within this interval. As with the func method we can also access multiple intervals at once:

### Coverage

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'coverage')
```

    array([25946., 38367.])

### Maximum value

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'max')
```

    array([1., 1.])

### Minimum value

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'min')
```

    array([0., 0.])

### Minimum value in regions with coverage

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'per_region_min')
```

    array([1., 1.])

### Standard deviation of values

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'std')
```

    array([0.05087109, 0.06182216])

### Standard deviation of values in regions with coverage

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'per_region_std')
```

    array([0., 0.])

### Mean value

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'mean')
```

    array([0.0025946, 0.0038367])

### Mean value in regions with coverage

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'per_region_mean')
```

    array([1., 1.])

### Sum of all values

```python
nanog.stats('chr1', [[0,1e7],[1e7,2e7]], stats_type= 'sum')
```

    array([25946., 38367.])

# BigWig files

BigWig data isn't actually included in the tutorial data since it is a

```python
nanog = dtbw("Nanog").from_bw(nanog_bigwig)
```

```python
nanog.chr_lims
```

    {'chr1': [0, 195471971],
     'chr3': [0, 160039680],
     'chr2': [0, 182113224],
     'chr4': [0, 156508116],
     'chr7': [0, 145441459],
     'chr6': [0, 149736546],
     'chr9': [0, 124595110],
     'chr5': [0, 151834684],
     'chr11': [0, 122082543],
     'chr12': [0, 120129022],
     'chr8': [0, 129401213],
     'chrY': [0, 91744698],
     'chrX': [0, 171031299],
     'chr10': [0, 130694993],
     'chr13': [0, 120421639],
     'chr15': [0, 104043685],
     'chr14': [0, 124902244],
     'chr16': [0, 98207768],
     'chr19': [0, 61431566],
     'chr18': [0, 90702639],
     'chr17': [0, 94987271]}

```python
nanog.header
```

    {'version': 4,
     'nLevels': 10,
     'nBasesCovered': 2722424231,
     'minVal': 0,
     'maxVal': 410,
     'sumData': 6747542629,
     'sumSquared': 99418129997}

```python
#Here is the start of a peak in our Nanog datatrack on chromosome 1
nanog.func('chr1',[3062586], buffer = 100)
```

    array([[ 5.,  5.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,
             6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  7.,
             7.,  7.,  7.,  7.,  7.,  7.,  7.,  7.,  6.,  6.,  6.,  6.,  6.,
             6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,  6.,
             6.,  6.,  6.,  7.,  7.,  7.,  7.,  7.,  7.,  7.,  7.,  7.,  7.,
             7.,  7.,  7.,  7.,  7.,  7.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,
             8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,  8.,
             8.,  8.,  7.,  8.,  8.,  8.,  9.,  9.,  9.,  9.,  9.,  9.,  9.,
             9.,  9.,  9., 10., 10., 10., 10.,  9.,  9.,  9.,  9.,  9.,  9.,
             9.,  9.,  9., 10., 10., 10., 10., 10., 10., 10., 10., 11., 11.,
            11., 11., 11., 11., 11., 12., 12., 12., 12., 13., 13., 12., 12.,
            12., 12., 13., 13., 13., 13., 13., 13., 13., 13., 13., 13., 14.,
            14., 14., 14., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15.,
            15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15., 15.,
            15., 15., 15., 16., 16., 16., 16., 16., 16., 16., 16., 16., 16.,
            16., 16., 16., 16., 16.]])

Lets reload our promoter info and see if we can form similar plots as above when we used the BED files:

```python
buffer = 1000
promoter_nanog_signal = np.zeros(200)
numproms = 0
for chrom in nanog.chromosomes:
    print(chrom)
    try:
        mids = np.mean(x[chrom], axis = 1).astype('int')
    except:
        print("couldn't find active promoter info for chromosome {}".format(chrom))
        continue

    promoter_nanog_signal += np.sum(nanog.func(chrom, mids, buffer = buffer, binSize = 10), axis = 0)
    numproms += len(mids)

promoter_nanog_signal /= numproms
```

    chr1
    chr3
    chr2
    chr4
    chr7
    chr6
    chr9
    chr5
    chr11
    chr12
    chr8
    chrY
    couldn't find active promoter info for chromosome chrY
    chrX
    chr10
    chr13
    chr15
    chr14
    chr16
    chr19
    chr18
    chr17

```python
import matplotlib.pyplot as plt
fig = plt.figure(figsize = (12,10))
plot = plt.plot(np.arange(len(promoter_nanog_signal)), promoter_nanog_signal)
xticks = plt.xticks([0,50,100,150,200], [-1000, -500, 0, 500,1000])
plt.tick_params(labelsize = 15)
plt.ylabel("Average Signal", size = 20)
plt.xlabel("Distance from active promoter centre", size = 20)
plt.title("Active promoter nanog signal\nNaive mESCs", size = 20)
```

```python

```

```python

```


# Week 4 practical - due Friday 9/27 at 11pm
* Make sure to put your name in the body of the notebook, and in the filename.
* You may discuss these problems with other students in class, but you must turn in your own notebook.
* List anyone with whom you discussed the work.
* Please document your code, label your plots, and use markdown cells to add explanatory comments to the notebook.
* Save your notebook as a pdf file and submit it in Canvas before the deadline.

You may not distribute this notebook beyond our class, or post it anywhere online, without permission from the instructor.

```python
import numpy as np
import sympy as sp
from scipy import integrate
from matplotlib import rc
rc('text', usetex=True)
from sympy import init_printing
init_printing(use_latex=True)
from astropy.cosmology import WMAP9 as cosmo
from astropy import constants as const

import matplotlib.pyplot as plt
%matplotlib inline
import matplotlib
matplotlib.rcParams['mathtext.fontset'] = 'stix'
matplotlib.rcParams['font.family'] = 'serif'
matplotlib.rcParams['font.sans-serif'] = 'stix'
matplotlib.rcParams['text.usetex'] = True
import matplotlib as mpl
```

# Hubble diagram
Type Ia supernovae can be used to measure the **Hubble diagram**, which plots the luminosity distance as a function of redshift. Here you will compute the Hubble diagram for different cosmologies.

Consider a universe with mass density parameter $\Omega_M$ and dark energy characterized by $\Omega_L$. Let $d_H = c/H_0$ be the Hubble distance, where the Hubble constant is $H_0 = 100 h$ km/s/Mpc. The comoving distance to redshift $z$, in units of $d_H$, is
$$
  {\hat d}_C(z) = \int_{0}^{z} \frac{dz}{\sqrt{\Omega_M(1+z)^3 + \Omega_\Lambda + \Omega_k (1+z)^2}}
$$
where
$$
  \Omega_k = 1 - \Omega_M - \Omega_\Lambda
$$
The luminosity distance is then
$$
  d_L(z) = (1+z) d_H \times \begin{cases}
   \Omega_k^{-1/2} \sinh\left(\Omega_k^{1/2} {\hat d}_C(z)\right) & \Omega_k > 0 \\
   {\hat d}_C(z) & \Omega_k = 0 \\
   |\Omega_k|^{-1/2} \sin\left(|\Omega_k|^{1/2} {\hat d}_C(z)\right) & \Omega_k < 0
 \end{cases}
$$
Plot Hubble diagrams out to $z=2$ for the following cases:
* flat cosmologies with $(\Omega_M,\Omega_\Lambda) = (0,1)$, $(0.3,0.7)$, $(0.7,0.3)$, and $(1,0)$
* matter-only cosmologies with $\Omega_\Lambda = 0$ and $\Omega_M = 0.1$, $0.3$, and $0.5$

Note: `astropy` has functions to compute cosmological distances. In case you are interested, here is sample code that you could modify to test your results. **But you must write your own code here.**

```python
ztry = np.arange(0.1,1.1,0.1)
Otry = 0.3
Ltry = 0.7

from astropy.cosmology import LambdaCDM
cosmo = LambdaCDM(H0=100.0,Om0=Otry,Ode0=Ltry,Tcmb0=0)
# note that since we set H0=100 in the previous command, we get distances in units of h^{-1} Mpc
cosmo.luminosity_distance(ztry).value
```

    array([ 322.20995547,  686.04752081, 1086.90115144, 1520.5152712 ,
           1983.05666573, 2471.13503803, 2981.79100204, 3512.46412019,
           4060.95098688, 4625.36032824])

```python
def f1(Ωm, ΩΛ):
    Ωk = 1 - Ωm - ΩΛ
    f = lambda z: (Ωm * (1+z)**3 + ΩΛ + Ωk * (1+z)**2)**(-1/2)
    return f

def d_C(Ωm, ΩΛ):
    ## Defines and evaluates the dc function and returns the value
    Ωk = 1 - Ωm - ΩΛ
    return integrate.quad(f1(Ωm, ΩΛ), 0 , 2)[0]

def partFunc(Ωm, ΩΛ):
    Ωk = 1 - Ωm - ΩΛ
    display(d_C(Ωm,ΩΛ))
    ### Need to define H with correct units
    d_H = (const.c/cosmo.H(0)).value

    if Ωk > 0:
        d_L = lambda z: (1+z) * d_H * (Ωk)**(-1/2) * np.sinh((Ωk**(1/2)) * d_C(Ωm, ΩΛ))

    elif Ωk == 0:
        d_L = lambda z: (1+z) * d_H * d_C(Ωm, ΩΛ)

    elif Ωk < 0:
        d_L = lambda z: (1+z) * d_H * np.absolute(Ωk**(-1/2))*np.sin(np.absolute(Ωk**(1/2))*d_C(Ωm, Ωk))

    return d_L

def hPlot(Ωm, ΩΛ):
    z = np.linspace(0, 2, 100)
    display(z)
    f = partFunc(Ωm, ΩΛ)
    display(f)
    plt.clf()
    plt.plot(z, f(z), 'r' )
    plt.show()

hPlot(0,1)
#print((const.c/cosmo.H(0)).value)

```

```python

```