Trusted Notebook" width="500 px" align="left">

## _*Comparing Classical and Quantum Finite Automata (QFA)*_

Finite Automaton has been a mathematical model for computation since its invention in the 1940s. The purpose of a Finite State Machine is to recognize patterns within an input taken from some character set and accept or reject the input based on whether the pattern defined by the machine occurs in the input. The machine requires a list of states, the initial state, and the conditions for each transition from state to state. Such classical examples are vending machines, coin-operated turnstiles, elevators, traffic lights, etc.

In the classical algorithm, the sequence begins in the start state, and will only make a transition if the next character in the input string matches the label on the transition from the current to the next state. The machine will continue making transitions on each input character until no move is possible. The string will be accepted if its final state is in the accept state and will be rejected if its final state is anywhere else.

As for Quantum Finite Automata (QFA), the machine works by accepting a finite-length string of letters from a finite alphabet and utilizing quantum properties such as superposition to assign the string a probability of being in either the accept or reject state.

***
### Contributors
Kaitlin Gili, Rudy Raymond

## Prime Divisibility Algorithm

Let's say that we have a string with $ a^i $ letters and we want to know whether the string is in the language $ L $ where $ L $ = {$ a^i $ | $ i $ is divisble by $ p $} and $ p $ is a prime number. If $ i $ is divisible by $ p $, we want to accept the string into the language, and if not, we want to reject it.
$|0\rangle $ and $ |1\rangle $ serve as our accept and reject states.

Classically, this algorithm requires a minimum of $ log(p) $ bits to store the information, whereas the quantum algorithm only requires $ log(log(p)) $ qubits. For example, using the highest known prime integer, the classical algorithm requires **a minimum of 77,232,917 bits**, whereas the quantum algorithm **only requires 27 qubits**.

## Introduction

The algorithm in this notebook follows that in [Ambainis et al. 1998](https://arxiv.org/pdf/quant-ph/9802062.pdf). We assume that we are given a string and a prime integer. If the user does not input a prime number, the output will be a ValueError. First, we demonstrate a simpler version of the quantum algorithm that uses $ log(p) $ qubits to store the information. Then, we can use this to more easily understand the quantum algorithm that requires only $ log(log(p)) $ qubits.

## The Algorithm for Log(p) Qubits

The algorithm is quite simple as follows.
1. Prepare quantum and classical registers for $ log(p) $ qubits initialized to zero.
$$ |0\ldots 0\rangle $$
2. Prepare $ log(p) $ random numbers k in the range {$ 1 $... $ p-1 $}. These numbers will be used to decrease the probability of a string getting accepted when $ i $ does not divide $ p $.
3. Perform a number of $ i $ Y-Rotations on each qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k}{p} $$
4. In the final state:
$$ \cos \theta |0\rangle + \sin \theta |1\rangle $$
$$ \theta = \frac{2 \pi k} p {i} $$
5. Measure each of the qubits in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every qubit and the state will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

## The Circuit

We now implement the QFA Prime Divisibility algorithm with QISKit by first preparing the environment.

```python
# useful additional packages
import random
import math
from sympy.ntheory import isprime

# importing QISKit
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit import Aer, IBMQ, execute

from qiskit.tools.monitor import job_monitor
from qiskit.providers.ibmq import least_busy
from qiskit.tools.visualization import plot_histogram
```

```python
IBMQ.load_accounts()
sim_backend = Aer.get_backend('qasm_simulator')
device_backend = least_busy(IBMQ.backends(operational=True, simulator=False))
```

We then use QISKit to program the algorithm.

```python
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(prime))) #Rounds up to the next integer of the log(prime)
        qr = QuantumRegister(n) #Creates a quantum register of length log(prime) for log(prime) qubits
        cr = ClassicalRegister(n) #Creates a classical register for measurement
        qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for x in range(n): #For each qubit, we want to apply a series of unitary operations with a random int
            random_value = random.randint(1,prime - 1) #Generates the random int for each qubit from {1, prime -1}
            for letter in string: #For each letter in the string, we want to apply the same unitary operation to each qubit
                qfaCircuit.ry((2*math.pi*random_value) / prime, qr[x]) #Applies the Y-Rotation to each qubit
            qfaCircuit.measure(qr[x], cr[x]) #Measures each qubit
        return qfaCircuit #Returns the created quantum circuit
```

The qfa_algorithm function returns the Quantum Circuit qfaCircuit.

## Experiment with Simulators

We can run the above circuit on the simulator.

```python
#A function that returns a string saying if the string is accepted into the language or rejected
def accept(parameter):
    states = list(result.get_counts(parameter))
    for s in states:
        for integer in s:
            if integer == "1":
                return "Reject: the string is not accepted into the language"
    return "Accept: the string is accepted into the language"
```

Insert your own parameters and try even larger prime numbers.

```python
range_lower = 0
range_higher = 36
prime_number = 11
```

```python
for length in range(range_lower,range_higher):
    params = qfa_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))
```

    Accept: the string is accepted into the language
     Length: 0   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 1   {'011': 233, '000': 109, '111': 12, '001': 542, '010': 45, '101': 50, '110': 3, '100': 6}
    Reject: the string is not accepted into the language
     Length: 2   {'011': 9, '000': 72, '111': 42, '001': 2, '010': 115, '101': 25, '110': 398, '100': 337}
    Reject: the string is not accepted into the language
     Length: 3   {'011': 21, '000': 156, '111': 25, '001': 227, '010': 15, '101': 320, '110': 11, '100': 225}
    Reject: the string is not accepted into the language
     Length: 4   {'101': 885, '110': 4, '011': 5, '100': 12, '111': 79, '001': 15}
    Reject: the string is not accepted into the language
     Length: 5   {'011': 7, '000': 1, '111': 452, '001': 6, '010': 9, '101': 94, '110': 359, '100': 72}
    Reject: the string is not accepted into the language
     Length: 6   {'011': 12, '000': 34, '111': 61, '001': 119, '010': 1, '101': 638, '110': 11, '100': 124}
    Reject: the string is not accepted into the language
     Length: 7   {'010': 295, '101': 5, '110': 416, '011': 112, '000': 5, '100': 6, '111': 161}
    Reject: the string is not accepted into the language
     Length: 8   {'011': 127, '000': 80, '111': 202, '001': 112, '010': 108, '101': 129, '110': 146, '100': 96}
    Reject: the string is not accepted into the language
     Length: 9   {'011': 123, '000': 7, '111': 549, '001': 23, '010': 22, '101': 121, '110': 118, '100': 37}
    Reject: the string is not accepted into the language
     Length: 10   {'011': 24, '000': 168, '111': 23, '001': 14, '010': 206, '101': 17, '110': 298, '100': 250}
    Accept: the string is accepted into the language
     Length: 11   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 12   {'010': 137, '110': 761, '011': 10, '000': 4, '100': 15, '111': 73}
    Reject: the string is not accepted into the language
     Length: 13   {'011': 64, '000': 112, '111': 20, '001': 537, '010': 9, '101': 203, '110': 4, '100': 51}
    Reject: the string is not accepted into the language
     Length: 14   {'011': 520, '000': 7, '111': 53, '001': 364, '010': 4, '101': 49, '110': 2, '100': 1}
    Reject: the string is not accepted into the language
     Length: 15   {'011': 56, '000': 144, '111': 94, '001': 46, '010': 189, '101': 74, '110': 237, '100': 160}
    Reject: the string is not accepted into the language
     Length: 16   {'010': 375, '110': 34, '011': 523, '000': 10, '100': 1, '111': 40, '001': 17}
    Reject: the string is not accepted into the language
     Length: 17   {'011': 5, '000': 122, '111': 14, '001': 42, '010': 10, '101': 230, '110': 46, '100': 531}
    Reject: the string is not accepted into the language
     Length: 18   {'010': 22, '101': 1, '110': 3, '011': 887, '000': 2, '111': 68, '001': 17}
    Reject: the string is not accepted into the language
     Length: 19   {'011': 40, '000': 3, '111': 252, '001': 2, '010': 101, '101': 3, '110': 585, '100': 14}
    Reject: the string is not accepted into the language
     Length: 20   {'011': 37, '000': 112, '111': 19, '001': 9, '010': 552, '101': 7, '110': 225, '100': 39}
    Reject: the string is not accepted into the language
     Length: 21   {'011': 103, '000': 3, '111': 468, '001': 68, '010': 1, '101': 337, '110': 12, '100': 8}
    Accept: the string is accepted into the language
     Length: 22   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 23   {'010': 1, '101': 417, '110': 11, '011': 10, '100': 8, '111': 542, '001': 11}
    Reject: the string is not accepted into the language
     Length: 24   {'011': 4, '000': 5, '111': 151, '001': 4, '010': 14, '101': 127, '110': 408, '100': 287}
    Reject: the string is not accepted into the language
     Length: 25   {'101': 162, '110': 21, '011': 20, '100': 2, '111': 792, '001': 3}
    Reject: the string is not accepted into the language
     Length: 26   {'011': 207, '000': 110, '111': 21, '001': 555, '010': 51, '101': 41, '110': 4, '100': 11}
    Reject: the string is not accepted into the language
     Length: 27   {'011': 2, '000': 115, '111': 29, '001': 46, '010': 9, '101': 202, '110': 44, '100': 553}
    Reject: the string is not accepted into the language
     Length: 28   {'011': 11, '000': 67, '111': 36, '001': 82, '010': 6, '101': 451, '110': 38, '100': 309}
    Reject: the string is not accepted into the language
     Length: 29   {'011': 87, '000': 13, '111': 415, '001': 55, '010': 23, '101': 270, '110': 89, '100': 48}
    Reject: the string is not accepted into the language
     Length: 30   {'011': 194, '000': 9, '111': 74, '001': 3, '010': 502, '101': 2, '110': 213, '100': 3}
    Reject: the string is not accepted into the language
     Length: 31   {'010': 659, '110': 244, '011': 60, '000': 11, '100': 7, '111': 18, '001': 1}
    Reject: the string is not accepted into the language
     Length: 32   {'010': 13, '101': 2, '110': 8, '011': 686, '000': 1, '111': 274, '001': 16}
    Accept: the string is accepted into the language
     Length: 33   {'000': 1000}
    Reject: the string is not accepted into the language
     Length: 34   {'011': 113, '000': 189, '111': 49, '001': 285, '010': 103, '101': 126, '110': 36, '100': 99}
    Reject: the string is not accepted into the language
     Length: 35   {'010': 2, '101': 73, '011': 64, '000': 10, '100': 2, '111': 10, '001': 839}

### Drawing the circuit of the QFA

Below is the snapshop of the QFA for reading the bitstring of length $3$. It can be seen that there are independent QFAs each of which performs $Y$ rotation for $3$ times.

```python
qfa_algorithm("a"* 3, prime_number).draw(output='mpl')
```

## The Algorithm for Log(Log(p)) Qubits

The algorithm is quite simple as follows.
1. Prepare a quantum register for $ log(log(p)) + 1 $ qubits initialized to zero. The $ log(log(p))$ qubits will act as your control bits and the 1 extra will act as your target bit. Also prepare a classical register for 1 bit to measure the target.
$$ |0\ldots 0\rangle |0\rangle  $$
2. Hadamard the control bits to put them in a superposition so that we can perform multiple QFA's at the same time.
3. For each of $s $ states in the superposition, we can perform an individual QFA with the control qubits acting as the random integer $ k $ from the previous algorithm. Thus, we need $ n $ values from $ 1... log(p)$ for $ k $. For each letter $ i $ in the string, we perform a controlled y-rotation on the target qubit, where $ \theta $ is initially zero and $ \Phi $ is the angle of rotation for each unitary. $$ \Phi = \frac{2 \pi k_{s}}{p} $$
4. The target qubit in the final state:
$$ \cos \theta |0\rangle + \sin \theta |1\rangle $$
$$ \theta = \sum_{s=0}^n \frac{2 \pi k_{s}} p {i} $$
5. Measure the target qubit in the classical register. If $ i $ divides $ p $, $ \cos \theta $ will be one for every QFA and the state of the target will collapse to $ |0\rangle $ to demonstrate an accept state with a probability of one. Otherwise, the output will consist of a small probability of accepting the string into the language and a higher probability of rejecting the string.

## The Circuit

We then use QISKit to program the algorithm.

