## Estimating Mutual Information with Generative Adversarial Networks (GAN)

___
$\def\abs#1{\left\lvert #1 \right\rvert}
\def\Set#1{\left\{ #1 \right\}}
\def\mc#1{\mathcal{#1}}
\def\M#1{\boldsymbol{#1}}
\def\R#1{\mathsf{#1}}
\def\RM#1{\boldsymbol{\mathsf{#1}}}
\def\op#1{\operatorname{#1}}
\def\E{\op{E}}
\def\d{\mathrm{\mathstrut d}}$

In last section, we have proved that

$$
\begin{equation}
    I(\R{X}\wedge \R{Y}) = \mathop{\inf}_{p_{\R{X}'},p_{\R{Y}'}} D(p_{\R{X}\R{Y}}||p_{\R{X}'}p_{\R{Y}'}).
\end{equation}
$$

We can further apply the DV variational formulation:

$$
I(\mathsf{X}\wedge \mathsf{Y}) = \mathop{\inf}_{p_{\R{X}'}, p_{\R{Y}'}} \mathop{\sup}_{f\in\mathcal{F}} \left\{E[f(\R{X},\R{Y})]-\log E[e^{f(\R{X}',\R{Y}')}]  \right\}.
$$

We can construct generative models to simulate the sampling process of the variational distributions $p_{\R{X}'},p_{\R{Y}'}$, and then the training of these models represents the density updating process. More precisely, we build two multilayer generative neural networks denoted as $g^1,g^2$, where $g^1$ and $g^2$ are differentiable functions mapping from the Gaussian noise $\R{Z}_1, \R{Z}_2$ to the targets $\R{X}, \R{Y}$, respectively. The samples generated by $g^1,g^2$ can be viewed as drawn from the variational distributions $p_{\R{X}'}$ and $p_{\R{Y}'}$. Therefore, optimizing $p_{\R{X}'},p_{\R{Y}'}$ is equal to train the generative models $g^1,g^2$.