```python
#Function that takes in a prime number and a string of letters and returns a quantum circuit
def qfa_controlled_algorithm(string, prime):
    if isprime(prime) == False:
        raise ValueError("This number is not a prime") #Raises a ValueError if the input prime number is not prime
    else:
        n = math.ceil((math.log(math.log(prime,2),2))) #Represents log(log(p)) control qubits
        states = 2 ** (n) #Number of states that the qubits can represent/Number of QFA's to be performed
        qr = QuantumRegister(n+1) #Creates a quantum register of log(log(prime)) control qubits + 1 target qubit
        cr = ClassicalRegister(1) #Creates a classical register of log(log(prime)) control qubits + 1 target qubit
        control_qfaCircuit = QuantumCircuit(qr, cr) #Defining the circuit to take in the values of qr and cr
        for q in range(n): #We want to take each control qubit and put them in a superposition by applying a Hadamard Gate
            control_qfaCircuit.h(qr[q])
        for letter in string: #For each letter in the string, we want to apply a series of Controlled Y-rotations
            for q in range(n):
                control_qfaCircuit.cu3(2*math.pi*(2**q)/prime, 0, 0, qr[q], qr[n]) #Controlled Y on Target qubit
        control_qfaCircuit.measure(qr[n], cr[0]) #Measure the target qubit
        return control_qfaCircuit #Returns the created quantum circuit
```

The qfa_algorithm function returns the Quantum Circuit control_qfaCircuit.

## Experiment with Simulators

We can run the above circuit on the simulator.

```python
for length in range(range_lower,range_higher):
    params = qfa_controlled_algorithm("a"* length, prime_number)
    job = execute(params, sim_backend, shots=1000)
    result = job.result()
    print(accept(params), "\n", "Length:",length," " ,result.get_counts(params))
```

    Accept: the string is accepted into the language
     Length: 0   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 1   {'1': 248, '0': 752}
    Reject: the string is not accepted into the language
     Length: 2   {'1': 506, '0': 494}
    Reject: the string is not accepted into the language
     Length: 3   {'1': 461, '0': 539}
    Reject: the string is not accepted into the language
     Length: 4   {'1': 364, '0': 636}
    Reject: the string is not accepted into the language
     Length: 5   {'1': 474, '0': 526}
    Reject: the string is not accepted into the language
     Length: 6   {'1': 479, '0': 521}
    Reject: the string is not accepted into the language
     Length: 7   {'1': 383, '0': 617}
    Reject: the string is not accepted into the language
     Length: 8   {'1': 452, '0': 548}
    Reject: the string is not accepted into the language
     Length: 9   {'1': 522, '0': 478}
    Reject: the string is not accepted into the language
     Length: 10   {'1': 249, '0': 751}
    Accept: the string is accepted into the language
     Length: 11   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 12   {'1': 212, '0': 788}
    Reject: the string is not accepted into the language
     Length: 13   {'1': 526, '0': 474}
    Reject: the string is not accepted into the language
     Length: 14   {'1': 451, '0': 549}
    Reject: the string is not accepted into the language
     Length: 15   {'1': 371, '0': 629}
    Reject: the string is not accepted into the language
     Length: 16   {'1': 475, '0': 525}
    Reject: the string is not accepted into the language
     Length: 17   {'1': 478, '0': 522}
    Reject: the string is not accepted into the language
     Length: 18   {'1': 364, '0': 636}
    Reject: the string is not accepted into the language
     Length: 19   {'1': 480, '0': 520}
    Reject: the string is not accepted into the language
     Length: 20   {'1': 525, '0': 475}
    Reject: the string is not accepted into the language
     Length: 21   {'1': 231, '0': 769}
    Accept: the string is accepted into the language
     Length: 22   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 23   {'1': 233, '0': 767}
    Reject: the string is not accepted into the language
     Length: 24   {'1': 513, '0': 487}
    Reject: the string is not accepted into the language
     Length: 25   {'1': 475, '0': 525}
    Reject: the string is not accepted into the language
     Length: 26   {'1': 377, '0': 623}
    Reject: the string is not accepted into the language
     Length: 27   {'1': 460, '0': 540}
    Reject: the string is not accepted into the language
     Length: 28   {'1': 456, '0': 544}
    Reject: the string is not accepted into the language
     Length: 29   {'1': 365, '0': 635}
    Reject: the string is not accepted into the language
     Length: 30   {'1': 443, '0': 557}
    Reject: the string is not accepted into the language
     Length: 31   {'1': 512, '0': 488}
    Reject: the string is not accepted into the language
     Length: 32   {'1': 231, '0': 769}
    Accept: the string is accepted into the language
     Length: 33   {'0': 1000}
    Reject: the string is not accepted into the language
     Length: 34   {'1': 223, '0': 777}
    Reject: the string is not accepted into the language
     Length: 35   {'1': 528, '0': 472}

### Drawing the circuit of the QFA

Below is the snapshot of the QFA for reading the bitstring of length $3$. It can be seen that there is a superposition of QFAs instead of independent QFAs.

```python
qfa_controlled_algorithm("a"* 3, prime_number).draw(output='mpl')
```

## Experimenting with Real Devices

Real-device backends have errors and if the above QFAs are executed on the noisy backends, errors in rejecting strings that should have been accepted can happen. Let us see how well the real-device backends can realize the QFAs.

Let us look an example when the QFA should reject the bitstring because the length of the bitstring is not divisible by the prime number.

```python
prime_number = 3
length = 2          # set the length so that it is not divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa1 = qfa_controlled_algorithm("a"* length, prime_number)
```

    The length of a is 2  while the prime number is 3

```python
job = execute(qfa1, backend=device_backend, shots=100)
job_monitor(job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
result = job.result()
plot_histogram(result.get_counts())
```

In the above, we can see that the probability of observing "1" is quite significant. Let us see how the circuit looks like.

```python
qfa1.draw(output='mpl')
```

Now, let us see what happens when the QFAs should accept the input string.

```python
print_number = length = 3 # set the length so that it is divisible by the prime_number
print("The length of a is", length, " while the prime number is", prime_number)
qfa2 = qfa_controlled_algorithm("a"* length, prime_number)
```

    The length of a is 3  while the prime number is 3

```python
job = execute(qfa2, backend=device_backend, shots=100)
job_monitor(job)
```

    HTML(value="
Job Status: job is being initialized
")

```python
result = job.result()
plot_histogram(result.get_counts())
```

The error of rejecting the bitstring is equal to the probability of observing "1" which can be checked from the above histogram. We can see that the noise of real-device backends prevents us to have a correct answer. It is left as future work on how to mitigate errors of the backends in the QFA models.

```python
qfa2.draw(output='mpl')
```


# Tutorial 2: Linear regression with MLE
**Week 1, Day 3: Model Fitting**

**By Neuromatch Academy**

**Content creators**: Pierre-Étienne Fiquet, Anqi Wu, Alex Hyafil  with help from Byron Galbraith

**Content reviewers**: Lina Teichmann, Madineh Sarvestani, Patrick Mineault, Ella Batty, Michael Waskom

---
#Tutorial Objectives

This is Tutorial 2 of a series on fitting models to data. We start with simple linear regression, using least squares optimization (Tutorial 1) and Maximum Likelihood Estimation (Tutorial 2). We will use bootstrapping to build confidence intervals around the inferred linear model parameters (Tutorial 3). We'll finish our exploration of regression models by generalizing to multiple linear regression and polynomial regression (Tutorial 4). We end by learning how to choose between these various models. We discuss the bias-variance trade-off (Tutorial 5) and Cross Validation for model selection (Tutorial 6).

In this tutorial, we will use a different approach to fit linear models that incorporates the random 'noise' in our data.
- Learn about probability distributions and probabilistic models
- Learn how to calculate the likelihood of our model parameters
- Learn how to implement the maximum likelihood estimator, to find the model parameter with the maximum likelihood

---
# Setup

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
```

```python
#@title Figure Settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
#@title Helper Functions
def plot_density_image(x, y, theta, sigma=1, ax=None):
  """ Plots probability distribution of y given x, theta, and sigma

  Args:

    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.
    theta (float): Slope parameter
    sigma (float): standard deviation of Gaussian noise

  """

  # plot the probability density of p(y|x,theta)
  if ax is None:
    fig, ax = plt.subplots()

  xmin, xmax = np.floor(np.min(x)), np.ceil(np.max(x))
  ymin, ymax = np.floor(np.min(y)), np.ceil(np.max(y))
  xx = np.linspace(xmin, xmax, 50)
  yy = np.linspace(ymin, ymax, 50)

  surface = np.zeros((len(yy), len(xx)))
  for i, x_i in enumerate(xx):
    surface[:, i] = stats.norm(theta * x_i, sigma).pdf(yy)

  ax.set(xlabel='x', ylabel='y')

  return ax.imshow(surface, origin='lower', aspect='auto', vmin=0, vmax=None,
            cmap=plt.get_cmap('Wistia'),
            extent=[xmin, xmax, ymin, ymax])

def solve_normal_eqn(x, y):
  """Solve the normal equations to produce the value of theta_hat that minimizes
    MSE.

    Args:
    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.
    theta_hat (float): An estimate of the slope parameter.

  Returns:
    float: The mean squared error of the data with the estimated parameter.
  """
  theta_hat = (x.T @ y) / (x.T @ x)
  return theta_hat
```

---
# Section 1: Maximum Likelihood Estimation (MLE)

```python
#@title Video 1: Maximum Likelihood Estimation
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="8mpNmzLKNfU", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

In the previous tutorial we made the assumption that the data was drawn from a linear relationship with noise added, and found an effective approach for estimating model parameters based on minimizing the mean squared error.

In that case we treated the noise as simply a nuisance, but what if we factored it directly into our model?

Recall our linear model:

\begin{align}
y = \theta x + \epsilon.
\end{align}

The noise component $\epsilon$ is often modeled as a random variable drawn from a Gaussian distribution (also called the normal distribution).

The Gaussian distribution is described by its [probability density function](https://en.wikipedia.org/wiki/Probability_density_function) (pdf)
\begin{align}
\mathcal{N}(x; \mu, \sigma^2) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{1}{2\sigma^2}(x-\mu)^2}
\end{align}

and is dependent on two parameters: the mean $\mu$ and the variance $\sigma^2$. We often consider the noise signal to be Gaussian "white noise", with zero mean and unit variance:

\begin{align}
\epsilon \sim \mathcal{N}(0, 1).
\end{align}

## Interactive Demo: Gaussian Distribution Explorer

Use the explorer widget below to see how varying the $\mu$ and $\sigma$ parameters change the location and shape of the samples.

```python
#@title

#@markdown Make sure you execute this cell to enable the widget!

@widgets.interact(mu=widgets.FloatSlider(0.0, min=-2.0, max=2.0),
                  sigma=widgets.FloatSlider(1.0, min=0.5, max=2.0))
def plot_normal_dist(mu=0, sigma=1):

  # Generate pdf & samples from normal distribution with mu/sigma
  rv = stats.norm(mu, sigma)
  x = np.linspace(-5, 5, 100)
  y = rv.pdf(x)
  samples = rv.rvs(1000)

  # Plot
  fig, ax = plt.subplots()
  ax.hist(samples, 20, density=True, color='g', histtype='stepfilled', alpha=0.8,
          label='histogram')
  ax.plot(x, y, color='orange', linewidth=3, label='pdf')
  ax.vlines(mu, 0, rv.pdf(mu), color='y', linewidth=3, label='$\mu$')
  ax.vlines([mu-sigma, mu+sigma], 0, rv.pdf([mu-sigma, mu+sigma]), colors='red',
            color='b', linewidth=3, label='$\sigma$')
  ax.set(xlabel='x', ylabel='probability density', xlim=[-5, 5], ylim=[0, 1.0])
  ax.legend()
```

## Section 1.1: Probabilistic Models

Now that we have a model of our noise component $\epsilon$ as random variable, how do we incorporate this back into our original linear model from before? Consider again our simplified model $y = \theta x + \epsilon$ where the noise has zero mean and unit variance $\epsilon \sim \mathcal{N}(0, 1)$. We can now also treat $y$ as a random variable drawn from a Gaussian distribution where $\mu = \theta x$ and $\sigma^2 = 1$:

\begin{align}
y \sim \mathcal{N}(\theta x, 1)
\end{align}

which is to say that the probability of observing $y$ given $x$ and parameter $\theta$ is
\begin{align}
p(y|x,\theta) = \frac{1}{\sqrt{2\pi}}e^{-\frac{1}{2}(y-\theta x)^2}
\end{align}

---

Let's revisit our original sample dataset where the true underlying model has $\theta = 1.2$.

```python
# @title

# @markdown Execute this cell to generate some simulated data

# setting a fixed seed to our random number generator ensures we will always
# get the same psuedorandom number sequence

np.random.seed(121)
theta = 1.2
n_samples = 30
x = 10 * np.random.rand(n_samples) # sample from a uniform distribution over [0,10)
noise = np.random.randn(n_samples) # sample from a standard normal distribution
y = theta * x + noise
```

This time we can plot the density of $p(y|x,\theta=1.2)$ and see how $p(y)$ changes for different values of $x$.

```python
#@title
#@markdown Execute this cell to visualize p(y|x, theta=1.2)

fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(10, 4))

# Invokes helper function to generate density image plots from data and parameters
im = plot_density_image(x, y, 1.2, ax=ax1)
plt.colorbar(im, ax=ax1)
ax1.axvline(8, color='k')
ax1.set(title=r'p(y | x, $\theta$=1.2)')

# Plot pdf for given x
ylim = ax1.get_ylim()
yy = np.linspace(ylim[0], ylim[1], 50)
ax2.plot(yy, stats.norm(theta * 8, 1).pdf(yy), color='orange', linewidth=2)
ax2.set(
    title=r'p(y|x=8, $\theta$=1.2)',
    xlabel='y',
    ylabel='probability density');
```

## Section 1.2: Likelihood Estimation

Now that we have our probabilistic model, we turn back to our original challenge of finding a good estimate for $\theta$ that fits our data. Given the inherent uncertainty when dealing in probabilities, we talk about the [likelihood](https://en.wikipedia.org/wiki/Likelihood_function) that some estimate $\hat \theta$ fits our data. The likelihood function $\mathcal{L(\theta)}$ is equal to the probability density function parameterized by that $\theta$:

\begin{align}
\mathcal{L}(\theta|x,y) = p(y|x,\theta) = \frac{1}{\sqrt{2\pi\sigma^2}}e^{-\frac{1}{2\sigma^2}(y-\theta x)^2}
\end{align}

### Exercise 1: Likelihood Function

In this exercise you will implement the likelihood function $\mathcal{L}(\theta|x,y)$ for our linear model where $\sigma = 1$.

After implementing this function, we can produce probabilities that our estimate $\hat{\theta}$ generated the provided observations. We will try with one of the samples from our dataset.

TIP: Use `np.exp` and `np.sqrt` for the exponential and square root functions, respectively.

```python
def likelihood(theta_hat, x, y):
  """The likelihood function for a linear model with noise sampled from a
    Gaussian distribution with zero mean and unit variance.

  Args:
    theta_hat (float): An estimate of the slope parameter.
    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.

  Returns:
    ndarray: the likelihood values for the theta_hat estimate
  """
  sigma = 1
  ##############################################################################
  ## TODO for students: implement the likelihood function
  # Fill out function and remove
  raise NotImplementedError("Student exercise: implement the likelihood function")
  ##############################################################################

  # Compute Gaussian likelihood
  pdf = ...

  return pdf

# Uncomment below to test your function
# print(likelihood(1.0, x[1], y[1]))
```

```python
# to_remove solution
def likelihood(theta_hat, x, y):
  """The likelihood function for a linear model with noise sampled from a
    Gaussian distribution with zero mean and unit variance.

  Args:
    theta_hat (float): An estimate of the slope parameter.
    x (ndarray): An array of shape (samples,) that contains the input values.
    y (ndarray): An array of shape (samples,) that contains the corresponding
      measurement values to the inputs.

  Returns:
    float: the likelihood value for the theta_hat estimate
  """
  sigma = 1

  # Compute Gaussian likelihood
  pdf = 1 / np.sqrt(2 * np.pi * sigma**2) * np.exp(-(y - theta_hat * x)**2 / (2 * sigma**2))

  return pdf

print(likelihood(1.0, x[1], y[1]))
```

We should see that $\mathcal{L}(\theta=1.0|x=2.1,y=3.7) \approx 0.11$. So far so good, but how does this tell us how this estimate is better than any others?

When dealing with a set of data points, as we are with our dataset, we are concerned with their joint probability -- the likelihood that all data points are explained by our parameterization. Since we have assumed that the noise affects each output independently, we can factorize the likelihood, and write:

\begin{align}
\mathcal{L}(\theta|X,Y) = \prod_{i=1}^N \mathcal{L}(\theta|x_i,y_i),
\end{align}

where we have $N$ data points $X = \{x_1,...,x_N\}$ and $Y = \{y_1,...,y_N\}$.

In practice, such a product can be numerically unstable. Indeed multiplying small values together can lead to [underflow](https://en.wikipedia.org/wiki/Arithmetic_underflow), the situation in which the digital representation of floating point number reaches its limit. This problem can be circumvented by taking the logarithm of the likelihood because the logarithm transforms products into sums:

\begin{align}
\operatorname{log}\mathcal{L}(\theta|X,Y) = \sum_{i=1}^N \operatorname{log}\mathcal{L}(\theta|x_i,y_i)
\end{align}

We can take the sum of the log of the output of our `likelihood` method applied to the full dataset to get a better idea of how different $\hat\theta$ compare. We can also plot the different distribution densities over our dataset and see how they line up qualitatively.

```python
#@title
#@markdown Execute this cell to visualize different distribution densities
theta_hats = [0.5, 1.0, 2.2]
fig, axes = plt.subplots(ncols=3, figsize=(16, 4))
for theta_hat, ax in zip(theta_hats, axes):
  ll = np.sum(np.log(likelihood(theta_hat, x, y)))  # log likelihood
  im = plot_density_image(x, y, theta_hat, ax=ax)
  ax.scatter(x, y)
  ax.set(title=fr'$\hat{{\theta}}$ = {theta_hat}, log likelihood: {ll:.2f}')
plt.colorbar(im, ax=ax);
```

Using the log likelihood calculation, we see that $\mathcal{L}(\theta=1.0) > \mathcal{L}(\theta=0.5) > \mathcal{L}(\theta=2.2)$.

This is great: now we have a way to compare estimators based on likelihood. But like with the MSE approach, we want an analytic solution to find the best estimator. In this case, we want to find the estimator that maximizes the likelihood.

## Section 1.3: Finding the Maximum Likelihood Estimator

We want to find the parameter value $\hat\theta$ that makes our data set most likely:

\begin{align}
\hat{\theta}_{\textrm{MLE}} = \underset{\theta}{\operatorname{argmax}} \mathcal{L}(\theta|X,Y)
\end{align}

We discussed how taking the logarithm of the likelihood helps with numerical stability, the good thing is that it does so without changing the parameter value that maximizes the likelihood. Indeed, the $\textrm{log}()$ function is *monotonically increasing*, which means that it preserves the order of its inputs. So we have:

\begin{align}
\hat{\theta}_{\textrm{MLE}} = \underset{\theta}{\operatorname{argmax}} \sum_{i=1}^m \textrm{log} \mathcal{L}(\theta|x_i,y_i)
\end{align}

Now substituting our specific likelihood function and taking its logarithm, we get:
\begin{align}
\hat{\theta}_{\textrm{MLE}} = \underset{\theta}{\operatorname{argmax}} [-\frac{N}{2} \operatorname{log} 2\pi\sigma^2 - \frac{1}{2\sigma^2}\sum_{i=1}^N (y_i-\theta x_i)^2].
\end{align}

Note that maximizing the log likelihood is the same as minimizing the negative log likelihood (in practice optimization routines are developed to solve minimization not maximization problems). Because of the convexity of this objective function, we can take the derivative of our negative log likelihhood, set it to 0, and solve - just like our solution to minimizing MSE.

\begin{align}
\frac{\partial\operatorname{log}\mathcal{L}(\theta|x,y)}{\partial\theta}=\frac{1}{\sigma^2}\sum_{i=1}^N(y_i-\theta x_i)x_i = 0
\end{align}

This looks remarkably like the equation we had to solve for the optimal MSE estimator, and, in fact, we arrive to the exact same solution!

\begin{align}
\hat{\theta}_{\textrm{MLE}} = \hat{\theta}_{\textrm{MSE}} = \frac{\sum_{i=1}^N x_i y_i}{\sum_{i=1}^N x_i^2}
\end{align}

```python
# Compute theta_hat_MLE
theta_hat_mle = (x @ y) / (x @ x)
```

```python
#@title
#@markdown Execute this cell to visualize density with theta_hat_mle

# Plot the resulting distribution density
fig, ax = plt.subplots()
ll = np.sum(np.log(likelihood(theta_hat_mle, x, y))) # log likelihood
im = plot_density_image(x, y, theta_hat_mle, ax=ax)
plt.colorbar(im, ax=ax);
ax.scatter(x, y)
ax.set(title=fr'$\hat{{\theta}}$ = {theta_hat_mle:.2f}, log likelihood: {ll:.2f}');
```

---
# Summary

- Likelihood vs probability
    - $\mathcal{L}(\theta|x, y) = p(y|\theta, x)$
    - $p(y|\theta, x)$ -> "probability of observing the response $y$ given parameter $\theta$ and input $x$"
    - $\mathcal{L}(\theta|x, y)$ -> "likelihood model that parameters $\theta$ produced response $y$ from input $x$"
- Log-likelihood maximization
    - We take the $\textrm{log}$ of the likelihood function for computational convenience
    - The parameters $\theta$ that maximize $\textrm{log}\mathcal{L}(\theta|x, y)$ are the model parameters that maximize the probability of observing the data.
- **Key point**:
  - the log-likelihood is a flexible cost function, and is often used to find model parameters that best fit the data.

---
# Appendix

We can also see $\mathrm{p}(\mathrm{y} | \mathrm{x}, \theta)$ as a function of $x$. This is the stimulus likelihood function, and it is useful in case we want to decode the input $x$ from observed responses $y$. This is what is relevant from the point of view of a neuron that does not have access to the outside world and tries to infer what's out there from the responses of other neurons!


```python
from geoscilabs.inversion.LinearInversionDirect import LinearInversionDirectApp
from ipywidgets import interact, FloatSlider, ToggleButtons, IntSlider, FloatText, IntText
```

```python
app = LinearInversionDirectApp()
```

# Linear Inversion App

This app is based upon the inversion tutorial: "INVERSION FOR APPLIED GEOPHYSICS" by Oldenburg and Li (2005).

Douglas W. Oldenburg and Yaoguo Li (2005) 5. Inversion for Applied Geophysics: A Tutorial. Near-Surface Geophysics: pp. 89-150.
eISBN: 978-1-56080-171-9
print ISBN: 978-1-56080-130-6
https://doi.org/10.1190/1.9781560801719.ch5

## Purpose

By using a simple decaying and oscillating kernel function, which emulates the physics of electromagnetic (EM) survey, we understand basic concepts of inverting data. Three items that we are going to explore are:

- Step1: Create a model ($\mathbf{m}$)
- Step2: Generate a sensitivity kernel (or matrix), $\mathbf{G}$
- Step3: Simulate data ($\mathbf{d} = \mathbf{G}\mathbf{m}$)
- Step4: All three steps together
- Step5: Invert the data, and explore inversion results

## Forward problem

Let $g_j(x)$ denote the kernel function for $j$th datum. With a given model $m(x)$, $j$th datum can be computed by solving following integral equation:

 $$ d_j = \int_a^{b} g_j(x) m(x) dx $$

where

$$ g_j(x) = e^{p_jx} cos (2 \pi q_jx) $$

By discretizing $g_j(x)$ we obtain

$$ \mathbf{g}_j(\mathbf{x}) = e^{p_j\mathbf{x}} cos (2 \pi q_j \mathbf{x})$$

where

- $\mathbf{g}_j$: $j$th row vector for the sensitivty matrix ($1 \times M$)
- $\mathbf{x}$: model location ($1 \times M$)
- $p_j$: decaying constant (<0)
- $q_j$: oscillating constant (>0)

By stacking multiple rows of $\mathbf{g}_j$, we obtain sensitivity matrix, $\mathbf{G}$:

\begin{align}
    \mathbf{G} =
    \begin{bmatrix}
        \mathbf{g}_1\\
        \vdots\\
        \mathbf{g}_{N}
    \end{bmatrix}
\end{align}

Here, the size of the matrix $\mathbf{G}$ is $(N \times M)$.
Finally data, $\mathbf{d}$, can be written as a linear equation:

$$ \mathbf{d} = \mathbf{G}\mathbf{m}$$

where $\mathbf{m}$ is an inversion model; this is a column vector ($M \times 1$).

In real measurments, there will be various noises source, and hence observation, $\mathbf{d}^{obs}$, can be written as

$$ \mathbf{d}^{obs} = \mathbf{G}\mathbf{m} + \mathbf{noise}$$

##  Step1: Create a model, $\mathbf{m}$

The model $m$ is a function defined on the interval (-2,2). Here we generate a model that is the sum of a: (a) background $m_{ref}$, (b) box car $m_1$ and (c) Gaussian $m_2$. The box car is defined by
- `m$_{background}$` : amplitude of the background
- `m1` : amplitude
- `$m1_{center}$` : center
- `$m1_{width}$` : width
the Gaussian is defined by
- `m2` : amplitude
- `$m2_{center}$` : center
- `$m2_{sigma}$` : width of Gaussian (as defined by a standard deviation)
- `M`: # of model parameters

```python
Q_model = app.interact_plot_model()
```

    interactive(children=(FloatSlider(value=0.0, continuous_update=False, description='m$_{background}$', max=2.0,…

##  Step2: Generate a sensitivity kernel (or matrix), $\mathbf{G}$

By using the following app, we explore each row vector of the sensitivity matrix, $\mathbf{g}_j$. Parameters of the apps are:

- `M`: # of model parameters
- `N`: # of data
- `p`: decaying constant (<0)
- `q`: oscillating constant (>0)
- `ymin`: maximum limit for y-axis
- `ymax`: minimum limit for y-axis
- `show_singular`: show singualr values

```python
Q_kernel = app.interact_plot_G()
```

    interactive(children=(IntSlider(value=20, continuous_update=False, description='N', min=1), IntSlider(value=10…

## Step3: Simulate data

The $j$-th datum is the inner product of the $j$-th kernel $g_j(x)$ and the model $m(x)$. In discrete form it can be written as the dot product of the vector $g_j$ and the model vector $m$.

### $$ d_j = \mathbf{g}_j \mathbf{m} $$

If there are $N$ data, these data can be written as a column vector, $\mathbf{d}$:

\begin{align}
    \mathbf{d} = \mathbf{G}\mathbf{m} =
    \begin{bmatrix}
        d_1\\
        \vdots\\
        d_{N}
    \end{bmatrix}
\end{align}

### Adding Noise

Observational data are always contaminated with noise. Here we add Gaussian noise $N(0,\epsilon)$ (zero mean and standard deviation $\sigma$). Here we choose

$$ \epsilon = \% |d| + \text{floor} $$

```python
Q_data = app.interact_plot_data()
```

    interactive(children=(Checkbox(value=False, description='add_noise'), FloatText(value=0.0, description='percen…

## Step4: All three steps together

```python
app.interact_plot_all_three_together()
```

    VBox(children=(HBox(children=(VBox(children=(Button(description='Model', layout=Layout(height='30px', width='1…

## Inverse Problem

In the inverse problem we attempt to find the model $\mathbf{m}$ that gave rise to the observational data $\mathbf{d}^{obs}$. The inverse problem is formulated as an optimization problem:

$$\text{minimize} \ \ \ \phi(\mathbf{m}) = \phi_d(\mathbf{m}) + \beta \phi_m(\mathbf{m}) $$

where

- $\phi_d$: data misfit
- $\phi_m$: model regularization
- $\beta$: trade-off (or Tikhonov) parameter  $0<\beta<\infty$

Data misfit is defined as

$$ \phi_d = \sum_{j=1}^{N}\Big(\frac{\mathbf{g}_j\mathbf{m}-d^{obs}_j}{\epsilon_j}\Big)^2$$

where $\epsilon_j$  is an estimate of the standard deviation of the $j$th datum.

The model regularization term, $\phi_m$, can be written as

$$ \phi_m(\mathbf{m}) = \alpha_s \int (\mathbf{m}-\mathbf{m}_{ref}) dx + \alpha_x \int (\frac{d \mathbf{m}}{dx}) dx$$

The first term is referred to as the "smallness" term. Minimizing this generates a model that is close to a reference model $m_{ref}$. The second term penalizes roughness of the model. It is generically referred to as a "flattest" or "smoothness" term.

## Step5: Invert the data, and explore inversion results

In the inverse problem we define parameters needed to evaluate the data misfit and the model regularization terms. We then deal with parameters associated with the inversion.

### Parameters

- `mode`: `Run` or `Explore`
    - `Run`: Each click of the app, will run `n_beta` times of inversion
    - `Explore`: Not running inversions, but explore result of the inversions

- `noise option`: `error contaminated` or `clean data`

#### Misfit
- `percent`: percentage of the uncertainty (%)

- `floor`: floor of the uncertainty (%)

- `chifact`: chi factor for stopping criteria (when $\phi_d^{\ast}=N \rightarrow$ `chifact=1`)

#### Model norm
- `mref`: reference model

- `alpha_s`: $\alpha_s$ for smallness

- `alpha_x`: $\alpha_x$ for smoothness

#### Beta
- `beta_min`: minimum $\beta$

- `beta_max`: maximum $\beta$

- `n_beta`: the number of $\beta$

#### Plotting options

- `data`: `obs & pred` or `normalized misfit`
    - `obs & pred`: show observed and predicted data
    - `normalized misfit`: show normalized misfit

- `tikhonov`: `phi_d & phi_m` or `phi_d vs phi_m`
    - `phi_d & phi_m`: show $\phi_d$ and $\phi_m$ as a function of $\beta$
    - `phi_d vs phi_m`: show tikhonov curve

- `i_beta`: i-th $\beta$ value

- `scale`: `linear` or `log`
    - `linear`: linear scale for plotting the third panel
    - `log`: log scale for plotting the third panel

```python
app.interact_plot_inversion()
```

    VBox(children=(HBox(children=(RadioButtons(description='mode', options=('Run', 'Explore'), value='Run'), Radio…


# Entropic Portfolio Optimization

In this notebook we show how to use the exponential cone to model the perspective of the `log_sum_exp` function and its application in portfolio optimization.

## 1. Entropic Value at Risk Optimization

### 1.1 The Entropic Value at Risk

The Entropic Value at Risk (EVaR) which is a new risk measure introduced by __[Ahmadi-Javid (2012)](https://link.springer.com/article/10.1007/s10957-011-9968-2?r=1&l=ri&fst=0&error=cookies_not_supported&code=ccfb8a5e-692b-43d1-b76e-ae596c7f0bed)__. It is the upper bound based on Chernoff Inequality of Value at Risk (VaR) and Conditional Value at Risk (CVaR), formally it is defined as:

$$
\text{EVaR}_{\alpha}(X) = \inf_{z>0} \left \{z\log \left ( \frac{1}{\alpha} M_{X} (\frac{1}{z}) \right ) \right \}
$$

Where $M_{X} (t) = \text{E} [e^{tX}]$ is the moment generating function and $\alpha \in [0,1]$ is the significance level.

### 1.2 EVaR Minimization

To discretize the EVaR we need the perspective of the `log_sum_exp` function, we can do this using the exponential cone in CVXPY. The discipined convex programming (DCP) problem of EVaR minimization was proposed by __[Cajas (2021)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3792520)__ and it is posed as:

$$
\begin{equation}
\begin{aligned}
& \underset{x, \, z, \, t, \, u}{\text{min}} & &  t + z \ln \left (  \frac{1}{\alpha T} \right ) \\
& \text{s.t.} & & \mu x^{\tau} \geq \bar{\mu} \\
& & &  \sum_{i=1}^{N} x_i = 1 \\
& & & z \geq \sum^{T}_{j=1} u_{j} \\
& & & (-r_{j}x^{\tau}-t, z, u_{j}) \in K_{\text{exp}} \; \forall \; j=1, \ldots, T \\
& & &  x_i \geq 0 \; ; \; \forall \; i =1, \ldots, N \\
\end{aligned}
\end{equation}
$$

Where $t$ is an auxiliar variable that represents the perspectives of the `log_sum_exp` function, $z$ is the factor of perspective function, $u_{j}$ is an auxiliary variable, $x$ are the weights of assets, $\mu$ is the mean vector of expected returns, $\bar{\mu}$ the minimum expected return of portfolio, $K_{\text{exp}}$ is an exponential cone and $r$ is the matrix of observed returns.

```python
####################################
# Downloading Data
####################################
!pip install --quiet yfinance

import numpy as np
import pandas as pd
import yfinance as yf
import warnings

warnings.filterwarnings("ignore")

yf.pdr_override()
pd.options.display.float_format = '{:.4%}'.format

# Date range
start = '2016-01-01'
end = '2019-12-30'

# Tickers of assets
assets = ['TGT', 'CMCSA', 'CPB', 'MO', 'T', 'BAX', 'BMY',
          'MSFT', 'SEE', 'VZ', 'CNP', 'NI', 'GE', 'GOOG']
assets.sort()

# Downloading data
data = yf.download(assets, start = start, end = end)
data = data.loc[:,('Adj Close', slice(None))]
data.columns = assets

# Calculating returns
Y = data[assets].pct_change().dropna()

display(Y.head())
```

    [*********************100%***********************]  14 of 14 completed

      BAX
      BMY
      CMCSA
      CNP
      CPB
      GE
      GOOG
      MO
      MSFT
      NI
      SEE
      T
      TGT
      VZ

      Date

      2016-01-05
      0.4036%
      1.9693%
      0.0180%
      0.9305%
      0.3678%
      0.0977%
      0.0998%
      2.0213%
      0.4562%
      1.5881%
      0.9758%
      0.6987%
      1.7539%
      1.3735%

      2016-01-06
      0.2412%
      -1.7556%
      -0.7727%
      -1.2473%
      -0.1736%
      -1.5940%
      0.1400%
      1.0589%
      -1.8165%
      0.5548%
      -1.5647%
      -0.1466%
      -1.0155%
      -0.9034%

      2016-01-07
      -1.6573%
      -2.7699%
      -1.1047%
      -1.9769%
      -1.2207%
      -4.2314%
      -2.3170%
      -1.7408%
      -3.4783%
      -2.2066%
      -3.1557%
      -1.6148%
      -0.2700%
      -0.5492%

      2016-01-08
      -1.6037%
      -2.5425%
      0.1099%
      -0.2241%
      0.5707%
      -1.7950%
      -1.6410%
      0.1720%
      0.3067%
      -0.1539%
      -0.1448%
      0.0895%
      -3.3838%
      -0.9719%

      2016-01-11
      -1.6851%
      -1.0215%
      0.0915%
      -1.1791%
      0.5674%
      0.4569%
      0.2184%
      2.0948%
      -0.0573%
      1.6436%
      -0.1451%
      1.2224%
      1.4570%
      0.5800%

```python
####################################
# Finding the Min EVaR Portfolio
####################################

import cvxpy as cp

# Defining initial inputs
mu = Y.mean().to_numpy().reshape(1,-1)
returns = Y.to_numpy()
n = returns.shape[0]

# Defining initial variables
w = cp.Variable((mu.shape[1], 1))
alpha = 0.05
ret = mu @ w
X = returns @ w

# Entropic Value at Risk Model Variables
t = cp.Variable((1, 1))
z = cp.Variable((1, 1), nonneg=True)
ui = cp.Variable((n, 1))
constraints = [cp.sum(ui) <= z,
               cp.constraints.ExpCone(-X - t, np.ones((n, 1)) @ z, ui)] # Exponential cone constraint

# Budget and weights constraints
constraints += [cp.sum(w) == 1,
                w >= 0]

# Defining risk objective
risk = t + z * np.log(1 / (alpha * n))
objective = cp.Minimize(risk)

# Solving problem
prob = cp.Problem(objective, constraints)
prob.solve()

# Showing Optimal Weights
weights = pd.DataFrame(w.value, index=assets, columns=['Weights'])
display(weights)
```

      Weights

      BAX
      5.8220%

      BMY
      8.9075%

      CMCSA
      7.9345%

      CNP
      15.6326%

      CPB
      15.5868%

      GE
      0.0000%

      GOOG
      4.7998%

      MO
      1.8486%

      MSFT
      0.0000%

      NI
      12.4118%

      SEE
      7.5494%

      T
      1.1044%

      TGT
      17.1416%

      VZ
      1.2611%

## 2. Entropic Drawdown at Risk Optimization

### 2.1 The Entropic Drawdown at Risk

The Entropic Drawdown at Risk (EDaR) which is a new risk measure introduced by __[Cajas (2021)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3792520)__. It is the upper bound based on Chernoff Inequality of Drawdown at Risk (DaR) and Conditional Drawdown at Risk (CDaR), formally it is defined as:

$$
\begin{equation}
\begin{aligned}
\text{EDaR}_{\alpha}(X) & =  \text{EVaR}_{\alpha}(\text{DD}(X)) \\
\text{EDaR}_{\alpha}(X) & = \inf_{z>0} \left \{ z \ln \left (\frac{1}{\alpha}M_{\text{DD}(X)} \left (\frac{1}{z} \right ) \right ) \right \}  \\
\end{aligned}
\end{equation}
$$

where $M_{X}(t)$ is the moment generating function of $t$, $\alpha \in [0,1]$ is the significance level and $\text{DD}(X)$ is the drawdown of $X$.

### 2.2 Maximization of Return/EDaR ratio

This problem is a __[linear fractional programming](https://en.wikipedia.org/wiki/Linear-fractional_programming)__ problem and can be converted to a DCP problem using __Charnes and Cooper transformation__. The DCP problem of maximization of return EDaR ratio was proposed by __[Cajas (2021)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3792520)__ and it is posed as:

$$
\begin{equation}
\begin{aligned}
& \underset{y, \, k, \, z, \, t, \, u, \, d}{\text{min}} & &   t + z \ln \left (  \frac{1}{\alpha T} \right )\\
& \text{s.t.} & &  \mu y^{\tau} - r_{f} k= 1 \\
& & &  \sum_{i=1}^{N} y_{i} = k \\
& & & z \geq \sum^{T}_{j=1} u_{j} \\
& & & (d_{j} - R_{j} y^{\tau} - t, z, u_{j}) \in K_{\text{exp}} \; \forall \; j =1, \ldots, T \\
& & & d_{j} \geq R_{j} y^{\tau} \; \forall \; j=1, \ldots, T \\
& & & d_{j} \geq d_{j-1}  \; \forall \; j=1, \ldots, T \\
& & & d_{j} \geq 0  \; \forall \; j=1, \ldots, T \\
& & & d_{0} = 0 \\
& & &  k \geq 0 \\
& & &  y_{i} \geq 0 \; ; \; \forall \; i =1, \ldots, N \\
\end{aligned}
\end{equation}
$$

where $R_{j} x^{\tau} = \sum^{j}_{i=1} r_{i} x^{\tau}$, $d_{j}$ is a variable that represents the uncompounded cumulative return of the portfolio and $r_{f}$ is the risk free rate.

Finally, the optimal portfolio is obtained making the transformation $x = y / k$.

```python
#######################################
# Finding the max return/EDaR Portfolio
#######################################

# Defining initial inputs
mu = Y.mean().to_numpy().reshape(1,-1)
returns = Y.to_numpy()
nav = Y.cumsum().to_numpy()
n = returns.shape[0]

# Defining initial variables
w = cp.Variable((mu.shape[1], 1))
k = cp.Variable((1, 1))
rf0 = 0
alpha = 0.05
ret = mu @ w
X1 = nav @ w

# Drawdown Variables
d = cp.Variable((nav.shape[0] + 1, 1))
constraints = [d[1:] >= X1,
               d[1:] >= d[:-1],
               d[1:] >= 0,
               d[0] == 0]

# Entropic Drawdown at Risk Model Variables
t = cp.Variable((1, 1))
z = cp.Variable((1, 1), nonneg=True)
ui = cp.Variable((n, 1))
constraints += [cp.sum(ui) <= z,
                cp.constraints.ExpCone(d[1:] - X1 - t, np.ones((n, 1)) @ z, ui)] # Exponential cone constraint

# Budget and weights constraints
constraints += [cp.sum(w) == k,
                ret - rf0 * k == 1,
                w >= 0,
                k >= 0]

# Defining risk objective
risk = t + z * np.log(1 / (alpha * n))
objective = cp.Minimize(risk)

# Solving problem
prob = cp.Problem(objective, constraints)
prob.solve()

# Showing Optimal Weights
weights = pd.DataFrame(w.value/k.value, index=assets, columns=['Weights'])
display(weights)
```

      Weights

      BAX
      0.6007%

      BMY
      0.0000%

      CMCSA
      0.0000%

      CNP
      40.7637%

      CPB
      0.0000%

      GE
      0.0000%

      GOOG
      0.0000%

      MO
      0.0000%

      MSFT
      53.4767%

      NI
      0.0000%

      SEE
      0.0000%

      T
      0.0000%

      TGT
      1.4542%

      VZ
      3.7048%

For more portfolio optimization models and applications, you can see the CVXPY based library __[Riskfolio-Lib](https://github.com/dcajasn/Riskfolio-Lib)__.


# Reduced Helmholtz equation of state: carbon dioxide

**Water equation of state:** You can see the full, state-of-the-art equation of state for water, which also uses a reduced Helmholtz approach: the IAPWS 1995 formulation (Wagner 2002). This equation is state is available using CoolProp with the `Water` fluid.

One modern approach for calculating thermodynamic properties of real fluids uses a reduced Helmholtz equation of state, using the reduced Helmholtz free energy function $\alpha$:
\begin{equation}
\alpha (\tau, \delta) = \frac{a}{RT} = \frac{u - Ts}{RT}
\end{equation}
which is a function of reduced density $\delta$ and reduced temperature $\tau$:
\begin{equation}
\delta = \frac{\rho}{\rho_{\text{crit}}} \quad \text{and} \quad \tau = \frac{T_{\text{crit}}}{T}
\end{equation}

The reduced Helmhotz free energy function, $\alpha(\tau, \delta)$, is given as the sum of ideal gas and residual components:
\begin{equation}
\alpha(\tau, \delta) = \alpha_{IG} (\tau, \delta) + \alpha_{\text{res}} (\tau, \delta) \;,
\end{equation}
which are both given as high-order fits using many coefficients:

```python
import matplotlib.pyplot as plt
%matplotlib inline

# these are mostly for making the saved figures nicer
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('pdf', 'png')
plt.rcParams['figure.dpi']= 150
plt.rcParams['savefig.dpi'] = 150

import numpy as np
import cantera as ct
from scipy import integrate, optimize

from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity
```

```python
import sympy
sympy.init_printing(use_latex='mathjax')

T, R, tau, delta = sympy.symbols('T, R, tau, delta', real=True)

a_vars = sympy.symbols('a0, a1, a2, a3, a4, a5, a6, a7', real=True)
theta_vars = sympy.symbols('theta3, theta4, theta5, theta6, theta7', real=True)
n_vars = sympy.symbols('n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10, n11', real=True)

alpha_ideal = sympy.log(delta) + a_vars[0] + a_vars[1]*tau + a_vars[2]*sympy.log(tau)
for i in range(3, 8):
    alpha_ideal += a_vars[i] * sympy.log(1.0 - sympy.exp(-tau * theta_vars[i-3]))
display(sympy.Eq(sympy.symbols('alpha_IG'), alpha_ideal))

alpha_res = (
    n_vars[0] * delta * tau**0.25 +
    n_vars[1] * delta * tau**1.25 +
    n_vars[2] * delta * tau**1.50 +
    n_vars[3] * delta**3 * tau**0.25 +
    n_vars[4] * delta**7 * tau**0.875 +
    n_vars[5] * delta * tau**2.375 * sympy.exp(-delta) +
    n_vars[6] * delta**2 * tau**2 * sympy.exp(-delta) +
    n_vars[7] * delta**5 * tau**2.125 * sympy.exp(-delta) +
    n_vars[8] * delta * tau**3.5 * sympy.exp(-delta**2) +
    n_vars[9] * delta * tau**6.5 * sympy.exp(-delta**2) +
    n_vars[10] * delta**4 * tau**4.75 * sympy.exp(-delta**2) +
    n_vars[11] * delta**2 * tau**12.5 * sympy.exp(-delta**3)
    )
display(sympy.Eq(sympy.symbols('alpha_res'), alpha_res))
```

$\displaystyle \alpha_{IG} = a_{0} + a_{1} \tau + a_{2} \log{\left(\tau \right)} + a_{3} \log{\left(1.0 - e^{- \tau \theta_{3}} \right)} + a_{4} \log{\left(1.0 - e^{- \tau \theta_{4}} \right)} + a_{5} \log{\left(1.0 - e^{- \tau \theta_{5}} \right)} + a_{6} \log{\left(1.0 - e^{- \tau \theta_{6}} \right)} + a_{7} \log{\left(1.0 - e^{- \tau \theta_{7}} \right)} + \log{\left(\delta \right)}$

$\displaystyle \alpha_{res} = \delta^{7} n_{4} \tau^{0.875} + \delta^{5} n_{7} \tau^{2.125} e^{- \delta} + \delta^{4} n_{10} \tau^{4.75} e^{- \delta^{2}} + \delta^{3} n_{3} \tau^{0.25} + \delta^{2} n_{11} \tau^{12.5} e^{- \delta^{3}} + \delta^{2} n_{6} \tau^{2} e^{- \delta} + \delta n_{0} \tau^{0.25} + \delta n_{1} \tau^{1.25} + \delta n_{2} \tau^{1.5} + \delta n_{5} \tau^{2.375} e^{- \delta} + \delta n_{8} \tau^{3.5} e^{- \delta^{2}} + \delta n_{9} \tau^{6.5} e^{- \delta^{2}}$

## Carbon dioxide equation of state

The coefficients $a_i$, $\theta_i$, and $n_i$ are given for carbon dioxide:

```python
# actual coefficients
coeffs_a = [
    8.37304456, -3.70454304, 2.500000, 1.99427042,
    0.62105248, 0.41195293, 1.04028922, 8.327678e-2
    ]
coeffs_theta = [
    3.151630, 6.111900, 6.777080, 11.32384, 27.08792
    ]
coeffs_n = [
    0.89875108, -0.21281985e1, -0.68190320e-1, 0.76355306e-1,
    0.22053253e-3, 0.41541823, 0.71335657, 0.30354234e-3,
    -0.36643143, -0.14407781e-2, -0.89166707e-1, -0.23699887e-1
    ]
```

Through some math, we can find an expression for pressure:
\begin{equation}
P = R T \rho \left[ 1 + \delta \left(\frac{\partial \alpha_{\text{res}}}{\partial \delta} \right)_{\tau} \right]
\end{equation}

Use this expression to estimate the pressure at $T$ = 350 K and $v$ = 0.01 m$^3$/kg, and compare against that obtained from Cantera. We can use our symbolic expression for $\alpha_{\text{res}} (\tau, \delta)$ and take the partial derivative:

```python
# use Cantera fluid to get specific gas constant and critical properties
f = ct.CarbonDioxide()
gas_constant = ct.gas_constant / f.mean_molecular_weight
temp_crit = f.critical_temperature
density_crit = f.critical_density

# conditions of interest
temp = 350
specific_volume = 0.01
density = 1.0 / specific_volume

# take the partial derivative of alpha_res with respect to delta
derivative_alpha_delta = sympy.diff(alpha_res, delta)

# substitute all coefficients
derivative_alpha_delta = derivative_alpha_delta.subs(
    [(n, n_val) for n, n_val in zip(n_vars, coeffs_n)]
    )

def get_pressure(
    temp, specific_vol, fluid, derivative_alpha_delta, tau, delta
    ):
    '''Calculates pressure for reduced Helmholtz equation of state'''
    red_density = (1.0 / specific_vol) / fluid.critical_density
    red_temp_inv = fluid.critical_temperature / temp

    gas_constant = ct.gas_constant / fluid.mean_molecular_weight

    dalpha_ddelta = derivative_alpha_delta.subs(
        [(delta, red_density), (tau, red_temp_inv)]
        )

    pres = (
        gas_constant * temp * (1.0 / specific_vol) *
        (1.0 + red_density * dalpha_ddelta)
        )
    return pres

pres = get_pressure(temp, specific_volume, f, derivative_alpha_delta, tau, delta)

print(f'Pressure: {pres / 1e6: .3f} MPa')

f.TV = temp, specific_volume
print(f'Cantera pressure: {f.P / 1e6: .3f} MPa')
```

    Pressure:  5.464 MPa
    Cantera pressure:  5.475 MPa

Our calculation and that from Cantera agree fairly well! They are not exactly the same because Cantera uses a slightly different formulation for the equation of state.

Let's compare the calculations now for a range of specific volumes and multiple temperatures:

```python
fig, ax = plt.subplots(figsize=(8, 4))

specific_volumes = np.geomspace(0.001, 0.01, num=20)
temperatures = [300, 400, 500]

for temp in temperatures:
    pressures = np.zeros(len(specific_volumes))
    pressures_cantera = np.zeros(len(specific_volumes))

    for idx, spec_vol in enumerate(specific_volumes):
        pressures[idx] = get_pressure(
            temp, spec_vol, f,
            derivative_alpha_delta, tau, delta
            )

        f.TV = temp, spec_vol
        pressures_cantera[idx] = f.P

    ax.loglog(specific_volumes, pressures/1000., 'o', color='blue')
    ax.loglog(specific_volumes, pressures_cantera/1000., color='blue')

bbox_props = dict(boxstyle='round', fc='w', ec='0.3', alpha=0.9)
ax.text(2e-3, 4e3, '300 K', ha='center', va='center', bbox=bbox_props)
ax.text(2e-3, 1.6e4, '400 K', ha='center', va='center', bbox=bbox_props)
ax.text(2e-3, 7e4, '500 K', ha='center', va='center', bbox=bbox_props)

ax.legend(['Reduced Helmholtz', 'Cantera'])
plt.grid(True, which='both')
plt.xlabel('Specific volume (m^3/kg)')
plt.ylabel('Pressure (kPa)')
fig.tight_layout()
plt.show()
```

We can see that the pressure calculated using the reduced Helmholtz equation of state matches closely with that from Cantera, which uses a different but similarly advanced equation of state.

## Bibliography

Wagner, W., & Pruß, A. (2002). The IAPWS Formulation 1995 for the Thermodynamic Properties of Ordinary Water Substance for General and Scientific Use. Journal of Physical and Chemical Reference Data, 31(2), 387–535. https://doi.org/10.1063/1.1461829


Computational Seismology

Finite Element Method - 1D Elastic Wave Equation

---

This notebook is part of the supplementary material
to [Computational Seismology: A Practical Introduction](https://global.oup.com/academic/product/computational-seismology-9780198717416?cc=de&lang=en&#),
Oxford University Press, 2016.

##### Authors:
* David Vargas ([@dvargas](https://github.com/davofis))
* Heiner Igel ([@heinerigel](https://github.com/heinerigel))

## Basic Equations
This notebook presents a finite element code for the 1D elastic wave equation. Additionally, a solution using finite difference scheme is given for comparison.

The problem of solving the wave equation

\begin{equation}
\rho(x) \partial_t^2 u(x,t) = \partial_x (\mu(x) \partial_x u(x,t)) + f(x,t)
\end{equation}

using the finite element method is done after a series of steps performed on the above equation.

1) We first obtain a weak form of the wave equation by integrating over the entire physical domain $D$ and at the same time multiplying by some basis $\varphi_{i}$.

2) Integration by parts and implementation of the stress-free boundary condition is performed.

3) We approximate our unknown displacement field $u(x, t)$ by a sum over space-dependent basis functions $\varphi_i$ weighted by time-dependent coefficients $u_i(t)$.

\begin{equation}
u(x,t) \ \approx \ \overline{u}(x,t) \ = \ \sum_{i=1}^{n} u_i(t) \ \varphi_i(x)
\end{equation}

4) Utilize the same basis functions used to expand $u(x, t)$ as test functions in the weak form, this is the Galerkin principle.

5) We can turn the continuous weak form into a system of linear equations by considering the approximated displacement field.

\begin{equation}
\mathbf{M}^T\partial_t^2 \mathbf{u} + \mathbf{K}^T\mathbf{u} = \mathbf{f}
\end{equation}

6) For the second time-derivative, we use a standard finite-difference approximation. Finally, we arrive at the explicit time extrapolation scheme.

\begin{equation}
\mathbf{u}(t + dt) = dt^2 (\mathbf{M}^T)^{-1}[\mathbf{f} - \mathbf{K}^T\mathbf{u}] + 2\mathbf{u} - \mathbf{u}(t-dt).
\end{equation}

where $\mathbf{M}$ is known as the mass matrix, and $\mathbf{K}$ the stiffness matrix.

7) As interpolating functions, we choose interpolants such that $\varphi_{i}(x_{i}) = 1$ and zero elsewhere. Then, we transform the space coordinate into a local system. According to $\xi = x − x_{i}$ and $h_{i} = x_{i+1} − x_{i}$, we have:

\begin{equation}
 \varphi_{i}(\xi) =
  \begin{cases}
    \frac{\xi}{h_{i-1}} + 1  & \quad \text{if} \quad -h_{i-1} \le \xi \le 0\\
    1 + \frac{\xi}{h_{i}}    & \quad \text{if} \quad 0 \le \xi \le h_{i}\\
    0  & \quad elsewhere\\
  \end{cases}
\end{equation}

with the corresponding derivatives

\begin{equation}
 \partial_{\xi}\varphi_{i}(\xi) =
  \begin{cases}
    \frac{1}{h_{i-1}}  & \quad \text{if} \quad -h_{i-1} \le \xi \le 0\\
   -\frac{1}{h_{i}}    & \quad \text{if} \quad 0 \le \xi \le h_{i}\\
    0  & \quad elsewhere\\
  \end{cases}
\end{equation}

The figure on the left-hand side illustrates the shape of $\varphi_{i}(\xi)$ and $\partial_{\xi}\varphi_{i}(\xi)$ with varying $h$.

Code implementation starts with the initialization of a particular setup of our problem. Then, we define the source that introduces perturbations following by initialization of the mass and stiffness matrices. Finally, time extrapolation is done.

```
# Import all necessary libraries, this is a configuration step for the exercise.
# Please run it before the simulation code!
import numpy as np
import matplotlib
# Show Plot in The Notebook
matplotlib.use("nbagg")
import matplotlib.pyplot as plt
import matplotlib.animation as animation

```

### 1. Initialization of setup

```
# Initialization of setup
# ---------------------------------------------------------------
# Basic parameters
nt    = 2000    # Number of time steps
vs    = 3000    # Wave velocity [m/s]
ro0   = 2500    # Density [kg/m^3]
nx    = 1000    # Number of grid points
isx   = 500     # Source location [m]
xmax  = 10000.  # Maximum length
eps   = 0.5     # Stability limit
iplot = 20      # Snapshot frequency

dx = xmax/(nx-1)           # calculate space increment
x  = np.arange(0, nx)*dx   # initialize space coordinates
x  = np.transpose(x)

h = np.diff(x)  # Element sizes [m]

# parameters
ro = x*0 + ro0
mu = x*0 + ro*vs**2

# time step from stabiity criterion
dt = 0.5*eps*dx/np.max(np.sqrt(mu/ro))
# initialize time axis
t   = np.arange(1, nt+1)*dt

# ---------------------------------------------------------------
# Initialize fields
# ---------------------------------------------------------------
u    = np.zeros(nx)
uold = np.zeros(nx)
unew = np.zeros(nx)

p    = np.zeros(nx)
pold = np.zeros(nx)
pnew = np.zeros(nx)
```

### 2. Source time function

In 1D the propagating signal is an integral of the source time function. As we look for a Gaussian waveform, we initialize the source time function $f(t)$ using the first derivative of a Gaussian function.

\begin{equation}
f(t) = -\dfrac{2}{\sigma^2}(t - t_0)e^{-\dfrac{(t - t_0)^2}{\sigma^2}}
\end{equation}

#### Exercise 1
Initialize a source time function called 'src'.  Use $\sigma = 20 dt$ as Gaussian width, and time shift $t_0 = 3\sigma$. Then, visualize the source in a given plot.

```

```

#### Solutions:

```
# Initialization of the source time function
# ---------------------------------------------------------------
pt  = 20*dt     # Gaussian width
t0  = 3*pt      # Time shift
src = -2/pt**2 * (t-t0) * np.exp(-1/pt**2 * (t-t0)**2)

# Source vector
# ---------------------------------------------------------------
f = np.zeros(nx); f[isx:isx+1] = f[isx:isx+1] + 1.

# ---------------------------------------------------------------
# Plot source time fuction
# ---------------------------------------------------------------
plt.plot(t, src, color='b', lw=2, label='Source time function')
plt.ylabel('Amplitude', size=16)
plt.xlabel('time', size=16)
plt.legend()
plt.grid(True)
plt.show()
```

### 3. The Mass Matrix
Having implemented the desired source, now we initialize the mass and stiffness matrices. In general, the mass matrix is given

\begin{equation}
M_{ij} = \int_{D} \rho \varphi_i \varphi_j dx = \int_{D_{\xi}} \rho \varphi_i \varphi_j d\xi
\end{equation}

next, the defined basis are introduced and some algebraic treatment is done to arrive at the explicit form of the mass matrix

#### Exercise 2
Implement the mass matrix

\begin{equation}
M_{ij} = \frac{\rho h}{6}
 \begin{pmatrix}
   \ddots  &    &    &    & 0\\
   1 & 4 &  1 &    &  \\
     & 1 &  4 &  1 &  \\
     &   &  1 &  4 & 1\\
   0 &   &    &    &  \ddots
 \end{pmatrix}
\end{equation}

Compute the inverse mass matrix and display your result to visually inspect how it looks like

```

```

#### Solutions:

```
# ---------------------------------------------------------------
# Mass matrix M_ij (Eq 6.56)
# ---------------------------------------------------------------
M = np.zeros((nx,nx), dtype=float)
for i in range(1, nx-1):
    for j in range (1, nx-1):
        if j==i:
            M[i,j] = (ro[i-1]*h[i-1] + ro[i]*h[i])/3
        elif j==i+1:
            M[i,j] = ro[i]*h[i]/6
        elif j==i-1:
            M[i,j] = ro[i-1]*h[i-1]/6
        else:
            M[i,j] = 0

# Corner elements
M[0,0] = ro[0]*h[0]/3
M[nx-1,nx-1] = ro[nx-1]*h[nx-2]/3
# Invert M
Minv = np.linalg.inv(M)

# ---------------------------------------------------------------
# Display inverse mass matrix inv(M)
# ---------------------------------------------------------------
plt.imshow(Minv)
plt.title('Mass Matrix $\mathbf{M}$')
plt.axis("off")
plt.tight_layout()
plt.show()
```

### 4. The Stiffness matrix
On the other hand, the general form of the stiffness matrix is

\begin{equation}
K_{ij} = \int_{D} \mu \partial_x\varphi_i \partial_x\varphi_j dx = \int_{D_{\xi}} \mu \partial_\xi\varphi_i \partial_\xi\varphi_j d\xi
\end{equation}

at this point, the defined basis are introduced. Again, with the help of some algebraic treatment, we arrive at the explicit form of the stiffness matrix

#### Exercise 3
Implement the stiffness matrix

\begin{equation}
K_{ij} = \frac{\mu}{h}
 \begin{pmatrix}
   \ddots  &    &    &    & 0\\
  -1 & 2 & -1 &    &  \\
     &-1 &  2 & -1 &  \\
     &   & -1 &  2 & -1\\
   0 &   &    &    &  \ddots
 \end{pmatrix}
\end{equation}

Display the stiffness matrix to visually inspect how it looks like

```

```

#### Solutions

```
# ---------------------------------------------------------------
# Stiffness matrix Kij (Eq 6.60)
# ---------------------------------------------------------------
K = np.zeros((nx,nx), dtype=float)
for i in range(1, nx-1):
    for j in range(1, nx-1):
        if i==j:
            K[i,j] = mu[i-1]/h[i-1] + mu[i]/h[i]
        elif i==j+1:
            K[i,j] = -mu[i-1]/h[i-1]
        elif i+1==j:
            K[i,j] = -mu[i]/h[i]
        else:
            K[i,j] = 0

K[0,0] = mu[0]/h[0]
K[nx-1,nx-1] = mu[nx-1]/h[nx-2]

# ---------------------------------------------------------------
# Display stiffness matrix K
# ---------------------------------------------------------------
plt.imshow(K)
plt.title('Stiffness Matrix $\mathbf{K}$')
plt.axis("off")

plt.tight_layout()
plt.show()
```

### 5. Finite differences matrices
We implement a finite difference scheme in order to compare with the finite elements solution.

#### Exercise 4
Implement the finite differences matrices $M$ and $D$. Where $M$ is a diagonal mass matrix containing the inverse densities, and differentiation matrix

\begin{equation}
D_{ij} = \frac{\mu}{dt^2}
 \begin{pmatrix}
   -2 & 1  &    &    &  \\
    1 & -2 & 1  &    &  \\
      &  & \ddots  &  & \\
      &   & 1 &  -2 & 1\\
      &   &    &  1 & -2
 \end{pmatrix}
\end{equation}

Display both matrices to visually inspect how they look like

```

```

#### Solutions:

```
# Initialize finite differences matrices (Eq 6.61)
# ---------------------------------------------------------------
Mf = np.zeros((nx,nx), dtype=float)
D  = np.zeros((nx,nx), dtype=float)
dx = h[1]

for i in range(nx):
    Mf[i,i] = 1./ro[i]
    if i>0:
        if i<nx-1:
            D[i+1,i] =1
            D[i-1,i] =1
            D[i,i] = -2

D = ro0 * vs**2 * D/dx**2

# ---------------------------------------------------------------
# Display differences matrices
# ---------------------------------------------------------------
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(D)
ax1.set_title('Stiffness Differential Matrix $\mathbf{D}$')
ax1.axis("off")

ax2.imshow(Mf)
ax2.set_title('Stiffness Differential Matrix $\mathbf{M_f}$')
ax2.axis("off")

plt.tight_layout()
plt.show()
```

### 6. Finite element solution

Finally we implement the finite element solution using the computed mass $M$ and stiffness $K$ matrices together with a finite differences extrapolation scheme

\begin{equation}
\mathbf{u}(t + dt) = dt^2 (\mathbf{M}^T)^{-1}[\mathbf{f} - \mathbf{K}^T\mathbf{u}] + 2\mathbf{u} - \mathbf{u}(t-dt).
\end{equation}

```
# Initialize animated plot
# ---------------------------------------------------------------
plt.figure(figsize=(12,4))

line1 = plt.plot(x, u, 'k', lw=1.5, label='FEM')
line2 = plt.plot(x, p, 'r', lw=1.5, label='FDM')
plt.title('Finite elements 1D Animation', fontsize=16)
plt.ylabel('Amplitude', fontsize=12)
plt.xlabel('x (m)', fontsize=12)

plt.ion()   # set interective mode
plt.show()

# ---------------------------------------------------------------
# Time extrapolation
# ---------------------------------------------------------------
for it in range(nt):
    # --------------------------------------
    # Finite Element Method
    unew = (dt**2) * Minv @ (f*src[it] - K @ u) + 2*u - uold
    uold, u = u, unew

    # --------------------------------------
    # Finite Difference Method
    pnew = (dt**2) * Mf @ (D @ p + f/dx*src[it]) + 2*p - pold
    pold, p = p, pnew

    # --------------------------------------
    # Animation plot. Display both solutions
    if not it % iplot:
        for l in line1:
            l.remove()
            del l
        for l in line2:
            l.remove()
            del l
        line1 = plt.plot(x, u, 'k', lw=1.5, label='FEM')
        line2 = plt.plot(x, p, 'r', lw=1.5, label='FDM')
        plt.legend()
        plt.gcf().canvas.draw()
```

```

```

notebooks/Computational Seismology/The Finite-Element Method/fe_elastic_1d_exercise.ipynb


# Assignment 2 | Programming Logic

Reminder: in all of the assignments this semester, the answer is not the only consideration, but also how you get to it. It's OK (suggested even!) to use the internet for help. But you _should_ be able to answer all of these questions using only the programming techniques you have learned in class and from the readings.

A few keys for success:
- Avoid manual data entry
- Emphasize logic and clarity
- Use comments, docstrings, and descriptive variable names
- In general, less code is better. But if more lines of code makes your program easier to read or understand what its doing, then go for it.

## Problem 1
Write a Python program to count the number of even and odd numbers from a list of numbers. Test your code by running it on a list of integers from 1 to 9. No need to make this a function unless you want to.

```python
odd = []
even = []
for i in range(1,10):
    if i % 2 == 1:
        odd.append(i)
    else:
        even.append(i)

print("There are " + str(len(odd)) + " odd numbers and " + str(len(even)) + " even numbers")
```

    There are 5 odd numbers and 4 even numbers

## Problem 2
Write a Python function that takes a list of numbers and returns a list containing only the even numbers from the original list. Test your function by running it on a list of integers from 1 to 9.

```python
numlist = [x for x in range(1,10)]
even2 = []

def only_evens(e):
    for f in numlist:
        if f % 2 == 0:
            even2.append(f)
    return even2

only_evens(numlist)
```

    [2, 4, 6, 8]

```python
def only_even(q):
    return [g for g in range(1,q) if g%2 == 0]

only_even(10)

```

    [2, 4, 6, 8]

## Problem 3

1. Create a function that accepts a list of integers as an argument and returns a list of floats which equals each number as a fraction of the sum of all the items in the original list.

2. Next, create a second function which is the same as the first, but limit each number in the output list to two decimals.

3. Create another function which builds on the previous one by allowing a "user" pass in an argument that defines the number of decimal places to use in the output list.

4. Test each of these functions with a list of integers

```python
##Problem 3.1
test_nums = [7,10,34,12]
out_list = []

def floating(away):
    for i in away:
        out_list.append(i/sum(test_nums))
    return out_list
floating(test_nums)
```

    [0.1111111111111111,
     0.15873015873015872,
     0.5396825396825397,
     0.19047619047619047]

```python
##Problem 3.2
test_nums = [7,10,34,12]
out_list = []

def floating(away):
    for i in away:
        out_list.append(round(i/sum(test_nums),2))
    return out_list
floating(test_nums)
```

    [0.11, 0.16, 0.54, 0.19]

```python
##Problem 3.3
test_nums = [7,10,34,12]
out_list = []
prompt = "How many decimal places should we round to? "

def floating(away):
    round_places = int(input(prompt))
    for i in away:
        out_list.append(round(i/sum(test_nums),round_places))
    return out_list

floating(test_nums)
```

    How many decimal places should we round to? 4

    [0.1111, 0.1587, 0.5397, 0.1905]

## Problem 4
A prime number is any whole number greater than 1 that has no positive divisors besides 1 and itself. In other words, a prime number must be:
1. an integer
2. greater than 1
3. divisible only by 1 and itself.

Write a function is_prime(n) that accepts an argument `n` and returns `True` (boolean) if `n` is a prime number and `False` if n is not prime. For example, `is_prime(11)` should return `True` and `is_prime(12)` should return `False`.

```python
from sympy import *
n = 11
isprime(n)
```

    True

```python
def is_prime(n):
    return n > 1 and all(n % i for i in range(2, int(n ** 0.5) + 1))

isprime(9)
```

    False

## Problem 5

1. Create a class called `Housing`, and add the following attributes to it:
  - type
  - area
  - number of bedrooms
  - value (price)
  - year built.
2. Create two instances of your class and populate their attributes (make 'em up)
3. Create a method called `rent()` that calculates the estimated monthly rent for each house (assume that monthly rent is 0.4% of the value of the house)
4. Print the rent for both instances.

```python
class Housing:
    def __init__(self,style,area,num_beds,price,yr_built):
        self.style = style
        self.area = area
        self.num_beds = num_beds
        self.price = price
        self.yr_built = yr_built

    def rent(self, rent_value =0.04):
        rent_value = (self.price * rent_value)
        return 'Rent is ${:,.0f}'.format(rent_value)

house_1 = Housing('Victorian', 3200, 4, 800000, '1900')
house_2 = Housing('Cape Cod', 1800, 2, 500000, '1945')

```

```python
print(Housing.rent(house_1))
```

    Rent is $32,000

```python
print(Housing.rent(house_2))
```

    Rent is $20,000

```python

```


# Change of variables formula

This reading reviews the change of variables formula for continuous random variables. This important formula is fundamental to the theory of normalising flows.

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

## Introduction

The change of variables formula tells us how to compute the probability density function of a random variable under a smooth invertible transformation.

In this reading notebook we will review the statement of the change of variables formula in various forms. We will then look at a simple example of a linear change of variables in two dimensions, where the probability density function of the transformed variable can easily be written by inspection and checked against the change of variables formula. In the following section we provide a sketch of the proof of the formula in one dimension. Finally, we will conclude the reading by discussing how the change of variables formula is applied to normalising flows.

## Statement of the formula

Let $Z := (z_1,\ldots,z_d)\in\mathbb{R}^d$ be a $d$-dimensional continuous random variable, and suppose that $f:\mathbb{R}^d\rightarrow\mathbb{R}^d$ is a smooth, invertible transformation. Now consider the change of variables $X = f(Z)$, with $X=(x_1,\ldots,x_d)$, and denote the probability density functions of the random variables $Z$ and $X$ by $p_Z$ and $p_X$ respectively.

The change of variables formula states that

$$
p_X(x) = p_Z(z)\cdot\left|\det J_f(z) \right|^{-1},\qquad (1)
$$

where $J_f(z)$ is the _Jacobian_ of the transformation $f$, given by the matrix of partial derivatives

$$
J_f(z) = \left[
\begin{array}{ccc}
\frac{\partial f_1}{\partial z_1} & \cdots & \frac{\partial f_1}{\partial z_d}\\
\vdots & \ddots & \vdots\\
\frac{\partial f_d}{\partial z_1} & \cdots & \frac{\partial f_d}{\partial z_d}\\
\end{array}
\right],
$$

and $\left|\det J_f(z) \right|$ is the absolute value of the determinant of the Jacobian matrix. Note that (1) can also be written in the log-form

$$
\log p_X(x) = \log p_Z(z) - \log \hspace{0.1ex}\left|\det J_f(z) \right|.\qquad (2)
$$

Furthermore, we can equivalently consider the transformation $Z = f^{-1}(X)$. Then the change of variables formulae can be written as

$$
\begin{align}
p_Z(z) &= p_X(x)\cdot\left|\det J_{f^{-1}}(x) \right|^{-1},\qquad &(3)\\
\log p_Z(z) &= \log p_X(x) - \log \hspace{0.1ex}\left|\det J_{f^{-1}}(x) \right|.\qquad &(4)
\end{align}
$$

## A simple example

We will demonstrate the change of variables formula with a simple example. Let $Z=(z_1, z_2)$ be a 2-dimensional random variable that is uniformly distributed on the unit square $[0, 1]^2 =: \Omega_Z$. We also define the transformation $f:\mathbb{R}^2 \rightarrow \mathbb{R}^2$ as

$$
\begin{align}
f(z_1, z_2) = (\lambda z_1, \mu z_2)
\end{align}
$$

for some nonzero $\lambda, \mu\in\mathbb{R}$. The random variable $X=(x_1, x_2)$ is given by $X = f(Z)$.

```python
# Run this cell to download and view a figure to sketch the simple example transformation

!wget -q -O change_of_variables.png --no-check-certificate "https://docs.google.com/uc?export=download&id=1XX9XUyOP47IdvOXA_XirzaZgrprajDbf"
Image("change_of_variables.png", width=800)
```

**Figure 1.** Linearly transformed uniformly distributed random variable.

Since $\int_{\Omega_Z}p_Z(z)dz = 1$ and $Z$ is uniformly distributed, we have that

$$
p_Z(z) = 1 \quad\text{for}\quad z\in\Omega_Z.
$$

The random variable $X$ is uniformly distributed on the region $\Omega_X = f(\Omega_Z)$ as shown in the figure above (for the case $\lambda, \mu>0$). Since again $\int_{\Omega_X}p_X(x)dx = 1$, the probability density function for $X$ must be given by

$$
p_X(x) = \frac{1}{|\Omega_X|} = \frac{1}{|\lambda\mu |}\quad\text{for}\quad x\in\Omega_X.
$$

This result corresponds to the equations (1)-(4) above. In this simple example, the transformation $f$ is linear, and the Jacobian matrix is given by

$$
\begin{align}
J_f(z) = \left[
\begin{array}{cc}
\lambda & 0\\
0 & \mu
\end{array}
\right].
\end{align}
$$

The absolute value of the determinant is $\left|\det J_f(x) \right| = |\lambda\mu | \ne 0$. Equation (1) then implies

$$
\begin{align}
p_X(x) &= p_Z(z)\cdot\left|\det J_f(z) \right|^{-1}\\
&= \frac{1}{|\lambda\mu|}.
\end{align}
$$

Writing in the log-form as in equation (2) gives

$$
\begin{align}
\log p_X(x) &= \log p_Z(z) - \log \hspace{0.1ex}\left|\det J_f(z) \right|\\
&= \log (1) - \log |\lambda\mu|\\
&= - \log |\lambda\mu|.
\end{align}
$$

## Sketch of proof in 1-D

We now provide a sketch of the proof of the change of variables formula in one dimension. Let $Z$ and $X$ be random variables such that $X = f(Z)$, where $f : \mathbb{R}\rightarrow\mathbb{R}$ is a $C^k$ diffeomorphism with $k\ge 1$. The change of variables formula in one dimension can be written

$$
p_X(x) = p_Z(z)\cdot\left| \frac{d}{dz}f(z) \right|^{-1},\qquad\text{(cf. equation (1))}
$$

or equivalently as

$$
p_X(x) = p_Z(z)\cdot\left| \frac{d}{dx}f^{-1}(x) \right|.\qquad\text{(cf. equation (3))}
$$

_Sketch of proof._ For $f$ to be invertible, it must be strictly monotonic. That means that for all $x^{(1)}, x^{(2)}\in\mathbb{R}$ with $x^{(1)} < x^{(2)}$, we have $f(x^{(1)}) < f(x^{(2)})$ (strictly monotonically increasing) or $f(x^{(1)}) > f(x^{(2)})$ (strictly monotonically decreasing).

```python
# Run this cell to download and view a sketch figure of monotonic functions

!wget -q -O change_of_variables_monotonic.png --no-check-certificate "https://docs.google.com/uc?export=download&id=1HnCZ8y6WdbBCroUR22ST4QNF8gKeFKH6"
Image("change_of_variables_monotonic.png", width=600)
```

**Figure 2.** Sketch of monotonic functions: (a) strictly increasing, (b) strictly decreasing.

Suppose first that $f$ is strictly increasing. Also let $F_X$ and $F_Z$ be the cumulative distribution functions of the random variables $X$ and $Z$ respectively. Then we have

$$
\begin{align}
F_X(x) &= P(X \le x)\\
&= P(f(Z) \le x)\\
&= P(Z \le f^{-1}(x))\qquad\text{(since $f$ is monotonically increasing)}\\
&= F_Z(f^{-1}(x))
\end{align}
$$

By differentiating on both sides with respect to $x$, we obtain the probability density function:

$$
\begin{align}
p_X(x) &= \frac{d}{dx}F_X(x)\\
&= \frac{d}{dx} F_Z(f^{-1}(x))\\
&= \frac{d}{dz}F_Z(z)\cdot\frac{d}{dx}f^{-1}(x)\\
&= p_Z(z)\frac{d}{dx}f^{-1}(x)\qquad\quad (5)
\end{align}
$$

Now suppose first that $f$ is strictly decreasing. Then

$$
\begin{align}
F_X(x) &= P(X \le x)\\
&= P(f(Z) \le x)\\
&= P(Z \ge f^{-1}(x))\qquad\text{(since $f$ is monotonically decreasing)}\\
&= 1 - F_Z(f^{-1}(x))
\end{align}
$$

Again differentiating on both sides with respect to $x$:

$$
\begin{align}
p_X(x) &= \frac{d}{dx}F_X(x)\\
&= -\frac{d}{dx} F_Z(f^{-1}(x))\\
&= -F_Z'(f^{-1}(x))\frac{d}{dx}f^{-1}(x)\\
&= -p_Z(z)\frac{d}{dx}f^{-1}(x)\qquad\quad (6)
\end{align}
$$

Now note that the inverse of a strictly monotonically increasing (resp. decreasing) function is again strictly monotonically increasing (resp. decreasing). This implies that the quantity $\frac{d}{dx} f^{-1}(x)$ is positive in (5) and negative in (6), and so these two equations can be combined into the single equation:

$$
p_X(x) = p_Z(z)\left|\frac{d}{dx}f^{-1}(x)\right|
$$

which completes the proof.

## Normalising flows

Normalising flows are a class of models that exploit the change of variables formula to estimate an unknown target data density.

Suppose we have data samples $\mathcal{D}:=\{x^{(1)}, \ldots, x^{(n)}\}$, with each $x^{(i)}\in\mathbb{R}^d$, and assume that these samples are generated i.i.d. from the underlying distribution $p_X$.

A normalising flow models the distribution $p_X$ using a random variable $Z$ (also of dimension $d$) with a simple distribution $p_Z$ (e.g. an isotropic Gaussian), such that the random variable $X$ can be written as a change of variables $X = f_\theta(Z)$, where $\theta$ is a parameter vector that parameterises the smooth invertible function $f_\theta$.

The function $f_\theta$ is modelled using a neural network with parameters $\theta$, which we want to learn from the data. An important point is that this neural network must be designed to be invertible, which is not the case in general with deep learning models. In practice, we often construct the neural network by composing multiple simpler blocks together. In TensorFlow Probability, these simpler blocks are the _bijectors_ that we will study in the first part of the week.

In order to learn the optimal parameters $\theta$, we apply the principle of maximum likelihood and search for $\theta_{ML}$ such that

$$
\begin{align}
\theta_{ML} &:= \arg \max_{\theta} P(\mathcal{D}; \theta)\\
&= \arg \max_{\theta} \log P(\mathcal{D}; \theta).
\end{align}
$$

In order to compute $\log P(\mathcal{D}; \theta)$ we can use the change of variables formula:

$$
\begin{align}
P(\mathcal{D}; \theta) &= \prod_{x\in\mathcal{D}}  p_Z(f_\theta^{-1}(x)) \cdot\left|\hspace{0.1ex}\det J_{f_\theta^{-1}}(x) \hspace{0.1ex}\right|\\
\log P(\mathcal{D}; \theta) &= \sum_{x\in\mathcal{D}} \log p_Z(f_\theta^{-1}(x)) + \log \hspace{0.1ex}\left|\hspace{0.1ex}\det J_{f_\theta^{-1}}(x) \hspace{0.1ex}\right|\qquad (7)
\end{align}
$$

The term $p_Z(f_\theta^{-1}(x))$ can be computed for a given data point $x\in\mathcal{D}$ since the neural network $f_\theta$ is designed to be invertible, and the distribution $p_Z$ is known. The term $\det J_{f_\theta^{-1}}(x)$ is also computable, although this also highlights another important aspect of normalising flow models: they should be designed such that the determinant of the Jacobian can be efficiently computed.

The log-likelihood (7) is usually optimised as usual in minibatches, with gradient-based optimisation methods.

### Further reading and resources

Some general resources related to the content of this reading are:

* https://en.wikipedia.org/wiki/Probability_density_function
* https://en.wikipedia.org/wiki/Cumulative_distribution_function
* https://en.wikipedia.org/wiki/Monotonic_function


# Puma Example

Kevin Walchko

created 7 Nov 2017

---

This is just an example of a more complex serial manipulator.

```python
%matplotlib inline

# Let's grab some libraries to help us manipulate symbolic equations
from __future__ import print_function
from __future__ import division
import numpy as np
import sympy
from sympy import symbols, sin, cos, pi, simplify

def makeT(a, alpha, d, theta):
    # create a modified DH homogenious matrix
    return np.array([
        [           cos(theta),           -sin(theta),           0,             a],
        [sin(theta)*cos(alpha), cos(theta)*cos(alpha), -sin(alpha), -d*sin(alpha)],
        [sin(theta)*sin(alpha), cos(theta)*sin(alpha),  cos(alpha),  d*cos(alpha)],
        [                    0,                     0,           0,             1]
    ])

def simplifyT(tt):
    """
    This goes through each element of a matrix and tries to simplify it.
    """
    for i, row in enumerate(tt):
        for j, col in enumerate(row):
            tt[i,j] = simplify(col)
    return tt
```

# Puma

[](dh_pics/puma.png)

Puma robot is an old, but classical serial manipulator. You can see Criag's example in section 3.7, pg 77. Once you have the DH parameters, you can use the above matrix to find the forward kinematics,

```python
# craig puma
t1,t2,t3,t4,t5,t6 = symbols('t1 t2 t3 t4 t5 t6')
a2, a3, d3, d4 = symbols('a2 a3 d3 d4')

T1 = makeT(0,0,0,t1)
T2 = makeT(0,-pi/2,0,t2)
T3 = makeT(a2,0,d3,t3)
T4 = makeT(a3,-pi/2,d4,t4)
T5 = makeT(0,pi/2,0,t5)
T6 = makeT(0,-pi/2,0,t6)

ans = np.eye(4)
for T in [T1, T2, T3, T4, T5, T6]:
    ans = ans.dot(T)

print(ans)
```

    [[(((-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3))*cos(t4) + 1.0*sin(t1)*sin(t4))*cos(t5) + (-1.0*sin(t2)*cos(t1)*cos(t3) - 1.0*sin(t3)*cos(t1)*cos(t2))*sin(t5))*cos(t6) - ((-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3))*sin(t4) - 1.0*sin(t1)*cos(t4))*sin(t6)
      -(((-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3))*cos(t4) + 1.0*sin(t1)*sin(t4))*cos(t5) + (-1.0*sin(t2)*cos(t1)*cos(t3) - 1.0*sin(t3)*cos(t1)*cos(t2))*sin(t5))*sin(t6) - ((-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3))*sin(t4) - 1.0*sin(t1)*cos(t4))*cos(t6)
      -((-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3))*cos(t4) + 1.0*sin(t1)*sin(t4))*sin(t5) + (-1.0*sin(t2)*cos(t1)*cos(t3) - 1.0*sin(t3)*cos(t1)*cos(t2))*cos(t5)
      1.0*a2*cos(t1)*cos(t2) + a3*(-1.0*sin(t2)*sin(t3)*cos(t1) + 1.0*cos(t1)*cos(t2)*cos(t3)) - 1.0*d3*sin(t1) + d4*(-1.0*sin(t2)*cos(t1)*cos(t3) - 1.0*sin(t3)*cos(t1)*cos(t2))]
     [(((-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3))*cos(t4) - 1.0*sin(t4)*cos(t1))*cos(t5) + (-1.0*sin(t1)*sin(t2)*cos(t3) - 1.0*sin(t1)*sin(t3)*cos(t2))*sin(t5))*cos(t6) - ((-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3))*sin(t4) + 1.0*cos(t1)*cos(t4))*sin(t6)
      -(((-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3))*cos(t4) - 1.0*sin(t4)*cos(t1))*cos(t5) + (-1.0*sin(t1)*sin(t2)*cos(t3) - 1.0*sin(t1)*sin(t3)*cos(t2))*sin(t5))*sin(t6) - ((-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3))*sin(t4) + 1.0*cos(t1)*cos(t4))*cos(t6)
      -((-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3))*cos(t4) - 1.0*sin(t4)*cos(t1))*sin(t5) + (-1.0*sin(t1)*sin(t2)*cos(t3) - 1.0*sin(t1)*sin(t3)*cos(t2))*cos(t5)
      1.0*a2*sin(t1)*cos(t2) + a3*(-1.0*sin(t1)*sin(t2)*sin(t3) + 1.0*sin(t1)*cos(t2)*cos(t3)) + 1.0*d3*cos(t1) + d4*(-1.0*sin(t1)*sin(t2)*cos(t3) - 1.0*sin(t1)*sin(t3)*cos(t2))]
     [((1.0*sin(t2)*sin(t3) - 1.0*cos(t2)*cos(t3))*sin(t5) + (-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2))*cos(t4)*cos(t5))*cos(t6) - (-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2))*sin(t4)*sin(t6)
      -((1.0*sin(t2)*sin(t3) - 1.0*cos(t2)*cos(t3))*sin(t5) + (-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2))*cos(t4)*cos(t5))*sin(t6) - (-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2))*sin(t4)*cos(t6)
      (1.0*sin(t2)*sin(t3) - 1.0*cos(t2)*cos(t3))*cos(t5) - (-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2))*sin(t5)*cos(t4)
      -1.0*a2*sin(t2) + a3*(-1.0*sin(t2)*cos(t3) - 1.0*sin(t3)*cos(t2)) + d4*(1.0*sin(t2)*sin(t3) - 1.0*cos(t2)*cos(t3))]
     [0 0 0 1.00000000000000]]

```python
ans = simplifyT(ans)
print(ans)
```

    [[1.0*((sin(t1)*sin(t4) + cos(t1)*cos(t4)*cos(t2 + t3))*cos(t5) - sin(t5)*sin(t2 + t3)*cos(t1))*cos(t6) + 1.0*(sin(t1)*cos(t4) - sin(t4)*cos(t1)*cos(t2 + t3))*sin(t6)
      1.0*(-(sin(t1)*sin(t4) + cos(t1)*cos(t4)*cos(t2 + t3))*cos(t5) + sin(t5)*sin(t2 + t3)*cos(t1))*sin(t6) + 1.0*(sin(t1)*cos(t4) - sin(t4)*cos(t1)*cos(t2 + t3))*cos(t6)
      -1.0*(sin(t1)*sin(t4) + cos(t1)*cos(t4)*cos(t2 + t3))*sin(t5) - 1.0*sin(t2 + t3)*cos(t1)*cos(t5)
      1.0*a2*cos(t1)*cos(t2) + 1.0*a3*cos(t1)*cos(t2 + t3) - 1.0*d3*sin(t1) - 1.0*d4*sin(t2 + t3)*cos(t1)]
     [1.0*((sin(t1)*cos(t4)*cos(t2 + t3) - sin(t4)*cos(t1))*cos(t5) - sin(t1)*sin(t5)*sin(t2 + t3))*cos(t6) - 1.0*(sin(t1)*sin(t4)*cos(t2 + t3) + cos(t1)*cos(t4))*sin(t6)
      1.0*((-sin(t1)*cos(t4)*cos(t2 + t3) + sin(t4)*cos(t1))*cos(t5) + sin(t1)*sin(t5)*sin(t2 + t3))*sin(t6) - 1.0*(sin(t1)*sin(t4)*cos(t2 + t3) + cos(t1)*cos(t4))*cos(t6)
      1.0*(-sin(t1)*cos(t4)*cos(t2 + t3) + sin(t4)*cos(t1))*sin(t5) - 1.0*sin(t1)*sin(t2 + t3)*cos(t5)
      1.0*a2*sin(t1)*cos(t2) + 1.0*a3*sin(t1)*cos(t2 + t3) + 1.0*d3*cos(t1) - 1.0*d4*sin(t1)*sin(t2 + t3)]
     [-1.0*(sin(t5)*cos(t2 + t3) + sin(t2 + t3)*cos(t4)*cos(t5))*cos(t6) + 1.0*sin(t4)*sin(t6)*sin(t2 + t3)
      1.0*(sin(t5)*cos(t2 + t3) + sin(t2 + t3)*cos(t4)*cos(t5))*sin(t6) + 1.0*sin(t4)*sin(t2 + t3)*cos(t6)
      1.0*sin(t5)*sin(t2 + t3)*cos(t4) - 1.0*cos(t5)*cos(t2 + t3)
      -1.0*a2*sin(t2) - 1.0*a3*sin(t2 + t3) - 1.0*d4*cos(t2 + t3)]
     [0 0 0 1.00000000000000]]

```python
print('position x: {}'.format(ans[0,3]))
print('position y: {}'.format(ans[1,3]))
print('position z: {}'.format(ans[2,3]))
```

    position x: 1.0*a2*cos(t1)*cos(t2) + 1.0*a3*cos(t1)*cos(t2 + t3) - 1.0*d3*sin(t1) - 1.0*d4*sin(t2 + t3)*cos(t1)
    position y: 1.0*a2*sin(t1)*cos(t2) + 1.0*a3*sin(t1)*cos(t2 + t3) + 1.0*d3*cos(t1) - 1.0*d4*sin(t1)*sin(t2 + t3)
    position z: -1.0*a2*sin(t2) - 1.0*a3*sin(t2 + t3) - 1.0*d4*cos(t2 + t3)

Looking at the position, this is the same position listed in Craig, eqn 3.14.

Also, **this is the simplified version!!!**. As you get more joints and degrees of freedom, the equations get nastier. You also can run into situations where you end up with singularities (like division by zero) and send your robot into a bad place!

-----------

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.