We apply the **reparametrization trick** with $p_{\R{Z}_1},p_{\R{Z}_2}$, which can provide tractable gradient estimator of GAN-MINEE. With the change of variable rule, we have
\begin{equation}
    p_{\R{Z}_1}(z_1) = p_{\R{X}'}(g^1(z_1))\left|\det \frac{\partial g^1(z_1)}{\partial z_1}\right|.
\end{equation}

Then we can derive that
\begin{align}
    \mathbb{E}[e^{f(\R{X}',\R{Y}')}]
    = \mathbb{E} [e^{f(g^1(\R{Z}_1),g^2(\R{Z}_2))}],
\end{align}

Therefore, the objective function can be rewritten as
\begin{equation}
    I(\R{X}\wedge \R{Y}) = \mathop{\inf}_{g^1,g^2} \mathop\sup_{f} \left\{E[f(\R{X},\R{Y})]-\log E\big[e^{f(g^1(\R{Z}_1),g^2(\R{Z}_2))}\big]  \right\},
\end{equation}
which is the proposed MI estimator GAN-MINEE. We can train this minimax formulation by optimizing $g^1,g^2$ and $f$ in order iteratively.

```python
import torch
import numpy as np
import matplotlib.pyplot as plt
import argparse
import itertools
import sys
sys.path.append("..")
from src.utils import *

cuda = True if torch.cuda.is_available() else False
```

```python
seed = 0
rep = 1
# use GPU if available
if torch.cuda.is_available():
    torch.set_default_tensor_type(torch.cuda.FloatTensor)
else:
    torch.set_default_tensor_type(torch.FloatTensor)

np.random.seed(seed)
# initialize random seed
torch.manual_seed(seed)
mu1 = 0
mu2 = 0
```

```python
parser = argparse.ArgumentParser()
parser.add_argument("--rho", type=float, default=0.9, help="Coefficient")
parser.add_argument("--ref_batch_factor", type=int, default=20, help="ref_batch_factor")
parser.add_argument("--d", type=int, default=1, help="dimension of X & Y")
parser.add_argument("--sample_size", type=int, default=1000, help="sample size")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_iters", type=int, default=2000, help="number of interations of training")
parser.add_argument("--batch_size", type=int, default=100, help="size of the batches")
parser.add_argument("--lr", type=float, default=1e-4, help="adam: learning rate")
parser.add_argument("--hidden_dim", type=int, default=100, help="Hidden dimension")
parser.add_argument("--alpha", type=float, default=0.99, help="RMSprop: smoothing constant ")
parser.add_argument("--ma_rate", type=float, default=0.1, help="move average rate")
parser.add_argument("--ma_ef", type=float, default=1, help="move average ef")
parser.add_argument("--chkpt_name", type=str, default="../models/GAN_MINEE/models.pt")
parser.add_argument("--load_available", type=bool, default=False)
# opt = parser.parse_args()
args, unknown = parser.parse_known_args()
```

```python
mi = -0.5*np.log(1-args.rho**2)*args.d
X = np.zeros((args.sample_size,args.d))
Y = np.zeros((args.sample_size,args.d))

for j in range(args.d):
    data = gaussian_data(sample_size=args.sample_size,rho=args.rho)
    X[:,j] = data[:,0]
    Y[:,j] = data[:,1]
X = torch.Tensor(X)
Y = torch.Tensor(Y)
```

```python
continue_train = True
args.load_available = True

if continue_train:
    mi_list = train_gan_minee(args, X, Y)
else:
    if args.load_available and os.path.exists(args.chkpt_name):
        checkpoint = torch.load(args.chkpt_name, map_location='cuda' if torch.cuda.is_available() else 'cpu')
        mi_list = checkpoint['mi_list']
```

    100%|██████████| 2000/2000 [01:24<00:00, 23.60it/s]

```python
mi_ma_rate = 0.01
mi_array = np.array(mi_list)
for i in range(1,len(mi_array)):
    mi_list[i] = (1-mi_ma_rate) * mi_list[i-1] + mi_ma_rate * mi_list[i]

plt.axhline(mi,label='ground truth',linestyle='--',color='red')
plt.plot(mi_list, label='MI estimation')

for t in range(len(mi_array)):
    if (mi_list[t]>.9*mi):
        plt.axvline(t,label='90% reached Ground Truth',linestyle=':',color='green')
        break

plt.xlim((0,len(mi_array)))
plt.ylim((-mi,mi*2))
plt.title('GAN-MINEE')
plt.xlabel("number of iterations")
plt.ylabel("MI estimate")
plt.legend()

plt.show()
print('mi',mi)
```

```python

```


# A Primer on Python for Statistical Programming and Data Science

### Chris Fonnesbeck
Senior Quantitative Analyst, The New York Yankees

---

(via [xkcd](http://imgs.xkcd.com/comics/python.png))

## What is Python?

Python is a modern, open source, object-oriented programming language, created by a Dutch programmer, Guido van Rossum. Officially, it is an **interpreted** scripting language (meaning that it is not compiled until it is run) for the C programming language; in fact, Python itself is coded in C (though there are other non-C implementations). Frequently, it is compared to languages like Perl and Ruby. It offers the power and flexibility of lower level (*i.e.* compiled) languages, without the steep learning curve, and without most of the associated programming overhead. The language is very clean and readable, and it is available for almost every modern computing platform.

## Why use Python for scientific programming?

Python offers a number of advantages to scientists, both for experienced and novice programmers alike:

### Free and open

Python is released on all platforms under an open license (Python Software Foundation License), meaning that the language and its source is freely distributable. Not only does this keep costs down for scientists and universities operating  under a limited budget, but it also frees programmers from licensing concerns for any software they may develop. There is little reason to buy expensive licenses for software such as Matlab or Maple, when Python can provide the same functionality for free!

### Powerful and easy to use

Python is simultaneously powerful, flexible and easy to learn and use (in general, these qualities are traded off for a given programming language). Anything that can be coded in C, FORTRAN, or Java can be done in Python, almost always in fewer lines of code, and with fewer debugging headaches. Its standard library is extremely rich, including modules for string manipulation, regular expressions, file compression, mathematics, profiling and debugging (to name only a few). Unnecessary language constructs, such as `END` statements and brackets are absent, making the code terse, efficient, and easy to read. Finally, Python is object-oriented, which is an important programming paradigm particularly well-suited to scientific programming, which allows data structures to be abstracted in a natural way.

> Python is a language that is very powerful for developers, but is also accessible to Astronomers
-- Perry Greenfield

Dynamic typing:

```python
my_variable = 42
print(my_variable)
```

```python
my_variable = 'Welcome to ENAR'
print(my_variable)
```

Python's syntax is clean and easy to read, often described as **runnable pseudocode**.

#### Example: Numerical Integration

As an example, consider a Python implementation of the **trapezoidal rule**, a method from numerical analysis for approximating a definite integral. Specifically, it allows us to approximate:

$$\int_a^b f(x) dx$$

using the approximation:

$$\int_a^b f(x) dx \approx h \left[ \sum_{i=0}^n f(a + ih)\right]\frac{f(b) + f(a)}{2}$$

Here is a Python function that implements this approximation. Notice a few things:

- no brackets, only whitespace
- commenting with # symbols
- functions can be passed as arguments to other functions
- `for` loop can be expressed in a single line, using a **comprehension**

```python
def trapezoidal(func, from_a, to_b, n_intervals):

    # Calculate the width of each chunk
    int_width = (to_b - from_a) / n_intervals

    # Loop using a comprehension
    sum_y = sum(func(from_a + i*int_width) for i in range(n_intervals))

    sum_y += 0.5 * (func(from_a) + func(to_b))

    return sum_y * int_width
```

Now we can use the `trapezoidal` function to approximate a polynomial function, for example:

```python
def some_polynomial_function(x):
    return 2 * x**2 + 3 * x + 1
```

```python
trapezoidal(some_polynomial_function, 1, 5, 1000)
```

### Interactive

Python may be run interactively on the command line, in much the same way as Octave or R. Rather than compiling and running a particular program, commands may entered serially followed by the `Return` key. This is often useful for mathematical programming and debugging.

### Extensible

Python is often referred to as a “glue” language, meaning that it is a useful in a mixed-language environment. Frequently, programmers must interact with colleagues that operate in other programming languages, or use significant quantities of legacy code that would be problematic or expensive to re-code. Python was designed to interact with other programming languages, and in many cases C or FORTRAN code can be compiled directly into Python programs (using packages such as `f2py` or `cython`). Additionally, since Python is an interpreted language, it can sometimes be slow relative to its compiled cousins. In many cases this performance deficit is due to a short loop of code that runs thousands or millions of times. Such bottlenecks may be removed by coding a function in FORTRAN, C or Cython, and compiling it into a Python module.

For example, let's time our `trapezoidal` function to see how fast it runs:

```python
%timeit trapezoidal(some_polynomial_function, 1, 5, 1000)
```

If this function happened to be the bottleneck in our code, we could replace it with faster code using a different language, and call it from Python.

Let's look at an implementation in Cython, which is a Python-like language that allows Python programmers to write fast code without having to write C/C++/Fortran directly. It looks much like Python code, but with type declarations. Cython code is translated it to C (or C++ or others), which is then compiled to create a Python extension that we can import and use.

```python
%load_ext cython
```

```cython
%%cython

cdef inline double fast_polynomial(double x):
    return 2*x*x + 3*x + 1

cpdef trapezodial_fast(double a, double b, int n):
    cdef double h, x, sumy
    cdef int i
    h = (b-a)/n
    sumy = 0
    x=a
    for i in range(n):
        x += h
        sumy += fast_polynomial(x)
    sumy += 0.5*(fast_polynomial(a) + fast_polynomial(b))
    return sumy*h
```

Verify that it does the same thing:

```python
trapezodial_fast(1, 5, 10000)
```

Using Cython, we can achieve speedups of several orders of magnitude, often faster than hand-coded C code. In addtion, Cython is compatible with core scientific programming tools like NumPy and IPython.

Cython has built-in support for multicore processing.

Cython is used to varying degrees by other packages in the Python scientific stack, such as pandas, sympy, scikit-learn and SciPy.

```python
%timeit trapezodial_fast(1, 5, 1000)
```

The extensibility of Python makes it useful as a **glue language**, in mixed-language environments.

### Third-party modules

There is a vast body of Python modules created outside the auspices of the Python Software Foundation. These include utilities for database connectivity, mathematics, statistics, and charting/plotting. Some notables include:

* ***NumPy***: Numerical Python (NumPy) is a set of extensions that provides the ability to specify and manipulate array data structures. It provides array manipulation and computational capabilities similar to those found in Matlab or Octave.
* ***SciPy***: An open source library of scientific tools for Python, SciPy supplements the NumPy module. SciPy gathering a variety of high level science and engineering modules together as a single package. SciPy includes modules for graphics and plotting, optimization, integration, special functions, signal and image processing, genetic algorithms, ODE solvers, and others.
* ***Matplotlib***: Matplotlib is a python 2D plotting library which produces publication-quality figures in a variety of hardcopy formats and interactive environments across platforms. Its syntax is very similar to Matlab.
* ***Pandas***: A module that provides high-performance, easy-to-use data structures and data analysis tools. In particular, the `DataFrame` class is useful for spreadsheet-like representation and mannipulation of data. Also includes high-level plotting functionality.
* ***IPython***: An enhanced Python shell, designed to increase the efficiency and usability of coding, testing and debugging Python. It includes both a Qt-based console and an interactive HTML notebook interface, both of which feature multiline editing, interactive plotting and syntax highlighting.

(courtesy Jake Vanderplas)

```python
import sympy

xs = sympy.symbols('xs')

polynomial = 2*xs*xs + 3*xs + 1
```

```python
print(polynomial)
```

```python
integrated_polynomial = sympy.integrate(polynomial, (xs, 1, 5))
integrated_polynomial.evalf()
```

## Idiomatic Python

Effective Python programming involves applying particular Python **idioms** effectively; these are idiosyncratic expressions that may only exist in Python (if you are coming from another language), but when used appropriately they can make your code more readable, faster, or both. You have seen some of these already -- for example, the **comprehension** as a means for succinctly implementing a `for` loop.

### Comprehensions

```python
import numpy as np

def do_math(x):
    return 3 + x**3
```

```python
%%timeit
squares = np.empty(1000000)
for i in range(1000000):
    squares[i] = do_math(i)
```

```python
%timeit squares = [do_math(i) for i in range(1000000)]
```

Here, not only is the list comprehension easier to write and read, it is also slightly faster.

### Generators

When you are dealing with a large number of elements that you do not need all at once, you can also consider another Python expression: the **generator**. For example, if we enclose the comprehension in parentheses instead of square brackets, we get a **generator expression** object:

```python
(i**2 for i in range(int(1e20)))
```

Now, rather than storing 100,000,000,000,000,000,000,000 elements in memory, we can produce values as needed:

```python
squares = (i**2 for i in range(int(1e10)))
next(squares)
```

```python
next(squares)
```

### Built-in functions

Before you go about coding your own functions, make sure that it isn't already provided as a **built-in function**. These are typically highly optimized, and written in C! [Here is a list of built-in functions](https://docs.python.org/3/library/functions.html).

### String concatenation

Just as you should avoid growing lists or arrays by concatenation or appending, iterating over strings and concatenating them manually is very inefficient. For example, let's say we want to concatente a list of strings into a single string:

```python
words = ["Six",
"days",
"in",
"to",
"what",
"should",
"be",
"a",
"greatest",
"two",
"months",
"of",
"my",
"life",
"and",
"it’s",
"turned",
"in",
"to",
"a",
"nightmare"]
```

One might be tempted to code the following:

```python
sentence = ""
for word in words:
    sentence += " " + word
sentence
```

However, this is inefficient; since strings is immutable in Python, every `+` operation involves creating a new string and copying the old content. Instead, we can use the string method `join`, which is not only faster, but more flexible. Here, we would like to separate the words by spaces, which is easily done:

```python
' '.join(words)
```

### Avoid loops

As we have seen, `for` loops in Python are slow. Wherever possible, avoid looping by using alternative strategies or vectorized operations. For example, say we wanted to return the common elements between two arrays. We might naively loop over both lists, comparing them elementwise to return their intersection:

```python
np.random.seed(1)
list1 = np.random.choice(np.arange(20), replace=False, size=10)
list2 = np.random.choice(np.arange(20), replace=False, size=10)

def common_elements(a, b):
    for i in a:
        for j in b:
            if i==j:
                yield i
```

```python
%timeit list(common_elements(list1, list2))
```

However, this involves two Python `for` loops and a conditional statement. Instead, we can use set operations on the built-in `set` type provided by Python:

```python
%timeit set(list1) & set(list2)
```

###  Use NumPy

Often, considerable performance gains can be achieved by replacing Python data structures and functions with corresponding NumPy versions. It provides a high-performance multidimensional array object, and tools for working with these arrays.

This example, borrowed from NumPy creator Travis Oliphant, solves Laplace's equation over a 2-d rectangular grid using a simple iterative method. The code finds a two-dimensional function, u, where ∇2 u = 0, given some fixed boundary conditions.

```python
dx = 0.1
dy = 0.1
dx2 = dx*dx
dy2 = dy*dy

def py_update(u):
    nx, ny = u.shape
    for i in range(1,nx-1):
        for j in range(1, ny-1):
            u[i,j] = ((u[i+1, j] + u[i-1, j]) * dy2 +
                      (u[i, j+1] + u[i, j-1]) * dx2) / (2*(dx2+dy2))
```

```python
def calc(N, Niter=100, func=py_update, args=()):
    u = np.zeros([N, N])
    u[0] = 1
    for i in range(Niter):
        func(u,*args)
    return u
```

This code takes a very long time to run in order to converge to the correct solution. For a 100x100 grid, visually-indistinguishable convergence occurs after about 8000 iterations.

```python
%timeit calc(10)
```

Using NumPy, we can speed this code up significantly by using slicing and vectorized (automatic looping) calculations that replace the explicit loops in the Python-only solution.

```python
def num_update(u):
    u[1:-1,1:-1] = ((u[2:,1:-1]+u[:-2,1:-1])*dy2 +
                    (u[1:-1,2:] + u[1:-1,:-2])*dx2) / (2*(dx2+dy2))
```

```python
%timeit calc(10, func=num_update)
```

Such speed-ups are not uncommon when using NumPy to replace Python loops where the inner loop is doing simple math on basic data-types.

# Example: Survival Analysis

To give you a feel for what using Python for data analysis is like, let's dive right in and look at a real-world example. This is a survival analysis of mastectomy data, based on an example by Austing Rochford of the PyMC3 team.

## Loading libraries

We use the **import** statement to load non-core modules into our Python environment. Packages with long names can be **aliased** to shorter names.

```python
import numpy as np
import pandas as pd
import pymc3 as pm
from matplotlib import pyplot as plt
import seaborn as sns
```

Pandas offers functions for automating the import of common data formats, such as comma-separated values (csv). Once imported, the data are stored in a tabular `DataFrame` object.

```python
df = pd.read_csv('../data/mastectomy.csv')
df.head()
```

Pandas offers functions and methods for cleaning and processing the data; for example, we want to recode all of our variables as numeric types.

```python
df.event = df.event.astype(int)
df.metastized = (df.metastized == 'yes').astype(int)
n_patients = df.shape[0]
```

```python
df.head()
```

In addition, we can summarize our data.

```python
df.event.mean()
```

## Plotting Data

The mathematician Richard Hamming once said,

> The purpose of computing is insight, not numbers

and the best way to develop insight is often to visualize data. Visualization deserves an entire lecture (or course) of its own, but we can explore a few features of Python's `matplotlib` library here.

While there is no "official" plotting library, this package is the *de facto* standard.

> ## Some IPython magic
>
> If you're using an IPython / Jupyter notebook,
> you'll need to execute the following command
> in order for your matplotlib images to appear
> in the notebook:
>
> ```
> % matplotlib inline
> ```
>
> The `%` indicates an IPython **magic function** -
> a function that is only valid within the notebook environment.
> Note that you only have to execute this function once per notebook.

```python
%matplotlib inline
```

The following function plots the survival data, showing both the groups (metastized or not) and the events (or censoring).

The `subplots` function creates a set of axes and a figure into which it is embedded. The function proceeds by drawing lines (`hlines`) and points (`scatter`) onto the axes, then adding labels and customizing the axes to best display survival information.

```python
def plot_group_events(df, group_col, xlabel='', figsize=(8, 6)):

    fig, ax = plt.subplots(figsize=figsize)

    blue, _, green = sns.color_palette()[:3]

    patients = df.index.values

    ax.hlines(patients[df[group_col].values == 1], 0, df[df[group_col].values == 1].time,
              color=blue, label=group_col)

    ax.hlines(patients[df[group_col].values == 0], 0, df[df[group_col].values == 0].time,
              color=green, label='not ' + group_col)

    ax.scatter(df[df.event.values == 1].time, patients[df.event.values == 1],
               color='k', zorder=10, label='event')

    ax.set_xlim(left=0)
    ax.set_xlabel(xlabel)
    ax.set_yticks([])
    ax.set_ylabel('Subject')

    ax.set_ylim(-0.25, n_patients + 0.25)

    ax.legend(loc='center right')

    return fig, ax
```

```python
plot_group_events(df, 'metastized', 'Months since mastectomy');
```

#### Bayesian proportional hazards model

The two most basic estimators in survial analysis are the [Kaplan-Meier estimator](https://en.wikipedia.org/wiki/Kaplan%E2%80%93Meier_estimator) of the survival function and the [Nelson-Aalen estimator](https://en.wikipedia.org/wiki/Nelson%E2%80%93Aalen_estimator) of the cumulative hazard function.  However, since we want to understand the impact of metastization on survival time, a risk regression model is more appropriate.  Perhaps the most commonly used risk regression model is [Cox's proportional hazards model](https://en.wikipedia.org/wiki/Proportional_hazards_model).  In this model, if we have covariates $\mathbf{x}$ and regression coefficients $\beta$, the hazard rate is modeled as

$$\lambda(t) = \lambda_0(t) \exp(\mathbf{x} \beta).$$

Here $\lambda_0(t)$ is the baseline hazard, which is independent of the covariates $\mathbf{x}$.  In this example, the covariates are the one-dimensonal vector `df.metastized`.

In order to perform Bayesian inference with the Cox model, we reparameterize it using a method called the **Poisson trick**, which changes the analysis from using time-to-event data to using count data. This requires us to partition the time range in question into intervals with endpoints $0 \leq s_1 < s_2 < \cdots < s_N$. With this partition, $\lambda_0 (t) = \lambda_j$ if $s_j \leq t < s_{j + 1}$.  With $\lambda_0(t)$ constrained to have this form, all we need to do is choose priors for the $N - 1$ values $\lambda_j$.  We use independent vague priors $\lambda_j \sim \operatorname{HalfCauchy}(1).$  For our mastectomy example, we make each interval three months long.

We also must specify priors on $\beta$ and $\lambda_0(t)$.  We place a normal prior on $\beta$, $\beta \sim N(\mu_{\beta}, \sigma_{\beta}^2),$ where $\mu_{\beta} \sim N(0, 10^2)$ and $\sigma_{\beta} \sim U(0, 10)$. The likelihood for the count data is a Poisson sampling distribution.

We can use `NumPy`'s functions to help us restructure our data to use the Poisson model. First, we can create a set of discrete time intervals of 3 months in length, using the `arange` function:

```python
interval_length = 3
interval_bounds = np.arange(0, df.time.max() + interval_length + 1, interval_length)
n_intervals = interval_bounds.size - 1
intervals = np.arange(n_intervals)
```

Then, our event data becomes a grid of patients and intervals, with a one in cells that a death occurs, and zero otherwise.

```python
last_period = np.floor((df.time - 0.01) / interval_length).astype(int)

patients = df.index.values

death = np.zeros((n_patients, n_intervals))
death[patients, last_period] = df.event

metastized = df.metastized.values
```

```python
death
```

To account for censoring and discretization, we also create an exposure variable that contains the number of months (0-3) in each interval that the patient was exposed to the risk of death.

```python
exposure = np.greater_equal.outer(df.time, interval_bounds[:-1]) * interval_length
exposure[patients, last_period] = df.time - interval_bounds[last_period]
```

```python
exposure
```

We fit the model using PyMC3, which allows us to specify the model and use Markov chain Monte Carlo (MCMC) to estimate it.

```python
outer = pm.math.tt.outer

with pm.Model() as model:

    λ_0 = pm.HalfCauchy('λ_0', 1, shape=n_intervals)

    α = pm.Normal('α', 0, sd=5)
    β = pm.Normal('β', 0, sd=5)

    λ = outer(pm.math.exp(α + β * metastized), λ_0)
    μ = exposure * λ

    obs = pm.Poisson('obs', μ, observed=death)
```

```python
with model:

    trace = pm.sample(1000, tune=2000)
```

PyMC3 includes a suite of functions for summarizing output, both in graphical and tabular forms.

```python
pm.summary(trace, ['β']).round(2)
```

```python
pm.plot_posterior(trace, varnames=['β'], color='#87ceeb');
```

## Getting Python

How do you obtain and configure Python?

Python comes pre-installed on some systems, but I recommend using the [Anaconda](https://www.anaconda.com) distribution because it includes enhancements that make configuring and maintaining Python on your computer much easier. Anaconda is freely available from the [Anaconda download page](https://www.anaconda.com/distribution/#download-section).

Download the Python 3.7 installer for your system, and execute the file, following the on-screen instructions.

### System requirements

- Operating system: Windows 7 or newer, 64-bit macOS 10.10+, or Linux, including Ubuntu, RedHat, CentOS 6+, and others.
- If your operating system is older than what is currently supported, you can find older versions of the Anaconda installers in our archive that might work for you.
- System architecture: Windows- 64-bit x86, 32-bit x86; MacOS- 64-bit x86; Linux- 64-bit x86, 64-bit Power8/Power9.
- Minimum 5 GB disk space to download and install.

On Windows, macOS, and Linux, it is best to install Anaconda for the local user, which does not require administrator permissions and is the most robust type of installation.

During the installation, you will be asked if Anaconda should modify the PATH variable on your machine. It is recommended that you allow this, as it permits Anaconda to become the default Python installation on your machine, incase there are other versions already on your system.

Once installed, you can run Python from either the native terminal (macOS or Linux) or the Anaconda Prompt (Windows).

### conda

One of the great advantages to using the Anaconda Python distribution is the `conda` utility that is bundled with it. Conda is a powerful package manager and environment manager that you use with command line commands at the Anaconda Prompt for Windows, or in a terminal window for macOS or Linux.

For example, we can use it to install third-party packages via `conda install`:

Or it can be used to update packages that are already installed:

And if you are unsure about whether a package is available in the Conda repository, you can search for it:

### conda environments

Conda allows you to create separate environments containing files, packages and their dependencies that will not interact with other environments.

When you begin using conda, you already have a default environment named base. You don't want to put programs into your base environment, though. Create separate environments to keep your programs isolated from each other.

For example, we can create a new environment and install a package in it:

Conda will determine the dependencies of any package that you specify for installation, and install them as well.

(via [xkcd](https://xkcd.com/1987/))

Once created, Conda tells you how to activate the environment, via `conda activate`.

The repository for this tutorial contains a file called `environment.yml` that includes a list of all the packages used for the tutorial. If you run

    conda env create

in the directory containing `environment.yml` it will create the environment for you and install all of the packages listed.

---

## To Learn More

[Software Carpentry](http://software-carpentry.org/lessons.html) is a great place to learn Python if you are not already a programmer. Here are some additional resources:

- Bassi S (2007) [A Primer on Python for Life Science Researchers](http://www.ploscompbiol.org/article/info%3Adoi%2F10.1371%2Fjournal.pcbi.0030199). PLoS Comput Biol 3(11): e199
- [Scipy Lecture Notes](https://scipy-lectures.org/)
- [Python Data Science Handbook](https://www.amazon.com/Python-Data-Science-Handbook-Essential/dp/1491912057), by Jake Vanderplas
- [Data Science with Python](https://www.datacamp.com/tracks/data-scientist-with-python) from DataCamp


# Roll decay equation

```python
%matplotlib inline
%load_ext autoreload
%autoreload 2
```

```python
import sympy as sp
#from rolldecayestimators.symbols import *
#import rolldecayestimators.equations as equations
from rolldecayestimators.symbols import *
import rolldecayestimators.equations as equations
import rolldecay

import rolldecayestimators.direct_estimator as direct_estimator
import rolldecayestimators.direct_linear_estimator as direct_linear_estimator
import rolldecayestimators.direct_estimator_improved as direct_estimator_improved

from latex_helpers import pylatex_extenders
import latex_helpers
import os.path
from rigidbodysimulator.substitute_dynamic_symbols import substitute_dynamic_symbols, find_name, find_derivative_name, lambdify, find_derivatives
import dill

from sympy.physics.vector.printing import vpprint, vlatex
from IPython.display import display, Math, Latex
from rolldecay.paper_writing import generate_nomenclature
```

## Save equations

```python
equation_dict = {key:value for key,value in equations.__dict__.items() if isinstance(value, sp.Eq)}
for name,equation in equation_dict.items():
    file_path = os.path.join(rolldecay.equations_path,name)
    eq=pylatex_extenders.Equation(equation,label='eq:%s'%name)
    eq.generate_tex(file_path)

```

## Generate nomenclature

```python
latex_nomenclature = generate_nomenclature(paper_path=rolldecay.paper_path)
```

```python
nomenclature_file_path = os.path.join(rolldecay.paper_path, 'nomenclature.tex')
with open(nomenclature_file_path, mode='w') as file:
    file.write(latex_nomenclature)
```

The roll motion can be written as [Himeno]:

```python
Math(vlatex(equations.roll_equation_himeno))
```

The equation express the roll moment (Nm) along a longitudinal axis though the centre of gravity.
Where $A_{44}$ is the virtual mass moment of intertia, $B_{44}$ is the roll damping moment and $C_{44}$ is the restoring moment. $M_{44}$ represents the external moment (usually moment from external waves).

The roll damping moment $B_{44}$ is the primary interest in this paper. The $B_{44}$ is determined using model scale roll decay tests. $B_{44}$ is determined using system identification, by finding the best fit to the following equation:

```python
Math(vlatex(equations.roll_decay_equation_general_himeno))
```

The external moment is zero during a roll decay test, since there are no external forces present.

The $B_{44}$ can be expressed as a series expansion:
$ B_{44} = B_1\cdot\dot{\phi} + B_2\cdot\dot{\phi}\left|\dot{\phi}\right| + B_3\cdot\dot{\phi}^3 + ...$

Truncating this series at the cubic term gives a "cubic damping model":

```python
Math(vlatex(equations.b44_cubic_equation))
```

Truncating this series at the quadratic term gives a "quadratic damping model":

```python
Math(vlatex(equations.b44_quadratic_equation))
```

Truncating this series at the linear term gives a "linear damping model":

```python
Math(vlatex(equations.b44_quadratic_equation))
```

Assuming quadratic damping the roll decay equation is written:

```python
Math(vlatex(equations.roll_decay_equation_himeno_quadratic))
```

The restoring moment can be expressed using the $GZ$ curve:

```python
Math(vlatex(equations.restoring_equation))
```

where $m$ is the mass of the ship

It is common that the restoring moment is linearized using the meta centric height $GM$:

```python
Math(vlatex(equations.restoring_equation_linear))
```

```python
Math(vlatex(equations.restoring_equation_cubic))
```

Introducing a helper coefficient $C$:

```python
Math(vlatex(equations.C_equation))
```

In the case of linearized restoring moment $C$ can be written as:

```python
equations.C_equation_linear
```

```python
equations.C_equation_cubic
```

```python
Math(vlatex(equations.roll_decay_equation_himeno_quadratic_c))
```

It is common to rewrite the roll decay equation by dividing with $A_{44}$ and conducting the following substitutions:

```python
equations.zeta_equation
```

```python
equations.d_equation
```

```python
Math(vlatex(equations.omega0_equation))
```

Commonly used quadratic roll decay equation:

```python
Math(vlatex(equations.roll_decay_equation_quadratic))
```

And linear roll decay equation is obtained when $d=0$

```python
Math(vlatex(equations.roll_decay_equation_linear))
```

## Dimensional cubic model:

```python
Math(vlatex(equations.roll_decay_equation_cubic))
```

This one has infinite number of solutions. Consider to multiply all coefficients in a sultions with for isntance 2 and the equation would sum to 0 still and thereby be another valid solution. Normalizeing with $A_44$ should give only one unique solution.

```python
for _,eq in equations.normalize_equations.items():
    display(Math(vlatex(eq)))
```

```python
Math(vlatex(equations.roll_decay_equation_cubic_A))
```

```python
Math(vlatex(sp.solve(equations.roll_decay_equation_cubic_A,phi_dot_dot)))
```

```python
acceleration = sp.solve(equations.roll_decay_equation_cubic_A,phi_dot_dot)[0]
calculate_acceleration = lambdify(acceleration)
```

```python
calculate_acceleration
```

```python
parameters={
        'B_1A':0.3,
        'B_2A':0.0,
        'B_3A':0.0,
        'C_1A':0.3,
        'C_3A':0.0,
        'C_5A':0.0,
    }

calculate_acceleration(**parameters,phi=1.0,phi1d=0)
```

```python
equations.omega_hat_equation
```

```python
sp.solve(equations.omega_hat_equation,omega)[0]
```

```python
equations.roll_decay_equation_himeno_linear
```

```python
equations.C_equation_linear
```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```


# 11 Ordinary differential equations – Integrators (Student's notebook)

*This notebook contains incomplete code that you will complete during class.* Copy the notebook and the file `integrators.py` to your work directory and work on the copies. The complete notebook and file will be posted after class.

More about integrators to solve ODEs.

* Euler's Rule (and it's shortcomings)
* Runge-Kutta methods

## Euler's Rule

From the last lecture: Given the $n$-dimensional vectors from the ODE standard form

$$
\frac{d\mathbf{y}}{dt} = \mathbf{f}(t, \mathbf{y})
$$
the **Euler rule** amounts to

\begin{align}
\mathbf{f}(t, \mathbf{y}) = \frac{d\mathbf{y}(t)}{dt} &\approx \frac{\mathbf{y}(t_{n+1}) - \mathbf{y}(t_n)}{h}\\
\mathbf{y}_{n+1} &\approx \mathbf{y}_n + h \mathbf{f}(t_n, \mathbf{y}_n) \quad \text{with} \quad \mathbf{y}_n := \mathbf{y}(t_n)
\end{align}

Euler's rule cannot obtain high precision without very small $h$ but this leads to higher round-off error accumulation.

### Euler's Rule applied to the 1D harmonic oscillator (theory)

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}_0 &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=   - \frac{k}{m} y^{(0)},
    &\quad y^{(1)}_0 &= v_0.
\end{alignat}

with $k=1$; $x_0 = 0$ and $v_0 = +1$.

First step from $t=0$ to $t=h$:

\begin{align}
y^{(0)}_1 &= y^{(0)}_0 + h y^{(1)}_0 = x_0 + v_0 h\\
y^{(1)}_1 &= y^{(1)}_0 + h \left(-\frac{k}{m}y^{(0)}_0\right) = v_0 + \frac{-k x_0}{m} h
\end{align}

Compare to the equations of movement under a constant force (*ballistic motion*):

$$
x = x_0 + v_0 h + \frac{1}{2} a h^2, \quad v = v_0 + a h
$$

In Euler, the position does not even contain the $h^2$ term, i.e., the acceleration only contributes *indirectly* via the velocity of the *next* step. (Remember: Euler's error is $\mathcal{O}(h^2)$!)

### Euler's Rule: Application to 3 oscillators (numerical)
`git pull` new resources.

Put the notebook `11-ODE_integrators-students.ipynb` *and* the module `integrators.py` into the same directory, for instance a work directory `~/PHY494/11_ODEs`.

Open [integrators.py](https://github.com/ASU-CompMethodsPhysics-PHY494/PHY494-resources/blob/master/08_ODEs/integrators.py) to have a look at it: it contains **incomplete** code to integrate Newton's equations of motions for different forces and with different integrators (based on the work done in the last lecture).

#### Problem: Add forces and potential energies to `integrators.py`
You need to complete the functions `F_anharmonic()` and `F_power()`.

Then follow the notebook to integrate the equations of motions with the Euler integrator and plot the trajectories $x(t)$ for the three oscillators.

```python
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
matplotlib.style.use('ggplot')

import integrators
```

```python
t_max = 20
t_harm, y_harm = integrators.integrate_newton(t_max=t_max)
t_anharm, y_anharm = integrators.integrate_newton(t_max=t_max,
                                                  force=integrators.F_anharmonic)
t_power, y_power = integrators.integrate_newton(t_max=t_max,
                                                force=integrators.F_power)
```

```python
plt.plot(t_harm, y_harm[:, 0], label=r"harmonic $\frac{k}{2} x^2$")
plt.plot(t_anharm, y_anharm[:, 0], label=r"anharmonic $\frac{k}{2} x^2(1 - \frac{2}{3} \alpha x)$")
plt.plot(t_power, y_power[:, 0], label=r"power $\frac{k}{6} x^6$")
plt.legend(loc="best")
```

#### Phase-space portrait
Plot position against momentum ($x, p$):
* Momentum is $p = mv$.
* Note that `integrators.integrate_newton()` returns an array of the $N$ time points and a $(N, 2)$ array of the positions and velocities.

```python
m = 1  # mass
# replace None with code
x = None
v = None
p = None
ax = plt.subplot(1,1,1)
ax.plot(x, p)
ax.set_aspect(1)
```

Phase space portrait for different time steps $h$:

```python
t, y = integrators.integrate_newton(t_max=40,
                                    force=integrators.F_harmonic,
                                    h=0.01)
t2, y2 = integrators.integrate_newton(t_max=40, h=0.001,
                                      force=integrators.F_harmonic)
```

```python
ax = plt.subplot(1,1,1)
ax.plot(y[:, 0], m * y[:, 1], 'b-', label=r"$h=0.01$")
ax.plot(y2[:, 0], m * y2[:, 1], 'r--', label="$h=0.001$")
ax.set_xlabel("position $x$")
ax.set_ylabel("momentum $p$")
ax.set_aspect(1)
ax.legend(loc="best")
```

#### Energy
Energy is conserved: stringent test for the integrator!

$$
E = T_\text{kin} + U\\
\frac{dE}{dt} = 0
$$

with $T_\text{kin} = \frac{1}{2} m v^2$.

Calculate and plot the three energy terms:

```python
KE = None
PE = None
energy = None
```

##### Quantify energy drift
Measure the relative departure from energy conservation

$$
\epsilon_E(t) = \log_{10}\left|\frac{E(t) - E(t=0)}{E(t=0)}\right|
$$

(By using the decadic logarithm, $-\epsilon_E$ is a direct measure of the precision.)

###### Harmonic oscillator
Analyze energy drift for the last harmonic oscillator example ($h=0.01$):

```python
energy_precision = np.log10(energy/energy[0] - 1)
plt.plot(t, energy_precision)
plt.ylabel("log(relative error in $E$)")
plt.xlabel("time")
```

As we already know, with such a large time step, energy conservation is very bad with the *Euler* integrator: The relative (!) error quickly becomes 100% (after $t=200$).

##### Code re-use: energy analysis as a function
The module `integrators` contains the function `integrators.analyze_energies(t, y, U)` to automate the task of analysing the energy. You *must* provide the appropriate potential energy function `U`.

##### Problem: Make `analyze_energies()` work and analyze the power law oscillator
The code is *incomplete*.
1. Open `integrators.py` in your editor.
2. Complete the `kinetic_energy()` function (remember: $T_\text{kin} = \frac{1}{2} m v^2$)

Then `reload()` the module:

```python
# when working on the integrator module you need to reload() whenever you make changes
from importlib import reload   # for Python 3
reload(integrators);
```

and try it out for power law oscillator with a large time step $h = 0.01$. Can you find a value of $h$ that will give you energy precision to 4th decimal over 40 time units?

###### Energy stability analysis of the 6-th power oscillator
$x^6$ potential with $h=0.01$:

```python
t, y = integrators.integrate_newton(t_max=40, force=integrators.F_power,
                                    h=0.01)
```

```python
integrators.analyze_energies(t, y, integrators.U_power)
```

Trying 100 times smaller $h$ (takes 100 times longer!!)

```python
t, y = integrators.integrate_newton(t_max=40, force=integrators.F_power,
                                    h=0.0001)
integrators.analyze_energies(t, y, integrators.U_power)
```

*Reducing $h$ further does not seem to help and takes order of magnitude longer to run*. **We need a better integrator!**

## Runge-Kutta methods
Explicit RK methods propagate with the general form

$$
y_{n+1} = y_n + h \sum_{i=1}^s b_i k_i, \quad t_{n+1} = t_n + h
$$

where $s$ is the number of *stages* and the coefficients $a_{ij}$, $b_i$ and $c_i$ define the method.

\begin{align}
k_1 &= f(t_n, y_n)\\
k_2 &= f(t_n + c_2 h, y_n + h(a_{21} k_1))\\
k_3 &= f(t_n + c_3 h, y_n + h(a_{31} k_1 + a_{32} k_2))\\
 \vdots& \\
k_s &= f(t_n + c_s h, y_n + h(a_{s1} k_1 + a_{s2} k_2 + \cdots + a_{s,s-1} k_{s-1})).
\end{align}

Basic idea: improve estimate for the slope by using weighted averages of slopes over the interval.

### RK4
General purpose ODE solver.

$$
y_{n+1} = y_n + \frac{h}{6}\Big(k_1 + 2 k_2 + 2 k_3 + k_4\Big), \quad t_{n+1} = t_n + h
$$

and

\begin{align}
k_1 &= f(t_n, y_n)\\
k_2 &= f(t_n + \frac{1}{2} h, y_n + \frac{1}{2} h k_1)\\
k_3 &= f(t_n + \frac{1}{2} h, y_n + \frac{1}{2} h k_2)\\
k_4 &= f(t_n + h, y_n + h k_3)
\end{align}

`rk4` can obtain $\mathcal{O}(h^4)$ precision.

### RK2
`rk2` is simpler to derive and understand than `rk4` (which we will use more often).

Start with the exact solution

$$
\frac{dy}{dt} = f(t, y) \quad\text{and}\quad y(t) = \int f(t, y)dt\\
y_{n+1} = y_n + \int_{t_n}^{t_{n+1}} f(t, y)dt
$$

Expand $f(t, y)$ around *midpoint* $t_{n+1/2} = t + \frac{h}{2}$ of integration interval

\begin{gather}
f(t, y) \approx f(t_{n+1/2}, y_{n+1/2}) + (t - t_{n+1/2})\,  f^{(1)}(t_{n+1/2}) +
   \mathcal{O}(h^2)
\end{gather}

\begin{align}
\int_{t_n}^{t_{n+1}} f(t, y) &\approx h\, f(t_{n+1/2}, y_{n+1/2}) + \mathcal{O}(h^3)\\
y_{n+1} &= y_n + h\, f(t_{n+1/2}, y_{n+1/2})
\end{align}

I.e., use slope at midpoint! (The $\mathcal{O}(h^2)$ term in the integral is zero because $[(h/2)^2 - (h/2)^2] = 0$.)

But we don't have $y_{n+1/2}$ so we use Euler's rule to estimate it to $\mathcal{O}(h^2)$:

$$
y_{n+1/2} \approx y_n + \frac{1}{2} h\, f(t_n, y_n)
$$

and insert into $y_{n+1} = y_n + h\, f(t_{n+1/2}, y_{n+1/2})$ (so it is still $\mathcal{O}(h^3)$ accurate).

Altogether: **RK2** algorithm

$$
\mathbf{y}_{n+1} = \mathbf{y}_n + h \mathbf{k}_2, \quad t_{n+1} = t_n + h
$$

and

\begin{align}
\mathbf{k}_1 &= \mathbf{f}(t_n, \mathbf{y}_n)\\
\mathbf{k}_2 &= \mathbf{f}(t_n + \frac{1}{2} h, \mathbf{y}_n + \frac{1}{2} h \mathbf{k}_1)
\end{align}

with $\mathcal{O}(h^3)$ precision.

#### RK2 applied to the 1D harmonic oscillator (theory)

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}_0 &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=   - \frac{k}{m} y^{(0)},
    &\quad y^{(1)}_0 &= v_0.
\end{alignat}

with $k=1$; $x_0 = 0$ and $v_0 = +1$.

First step from $t=0$ to $t=h$:

\begin{align}
  y^{(0)}_{1} &=
    y^{(0)} _{0} + hf^{(0)} \Big(\frac{h}{2}, y^{(0)}_{0} + hk_{1} \Big)\\
    &= x_{0} + h\left[v_{0} + \frac{h}{2} \left(-\frac{k}{m} x_{0}\right)  \right]\\
    &= x_0 + v_0 h + \frac{1}{2} \left(-\frac{k}{m} x_{0}\right) h^2\\
  y^{(1)}_{1} &= y^{(1)}_{0} +
                h f^{(1)}\left(\frac{h}{2},
                     \mathbf{y}_{0} + \frac{h}{2} \mathbf{f}(0, \mathbf{y}_{0})\right) \\
             &= v_{0} + \frac{h}{m} \left[-k\left(x_{0} - \frac{h}{2} k x_{0}\right)\right]
\end{align}

Note that `rk2` contains a $h^2$ term in the position update.

#### RK2 applied in practice
Let's implement `rk2`

\begin{align}
\mathbf{y}_{n+1} &= \mathbf{y}_n + h \mathbf{k}_2\\
\mathbf{k}_1 &= \mathbf{f}(t_n, \mathbf{y}_n)\\
\mathbf{k}_2 &= \mathbf{f}(t_n + \frac{1}{2} h, \mathbf{y}_n + \frac{1}{2} h \mathbf{k}_1)
\end{align}

in our `integrators.py`.

```python
from importlib import reload
reload(integrators);
```

##### RK2 for the power potential

```python
t, y = integrators.integrate_newton(t_max=40, force=integrators.F_power,
                                    integrator=integrators.rk2, h=0.01)
```

###### phase-space portrait

```python
m = 1
plt.plot(y[:, 0], m*y[:, 1])
```

###### energy drift

```python
integrators.analyze_energies(t, y, integrators.U_power)
```

#### RK4 applied in practice
Implement `rk4` in `integrator.py`:

\begin{align}
y_{n+1} &= y_n + \frac{h}{6}\Big(k_1 + 2 k_2 + 2 k_3 + k_4\Big)\\
k_1 &= f(t_n, y_n)\\
k_2 &= f(t_n + \frac{1}{2} h, y_n + \frac{1}{2} h k_1)\\
k_3 &= f(t_n + \frac{1}{2} h, y_n + \frac{1}{2} h k_2)\\
k_4 &= f(t_n + h, y_n + h k_3)
\end{align}

```python
reload(integrators)
```

and test the 6th power potential again. What level of energy precision can you obtain over 40 time units?

##### RK4 for the 6th power potential
Note that we can easily run `rk4` with a $h=0.01$!

```python
t, y = integrators.integrate_newton(t_max=40, force=integrators.F_power,
                                    integrator=integrators.rk4, h=0.01)
```

###### phase-space protrait

```python
m = 1
plt.plot(y[:, 0], m*y[:, 1])
```

###### energy drift

```python
integrators.analyze_energies(t, y, integrators.U_power)
```

You should be able to get 8 - 11 decimals precision from `rk4` with a smaller $h$.

##### High precision RK4 for the 6th power potential
Try a 10 times smaller step $h=0.001$

```python

```


$$Exercise: 2$$

$Q:1 \\
Let,\; x_1, \;x_2, \;x_3,\; x_4,\; x_5\; denote\; the \;number \;of \;different \;types \;of \;media\; slots\; (assumed\; to\; be \;nonnegative \;integers)\; to\; be\; used\; for \;advertisement, \;where\; x_1 \;corresponds \;to \;website, \;x_2 \;corresponds\;to \;TV, \;x_3 \;corresponds \;to\; daily \;newspaper, \;x_4 \;corresponds \;to\; weekly \;magazine\; and \;x_5 \;corresponds \;to\; monthly \;magazine \;and \;hence,\; they\; are\; nonnegetive\; integers. \\
Let\; z_1 \;denote\; the\; total\; number\; of\; media \;slots\; used \;for \;website \;and \;TV \;and\; let \;z_2 \;denote\; the \;total\; number\; of \;media\; slots\; used \;for\; other \;mediums. \;Therefore,\;z_1=x_1+x_2\; and \;z_2=x_3+x_4+x_5 \\
According\; to\; the \;question,\; |z_1-z_2| \leq18 \\
Hence,\; the\; optimization\; problem \;is\; as \;follows: $
\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000, \\
| x_3+x_4+x_5-x_1-x_2| \leq18 \\
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}. \\
\end{align} \\
$The\; above \;resultant\; model \;is\; not \;a \;linear\; program,\; because \;the\; constraint\; we \;just \;added\; contains \;modulus\; function \;which \;is\; a \;piecewise \;fuinction \;i.e.\;nonlinear.\; Therefore, \;the\; model \;itself\; is\; not\; be \;a\; linear\; program. $

$Q:2 \\
Approach \;1: \\
We \;take\; a \;new\; variable \;u = |x_3+x_4+x_5-x_1-x_2| \\
Therefore,\;  u \geq 0 \\
Hence,\; the\; optimization\; problem\; will \;be \;as \;follows:$ \\
\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000, \\
x_3+x_4+x_5-x_1-x_2 \leq u \\
x_1+x_2-x_3-x_4-x_5 \leq u \\
0 \leq u \leq18 \\
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}. \\
\end{align} \\

$Approach\; 2: \\
In \;this\; approach,\; we\; substitute \;|x_3+x_4+x_5-x_1-x_2| = |a-b|, \ a\geq 0, b\geq 0 \\
Hence,\; the \;optimization \;problem \;will\; be\; as\; follows: $ \\
\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000, \\
x_3+x_4+x_5-x_1-x_2 = a - b \\
a + b \leq 18 \\
a \geq 0 , \; b \geq 0;
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}. \\
\end{align} \\

$Q:3 \\
Using \;the \;code \;used \;in \;Lab \;4 \;practice\; session\; and \;modifying\; it \;so \;that \;we\; can \;solve\; the\;new \;linear\;programs \;resulting \;from \;Approach \;1 \;and\; Approach \;2.$

```python
!pip install -q pyomo
from pyomo.environ import *
import numpy as np
import pandas as pd
```

         |████████████████████████████████| 9.1 MB 18.9 MB/s
         |████████████████████████████████| 49 kB 6.6 MB/s

```python
data_csvfile = pd.read_csv('file32.csv')
print (type(data_csvfile))
```

    <class 'pandas.core.frame.DataFrame'>

```python
model_lab6_ex2 = ConcreteModel()
```

```python
N = len(data_csvfile.index)
col_indices = range(N)
M = 5
model_lab6_ex2.x = Var(col_indices, domain=NonNegativeIntegers)
model_lab6_ex2.u = Var(domain=NonNegativeIntegers, bounds=(0,18))
```

```python
model_lab6_ex2.objective = Objective(expr=summation(data_csvfile['Reach Quality'], model_lab6_ex2.x),sense=maximize)
```

```python
model_lab6_ex2.constraints = ConstraintList()
```

```python
model_lab6_ex2.constraints.add(expr= model_lab6_ex2.x[0] + model_lab6_ex2.x[1] + model_lab6_ex2.x[2]<= 75  )
model_lab6_ex2.constraints.add(expr= model_lab6_ex2.x[2] + model_lab6_ex2.x[3] +  model_lab6_ex2.x[4] <= 100  )
model_lab6_ex2.constraints.add(expr= model_lab6_ex2.x[0] + model_lab6_ex2.x[1] >= 10  )
model_lab6_ex2.constraints.add(expr=model_lab6_ex2.x[4]+model_lab6_ex2.x[3]+model_lab6_ex2.x[2]-model_lab6_ex2.x[0]-model_lab6_ex2.x[1]<=model_lab6_ex2.u)
model_lab6_ex2.constraints.add(expr=-model_lab6_ex2.x[4]-model_lab6_ex2.x[3]-model_lab6_ex2.x[2]+model_lab6_ex2.x[0]+model_lab6_ex2.x[1]<=model_lab6_ex2.u)
```

    <pyomo.core.base.constraint._GeneralConstraintData at 0x7f7c27266c20>

```python
model_lab6_ex2.constraints.add(summation(data_csvfile['Cost per advt'], model_lab6_ex2.x) <= 1500000)
```

    <pyomo.core.base.constraint._GeneralConstraintData at 0x7f7c27229f30>

```python
model_lab6_ex2.constraints.add(summation(data_csvfile['Num Customers'], model_lab6_ex2.x) >= 900000)
```

    <pyomo.core.base.constraint._GeneralConstraintData at 0x7f7c272291a0>

```python
lower_bound = 0
upper_bound = np.inf
for j in col_indices:
  model_lab6_ex2.x[j].setlb(lower_bound)
  model_lab6_ex2.x[j].setub(upper_bound)
```

```python
model_lab6_ex2.pprint()
```

    2 Set Declarations
        constraints_index : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    7 : {1, 2, 3, 4, 5, 6, 7}
        x_index : Size=1, Index=None, Ordered=False
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    5 : {0, 1, 2, 3, 4}

    2 Var Declarations
        u : Size=1, Index=None
            Key  : Lower : Value : Upper : Fixed : Stale : Domain
            None :     0 :  None :    18 : False :  True : NonNegativeIntegers
        x : Size=5, Index=x_index
            Key : Lower : Value : Upper : Fixed : Stale : Domain
              0 :     0 :  None :   inf : False :  True : NonNegativeIntegers
              1 :     0 :  None :   inf : False :  True : NonNegativeIntegers
              2 :     0 :  None :   inf : False :  True : NonNegativeIntegers
              3 :     0 :  None :   inf : False :  True : NonNegativeIntegers
              4 :     0 :  None :   inf : False :  True : NonNegativeIntegers

    1 Objective Declarations
        objective : Size=1, Index=None, Active=True
            Key  : Active : Sense    : Expression
            None :   True : maximize : 35*x[0] + 57*x[1] + 48*x[2] + 20*x[3] + 15*x[4]

    1 Constraint Declarations
        constraints : Size=7, Index=constraints_index, Active=True
            Key : Lower    : Body                                                              : Upper     : Active
              1 :     -Inf :                                                x[0] + x[1] + x[2] :      75.0 :   True
              2 :     -Inf :                                                x[2] + x[3] + x[4] :     100.0 :   True
              3 :     10.0 :                                                       x[0] + x[1] :      +Inf :   True
              4 :     -Inf :                              x[4] + x[3] + x[2] - x[0] - x[1] - u :       0.0 :   True
              5 :     -Inf :                            - x[4] - x[3] - x[2] + x[0] + x[1] - u :       0.0 :   True
              6 :     -Inf :      30000*x[0] + 95000*x[1] + 10000*x[2] + 5000*x[3] + 4000*x[4] : 1500000.0 :   True
              7 : 900000.0 : 100000*x[0] + 500000*x[1] + 200000*x[2] + 45000*x[3] + 25000*x[4] :      +Inf :   True

    6 Declarations: x_index x u objective constraints_index constraints

```python
!apt-get install -y -qq coinor-cbc
opt_cbc = SolverFactory('cbc')
```

    Selecting previously unselected package coinor-libcoinutils3v5.
    (Reading database ... 148492 files and directories currently installed.)
    Preparing to unpack .../0-coinor-libcoinutils3v5_2.10.14+repack1-1_amd64.deb ...
    Unpacking coinor-libcoinutils3v5 (2.10.14+repack1-1) ...
    Selecting previously unselected package coinor-libosi1v5.
    Preparing to unpack .../1-coinor-libosi1v5_0.107.9+repack1-1_amd64.deb ...
    Unpacking coinor-libosi1v5 (0.107.9+repack1-1) ...
    Selecting previously unselected package coinor-libclp1.
    Preparing to unpack .../2-coinor-libclp1_1.16.11+repack1-1_amd64.deb ...
    Unpacking coinor-libclp1 (1.16.11+repack1-1) ...
    Selecting previously unselected package coinor-libcgl1.
    Preparing to unpack .../3-coinor-libcgl1_0.59.10+repack1-1_amd64.deb ...
    Unpacking coinor-libcgl1 (0.59.10+repack1-1) ...
    Selecting previously unselected package coinor-libcbc3.
    Preparing to unpack .../4-coinor-libcbc3_2.9.9+repack1-1_amd64.deb ...
    Unpacking coinor-libcbc3 (2.9.9+repack1-1) ...
    Selecting previously unselected package coinor-cbc.
    Preparing to unpack .../5-coinor-cbc_2.9.9+repack1-1_amd64.deb ...
    Unpacking coinor-cbc (2.9.9+repack1-1) ...
    Setting up coinor-libcoinutils3v5 (2.10.14+repack1-1) ...
    Setting up coinor-libosi1v5 (0.107.9+repack1-1) ...
    Setting up coinor-libclp1 (1.16.11+repack1-1) ...
    Setting up coinor-libcgl1 (0.59.10+repack1-1) ...
    Setting up coinor-libcbc3 (2.9.9+repack1-1) ...
    Setting up coinor-cbc (2.9.9+repack1-1) ...
    Processing triggers for man-db (2.8.3-2ubuntu0.1) ...
    Processing triggers for libc-bin (2.27-3ubuntu1.2) ...
    /sbin/ldconfig.real: /usr/local/lib/python3.7/dist-packages/ideep4py/lib/libmkldnn.so.0 is not a symbolic link

$Q:\;4 -(i)\\
Solving \;the \;respective \;linear\; program \;obtained \;from \;Approach\; 1\; and\;reporting\; the \;solver \;status\; and \;termination\; conditions.$

```python
result = opt_cbc.solve(model_lab6_ex2)
print('Solver status:', result.solver.status)
print('Solver termination condition:',result.solver.termination_condition)
```

    Solver status: ok
    Solver termination condition: optimal

$Since, \;the \;solver \;status\; is \;optimal, \;therefore \;no \;remedy \;measures\; is\; needed.$

$Q:\;5-(i) \\
Reporting\; the\; optimal\; objective \;function\; value,\; optimal \;values \;of\; the\; original \;decision\; variables\;and \;the\; new\; decision\; variables \;and \;the\;constraint \;activities \;in\; Approach\; 1$

```python
print('\nOptimal objective function value = ', model_lab6_ex2.objective())

print('\nOptimal values of the decision Variables:')
for i in col_indices:
  print('Num slots for medium ',data_csvfile['Medium'][i],' : ', model_lab6_ex2.x[i].value)

print('\nConstraints')
model_lab6_ex2.constraints.display()
```

    Optimal objective function value =  3378.0

    Optimal values of the decision Variables:
    Num slots for medium  Websites  :  34.0
    Num slots for medium  TV  :  0.0
    Num slots for medium  DailyNewspaper  :  41.0
    Num slots for medium  WeeklyMagazine  :  11.0
    Num slots for medium  MonthlyMagazine  :  0.0

    Constraints
    constraints : Size=7
        Key : Lower    : Body       : Upper
          1 :     None :       75.0 :      75.0
          2 :     None :       52.0 :     100.0
          3 :     10.0 :       34.0 :      None
          4 :     None :        0.0 :       0.0
          5 :     None :      -36.0 :       0.0
          6 :     None :  1485000.0 : 1500000.0
          7 : 900000.0 : 12095000.0 :      None

$Modifying\; again:$

```python
model_lab6_ex2.constraints[4].deactivate()
model_lab6_ex2.constraints[5].deactivate()
```

```python
model_lab6_ex2.reconstruct
model_lab6_ex2.y = Var(range(0,2), domain=NonNegativeIntegers)
```

```python
model_lab6_ex2.constraints.add(expr=model_lab6_ex2.x[4]+model_lab6_ex2.x[3]+model_lab6_ex2.x[2]-model_lab6_ex2.x[0]-model_lab6_ex2.x[1]<=model_lab6_ex2.y[0]-model_lab6_ex2.y[1])
model_lab6_ex2.constraints.add(expr=model_lab6_ex2.y[0]+model_lab6_ex2.y[1]<=18)
```

    <pyomo.core.base.constraint._GeneralConstraintData at 0x7f7c27229fa0>

```python
model_lab6_ex2.pprint()
```

    3 Set Declarations
        constraints_index : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    9 : {1, 2, 3, 4, 5, 6, 7, 8, 9}
        x_index : Size=1, Index=None, Ordered=False
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    5 : {0, 1, 2, 3, 4}
        y_index : Size=1, Index=None, Ordered=False
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    2 : {0, 1}

    3 Var Declarations
        u : Size=1, Index=None
            Key  : Lower : Value : Upper : Fixed : Stale : Domain
            None :     0 :  18.0 :    18 : False : False : NonNegativeIntegers
        x : Size=5, Index=x_index
            Key : Lower : Value : Upper : Fixed : Stale : Domain
              0 :     0 :  34.0 :   inf : False : False : NonNegativeIntegers
              1 :     0 :   0.0 :   inf : False : False : NonNegativeIntegers
              2 :     0 :  41.0 :   inf : False : False : NonNegativeIntegers
              3 :     0 :  11.0 :   inf : False : False : NonNegativeIntegers
              4 :     0 :   0.0 :   inf : False : False : NonNegativeIntegers
        y : Size=2, Index=y_index
            Key : Lower : Value : Upper : Fixed : Stale : Domain
              0 :     0 :  None :  None : False :  True : NonNegativeIntegers
              1 :     0 :  None :  None : False :  True : NonNegativeIntegers

    1 Objective Declarations
        objective : Size=1, Index=None, Active=True
            Key  : Active : Sense    : Expression
            None :   True : maximize : 35*x[0] + 57*x[1] + 48*x[2] + 20*x[3] + 15*x[4]

    1 Constraint Declarations
        constraints : Size=9, Index=constraints_index, Active=True
            Key : Lower    : Body                                                              : Upper     : Active
              1 :     -Inf :                                                x[0] + x[1] + x[2] :      75.0 :   True
              2 :     -Inf :                                                x[2] + x[3] + x[4] :     100.0 :   True
              3 :     10.0 :                                                       x[0] + x[1] :      +Inf :   True
              4 :     -Inf :                              x[4] + x[3] + x[2] - x[0] - x[1] - u :       0.0 :  False
              5 :     -Inf :                            - x[4] - x[3] - x[2] + x[0] + x[1] - u :       0.0 :  False
              6 :     -Inf :      30000*x[0] + 95000*x[1] + 10000*x[2] + 5000*x[3] + 4000*x[4] : 1500000.0 :   True
              7 : 900000.0 : 100000*x[0] + 500000*x[1] + 200000*x[2] + 45000*x[3] + 25000*x[4] :      +Inf :   True
              8 :     -Inf :                  x[4] + x[3] + x[2] - x[0] - x[1] - (y[0] - y[1]) :       0.0 :   True
              9 :     -Inf :                                                       y[0] + y[1] :      18.0 :   True

    8 Declarations: x_index x u objective constraints_index constraints y_index y

$Q:\;4 -(ii)\\
Solving \;the \;respective \;linear\; program \;obtained \;from \;Approach\; \; and\;reporting\; the \;solver \;status\; and \;termination\; conditions.$

```python
result = opt_cbc.solve(model_lab6_ex2)
print('Solver status:', result.solver.status)
print('Solver termination condition:',result.solver.termination_condition)

```

    Solver status: ok
    Solver termination condition: optimal

$Q:\;5-(ii) \\
Reporting\; the\; optimal\; objective \;function\; value,\; optimal \;values \;of\; the\; original \;decision\; variables\;and \;the\; new\; decision\; variables \;and \;the\;constraint  \;activities \;in\; Approach\; 2$

```python
print('\nObjective = ', model_lab6_ex2.objective())

print('\nDecision Variables')
for i in col_indices:
  print('Num slots for medium ',data_csvfile['Medium'][i], model_lab6_ex2.x[i].value)

print('\nConstraints')
model_lab6_ex2.constraints.display()
```

    Objective =  3378.0

    Decision Variables
    Num slots for medium  Websites 34.0
    Num slots for medium  TV 0.0
    Num slots for medium  DailyNewspaper 41.0
    Num slots for medium  WeeklyMagazine 11.0
    Num slots for medium  MonthlyMagazine 0.0

    Constraints
    constraints : Size=9
        Key : Lower    : Body       : Upper
          1 :     None :       75.0 :      75.0
          2 :     None :       52.0 :     100.0
          3 :     10.0 :       34.0 :      None
          6 :     None :  1485000.0 : 1500000.0
          7 : 900000.0 : 12095000.0 :      None
          8 :     None :        0.0 :       0.0
          9 :     None :       18.0 :      18.0

$Q: \;6 \\
Since,\;the\; optimal\; objective \;function\; value,\; optimal \;values \;of\; the\; original \;decision\; variables\; are \;same \;in \;both \;the \;approaches,\; i.e. \\
Optimal\; objective \;function \;value =  3391.5 \\
Optimal \;values\;of \;the\; decision \;Variables: \\
Num \;slots\; for \;medium \; Websites  :  34.5 \\
Num \;slots \;for \;medium\;  TV  :  0.0 \\
Num \;slots\; for\; medium  \;DailyNewspaper  :  40.5 \\
Num \;slots\; for \;medium \; WeeklyMagazine  :  12.0 \\
Num\; slots\; for\; medium \; MonthlyMagazine  :  0.0 \\
Hence,\; both \;the \;approaches\; are \;equivalent. $

$Q:7 \\
Let\; z_3 \;denote\; the\; total\; number\; of\; media \;slots\; allotted \;for \; weekly\; and\;monthly\;
magazines. \;Therefore,\;z_3=x_5+x_6\; \\
According \;to\; the\; problem,\; |z_3-x_3|\geq4 \\
Hence, \;the \;new \;optimization \;problem \;will \;be\; as\; follows: $
\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000, \\
| x_5+x_6-x_3| \geq4 \\
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}. \\
\end{align} \\
$Now,\; the\; above \;optimization\; model\; is\; not\; linear\; since,\; it \;contains\; the \;modulus \;function,\; which\; is \;piecewise. \;Hence,\; for\; converting \;this\; model \;into \;linear\; program, \; We \;can\; use\; the\;following \;two\; approaches : \\
Approach\; 1: \\
We \;take \;a\; new \;variable \;u = |x_4+x_5-x_3|\; where, \; u\geq 0 \;also, \;according \;to \;the \;question,\; u\geq4  \\
Hence,\; the\; optimization\; problem\; will \;be \;as \;follows: $ \\

\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000,\\
x_3-x_4-x_5 \leq u \\
-x_3+x_4+x_5 \leq u,\\
u \geq 4,\\
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}.
\end{align} \\

$The\; above \;model \;is \;linear.$

$Approach\;2: \\
Let's\; take \; -x_3+x_4+x_5 = a-b , \;Therefore,\; |-x_3+x_4+x_5| = |a-b| = a+b, \;where \;a,\; b \geq 0  \\
Hence,\; the\; optimization\; problem\; will \;be \;as \;follows:$
\begin{align}
\max \ 35 x_1 + 57 x_2 + 48 x_3 + 20 x_4 + 15 x_5, \\
{\rm{s.t.}} \; x_1 + x_2 + x_3 & \leq 75, \\
x_3 + x_4 + x_5 & \leq 100, \\
x_1 + x_2 & \geq 10, \\
30000 x_1 + 95000 x_2 + 10000 x_3 + 5000 x_4 + 4000 x_5 & \leq 1500000, \\
100000 x_1 + 500000 x_2 + 200000 x_3 + 45000 x_4 + 25000 x_5 & \geq 900000, \\
-x_3+x_4+x_5-a+b=0\\
a+b \geq 4,\\
x_i & \geq 0, \quad i \in \{1,2,3,4,5\}. \\ a,b \geq 0
\end{align}

$The\; above \;model \;is \;also\;linear.$

An optimization problem with non-linear absolute value function in the constraints/An optimization problem with non-linear absolute value function in the, provided file32.ipynb


## equations printer
Code to printe the 2WDD model's equations to nicely formatted latex with sympy

```python
from sympy import latex, init_printing, symbols, Eq, pprint
import IPython.display as disp
from control.dynamics import ModelDynamics
from control.control import Controller
init_printing(use_latex='mathjax')

```

    C:\Users\Federico\.conda\envs\cai\lib\site-packages\numpy\_distributor_init.py:32: UserWarning: loaded more than 1 DLL from .libs:
    C:\Users\Federico\.conda\envs\cai\lib\site-packages\numpy\.libs\libopenblas.QVLO2T66WEPI7JZ63PS3HMOHFEY472BC.gfortran-win_amd64.dll
    C:\Users\Federico\.conda\envs\cai\lib\site-packages\numpy\.libs\libopenblas.TXA6YQSD3GCQQC22GEQ54J2UDCXDXHWN.gfortran-win_amd64.dll
      stacklevel=1)

```python
xdot, ydot, tdot, vdot, omegadot, tauldot, taurdot = symbols('xdot, ydot, thetadot, vdot, omegadot, taudot_l, taudot_r', real=True)
```

```python
model = ModelDynamics()
model.get_combined_dynamics_kinematics()
model.get_jacobians()
```

### model dynamics

```python
# Model dynamics
disp.display(
    Eq(xdot, model.equations_symbolic['xdot']),
    Eq(ydot, model.equations_symbolic['ydot']),
    Eq(tdot, model.equations_symbolic['thetadot']),
    Eq(vdot, model.equations_symbolic['vdot']),
    Eq(omegadot, model.equations_symbolic['omegadot']),
    Eq(tauldot, model.equations_symbolic['tauldot']),
    Eq(taurdot, model.equations_symbolic['taurdot']),
)
```

$\displaystyle \dot{x} = v \cos{\left(\theta \right)}$

$\displaystyle \dot{y} = v \sin{\left(\theta \right)}$

$\displaystyle \dot{\theta} = \omega$

$\displaystyle \dot{v} = \frac{d m \omega^{2} + \frac{\tau_{l} + \tau_{r}}{R}}{\frac{2 I_{w}}{R^{2}} + m}$

$\displaystyle \dot{\omega} = \frac{\frac{L \left(- \tau_{l} + \tau_{r}\right)}{R} + d m \omega v}{I + \frac{2 I_{w} L^{2}}{R^{2}}}$

$\displaystyle \dot{\tau}_{l} = - N_{l} + P$

$\displaystyle \dot{\tau}_{r} = - N_{r} + P$

```python
lhss = [xdot, ydot, tdot, vdot, omegadot, tauldot, taurdot]
for lhs, rhs in zip(lhss, model.equations_symbolic.values()):
    print(latex(Eq(lhs, rhs)), '\n')

```

    \dot{x} = v \cos{\left(\theta \right)}

    \dot{y} = v \sin{\left(\theta \right)}

    \dot{\theta} = \omega

    \dot{v} = \frac{d m \omega^{2} + \frac{\tau_{l} + \tau_{r}}{R}}{\frac{2 I_{w}}{R^{2}} + m}

    \dot{\omega} = \frac{\frac{L \left(- \tau_{l} + \tau_{r}\right)}{R} + d m \omega v}{I + \frac{2 I_{w} L^{2}}{R^{2}}}

    \dot{\tau}_{l} = - N_{r} + P

    \dot{\tau}_{r} = - N_{l} + P

```python
model.equations
```

    {'xdot': v*cos(theta),
     'ydot': v*sin(theta),
     'thetadot': omega,
     'vdot': (d*m*omega**2 + (tau_l + tau_r)/R)/(m + m_w),
     'omegadot': (L*(-tau_l + tau_r)/R + d*m*omega*v)/(3*L**2*m_w + 3.0*R**2*m_w + 2*d**2*m),
     'taurdot': -N_r + P,
     'tauldot': -N_l + P}

```python
### moments of inertia definition
I_c, I_w, I_m, I = symbols("I_c, I_w, I_m, I", real=True)
disp.display(
    Eq(I_c, model.equations_symbolic['I_c']),
    Eq(I_w, model.equations_symbolic['I_w']),
    Eq(I_m, model.equations_symbolic['I_m']),
    Eq(I, model.equations_symbolic['I']),
)
```

$\displaystyle I_{c} = d^{2} m$

$\displaystyle I_{w} = \frac{R^{2} m_{w}}{2}$

$\displaystyle I_{m} = 1.5 R^{2} m_{w}$

$\displaystyle I = I_{c} + 2 I_{m} + 2 L^{2} m_{w} + d^{2} m$

```python
## Invert the dynamics to get torques from velocities
e1 = Eq(vdot, model.equations_symbolic['vdot'])
from sympy import simplify
taul = symbols('tau_l')
simplify(model.equations_symbolic['vdot'])
```

$\displaystyle \frac{R \left(R d m \omega^{2} + \tau_{l} + \tau_{r}\right)}{2 I_{w} + R^{2} m}$

### jacobians

```python
model.model_jacobian_state
```

```python
model.model_jacobian_input
```

```python

```

## cost function

```python
controller = Controller(model)
```

```python
controller.cost_function
```

$\displaystyle \left(U^{T} - U_{p}^{T}\right) Z \left(U - U_{p}\right) + \left(X^{T} - X_{g}^{T}\right) Q \left(X - X_{g}\right) + U^{T} R U + U^{T} W$

```python
print(latex(controller.cost_function))
```

    \left(U^{T} - U_{p}^{T}\right) Z \left(U - U_{p}\right) + \left(X^{T} - X_{g}^{T}\right) Q \left(X - X_{g}\right) + U^{T} R U + U^{T} W

```python

```


# 11 Ordinary Differential Equations (ODEs)

[ODE](http://mathworld.wolfram.com/OrdinaryDifferentialEquation.html)s describe many phenomena in physics. They describe the changes of a **dependent variable** $y(t)$ as a function of a **single independent variable** (e.g. $t$ or $x$).

An ODE of **order** $n$

$$
F(t, y^{(0)}, y^{(1)}, ..., y^{(n)}) = 0
$$

contains derivatives $y^{(k)}(t) \equiv y^{(k)} \equiv \frac{d^{k}y(t)}{dt^{k}}$ up to the $n$-th derivative (and $y^{(0)} \equiv y$).

### Initial and boundary conditions

* $n$ **initial conditions** are needed to *uniquely determine* the solution of a $n$-th order ODE, e.g, initial position and velocities.

* **Boundary conditions** (values of solution on domain boundries) can additionaly restrict solutions but the resulting *eigenvalue problems* are more difficult, e.g, wavefunction goes towards 0 for $\pm\infty$.

### Linear ODEs
A **linear** ODE contains no higher powers than 1 of any of the $y^{(k)}$.

*Superposition principle*: Linear combinations of solutions are also solutions.

#### Example: First order linear ODE

\begin{align}
\frac{dy}{dt} &= f(t)y + g(t)\\
y^{(1)} &= f(t)y + g(t)\\
% y^{(1)} - f(t)y - g(t) &= 0
\end{align}

##### Radioactive decay
$$
\frac{dN}{dt} = -k N
$$

### Non-linear ODEs
**Non-linear** ODEs can contain any powers in the dependent variable and its derivatives.

No superposition of solutions. Often impossible to solve analytically.

#### Example: Second order (general) ODE
\begin{gather}
\frac{d^2 y}{dt^2} + \lambda(t) \frac{dy}{dt} = f\left(t, y, \frac{dy}{dt}\right)\\
\end{gather}

##### Newton's equations of motion
$$
m\frac{d^2 x}{dt^2} = F(x) + F_\text{ext}(x, t) \quad \text{with}
\quad F(x) = -\frac{dU}{dx}
$$
(Force is often derived from a potential energy $U(x)$ and may contain non-linear terms such as $x^{-2}$ or $x^3$.)

## Partial differential equations (PDEs)
* more than one independent variable (e.g. $x$ and $t$)
* partial derivatives
* much more difficult than ODEs

#### Example: Schrödinger equation (Quantum Mechanics)
$$
i\hbar \frac{\partial\psi(\mathbf{x}, t)}{\partial t} = -\frac{\hbar^2}{2m}
   \left(\frac{\partial^2 \psi}{\partial x^2} +
         \frac{\partial^2 \psi}{\partial y^2} +
         \frac{\partial^2 \psi}{\partial z^2}
   \right) + V(\mathbf{x})\, \psi(\mathbf{x}, t)
$$

## Harmonic and anharmonic oscillator
* particle with mass $m$ connected to a spring
* spring described by a harmonic potential or anharmonic ones in the displacements from equilibrium $x$

\begin{align}
U_1(x) &= \frac{1}{2} k x^2, \quad k=1\\
U_2(x) &= \frac{1}{2} k x^2 \left(1 - \frac{2}{3}\alpha x\right), \quad k=1,\ \alpha=\frac{1}{2}\\
U_3(x) &= \frac{1}{p} k x^p, \quad k=1,\ p=6
\end{align}

1. What do these potentials look like? Sketch or plot.
2. Calculate the forces.

#### Potentials

```python
import numpy as np

def U1(x, k=1):
    return 0.5 * k * x*x

def U2(x, k=1, alpha=0.5):
    return 0.5 * k * x*x * (1 - (2/3)*alpha*x)

def U3(x, k=1, p=6):
    return (k/p) * np.power(x, p)
```

```python
import matplotlib
import matplotlib.pyplot as plt
matplotlib.style.use('seaborn-talk')
%matplotlib inline
```

```python
X = np.linspace(-3, 3, 100)
ax = plt.subplot(1,1,1)
ax.plot(X, U1(X), label=r"$U_1$")
ax.plot(X, U2(X), label=r"$U_2$")
ax.plot(X, U3(X), label=r"$U_3$")
ax.set_ylim(None, 10)
ax.legend(loc="best")
```

#### Forces

\begin{align}
F_1(x) &= -kx\\
F_2(x) &= -kx(1 + \alpha x)\\
F_3(x) &= -k x^{p-1}
\end{align}

## ODE Algorithms
Basic idea:

1. Start with initial conditions, $y_0 \equiv y(t=0)$
2. Use $\frac{dy}{dt} = f(t, y)$ (the RHS!) to advance solution a small step $h$ forward in time: $y(t=h) \equiv y_1$
3. Repeat with $y_1$ to obtain $y_2 \equiv y(t=2h)$... and for all future values of $t$.

Possible issues
* small differences: subtractive cancelation and round-off error accumulation
* extrapolation: numerical "solution" can deviate wildly from exact
* possibly need adaptive $h$

### Euler's rule
Simple: forward difference

\begin{align}
f(t, y) = \frac{dy(t)}{dt} &\approx \frac{y(t_{n+1}) - y(t_n)}{h}\\
y_{n+1} &\approx y_n + h f(t_n, y_n) \quad \text{with} \quad y_n := y(t_n)
\end{align}

Error will be $\mathcal{O}(h^2)$ (bad!).

Also: what if we have a second order ODE ?!?! We only used $dy/dt$.

### Standard (dynamic) form of ODEs
1 ODE of *any order* $n$ $\rightarrow$ $n$ coupled simultaneous first-order ODEs in $n$ unknowns  $y^{(0)}, \dots, y^{(n-1)}$:

\begin{align}
\frac{dy^{(0)}}{dt} &= f^{(0)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\frac{dy^{(1)}}{dt} &= f^{(1)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\vdots &  \\
\frac{dy^{(n-1)}}{dt} &= f^{(n-1)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\end{align}

In $n$-dimensional vector notation:

\begin{align}
\frac{d\mathbf{y}(t)}{dt} &= \mathbf{f}(t, \mathbf{y})\\
\mathbf{y} &= \left(\begin{array}{c}
  y^{(0)}(t) \\
  y^{(1)}(t) \\
  \vdots \\
  y^{(n-1)}(t)
  \end{array}\right),
\quad
\mathbf{f} = \left(\begin{array}{c}
  f^{(0)}(t, \mathbf{y}) \\
  f^{(1)}(t, \mathbf{y}) \\
  \vdots \\
  f^{(n-1)}(t, \mathbf{y})
  \end{array}\right)
\end{align}

#### Example: Convert Newton's EOMs to standard form

$$
\frac{d^2 x}{dt^2} = m^{-1} F\Big(t, x, \frac{dx}{dt}\Big)
$$

RHS may *not contain any explicit derivatives* but components of $\mathbf{y}$ can represent derivatives.
* position $x$ as first dependent variable $y^{(0)}$ (as usual).
* velocity $dx/dt$ as second dependent variable $y^{(1)}$

$$
y^{(0)}(t) := x(t), \quad y^{(1)}(t) := \frac{dx}{dt} = \frac{dy^{(0)}}{dt}
$$

One 2nd order ODE

$$
\frac{d^2 x}{dt^2} = m^{-1} F\Big(t, x, \frac{dx}{dt}\Big)
$$

to two simultaneous 1st order ODEs:

\begin{align}
\frac{dy^{(0)}}{dt} &= y^{(1)}(t)\\
\frac{dy^{(1)}}{dt} &= m^{-1} F\Big(t, y^{(0)}, y^{(1)}\Big)
\end{align}

\begin{align}
\frac{d\mathbf{y}(t)}{dt} &= \mathbf{f}(t, \mathbf{y})\\
\mathbf{y} &= \left(\begin{array}{c}
     y^{(0)} \\
     y^{(1)}
  \end{array}\right) =
  \left(\begin{array}{c}
     x(t) \\
     \frac{dx}{dt}
  \end{array}\right),\\
\mathbf{f} &= \left(\begin{array}{c}
    y^{(1)}(t) \\
    m^{-1} F\Big(t, y^{(0)}, y^{(1)}\Big)
  \end{array}\right) =
  \left(\begin{array}{c}
    \frac{dx}{dt} \\
    m^{-1} F\Big(t, x(t), \frac{dx}{dt}\Big)
  \end{array}\right)
\end{align}

#### Example: Driven 1D harmonic oscillator in standard form
With $F_1 = -k x$:
$$
\frac{d^2 x}{dt^2} = F_\text{ext}(x, t) - k x
$$
convert to

\begin{align}
\frac{dy^{(0)}}{dt} &= y^{(1)}(t) \\
\frac{dy^{(1)}}{dt} &= m^{-1}[F_\text{ext}(y^{(0)}, t) - k y^{(0)}]
\end{align}

Force (or derivative) function $\mathbf{f}$ and initial conditions:

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}(0) &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=  m^{-1}[F_\text{ext}(y^{(0)}, t) - k y^{(0)}],
    &\quad y^{(1)}(0) &= v_0.
\end{alignat}

### Euler's rule (standard form)
Given the $n$-dimensional vectors from the ODE standard form

$$
\frac{d\mathbf{y}}{dt} = \mathbf{f}(t, \mathbf{y})
$$
the **Euler rule** amounts to

\begin{align}
\mathbf{f}(t, \mathbf{y}) = \frac{d\mathbf{y}(t)}{dt} &\approx \frac{\mathbf{y}(t_{n+1}) - \mathbf{y}(t_n)}{h}\\
\mathbf{y}_{n+1} &\approx \mathbf{y}_n + h \mathbf{f}(t_n, \mathbf{y}_n) \quad \text{with} \quad \mathbf{y}_n := \mathbf{y}(t_n)
\end{align}

## Problem: Numerically integrate the 1D harmonic oscillator with Euler

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}(0) &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=   - \frac{k}{m} y^{(0)},
    &\quad y^{(1)}(0) &= v_0.
\end{alignat}

with $k=1$; $x_0 = 0$ and $v_0 = +1$.

### Explicit implementation:
* Note how in `f_hramonic` we are constructing the force vector of the standard ODE representation
* `y` is the vector of dependents in the standard representation
* We pre-allocate the array for `y` and then assign to individual elements with the
  ```python
  y[:] = ...
  ```
  notation, which has higher performance than creating the array anew every time.

```python
import numpy as np

def F1(x, k=1):
    """Harmonic force"""
    return -k*x

def f_harmonic(t, y, k=1, m=1):
    """Force vector in standard ODE form (n=2)"""
    return np.array([y[1], F1(y[0], k=k)/m])

t_max = 100
h = 0.01
Nsteps = t_max/h
t_range = h * np.arange(Nsteps)
x = np.empty_like(t_range)

y = np.zeros(2)

# initial conditions
x0, v0 = 0.0, 1.0
y[:] = x0, v0

for i, t in enumerate(t_range):
    # store position that corresponds to time t_i
    x[i] = y[0]
    # Euler integrator
    y[:] = y + h * f_harmonic(t, y)
```

Plot the position $x(t)$ (which is $y_0$) against time:

```python
plt.plot(t_range, x)
```

### Modular solution with functions
We can make the Euler integrator a function, which makes the code more readable and modular and we can make the whole integration a function, too. This will allow us to easily run the integration with different initial values or `h` steps.

```python
import numpy as np

def F1(x, k=1):
    """Harmonic force"""
    return -k*x

def f_harmonic(t, y, k=1, m=1):
    """Force vector in standard ODE form (n=2)"""
    return np.array([y[1], F1(y[0], k=k)/m])

def euler(y, f, t, h):
    """Euler integrator.

    Returns new y at t+h.
    """
    return y + h * f(t, y)

def integrate(x0=0, v0=1, t_max=100, h=0.001):
    """Integrate the harmonic oscillator with force F1.

    Note that the spring constant k and particle mass m are currently
    pre-defined.

    Arguments
    ---------
    x0 : float
       initial position
    v0 : float
       initial velocity
    t_max : float
       time to integrate out to
    h : float, default 0.001
       integration time step

    Returns
    -------
    Tuple ``(t, x)`` with times and positions.
    """

    Nsteps = t_max/h
    t_range = h * np.arange(Nsteps)
    x = np.empty_like(t_range)
    y = np.zeros(2)
    # initial conditions
    y[:] = x0, v0

    for i, t in enumerate(t_range):
        # store position that corresponds to time t_i
        x[i] = y[0]
        # Euler integrator
        y[:] = euler(y, f_harmonic, t, h)

    return t_range, x
```

Plot the position as a function of time, $x(t)$.

```python
t, x = integrate(h=0.01)
plt.plot(t, x)
```

Note the increase in amplitude. Explore if smaller $h$ fixes this obvious problem.

```python
t, x = integrate(h=0.001)
plt.plot(t, x)
```

Smaller $h$ improves the integration (but Euler is still a bad algorithm... just run out for longer, i.e., higher `t_max`.)