# Programming Exercise 4:  Neural Networks Learning

## Introduction

In this exercise, you will implement the backpropagation algorithm for neural networks and apply it to the task of hand-written digit recognition. Before starting on the programming exercise, we strongly recommend watching the video lectures and completing the review questions for the associated topics.

All the information you need for solving this assignment is in this notebook, and all the code you will be implementing will take place within this notebook. The assignment can be promptly submitted to the coursera grader directly from this notebook (code and instructions are included below).

Before we begin with the exercises, we need to import all libraries required for this programming exercise. Throughout the course, we will be using [`numpy`](http://www.numpy.org/) for all arrays and matrix operations, [`matplotlib`](https://matplotlib.org/) for plotting, and [`scipy`](https://docs.scipy.org/doc/scipy/reference/) for scientific and numerical computation functions and tools. You can find instructions on how to install required libraries in the README file in the [github repository](https://github.com/dibgerge/ml-coursera-python-assignments).

```python
# used for manipulating directory paths
import os

# Scientific and vector computation for python
import numpy as np

# Plotting library
from matplotlib import pyplot

# Optimization module in scipy
from scipy import optimize

# will be used to load MATLAB mat datafile format
from scipy.io import loadmat

# library written for this exercise providing additional functions for assignment submission, and others
import utils

# define the submission/grader object for this exercise
grader = utils.Grader()

# tells matplotlib to embed plots within the notebook
%matplotlib inline
```

## Submission and Grading

After completing each part of the assignment, be sure to submit your solutions to the grader. The following is a breakdown of how each part of this exercise is scored.

| Section | Part                                             | Submission function | Points
| :-      |:-                                                | :-                  | :-:
| 1       | [Feedforward and Cost Function](#section1)                    | [`nnCostFunction`](#nnCostFunction)   | 30
| 2       | [Regularized Cost Function](#section2)                        | [`nnCostFunction`](#nnCostFunction)   | 15
| 3       | [Sigmoid Gradient](#section3)                                 | [`sigmoidGradient`](#sigmoidGradient) | 5
| 4       | [Neural Net Gradient Function (Backpropagation)](#section4)   | [`nnCostFunction`](#nnCostFunction)   | 40
| 5       | [Regularized Gradient](#section5)                             | [`nnCostFunction`](#nnCostFunction)   |10
|         | Total Points                                     |    | 100

You are allowed to submit your solutions multiple times, and we will take only the highest score into consideration.

At the end of each section in this notebook, we have a cell which contains code for submitting the solutions thus far to the grader. Execute the cell to see your score up to the current section. For all your work to be submitted properly, you must execute those cells at least once.

## Neural Networks

In the previous exercise, you implemented feedforward propagation for neural networks and used it to predict handwritten digits with the weights we provided. In this exercise, you will implement the backpropagation algorithm to learn the parameters for the neural network.

We start the exercise by first loading the dataset.

```python
#  training data stored in arrays X, y
data = loadmat(os.path.join('Data', 'ex4data1.mat'))
X, y = data['X'], data['y'].ravel()

# set the zero digit to 0, rather than its mapped 10 in this dataset
# This is an artifact due to the fact that this dataset was used in
# MATLAB where there is no index 0
y[y == 10] = 0

# Number of training examples
m = y.size
```

### 1.1 Visualizing the data

You will begin by visualizing a subset of the training set, using the function `displayData`, which is the same function we used in Exercise 3. It is provided in the `utils.py` file for this assignment as well. The dataset is also the same one you used in the previous exercise.

There are 5000 training examples in `ex4data1.mat`, where each training example is a 20 pixel by 20 pixel grayscale image of the digit. Each pixel is represented by a floating point number indicating the grayscale intensity at that location. The 20 by 20 grid of pixels is “unrolled” into a 400-dimensional vector. Each
of these training examples becomes a single row in our data matrix $X$. This gives us a 5000 by 400 matrix $X$ where every row is a training example for a handwritten digit image.

$$ X = \begin{bmatrix} - \left(x^{(1)} \right)^T - \\
- \left(x^{(2)} \right)^T - \\
\vdots \\
- \left(x^{(m)} \right)^T - \\
\end{bmatrix}
$$

The second part of the training set is a 5000-dimensional vector `y` that contains labels for the training set.
The following cell randomly selects 100 images from the dataset and plots them.

```python
# Randomly select 100 data points to display
rand_indices = np.random.choice(m, 100, replace=False)
sel = X[rand_indices, :]

utils.displayData(sel)
```

### 1.2 Model representation

Our neural network is shown in the following figure.

It has 3 layers - an input layer, a hidden layer and an output layer. Recall that our inputs are pixel values
of digit images. Since the images are of size $20 \times 20$, this gives us 400 input layer units (not counting the extra bias unit which always outputs +1). The training data was loaded into the variables `X` and `y` above.

You have been provided with a set of network parameters ($\Theta^{(1)}, \Theta^{(2)}$) already trained by us. These are stored in `ex4weights.mat` and will be loaded in the next cell of this notebook into `Theta1` and `Theta2`. The parameters have dimensions that are sized for a neural network with 25 units in the second layer and 10 output units (corresponding to the 10 digit classes).

```python
# Setup the parameters you will use for this exercise
input_layer_size  = 400  # 20x20 Input Images of Digits
hidden_layer_size = 25   # 25 hidden units
num_labels = 10          # 10 labels, from 0 to 9

# Load the weights into variables Theta1 and Theta2
weights = loadmat(os.path.join('Data', 'ex4weights.mat'))

# Theta1 has size 25 x 401
# Theta2 has size 10 x 26
Theta1, Theta2 = weights['Theta1'], weights['Theta2']

# swap first and last columns of Theta2, due to legacy from MATLAB indexing,
# since the weight file ex3weights.mat was saved based on MATLAB indexing
Theta2 = np.roll(Theta2, 1, axis=0)

# Unroll parameters
nn_params = np.concatenate([Theta1.ravel(), Theta2.ravel()])
```

### 1.3 Feedforward and cost function

Now you will implement the cost function and gradient for the neural network. First, complete the code for the function `nnCostFunction` in the next cell to return the cost.

Recall that the cost function for the neural network (without regularization) is:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right]$$

where $h_\theta \left( x^{(i)} \right)$ is computed as shown in the neural network figure above, and K = 10 is the total number of possible labels. Note that $h_\theta(x^{(i)})_k = a_k^{(3)}$ is the activation (output
value) of the $k^{th}$ output unit. Also, recall that whereas the original labels (in the variable y) were 0, 1, ..., 9, for the purpose of training a neural network, we need to encode the labels as vectors containing only values 0 or 1, so that

$$ y =
\begin{bmatrix} 1 \\ 0 \\ 0 \\\vdots \\ 0 \end{bmatrix}, \quad
\begin{bmatrix} 0 \\ 1 \\ 0 \\ \vdots \\ 0 \end{bmatrix}, \quad \cdots  \quad \text{or} \qquad
\begin{bmatrix} 0 \\ 0 \\ 0 \\ \vdots \\ 1 \end{bmatrix}.
$$

For example, if $x^{(i)}$ is an image of the digit 5, then the corresponding $y^{(i)}$ (that you should use with the cost function) should be a 10-dimensional vector with $y_5 = 1$, and the other elements equal to 0.

You should implement the feedforward computation that computes $h_\theta(x^{(i)})$ for every example $i$ and sum the cost over all examples. **Your code should also work for a dataset of any size, with any number of labels** (you can assume that there are always at least $K \ge 3$ labels).

**Implementation Note:** The matrix $X$ contains the examples in rows (i.e., X[i,:] is the i-th training example $x^{(i)}$, expressed as a $n \times 1$ vector.) When you complete the code in `nnCostFunction`, you will need to add the column of 1’s to the X matrix. The parameters for each unit in the neural network is represented in Theta1 and Theta2 as one row. Specifically, the first row of Theta1 corresponds to the first hidden unit in the second layer. You can use a for-loop over the examples to compute the cost.

```python
def nnCostFunction(nn_params,
                   input_layer_size,
                   hidden_layer_size,
                   num_labels,
                   X, y, lambda_=0.0):
    """
    Implements the neural network cost function and gradient for a two layer neural
    network which performs classification.

    Parameters
    ----------
    nn_params : array_like
        The parameters for the neural network which are "unrolled" into
        a vector. This needs to be converted back into the weight matrices Theta1
        and Theta2.

    input_layer_size : int
        Number of features for the input layer.

    hidden_layer_size : int
        Number of hidden units in the second layer.

    num_labels : int
        Total number of labels, or equivalently number of units in output layer.

    X : array_like
        Input dataset. A matrix of shape (m x input_layer_size).

    y : array_like
        Dataset labels. A vector of shape (m,).

    lambda_ : float, optional
        Regularization parameter.

    Returns
    -------
    J : float
        The computed value for the cost function at the current weight values.

    grad : array_like
        An "unrolled" vector of the partial derivatives of the concatenatation of
        neural network weights Theta1 and Theta2.

    Instructions
    ------------
    You should complete the code by working through the following parts.

    - Part 1: Feedforward the neural network and return the cost in the
              variable J. After implementing Part 1, you can verify that your
              cost function computation is correct by verifying the cost
              computed in the following cell.

    - Part 2: Implement the backpropagation algorithm to compute the gradients
              Theta1_grad and Theta2_grad. You should return the partial derivatives of
              the cost function with respect to Theta1 and Theta2 in Theta1_grad and
              Theta2_grad, respectively. After implementing Part 2, you can check
              that your implementation is correct by running checkNNGradients provided
              in the utils.py module.

              Note: The vector y passed into the function is a vector of labels
                    containing values from 0..K-1. You need to map this vector into a
                    binary vector of 1's and 0's to be used with the neural network
                    cost function.

              Hint: We recommend implementing backpropagation using a for-loop
                    over the training examples if you are implementing it for the
                    first time.

    - Part 3: Implement regularization with the cost function and gradients.

              Hint: You can implement this around the code for
                    backpropagation. That is, you can compute the gradients for
                    the regularization separately and then add them to Theta1_grad
                    and Theta2_grad from Part 2.

    Note
    ----
    We have provided an implementation for the sigmoid function in the file
    `utils.py` accompanying this assignment.
    """
    # Reshape nn_params back into the parameters Theta1 and Theta2, the weight matrices
    # for our 2 layer neural network
    Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                        (hidden_layer_size, (input_layer_size + 1)))

    Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                        (num_labels, (hidden_layer_size + 1)))

    # Setup some useful variables
    m = y.size

    # You need to return the following variables correctly
    J = 0
    Theta1_grad = np.zeros(Theta1.shape)
    Theta2_grad = np.zeros(Theta2.shape)

    # ====================== YOUR CODE HERE ======================
    # Forward Propagation
    a1 = np.concatenate([np.ones((m,1)), X], axis = 1)
    z2 = np.dot(a1, Theta1.T)
    a2 = utils.sigmoid(z2)
    a2 = np.concatenate([np.ones((m,1)), a2], axis = 1)
    z3 = np.dot(a2, Theta2.T)
    a3 = utils.sigmoid(z3) # (m,k)

    # Calculate Cost
    y_onehot = np.eye(np.unique(y).size)[y] #One-Hot Encoding for y into (m, k) matrix
    J = 1/m * np.sum(-y_onehot*np.log(a3)-(1-y_onehot)*np.log(1-a3)) + lambda_/(2*m)*(np.sum(Theta1[:,1:]**2)+np.sum(Theta2[:,1:]**2))

    # Backward Propagation
    l3 = a3 - y_onehot
    Theta2_grad = 1/m*np.dot(l3.T, a2)
    Theta2_grad[:, 1:] = Theta2_grad[:, 1:] + lambda_/m*Theta2[:, 1:]  #Regularization Without Regularizing Bias
    l2 = np.dot(l3, Theta2[:,1:]) * sigmoidGradient(z2)
    Theta1_grad = 1/m*np.dot(l2.T, a1)
    Theta1_grad[:, 1:] = Theta1_grad[:, 1:] + lambda_/m*Theta1[:, 1:]  #Regularization Without Regularizing Bias
    # ================================================================
    # Unroll gradients
    # grad = np.concatenate([Theta1_grad.ravel(order=order), Theta2_grad.ravel(order=order)])
    grad = np.concatenate([Theta1_grad.ravel(), Theta2_grad.ravel()])

    return J, grad
```

Use the following links to go back to the different parts of this exercise that require to modify the function `nnCostFunction`.

Back to:
- [Feedforward and cost function](#section1)
- [Regularized cost](#section2)
- [Neural Network Gradient (Backpropagation)](#section4)
- [Regularized Gradient](#section5)

Once you are done, call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`. You should see that the cost is about 0.287629.

```python
lambda_ = 0
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                   num_labels, X, y, lambda_)
print('Cost at parameters (loaded from ex4weights): %.6f ' % J)
print('The cost should be about                   : 0.287629.')
```

    (5000, 10)
    Cost at parameters (loaded from ex4weights): 0.287629
    The cost should be about                   : 0.287629.

*You should now submit your solutions.*

```python
grader = utils.Grader()
grader[1] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    (16, 4)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |   0 /  15 |
                               Sigmoid Gradient |   0 /   5 |
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  30 / 100 |

### 1.4 Regularized cost function

The cost function for neural networks with regularization is given by:

$$ J(\theta) = \frac{1}{m} \sum_{i=1}^{m}\sum_{k=1}^{K} \left[ - y_k^{(i)} \log \left( \left( h_\theta \left( x^{(i)} \right) \right)_k \right) - \left( 1 - y_k^{(i)} \right) \log \left( 1 - \left( h_\theta \left( x^{(i)} \right) \right)_k \right) \right] + \frac{\lambda}{2 m} \left[ \sum_{j=1}^{25} \sum_{k=1}^{400} \left( \Theta_{j,k}^{(1)} \right)^2 + \sum_{j=1}^{10} \sum_{k=1}^{25} \left( \Theta_{j,k}^{(2)} \right)^2 \right] $$

You can assume that the neural network will only have 3 layers - an input layer, a hidden layer and an output layer. However, your code should work for any number of input units, hidden units and outputs units. While we
have explicitly listed the indices above for $\Theta^{(1)}$ and $\Theta^{(2)}$ for clarity, do note that your code should in general work with $\Theta^{(1)}$ and $\Theta^{(2)}$ of any size. Note that you should not be regularizing the terms that correspond to the bias. For the matrices `Theta1` and `Theta2`, this corresponds to the first column of each matrix. You should now add regularization to your cost function. Notice that you can first compute the unregularized cost function $J$ using your existing `nnCostFunction` and then later add the cost for the regularization terms.

[Click here to go back to `nnCostFunction` for editing.](#nnCostFunction)

Once you are done, the next cell will call your `nnCostFunction` using the loaded set of parameters for `Theta1` and `Theta2`, and $\lambda = 1$. You should see that the cost is about 0.383770.

```python
# Weight regularization parameter (we set this to 1 here).
lambda_ = 1
J, _ = nnCostFunction(nn_params, input_layer_size, hidden_layer_size,
                      num_labels, X, y, lambda_)

print('Cost at parameters (loaded from ex4weights): %.6f' % J)
print('This value should be about                 : 0.383770.')
```

    (10, 26)
    (10, 26)
    Cost at parameters (loaded from ex4weights): 0.383770
    This value should be about                 : 0.383770.

*You should now submit your solutions.*

```python
grader[2] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    (16, 4)
    (16, 5)
    (16, 4)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  50 / 100 |

## 2 Backpropagation

In this part of the exercise, you will implement the backpropagation algorithm to compute the gradient for the neural network cost function. You will need to update the function `nnCostFunction` so that it returns an appropriate value for `grad`. Once you have computed the gradient, you will be able to train the neural network by minimizing the cost function $J(\theta)$ using an advanced optimizer such as `scipy`'s `optimize.minimize`.
You will first implement the backpropagation algorithm to compute the gradients for the parameters for the (unregularized) neural network. After you have verified that your gradient computation for the unregularized case is correct, you will implement the gradient for the regularized neural network.

### 2.1 Sigmoid Gradient

To help you get started with this part of the exercise, you will first implement
the sigmoid gradient function. The gradient for the sigmoid function can be
computed as

$$ g(prime)(z) = \frac{d}{dz} g(z) = g(z)\left(1-g(z)\right) $$

where

$ \text{sigmoid}(z) = g(z) = \frac{1}{1 + e^{-z}} $

Now complete the implementation of `sigmoidGradient` in the next cell.

```python
def sigmoidGradient(z):
    """
    Computes the gradient of the sigmoid function evaluated at z.
    This should work regardless if z is a matrix or a vector.
    In particular, if z is a vector or matrix, you should return
    the gradient for each element.

    Parameters
    ----------
    z : array_like
        A vector or matrix as input to the sigmoid function.

    Returns
    --------
    g : array_like
        Gradient of the sigmoid function. Has the same shape as z.

    Instructions
    ------------
    Compute the gradient of the sigmoid function evaluated at
    each value of z (z can be a matrix, vector or scalar).

    Note
    ----
    We have provided an implementation of the sigmoid function
    in `utils.py` file accompanying this assignment.
    """

    g = np.zeros(z.shape)

    # ====================== YOUR CODE HERE ======================

    g = utils.sigmoid(z)*(1-utils.sigmoid(z))

    # =============================================================
    return g
```

When you are done, the following cell call `sigmoidGradient` on a given vector `z`. Try testing a few values by calling `sigmoidGradient(z)`. For large values (both positive and negative) of z, the gradient should be close to 0. When $z = 0$, the gradient should be exactly 0.25. Your code should also work with vectors and matrices. For a matrix, your function should perform the sigmoid gradient function on every element.

```python
z = np.array([-1, -0.5, 0, 0.5, 1])
g = sigmoidGradient(z)
print('Sigmoid gradient evaluated at [-1 -0.5 0 0.5 1]:\n  ')
print(g)
```

    Sigmoid gradient evaluated at [-1 -0.5 0 0.5 1]:

    [0.19661193 0.23500371 0.25       0.23500371 0.19661193]

*You should now submit your solutions.*

```python
grader[3] = sigmoidGradient
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    (16, 4)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |   0 /  40 |
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  50 / 100 |

## 2.2 Random Initialization

When training neural networks, it is important to randomly initialize the parameters for symmetry breaking. One effective strategy for random initialization is to randomly select values for $\Theta^{(l)}$ uniformly in the range $[-\epsilon_{init}, \epsilon_{init}]$. You should use $\epsilon_{init} = 0.12$. This range of values ensures that the parameters are kept small and makes the learning more efficient.

One effective strategy for choosing $\epsilon_{init}$ is to base it on the number of units in the network. A good choice of
$\epsilon_{init}$ is $\epsilon_{init} = \frac{\sqrt{6}}{\sqrt{L_{in} + L_{out}}}$ where $L_{in} = s_l$ and $L_{out} = s_{l+1}$ are the number of units in the layers adjacent to $\Theta^{l}$.

Your job is to complete the function `randInitializeWeights` to initialize the weights for $\Theta$. Modify the function by filling in the following code:

```python
# Randomly initialize the weights to small values
W = np.random.rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init
```
Note that we give the function an argument for $\epsilon$ with default value `epsilon_init = 0.12`.

```python
def randInitializeWeights(L_in, L_out, epsilon_init=0.12):
    """
    Randomly initialize the weights of a layer in a neural network.

    Parameters
    ----------
    L_in : int
        Number of incomming connections.

    L_out : int
        Number of outgoing connections.

    epsilon_init : float, optional
        Range of values which the weight can take from a uniform
        distribution.

    Returns
    -------
    W : array_like
        The weight initialiatized to random values.  Note that W should
        be set to a matrix of size(L_out, 1 + L_in) as
        the first column of W handles the "bias" terms.

    Instructions
    ------------
    Initialize W randomly so that we break the symmetry while training
    the neural network. Note that the first column of W corresponds
    to the parameters for the bias unit.
    """

    # You need to return the following variables correctly
    W = np.zeros((L_out, 1 + L_in))

    # ====================== YOUR CODE HERE ======================
    W = np.random.rand(L_out, 1 + L_in) * 2 * epsilon_init - epsilon_init

    # ============================================================
    return W
```

*You do not need to submit any code for this part of the exercise.*

Execute the following cell to initialize the weights for the 2 layers in the neural network using the `randInitializeWeights` function.

```python
print('Initializing Neural Network Parameters ...')

initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size)
initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels)

# Unroll parameters
initial_nn_params = np.concatenate([initial_Theta1.ravel(), initial_Theta2.ravel()], axis=0)
```

    Initializing Neural Network Parameters ...

### 2.4 Backpropagation

Now, you will implement the backpropagation algorithm. Recall that the intuition behind the backpropagation algorithm is as follows. Given a training example $(x^{(t)}, y^{(t)})$, we will first run a “forward pass” to compute all the activations throughout the network, including the output value of the hypothesis $h_\theta(x)$. Then, for each node $j$ in layer $l$, we would like to compute an “error term” $\delta_j^{(l)}$ that measures how much that node was “responsible” for any errors in our output.

For an output node, we can directly measure the difference between the network’s activation and the true target value, and use that to define $\delta_j^{(3)}$ (since layer 3 is the output layer). For the hidden units, you will compute $\delta_j^{(l)}$ based on a weighted average of the error terms of the nodes in layer $(l+1)$. In detail, here is the backpropagation algorithm (also depicted in the figure above). You should implement steps 1 to 4 in a loop that processes one example at a time. Concretely, you should implement a for-loop `for t in range(m)` and place steps 1-4 below inside the for-loop, with the $t^{th}$ iteration performing the calculation on the $t^{th}$ training example $(x^{(t)}, y^{(t)})$. Step 5 will divide the accumulated gradients by $m$ to obtain the gradients for the neural network cost function.

1. Set the input layer’s values $(a^{(1)})$ to the $t^{th }$training example $x^{(t)}$. Perform a feedforward pass, computing the activations $(z^{(2)}, a^{(2)}, z^{(3)}, a^{(3)})$ for layers 2 and 3. Note that you need to add a `+1` term to ensure that the vectors of activations for layers $a^{(1)}$ and $a^{(2)}$ also include the bias unit. In `numpy`, if a 1 is a column matrix, adding one corresponds to `a_1 = np.concatenate([np.ones((m, 1)), a_1], axis=1)`.

1. For each output unit $k$ in layer 3 (the output layer), set
$$\delta_k^{(3)} = \left(a_k^{(3)} - y_k \right)$$
where $y_k \in \{0, 1\}$ indicates whether the current training example belongs to class $k$ $(y_k = 1)$, or if it belongs to a different class $(y_k = 0)$. You may find logical arrays helpful for this task (explained in the previous programming exercise).

1. For the hidden layer $l = 2$, set
$$ \delta^{(2)} = \left( \Theta^{(2)} \right)^T \delta^{(3)} * g'\left(z^{(2)} \right)$$
Note that the symbol $*$ performs element wise multiplication in `numpy`.

1. Accumulate the gradient from this example using the following formula. Note that you should skip or remove $\delta_0^{(2)}$. In `numpy`, removing $\delta_0^{(2)}$ corresponds to `delta_2 = delta_2[1:]`.
$$ \Delta^{(l)} = \Delta^{(l)} + \delta^{(l+1)} (a^{(l)})^{(T)} $$

1. Obtain the (unregularized) gradient for the neural network cost function by dividing the accumulated gradients by $\frac{1}{m}$:
$$ \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)}$$

**Python/Numpy tip**: You should implement the backpropagation algorithm only after you have successfully completed the feedforward and cost functions. While implementing the backpropagation alogrithm, it is often useful to use the `shape` function to print out the shapes of the variables you are working with if you run into dimension mismatch errors.

[Click here to go back and update the function `nnCostFunction` with the backpropagation algorithm](#nnCostFunction).

**Note:** If the iterative solution provided above is proving to be difficult to implement, try implementing the vectorized approach which is easier to implement in the opinion of the moderators of this course. You can find the tutorial for the vectorized approach [here](https://www.coursera.org/learn/machine-learning/discussions/all/threads/a8Kce_WxEeS16yIACyoj1Q).

After you have implemented the backpropagation algorithm, we will proceed to run gradient checking on your implementation. The gradient check will allow you to increase your confidence that your code is
computing the gradients correctly.

### 2.4  Gradient checking

In your neural network, you are minimizing the cost function $J(\Theta)$. To perform gradient checking on your parameters, you can imagine “unrolling” the parameters $\Theta^{(1)}$, $\Theta^{(2)}$ into a long vector $\theta$. By doing so, you can think of the cost function being $J(\Theta)$ instead and use the following gradient checking procedure.

Suppose you have a function $f_i(\theta)$ that purportedly computes $\frac{\partial}{\partial \theta_i} J(\theta)$; you’d like to check if $f_i$ is outputting correct derivative values.

$$
\text{Let } \theta^{(i+)} = \theta + \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
\quad \text{and} \quad \theta^{(i-)} = \theta - \begin{bmatrix} 0 \\ 0 \\ \vdots \\ \epsilon \\ \vdots \\ 0 \end{bmatrix}
$$

So, $\theta^{(i+)}$ is the same as $\theta$, except its $i^{th}$ element has been incremented by $\epsilon$. Similarly, $\theta^{(i−)}$ is the corresponding vector with the $i^{th}$ element decreased by $\epsilon$. You can now numerically verify $f_i(\theta)$’s correctness by checking, for each $i$, that:

$$ f_i\left( \theta \right) \approx \frac{J\left( \theta^{(i+)}\right) - J\left( \theta^{(i-)} \right)}{2\epsilon} $$

The degree to which these two values should approximate each other will depend on the details of $J$. But assuming $\epsilon = 10^{-4}$, you’ll usually find that the left- and right-hand sides of the above will agree to at least 4 significant digits (and often many more).

We have implemented the function to compute the numerical gradient for you in `computeNumericalGradient` (within the file `utils.py`). While you are not required to modify the file, we highly encourage you to take a look at the code to understand how it works.

In the next cell we will run the provided function `checkNNGradients` which will create a small neural network and dataset that will be used for checking your gradients. If your backpropagation implementation is correct,
you should see a relative difference that is less than 1e-9.

**Practical Tip**: When performing gradient checking, it is much more efficient to use a small neural network with a relatively small number of input units and hidden units, thus having a relatively small number
of parameters. Each dimension of $\theta$ requires two evaluations of the cost function and this can be expensive. In the function `checkNNGradients`, our code creates a small random model and dataset which is used with `computeNumericalGradient` for gradient checking. Furthermore, after you are confident that your gradient computations are correct, you should turn off gradient checking before running your learning algorithm.

    Practical Tip: Gradient checking works for any function where you are computing the cost and the gradient. Concretely, you can use the same `computeNumericalGradient` function to check if your gradient implementations for the other exercises are correct too (e.g., logistic regression’s cost function).

```python
utils.checkNNGradients(nnCostFunction)
```

    [[-9.27825235e-03 -9.27825236e-03]
     [-3.04978709e-06 -3.04978914e-06]
     [-1.75060084e-04 -1.75060082e-04]
     [-9.62660640e-05 -9.62660620e-05]
     [ 8.89911959e-03  8.89911960e-03]
     [ 1.42869450e-05  1.42869443e-05]
     [ 2.33146358e-04  2.33146357e-04]
     [ 1.17982666e-04  1.17982666e-04]
     [-8.36010761e-03 -8.36010762e-03]
     [-2.59383093e-05 -2.59383100e-05]
     [-2.87468729e-04 -2.87468729e-04]
     [-1.37149709e-04 -1.37149706e-04]
     [ 7.62813550e-03  7.62813551e-03]
     [ 3.69883257e-05  3.69883234e-05]
     [ 3.35320351e-04  3.35320347e-04]
     [ 1.53247082e-04  1.53247082e-04]
     [-6.74798369e-03 -6.74798370e-03]
     [-4.68759742e-05 -4.68759769e-05]
     [-3.76215583e-04 -3.76215587e-04]
     [-1.66560294e-04 -1.66560294e-04]
     [ 3.14544970e-01  3.14544970e-01]
     [ 1.64090819e-01  1.64090819e-01]
     [ 1.64567932e-01  1.64567932e-01]
     [ 1.58339334e-01  1.58339334e-01]
     [ 1.51127527e-01  1.51127527e-01]
     [ 1.49568335e-01  1.49568335e-01]
     [ 1.11056588e-01  1.11056588e-01]
     [ 5.75736494e-02  5.75736493e-02]
     [ 5.77867378e-02  5.77867378e-02]
     [ 5.59235296e-02  5.59235296e-02]
     [ 5.36967009e-02  5.36967009e-02]
     [ 5.31542052e-02  5.31542052e-02]
     [ 9.74006970e-02  9.74006970e-02]
     [ 5.04575855e-02  5.04575855e-02]
     [ 5.07530173e-02  5.07530173e-02]
     [ 4.91620841e-02  4.91620841e-02]
     [ 4.71456249e-02  4.71456249e-02]
     [ 4.65597186e-02  4.65597186e-02]]
    The above two columns you get should be very similar.
    (Left-Your Numerical Gradient, Right-Analytical Gradient)

    If your backpropagation implementation is correct, then
    the relative difference will be small (less than 1e-9).
    Relative Difference: 2.41759e-11

*Once your cost function passes the gradient check for the (unregularized) neural network cost function, you should submit the neural network gradient function (backpropagation).*

```python
grader[4] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    (16, 4)
    (16, 5)
    (16, 4)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |  40 /  40 | Nice work!
                           Regularized Gradient |   0 /  10 |
                                      --------------------------------
                                                |  90 / 100 |

### 2.5 Regularized Neural Network

After you have successfully implemented the backpropagation algorithm, you will add regularization to the gradient. To account for regularization, it turns out that you can add this as an additional term *after* computing the gradients using backpropagation.

Specifically, after you have computed $\Delta_{ij}^{(l)}$ using backpropagation, you should add regularization using

$$ \begin{align}
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} & \qquad \text{for } j = 0 \\
& \frac{\partial}{\partial \Theta_{ij}^{(l)}} J(\Theta) = D_{ij}^{(l)} = \frac{1}{m} \Delta_{ij}^{(l)} + \frac{\lambda}{m} \Theta_{ij}^{(l)} & \qquad \text{for } j \ge 1
\end{align}
$$

Note that you should *not* be regularizing the first column of $\Theta^{(l)}$ which is used for the bias term. Furthermore, in the parameters $\Theta_{ij}^{(l)}$, $i$ is indexed starting from 1, and $j$ is indexed starting from 0. Thus,

$$
\Theta^{(l)} = \begin{bmatrix}
\Theta_{1,0}^{(i)} & \Theta_{1,1}^{(l)} & \cdots \\
\Theta_{2,0}^{(i)} & \Theta_{2,1}^{(l)} & \cdots \\
\vdots &  ~ & \ddots
\end{bmatrix}
$$

[Now modify your code that computes grad in `nnCostFunction` to account for regularization.](#nnCostFunction)

After you are done, the following cell runs gradient checking on your implementation. If your code is correct, you should expect to see a relative difference that is less than 1e-9.

```python
#  Check gradients by running checkNNGradients
lambda_ = 3
utils.checkNNGradients(nnCostFunction, lambda_)

# Also output the costFunction debugging values
debug_J, _  = nnCostFunction(nn_params, input_layer_size,
                          hidden_layer_size, num_labels, X, y, lambda_)

print('\n\nCost at (fixed) debugging parameters (w/ lambda = %f): %f ' % (lambda_, debug_J))
print('(for lambda = 3, this value should be about 0.576051)')
```

    [[-9.27825235e-03 -9.27825236e-03]
     [-1.67679797e-02 -1.67679797e-02]
     [-6.01744725e-02 -6.01744725e-02]
     [-1.73704651e-02 -1.73704651e-02]
     [ 8.89911959e-03  8.89911960e-03]
     [ 3.94334829e-02  3.94334829e-02]
     [-3.19612287e-02 -3.19612287e-02]
     [-5.75658668e-02 -5.75658668e-02]
     [-8.36010761e-03 -8.36010762e-03]
     [ 5.93355565e-02  5.93355565e-02]
     [ 2.49225535e-02  2.49225535e-02]
     [-4.51963845e-02 -4.51963845e-02]
     [ 7.62813550e-03  7.62813551e-03]
     [ 2.47640974e-02  2.47640974e-02]
     [ 5.97717617e-02  5.97717617e-02]
     [ 9.14587966e-03  9.14587966e-03]
     [-6.74798369e-03 -6.74798370e-03]
     [-3.26881426e-02 -3.26881426e-02]
     [ 3.86410548e-02  3.86410548e-02]
     [ 5.46101547e-02  5.46101547e-02]
     [ 3.14544970e-01  3.14544970e-01]
     [ 1.18682669e-01  1.18682669e-01]
     [ 2.03987128e-01  2.03987128e-01]
     [ 1.25698067e-01  1.25698067e-01]
     [ 1.76337550e-01  1.76337550e-01]
     [ 1.32294136e-01  1.32294136e-01]
     [ 1.11056588e-01  1.11056588e-01]
     [ 3.81928689e-05  3.81928696e-05]
     [ 1.17148233e-01  1.17148233e-01]
     [-4.07588279e-03 -4.07588279e-03]
     [ 1.13133142e-01  1.13133142e-01]
     [-4.52964427e-03 -4.52964427e-03]
     [ 9.74006970e-02  9.74006970e-02]
     [ 3.36926556e-02  3.36926556e-02]
     [ 7.54801264e-02  7.54801264e-02]
     [ 1.69677090e-02  1.69677090e-02]
     [ 8.61628953e-02  8.61628953e-02]
     [ 1.50048382e-03  1.50048382e-03]]
    The above two columns you get should be very similar.
    (Left-Your Numerical Gradient, Right-Analytical Gradient)

    If your backpropagation implementation is correct, then
    the relative difference will be small (less than 1e-9).
    Relative Difference: 2.30082e-11

    Cost at (fixed) debugging parameters (w/ lambda = 3.000000): 0.576051
    (for lambda = 3, this value should be about 0.576051)

```python
grader[5] = nnCostFunction
grader.grade()
```

    Submitting Solutions | Programming Exercise neural-network-learning

    (16, 4)
    (16, 5)
    (16, 4)
                                      Part Name |     Score | Feedback
                                      --------- |     ----- | --------
                  Feedforward and Cost Function |  30 /  30 | Nice work!
                      Regularized Cost Function |  15 /  15 | Nice work!
                               Sigmoid Gradient |   5 /   5 | Nice work!
      Neural Network Gradient (Backpropagation) |  40 /  40 | Nice work!
                           Regularized Gradient |  10 /  10 | Nice work!
                                      --------------------------------
                                                | 100 / 100 |

### 2.6 Learning parameters using `scipy.optimize.minimize`

After you have successfully implemented the neural network cost function
and gradient computation, the next step we will use `scipy`'s minimization to learn a good set parameters.

```python
#  After you have completed the assignment, change the maxiter to a larger
#  value to see how more training helps.
options= {'maxiter': 100}

#  You should also try different values of lambda
lambda_ = 1

# Create "short hand" for the cost function to be minimized
costFunction = lambda p: nnCostFunction(p, input_layer_size,
                                        hidden_layer_size,
                                        num_labels, X, y, lambda_)

# Now, costFunction is a function that takes in only one argument
# (the neural network parameters)
res = optimize.minimize(costFunction,
                        initial_nn_params,
                        jac=True,
                        method='TNC',
                        options=options)

# get the solution of the optimization
nn_params = res.x

# Obtain Theta1 and Theta2 back from nn_params
Theta1 = np.reshape(nn_params[:hidden_layer_size * (input_layer_size + 1)],
                    (hidden_layer_size, (input_layer_size + 1)))

Theta2 = np.reshape(nn_params[(hidden_layer_size * (input_layer_size + 1)):],
                    (num_labels, (hidden_layer_size + 1)))
```

After the training completes, we will proceed to report the training accuracy of your classifier by computing the percentage of examples it got correct. If your implementation is correct, you should see a reported
training accuracy of about 95.3% (this may vary by about 1% due to the random initialization). It is possible to get higher training accuracies by training the neural network for more iterations. We encourage you to try
training the neural network for more iterations (e.g., set `maxiter` to 400) and also vary the regularization parameter $\lambda$. With the right learning settings, it is possible to get the neural network to perfectly fit the training set.

```python
pred = utils.predict(Theta1, Theta2, X)
print('Training Set Accuracy: %f' % (np.mean(pred == y) * 100))
```

    Training Set Accuracy: 95.940000

## 3 Visualizing the Hidden Layer

One way to understand what your neural network is learning is to visualize what the representations captured by the hidden units. Informally, given a particular hidden unit, one way to visualize what it computes is to find an input $x$ that will cause it to activate (that is, to have an activation value
($a_i^{(l)}$) close to 1). For the neural network you trained, notice that the $i^{th}$ row of $\Theta^{(1)}$ is a 401-dimensional vector that represents the parameter for the $i^{th}$ hidden unit. If we discard the bias term, we get a 400 dimensional vector that represents the weights from each input pixel to the hidden unit.

Thus, one way to visualize the “representation” captured by the hidden unit is to reshape this 400 dimensional vector into a 20 × 20 image and display it (It turns out that this is equivalent to finding the input that gives the highest activation for the hidden unit, given a “norm” constraint on the input (i.e., $||x||_2 \le 1$)).

The next cell does this by using the `displayData` function and it will show you an image with 25 units,
each corresponding to one hidden unit in the network. In your trained network, you should find that the hidden units corresponds roughly to detectors that look for strokes and other patterns in the input.

```python
utils.displayData(Theta1[:, 1:])
```

### 3.1 Optional (ungraded) exercise

In this part of the exercise, you will get to try out different learning settings for the neural network to see how the performance of the neural network varies with the regularization parameter $\lambda$ and number of training steps (the `maxiter` option when using `scipy.optimize.minimize`). Neural networks are very powerful models that can form highly complex decision boundaries. Without regularization, it is possible for a neural network to “overfit” a training set so that it obtains close to 100% accuracy on the training set but does not as well on new examples that it has not seen before. You can set the regularization $\lambda$ to a smaller value and the `maxiter` parameter to a higher number of iterations to see this for youself.

Coursera Machine Learning Python Assignments/Exercise4/exercise4.ipynb


# Extend the concentrated liquidity
From the balencor paper, we have the invariance function defined as (1), where $L$ is a constant.
\begin{equation*}
 B_x^{w_x} B_y^{w_y} = L  \tag{1}
\end{equation*}

Let $A_i$ and $A_o$ as the amount of tokens i and o exchanged, when a user sends token i (in) to get tokens o (out).  The price of

Out-Given-in:

$$ A_o = B_o (1- (\frac{B_i}{B_i+A_i}))^\frac{w_i}{w_o}$$

In-Given-Out:

$$A_i = B_i((\frac{B_o}{B_o - A_o})^\frac{w_o}{w_i} -1) $$

Spotprice:
$$SP_{i}^o = \frac{B_{i} \cdot w_o}{B_o \cdot w_{i}}$$

In-Given-Price:

$$A_i = B_i((\frac{sp_i^{o'}}{sp_i^o})^{w_o} - 1) $$

Liquidity fingerprint:

If follow the definition of Uniswap v3, where liquidity is defined as $ L =  \frac{\partial y}{\partial \sqrt{P}}$ and let price tick $t_i = log(P)$, the liquidity fingerprint is

\begin{equation}
L(t_i) = 2Lw_x^{w_y}w_y^{w_x}exp( (w_x- \frac{1}{2})t_i)
\end{equation}

Concentrated liquilidy
There is a trading function that describes the relationship between the reserves while its liquidity is in the range:

$$ (x + x_{offset})^{w_x} (y+y_{offset})^{w_y} = L $$

$$ L =  \frac{\partial y}{\partial P^{w_x}} (\frac{w_x}{w_y})^{w_x} $$

or equivalently by switch symbol x and y (and take price reciprocal)
$$ L =  \frac{\partial x}{\partial P^{-w_y}} (\frac{w_y}{w_x})^{w_y} $$

For a price range $[p_a, p_b]$  (price is price of x interms of y) and let tick $t_i = log(P)$

$$ y_{offset} = L \cdot exp(w_x t_i) (\frac{w_y}{w_x})^{w_x} $$

$$ x_{offset} = L \cdot exp(-w_y t_i) (\frac{w_x}{w_y})^{w_y} $$

### Prove of Liquidity fingerprint:

Start with the invariant trading function:

$$ x^{w_x} \cdot y^{w_y} = L$$
Solving for y:
$$ y = (\frac{L}{x^{w_x}})^{\frac{1}{w_y}}$$
Given the sport price P (price of x as of y):
$$ P = \frac{y\cdot w_x}{x \cdot w_y} $$
We can rewrite P as
$$ P = (\frac{L}{x})^\frac{1}{w_y}\cdot \frac{w_x}{w_y}$$

To find the same price, but as a function of y rather than x, we can swithing x and y and taking the reciprocal:
$$ P_y = (\frac{y}{L})^\frac{1}{w_x} \cdot \frac{w_x}{w_y}$$

Solving Y:
$$y = L \cdot P^{w_x} (\frac{w_y}{w_x})^{w_x} \tag{2}$$

If following the definition in Uniswap v3, where liquidity is defined as $ L =  \frac{\partial y}{\partial \sqrt{P}}$ and let price tick $t_i = log(P)$, the liquidity fingerprint is

\begin{equation}
L(t_i) = 2Lw_x^{w_y}w_y^{w_x}exp((w_x- \frac{1}{2})t_i)
\end{equation}

In stead of define liquidy fingerprint as$\frac{\partial y}{\partial \sqrt{P}} $ a special case when $w_x = \frac{1}{2}$, we re-define the it as
$$ \frac{\partial y}{\partial P^{w_x}} = L \cdot (\frac{w_y}{w_x})^{w_x} $$  It indicates that liquidity is constant for every price tick chage in unit of $w_ilog(P)$.

To calculate the concentrated liquity boundary, we take the derivative w.r.t $P^{w_x}$:
$$ \frac{\partial y}{\partial P^{w_x}} = L \cdot (\frac{w_y}{w_x})^{w_x} $$

For a price range $[p_a, p_b]$  (price is price of x interms of y), and let price tick $t_i = log(P)$,
$$ \Delta y =  \Delta P^{w_x} L\cdot \frac{w_y}{w_x}^{w_x}$$

$$ y_{offset} = L \cdot exp(w_x t_i) (\frac{w_y}{w_x})^{w_x} $$

$$ x_{offset} = L \cdot exp(-w_y t_i) (\frac{w_x}{w_y})^{w_y} $$

```python
# Illustrations
```

```python
import scipy
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import random
import pandas as pd

#Liquity fingerprint
w_x=0.25
w_y= 1-w_x
t =  np.array(list(range(-40,50)))/5
p = np.exp(t)
L = 1000

lt = L*2*w_x**w_y*w_y**w_x*np.exp((w_x - 0.5)*t)  # w.r.t sqrt(P)
#lt1 = L * (w_y/w_x)**w_x * w_x* np.exp(-w_y*t)    # w.r.t P
lt2 = L * (w_y/w_x)**w_x   # w.r. t p^w_x

fig, ax = plt.subplots()
#sns.lineplot(t,lt1, label="liquidity space - w.r.t p",)
sns.lineplot(t,lt, label="liquidity space - w.r.t sqrt(p) (t = log(p) ) ",)
sns.lineplot(t,lt2, label="liquidity space 2 - w.r.t p^w_x (t= w_x *log(p)",)
ax.set_xticks(range(-10,10))
ax.vlines(x = [0,1], ymin = [0, 0], ymax = [lt2, lt2], linestyle='--', color = 'orange')
ax.vlines(x = 2, ymin=0, ymax= 441, linestyle='--')
ax.set_ylim(0,)
ax.set_xlabel('price tick')
plt.show()

```

```python

```

```python
x = np.array(list(range(1,2500)))
w_x = 0.25
w_y = 1-w_x

L = 1000
y = (L/(x**w_x))**(1/w_y)

# suppose set price range [pa, pb] = [0.1 2], then ti = -/+ 0.69
pa = 0.5
pb = 2

y_offset = L*np.exp(w_y*np.log(pa))*(w_y/w_x)**(w_x)

x_offset = L*np.exp(-w_y*np.log(pb))*(w_x/w_y)**(w_y)

print("the virtual reserve of x is {}".format(x_offset))
print("the virtual reserve of y is {}".format(y_offset))

x_tilta = x - x_offset
y_tilta = (L/x**w_x)**(1/w_y) - y_offset

df = pd.DataFrame({'x_reserve':x, 'y_reserve':y, 'x_offset': x_tilta, 'y_offset': y_tilta})
ya = y_offset
xa = (L/(ya**w_y))**(1/w_x)

xb = x_offset
yb = (L/(xb**w_x))**(1/w_y)

fig, ax = plt.subplots()
sns.lineplot(data=df, x='x_reserve', y='y_reserve', color = 'blue', label= 'virtual reserves')
sns.lineplot(data=df, x='x_offset', y='y_offset', color= 'orange', label= 'real reserves')
plt.scatter(x=xb, y=yb, color='r', label = 'Pb')
plt.scatter(x=xa, y=ya, color='g', label = 'Pa')

plt.scatter(x=xa-x_offset, y=ya-y_offset, color='g', label = 'Pa')
plt.scatter(x=xb-x_offset, y=yb-y_offset, color='r', label = 'Pa')

ax.set_xlim(0,)
ax.set_ylim(0, 2500)
plt.show()
```

```python

```

```python

```

```python

```

```python

```

# New Section


# Numerical Assignment 2 Dodos and Humans
Name and Student ID

Adapt the python code to model the initial value problem, which describes the interaction of the dodo population (p) and human population h in the 17th century the year (t=0) that they are exposed to man and subsequent interactions modeled represented by x the over 10 years using a stepsize of h=0.1.

```python
## Library
import numpy as np
import matplotlib.pyplot as plt
```

## Dodo and Human Population Equations before man
The Dodo population is described by the non-linear differential equation,
\begin{equation}
\frac{d p}{dt}=\alpha_{Birth} p-\alpha_{Death}p^2-p(h-1),
\end{equation}
the Human population is descirbed by the linear differential equation
\begin{equation}
\frac{d h}{dt}=0.1h,
\end{equation}
where $p$ is the Dodo population and $h$ is the human population at time $t$,
for $a\leq t \leq b$ years, where  $\alpha_{Birth}$ is the birth rate and $\alpha_{Death}$ is the death rate, with the initial condition
$$ p(a)=A. $$
TIn this simulation we set the parameters of the different equations:
*  as $\alpha_{Birth}=0.3$, and $\alpha_{Death}=0.01$ with the inital conditions $p(0)=30$ and $h(0)=1$, over the time period $0\leq t \leq 10$.

```python
alpha_birth=0.3
alpha_death=0.01
```

## Discrete Time Domain
To numerically approximate the solution we discretise the continuous domain using a step size, $h=0.1$,
which gives
\begin{equation}
t_i=0+i h,
\end{equation}
for $i=0,...10$.

The Figure below illustrate the discrete domain.

```python
h=0.1
t_end=10
t=np.arange(0,t_end+h/2,h)
fig = plt.figure(1,figsize=(14,4))
ax = fig.add_subplot(1,1,1)
ax.plot(t,0*t,'o:',color='k')
ax.set_xlabel('Time (yrs)')
plt.title('Discrete Time')
plt.show()
```

```python
N=int(t_end/h) # Number of Steps
P=np.zeros(N+1) # Numerical Solution P
P[0]=30
H=np.ones(N+1) # Numerical Solution P
```

# Problem 1
Adapt the text in blue and the code below to model the impact of humans on the Dodo population.

We numerically approximate the differential equation using Eulers method to give the difference equation
\begin{equation}
P_{i}=P_{i-1}+h\big( \color{blue}{\text{DODO AND HUMANS}} \big),
\end{equation}
\begin{equation}
H_{i}=H_{i-1}+h\big( \color{blue}{\text{HUMANS}} \big),
\end{equation}
where $h$ is the stepsize and where $P_i$ and $H_i$ is the numerical approximation of $p(t_i)$ and $h(t_i)$ at distance $t_i$, for $i=0,...,10.$

```python
for i in range (1,N+1):
    ### UPDATED DIFFERENCE FOR DODOS TO INCLUDE HUMANS
    P[i]=P[i-1]##
    H[i]=H[i-1]##

    ###

```

The plot below shows the numerical solution $P_i$ for the Dodo population as a function of time $t$.

```python
## Plotting Figure
fig = plt.figure(figsize=(14,4))
plt.plot(t,P,'o:',color='black',label='Dodo')
plt.plot(t,H,'o:',color='red',label='Humans')
plt.legend()
plt.xlabel('Time (yrs)')
plt.ylabel('Population (thousands)')
#ax.legend(loc='best')
plt.title('Dodo Population, P')
plt.show()
```

# Problem 2
Write a brief description of what the solution of the model tells you about the population of Dodos and man.

```python

```


```python
%matplotlib inline
import warnings

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import gridspec

warnings.filterwarnings('ignore')
```
<style type="text/css">
.input, .output_prompt {
display:none !important;
}
</style>

# Introduction to Pulsar Timing

[](http://mybinder.org/repo/matteobachetti/timing-lectures)

(These slides are obtained from the iPython notebook that can be found [here](https://github.com/matteobachetti/timing-lectures))

## Contents

* Finding pulsars: the buried clock

* Frequency analysis: the Fourier Transform and the Power Density Spectrum

* Refine the search: Folding search (+ $Z^2_n$, $H$-test, ...)

* Getting pulse arrival times

# Finding pulsars: the buried clock

# Finding pulsars: the buried clock

* Pulsars are stable rotators: very predictable "clocks"

# Finding pulsars: the buried clock

* Pulsars are stable rotators: very predictable "clocks"

* Often signal buried in noise (below: a 0.853-Hz sinusoidal pulse buried in noise ~30 times stronger)

```python
def pulsar(time, period=1):
    return (1 + np.sin(2 * np.pi / period * time)) / 2.

time_bin = 0.009

# --- Parameters ----
period = 0.8532
pulsar_amp = 0.5
pulsar_stdev = 0.05
noise_amp = 15
noise_stdev = 1.5
# --------------------

# refer to the center of the time bin
time = np.arange(0, 100, time_bin) + time_bin / 2

signal = np.random.normal(pulsar_amp * pulsar(time, period), pulsar_stdev)
noise = np.random.normal(noise_amp, noise_stdev, len(time))
total = signal + noise

# PLOTTING -------------------------
plt.plot(time, signal, 'r-', label='signal')
plt.plot(time, noise, 'b-', label='noise')
plt.plot(time, total, 'k-', label='total')
plt.xlim(0, 30)
plt.xlabel('Time')
plt.ylabel('Flux')
a = plt.legend()
# -----------------------------------
```

# Frequency analysis: the Fourier Transform

Through the Fourier transform, we can decompose a function of time into a series of functions of frequency:

\begin{equation}
\mathcal{F}(\omega) = \int^{\infty}_{-\infty} e^{-i\omega t} f(t)
\end{equation}

or, more appropriate to our case, in the discrete form, we can decompose a time series into a frequency series:

\begin{equation}
F_k = \sum^{N-1}_{k=0} e^{-2\pi i k n/N} t_n
\end{equation}

it is, in general, a **complex** function.

The Fourier transform of a sinusoid will give a high (in absolute terms) value of the $F_k$ corresponding to the frequency of the sinusoid. Other periodic functions will produce high contribution at the fundamental frequency plus one or more multiples of the fundamental, called *harmonics*.

## Our example

Let's take the Fourier transform of the signal we simulated above (only taking *positive* frequencies)

```python
ft = np.fft.fft(total)
freqs = np.fft.fftfreq(len(total), time[1] - time[0])
good = freqs >0
freqs = freqs[good]
ft = ft[good]

# PLOTTING ---------------------------
plt.plot(freqs, ft.real, 'r-', label='real')
plt.plot(freqs, ft.imag, 'b-', label='imag')
plt.xlim([-0.1, 10])
a = plt.legend()
_ = plt.xlabel('Frequency (Hz)')
_ = plt.ylabel('FT')
# -------------------------------------
```

Note that the imaginary part and real part of the Fourier transform have different contributions at the pulsar frequency (1/0.85 s ~ 1.2 Hz). This is because they depend strongly on the phase of the signal [Exercise: **why?**].

## Our example - 2

 If we applied a shift of 240 ms (just any value) to the signal:

```python
shift = 0.240
signal_shift = np.roll(total, int(shift / time_bin))
ft_shift = np.fft.fft(signal_shift)
freqs_shift = np.fft.fftfreq(len(total), time[1] - time[0])
good = freqs_shift >0
freqs_shift = freqs_shift[good]
ft_shift = ft_shift[good]

# PLOTTING -------------------------------------
plt.plot(freqs_shift, ft_shift.real, 'r-', label='real')
plt.plot(freqs_shift, ft_shift.imag, 'b-', label='imag')
plt.xlim([-0.1, 10])
a = plt.legend()
_ = plt.xlabel('Frequency (Hz)')
_ = plt.ylabel('FT')
# ----------------------------------------------
```

we would clearly have non-zero values at ~0.85 Hz both in the real and the imaginary parts.

# The Power Density Spectrum

To solve these issues with real and imaginary parts, we can instead take the *squared modulus* of the Fourier transform. This is called **Periodogram**, but most people use the word **Power Density Spectrum** (a periodogram is actually a single realization of the underlying PDS).

\begin{equation}
\mathcal{P}(\omega) = \mathcal{F}(\omega) \cdot \mathcal{F}^*(\omega)
\end{equation}

This function is positive-definite and in our case results in a clear peak at the pulse frequency, *consistent* between original and shifted signal:

```python
pds = np.abs(ft*ft.conj())
pds_shift = np.abs(ft_shift*ft_shift.conj())

fmax = freqs[np.argmax(pds)]
pmax = 1 / fmax

# PLOTTING ---------------------------------
plt.plot(freqs, pds, 'r-', label='PDS of signal')
plt.plot(freqs_shift, pds_shift, 'b-', label='PDS of shifted signal')
a = plt.legend()
a = plt.xlabel('Frequency (Hz)')
a = plt.ylabel('PDS')
plt.xlim([-0.1, 3.5])
_ = plt.gca().annotate('max = {:.2f} s ({:.2f} Hz)'.format(pmax, fmax), xy=(2., max(pds) / 2))
# -------------------------------------------
```

## The Power Density Spectrum -2

The PDS of a generic non-sinusoidal pulse profile will, in general, contain more than one harmonic, with the fundamental not always predominant.

```python
def gaussian_periodic(x, x0, amp, width):
    '''Approximates a Gaussian periodic function by summing the contributions in the phase
    range 0--1 with those in the phase range -1--0 and 1--2'''
    phase = x - np.floor(x)
    lc = np.zeros_like(x)
    for shift in [-1, 0, 1]:
        lc += amp * np.exp(-(phase + shift - x0)**2 / width ** 2)

    return lc

def generate_profile(time, period):
    '''Simulate a given profile with 1-3 Gaussian components'''
    total_phase = time / period
    ngauss = np.random.randint(1, 3)
    lc = np.zeros_like(total_phase)
    for i in range(ngauss):
        ph0 = np.random.uniform(0, 1)
        amp = np.random.uniform(0.1, 1)
        width = np.random.uniform(0.01, 0.2)
        lc += gaussian_periodic(total_phase, ph0, amp, width)

    return lc

# PLOTTING -------------------------
ncols = 2
nrows = 3
fig = plt.figure(figsize=(12, 8))
fig.suptitle('Profiles and their PDSs')

gs = gridspec.GridSpec(nrows, ncols)

for c in range(ncols):
    for r in range(nrows):
# ----------------------------------
        noise = np.random.normal(noise_amp, noise_stdev, len(time))

        lc = generate_profile(time, period)
        lc_noisy = np.random.normal(2 * lc, 0.2) + noise

        lcft = np.fft.fft(lc_noisy)
        lcfreq = np.fft.fftfreq(len(lc_noisy), time[1] - time[0])
        lcpds = np.absolute(lcft) ** 2

        # PLOTTING -------------------------

        gs_2 = gridspec.GridSpecFromSubplotSpec(1, 2, subplot_spec=gs[r, c])
        ax = plt.subplot(gs_2[0])
        good = time < period * 3
        ax.plot(time[good] / period, lc[good])

        ax.set_xlim([0,3])
        ax.set_xlabel('Phase')

        ax = plt.subplot(gs_2[1])
        ax.plot(lcfreq[lcfreq > 0], lcpds[lcfreq > 0] / max(lcpds[lcfreq > 0]))
        ax.set_xlabel('Frequency')
        ax.set_xlim([0, 10])

        # ----------------------------------
```

## Pulsation?

Here are some examples of power density spectra. In some cases, it might look like a pulsation is present in the data. How do we assess this?

```python
# PLOTTING -------------------------
ncols = 2
nrows = 3
fig = plt.figure(figsize=(12, 8))
fig.suptitle('Profiles and their PDSs')

gs = gridspec.GridSpec(nrows, ncols)

for c in range(ncols):
    for r in range(nrows):
# ----------------------------------

        noise = np.random.normal(noise_amp, noise_stdev, len(time))
        lc = np.zeros_like(time)
        lc_noisy = np.random.normal(2 * lc, 0.2) + noise

        lcft = np.fft.fft(lc_noisy)
        lcfreq = np.fft.fftfreq(len(lc_noisy), time[1] - time[0])
        lcpds = np.absolute(lcft) ** 2

        # PLOTTING -------------------------

        gs_2 = gridspec.GridSpecFromSubplotSpec(1, 2, subplot_spec=gs[r, c])
        ax = plt.subplot(gs_2[0])
        good = time < period * 3
        ax.plot(time[good] / period, lc[good])

        ax.set_xlim([0,3])
        ax.set_xlabel('Phase')

        ax = plt.subplot(gs_2[1])
        ax.plot(lcfreq[lcfreq > 0], lcpds[lcfreq > 0] / max(lcpds[lcfreq > 0]))
        ax.set_xlabel('Frequency')
        ax.set_xlim([0, 10])

        # ----------------------------------
```

# Epoch folding

Epoch folding consists of summing equal, one pulse period-long, chunks of data. If the period is just right, the crests will sum up in phase, gaining signal over noise [Exercise: **how much will we gain** by summing up in phase $N$ chunks of data at the right period?].

```python
def epoch_folding(time, signal, period, nperiods=3, nbin=16):
    # The phase of the pulse is always between 0 and 1.
    phase = time / period
    phase -= phase.astype(int)

    # First histogram: divide phase range in nbin bins, and count how many signal bins
    # fall in each histogram bin. The sum is weighted by the value of the signal at
    # each phase
    prof_raw, bins = np.histogram(
        phase, bins=np.linspace(0, 1, nbin + 1),
        weights=signal)
    # "Exposure": how many signal bins have been summed in each histogram bin
    expo, bins = np.histogram(phase, bins=np.linspace(0, 1, nbin + 1))

    # ---- Evaluate errors -------
    prof_sq, bins = np.histogram(
        phase, bins=np.linspace(0, 1, nbin + 1),
        weights=signal ** 2)
    # Variance of histogram bin: "Mean of squares minus square of mean" X N
    hist_var = (prof_sq / expo - (prof_raw / expo) ** 2) * expo
    # Then, take square root -> Stdev, then normalize / N.
    prof_err = np.sqrt(hist_var)
    #-----------------------------
    # Normalize by exposure
    prof = prof_raw / expo
    prof_err = prof_err / expo

    # histogram returns all bin edges, including last one. Here we take the
    # center of each bin.
    phase_bins = (bins[1:] + bins[:-1]) / 2

    # ---- Return the same pattern 'nperiods' times, for visual purposes -----
    final_prof = np.array([])
    final_phase = np.array([])
    final_prof_err = np.array([])
    for n in range(nperiods):
        final_prof = np.append(final_prof, prof)
        final_phase = np.append(final_phase, phase_bins + n)
        final_prof_err = np.append(final_prof_err, prof_err)
    # ---------------------------
    return final_phase, final_prof, final_prof_err

phase, profile, profile_err = epoch_folding(time, total, period)
phase_shift, profile_shift, profile_shift_err = epoch_folding(time, signal_shift, period)

# PLOTTING -------------------------------------------------------------
plt.errorbar(
    phase, profile, yerr=profile_err, drawstyle='steps-mid',
    label='Signal')
plt.errorbar(
    phase_shift, profile_shift, yerr=profile_shift_err,
    drawstyle='steps-mid', label='Shifted signal')

_ = plt.legend()
_ = plt.xlabel('Phase')
_ = plt.ylabel('Counts/bin')
# -------------------------------------------------------------------
```

# Epoch folding search

Now, let's run epoch folding at a number of trial periods around the pulse period. To evaluate how much a given profile "looks pulsar-y", we can use the $\chi^2$ statistics, as follows:

\begin{equation}
\mathcal{S} = \sum_{i=0}^N \frac{(p_i - \bar{p})^2}{\sigma_p^2}
\end{equation}

for each profile obtained for each trial value of the pulse frequency and look for peaks$^1$. [Exercise: do you know what statistics this is? And why does that statistics work for our case? Exercise-2: Note the very large number of trials. Can we optimize the search so that we use less trials without losing sensitivity?]

$^1$ 1.	Leahy, D. A. et al. On searches for pulsed emission with application to four globular cluster X-ray sources - NGC 1851, 6441, 6624, and 6712. _ApJ_ **266**, 160 (1983).

```python
def pulse_profile_stat(profile, profile_err):
    return np.sum(
        (profile - np.mean(profile)) ** 2 / profile_err ** 2)

trial_periods = np.arange(0.7, 1.0, 0.0002)
stats = np.zeros_like(trial_periods)
for i, p in enumerate(trial_periods):
    phase, profile, profile_err = epoch_folding(time, total, p)
    stats[i] = pulse_profile_stat(profile, profile_err)

bestp = trial_periods[np.argmax(stats)]

phase_search, profile_search, profile_search_err = \
    epoch_folding(time, total, bestp)
phase, profile, profile_err = epoch_folding(time, total, period)

# PLOTTING -------------------------------
fig = plt.figure(figsize=(10, 3))
gs = gridspec.GridSpec(1, 2)
ax = plt.subplot(gs[0])
ax.plot(trial_periods, stats)
ax.set_xlim([0.7, 1])
ax.set_xlabel('Period (s)')
ax.set_ylabel('$\chi^2$')
ax.axvline(period, color='r', label="True value")
_ = ax.legend()
ax.annotate('max = {:.5f} s'.format(pmax), xy=(.9, max(stats) / 2))

ax2 = plt.subplot(gs[1])

ax2.errorbar(phase_search, profile_search, yerr=profile_search_err,
             drawstyle='steps-mid', label='Search')
ax2.errorbar(phase, profile, yerr=profile_err, drawstyle='steps-mid',
             label='True period')
ax2.set_xlabel('Phase')
ax2.set_ylabel('Counts/bin')

_ = ax2.legend()
# ------------------------------------------
```

## The Z_n search

$Z_n^2$ is another widely used statistics for high-energy pulsar searches.
It measures how the probability of photons in a given phase is proportional to a given combination of $n$ harmonics. Or in other words, how well the pulse profile is described by a combination of sinusoidal harmonics.
The definition of this statistical indicator is (Buccheri+1983):

$$
Z^2_n = \dfrac{2}{N} \sum_{k=1}^n \left[{\left(\sum_{j=1}^N \cos k \phi_j\right)}^2 + {\left(\sum_{j=1}^N \sin k \phi_j\right)}^2\right] \; ,
$$

The formula can be slightly modified for binned data, by introducing a `weight` quantity giving the number of photons (or another measure of flux) in a given bin (Huppenkothen+2019):

$$
Z^2_n \approx \dfrac{2}{\sum_j{w_j}} \sum_{k=1}^n \left[{\left(\sum_{j=1}^m w_j \cos k \phi_j\right)}^2 + {\left(\sum_{j=1}^m w_j \sin k \phi_j\right)}^2\right]
$$

```python
def z_n(time, p, n=2, weight=1):
    '''Z^2_n statistics, a` la Buccheri+03, A&A, 128, 245, eq. 2.

    Parameters
    ----------
    phase : array of floats
        The phases of the events
    n : int, default 2
        Number of harmonics, including the fundamental

    Other Parameters
    ----------------
    norm : float or array of floats
        A normalization factor that gets multiplied as a weight.

    Returns
    -------
    z2_n : float
        The Z^2_n statistics of the events.
    '''
    phase = time / p

    nbin = len(phase)

    if nbin == 0:
        return 0

    weight = np.asarray(weight)
    if weight.size == 1:
        total_weight = nbin * weight
    else:
        total_weight = np.sum(weight)
    phase = phase * 2 * np.pi
    return 2 / total_weight * \
        np.sum([np.sum(np.cos(k * phase) * weight) ** 2 +
                np.sum(np.sin(k * phase) * weight) ** 2
                for k in range(1, n + 1)])

trial_periods = np.arange(0.7, 1.0, 0.0002)
stats = np.zeros_like(trial_periods)
for i, p in enumerate(trial_periods):
    stats[i] = z_n(time, p, weight=total)

bestp = trial_periods[np.argmax(stats)]

phase_search, profile_search, profile_search_err = \
    epoch_folding(time, total, bestp)
phase, profile, profile_err = epoch_folding(time, total, period)

# PLOTTING -------------------------------
fig = plt.figure(figsize=(10, 3))
gs = gridspec.GridSpec(1, 2)
ax = plt.subplot(gs[0])
ax.plot(trial_periods, stats)
ax.set_xlim([0.7, 1])
ax.set_xlabel('Period (s)')
ax.set_ylabel('$\chi^2$')
ax.axvline(period, color='r', label="True value")
_ = ax.legend()
ax.annotate('max = {:.5f} s'.format(pmax), xy=(.9, max(stats) / 2))

ax2 = plt.subplot(gs[1])

ax2.errorbar(phase_search, profile_search, yerr=profile_search_err,
             drawstyle='steps-mid', label='Search')
ax2.errorbar(phase, profile, yerr=profile_err, drawstyle='steps-mid',
             label='True period')
ax2.set_xlabel('Phase')
ax2.set_ylabel('Counts/bin')

_ = ax2.legend()
# ------------------------------------------
```

# Times of arrival (TOA)
To calculate the time of arrival of the pulses, we need to:

* Choose what **part of the pulse** is the reference (e.g., the maximum). Once we know that, if $\phi_{max}$ is the phase of the maximum of the pulse, $t_{start}$ the time at the start of the folded light curve, and $p$ is the folding period,

$TOA = t_{start} + \phi_{max} \cdot p$

* Choose a **method** to calculate the TOA:

  + The maximum bin?

  + The phase of a sinusoidal fit?

  + The phase of a more complicated fit?

Hereafter, we are going to use the maximum of the pulse as a reference, and we will calculate the TOA with the three methods above.

## TOA from the maximum bin

**Advantage**

* Very fast and easy to implement

**Disadvantages**

* Very rough (maximum precision, the width of the bin)

* Very uncertain (if statistics is low and/or the pulse is broad, many close-by bins can randomly be the maximum)

```python
phase_bin = 1 / 32.
ph = np.arange(phase_bin / 2, phase_bin / 2 + 1, phase_bin)
shape = np.sin(2 * np.pi * ph) + 2
pr_1 = np.random.poisson(shape * 10000) / 10000
pr_2 = np.random.poisson(shape * 10) / 10

# PLOTTING -----------------------------
plt.plot(ph, shape, label='Theoretical shape', color='k')
plt.plot(
    ph, pr_1, drawstyle='steps-mid', color='r',
    label='Shape - good stat')
plt.plot(
    ph, pr_2, drawstyle='steps-mid', color='b',
    label='Shape - bad stat')
plt.axvline(0.25, ls=':', color='k', lw=2, label='Real maximum')
plt.axvline(
    ph[np.argmax(pr_1)], ls='--', color='r', lw=2,
    label='Maximum - good stat')
plt.axvline(
    ph[np.argmax(pr_2)], ls='--', color='b', lw=2,
    label='Maximum - bad stat')

_ = plt.legend()
# --------------------------------------
```

## TOA from single sinusoidal fit

**Advantage**

* Relatively easy task (fitting with a sinusoid)

* Errors are well determined provided that the pulse is broad

**Disadvantages**

* If profile is not sinusoidal, might not be well determined

Below, the phase of the pulse is always 0.25

```python
def sinusoid(phase, phase0, amplitude, offset):
    return offset + np.cos(2 * np.pi * (phase - phase0))

from scipy.optimize import curve_fit

# PLOTTING ------------------
fig = plt.figure(figsize=(12, 3))
gs = gridspec.GridSpec(1, 4)
ax1 = plt.subplot(gs[0])
ax1.set_title('Theoretical')
ax2 = plt.subplot(gs[1])
ax2.set_title('Sinusoidal, good stat')
ax3 = plt.subplot(gs[2])
ax3.set_title('Sinusoidal, noisy')
ax4 = plt.subplot(gs[3])
ax4.set_title('Complicated profile')
# ---------------------------

# Fit sinusoid to theoretical shape
par, pcov = curve_fit(sinusoid, ph, shape)

# PLOTTING -----------------------------------------------
ax1.plot(ph, sinusoid(ph, *par))
ax1.plot(ph, shape)
par[0] -= np.floor(par[0])
ax1.annotate('phase = {:.2f}'.format(par[0]), xy=(.5, .3))
ax1.set_ylim([0, 4])

# Fit to good-stat line
# ---------------------------------------------------------

par, pcov = curve_fit(sinusoid, ph, pr_1)

# PLOTTING -----------------------------------------------

ax2.plot(ph, sinusoid(ph, *par))
ax2.plot(ph, pr_1)
par[0] -= np.floor(par[0])
ax2.annotate('phase = {:.2f}'.format(par[0]), xy=(.5, .3))
ax2.set_ylim([0, 4])

# Fit to bad-stat line
# ---------------------------------------------------------

par, pcov = curve_fit(sinusoid, ph, pr_2)

# PLOTTING -----------------------------------------------

ax3.plot(ph, sinusoid(ph, *par))
ax3.plot(ph, pr_2, drawstyle='steps-mid')
par[0] -= np.floor(par[0])
ax3.annotate('phase = {:.2f}'.format(par[0]), xy=(.5, .3))
ax3.set_ylim([0, 4])

# Now try with a complicated profile (a double Gaussian)
# ---------------------------------------------------------

pr_3_clean = 0.3 + np.exp(- (ph - 0.25) ** 2 / 0.001) + 0.5 * np.exp(- (ph - 0.75) ** 2 / 0.01)
pr_3 = np.random.poisson(pr_3_clean * 100) / 50
# Let us normalize the template with the same factor (100 / 50) of the randomized one. It will be helpful later
pr_3_clean *= 2

par, pcov = curve_fit(sinusoid, ph, pr_3, maxfev=10000)

# PLOTTING -----------------------------------------------

ax4.plot(ph, sinusoid(ph, *par), label='Fit')
ax4.plot(ph, pr_3, drawstyle='steps-mid', label='Noisy profile')
ax4.plot(ph, pr_3_clean, label='Real profile')

par[0] -= np.floor(par[0])
ax4.annotate('phase = {:.2f}'.format(par[0]), xy=(.5, .3))
ax4.set_ylim([0, 4])
_ = ax4.legend()
# ---------------------------------------------------------
```

## TOA from non-sinusoidal fit: multiple harmonic fitting

**Multiple harmonic fitting**$^1$ (the profile is described by a sum of sinusoids) is just an extension of the single-harmonic fit by adding additional sinusoidal components at multiple frequencies.

**Advantages**

* Still conceptually easy, but more robust and reliable

**Disadvantages**

* The phase is not determined by the fit (in general, it isn't he phase of any of the sinusoids [Exercise: why?]) and needs to be determined from the maximum of the profile. Errors are not straightforward to implement.

$^1$e.g. Riggio, A. et al. Timing of the accreting millisecond pulsar IGR J17511-3057. _A&A_ **526**, 95 (2011).

## TOA from non-sinusoidal fit: Template pulse shapes

* **Cross-correlation** of template pulse shape

* **Fourier-domain fitting (FFTFIT)**$^1$ -> the usual choice. Consists of taking the Fourier transform of the profile $\mathcal{P}$ and of the template $\mathcal{T}$ and minimizing the following objective function (similar to $\chi^2$):
\begin{equation}
F = \sum_k \frac{|\mathcal{P}_k - a\mathcal{T}_k e^{-2\pi i k\phi}|^2}{\sigma^2}
\end{equation}

**Advantages**

* Much more robust and reliable

* Errors well determined whatever the pulse shape

**Disadvantages**

* Relatively trickier to implement

* Needs good template pulse profile

$^1$Taylor, J. H. Pulsar Timing and Relativistic Gravity. _Philosophical Transactions: Physical Sciences and Engineering_ **341**, 117â€“134 (1992).

```python
def fftfit_fun(profile, template, amplitude, phase):
    '''Objective function to be minimized - \'a la Taylor (1992).'''

    prof_ft = np.fft.fft(profile)
    temp_ft = np.fft.fft(template)
    freq = np.fft.fftfreq(len(profile))
    good = freq > 0
    idx = np.arange(0, prof_ft.size, dtype=int)
    sigma = np.std(prof_ft[good])
    return np.sum(np.absolute(prof_ft - temp_ft*amplitude*np.exp(-2*np.pi*1.0j*idx*phase))**2 / sigma)

def obj_fun(pars, data):
    '''Wrap parameters and input data up in order to be used with minimization
    algorithms.'''
    amplitude, phase = pars
    profile, template = data
    return fftfit_fun(profile, template, amplitude, phase)

# Produce 16 realizations of pr_3, at different amplitudes and phases, and reconstruct the phase
from scipy.optimize import fmin, basinhopping

# PLOTTING --------------------------

fig = plt.figure(figsize=(10, 10))
fig.suptitle('FFTfit results')
gs = gridspec.GridSpec(4, 4)

# -----------------------------------
amp0 = 1
phase0 = 0
p0 = [amp0, phase0]
for i in range(16):
    # PLOTTING --------------------------
    col = i % 4
    row = i // 4
    # -----------------------------------

    factor = 10 ** np.random.uniform(1, 3)
    pr_orig = np.random.poisson(pr_3_clean * factor)
    roll_len = np.random.randint(0, len(pr_orig) - 1)
    pr = np.roll(pr_orig, roll_len)

#    # Using generic minimization algorithms is faster, but local minima can be a problem
#    res = fmin(obj_fun, p0, args=([pr, pr_3_clean],), disp=False, full_output=True)
#    amplitude_res, phase_res = res[0]

    # The basinhopping algorithm is very slow but very effective in finding
    # the global minimum of functions with local minima.
    res = basinhopping(obj_fun, p0, minimizer_kwargs={'args':([pr, pr_3_clean],)})
    amplitude_res, phase_res = res.x

    phase_res -= np.floor(phase_res)
    newphase = ph + phase_res
    newphase -= np.floor(newphase)

    # Sort arguments of phase so that they are ordered in plot
    # (avoids ugly lines traversing the plot)
    order = np.argsort(newphase)

    # PLOTTING --------------------------

    ax = plt.subplot(gs[row, col])
    ax.plot(ph, pr, 'k-')

    ax.plot(newphase[order], amplitude_res * pr_3_clean[order], 'r-')
    # -------------------------------------
```

## Pulsation searches with HENDRICS

1. To read a fits file into an event list file:

```
$ HENreadevents file.evt.gz
```
a file called something like `file_mission_instr_ev.nc` appears

2. To calculate the light curve (binning the events) with a sample time of 1 s:

```
$ HENlcurve file_mission_instr_ev.nc -b 1
```

3. To calculate the averaged power density spectrum cutting the data by chunks of 128 s:

```
$ HENfspec file_mission_instr_lc.nc -f 128
```

4. To watch the power density spectrum:

```
$ HENplot file_mission_instr_pds.nc
```

5. To run a $Z^2_4$ search, e.g.  between frequencies 0.5 and 0.6:

```
$ HENzsearch file_mission_instr_ev.nc -f 0.5 -F 0.6 -N 4
```

6. To run a $Z^2_2$ search searching in the frequency -- fdot space

```
$ HENzsearch file_mission_instr_ev.nc -f 0.5 -F 0.6 -N 2 --fast
$ HENplot file_mission_instr_Z2n.nc
```

7. Then... follow the instructions...

### BONUS

8. Calculate the TOAs and create a parameter and timing file (can you find how?)

9. Use `pintk` (from `github.com/nanograv/PINT`) to fit the pulse solution

```
$ pintk parfile.par timfile.tim
```

NB: due to a bug to PINT (under investigation), you might need to add the line
```
TZRMJD    55555
```

Substitute 55555 with the value of PEPOCH in the parameter file.

```python

```


# Optimización de funciones escalares diferenciables con `SymPy`

> - Mediante optimización se obtienen soluciones elegantes tanto en teoría como en ciertas aplicaciones.
> - La teoría de optimización usa elementos comenzando con cálculo elemental y álgebra lineal básica, y luego se extiende con análisis funcional y convexo.
> - Las aplicaciones en optimización involucran ciencia, ingeniería, economía, finanzas e industria.
> - El amplio y creciente uso de la optimización lo hace escencial para estudiantes y profesionales de cualquier rama de la ciencia y la tecnología.

**Referencia:**
- http://www.math.uwaterloo.ca/~hwolkowi//henry/reports/talks.d/t06talks.d/06msribirs.d/optimportance.shtml

Algunas aplicaciones son:

1. Ingeniería
 - Encontrar la composición de equilibrio de una mezcla de diferentes átomos.
 - Planeación de ruta para un robot (o vehículo aéreo no tripulado).
 - Planeación de la mano de obra óptima en una construcción o planta de producción.
2. Distribución óptima de recursos.
 - Distribución de rutas de vuelo.
 - Encontrar una dieta óptima.
 - Planeación de ruta óptima.
3. Optimización financiera
 - Administración de riesgos.
 - Portafolios de inversión.

En esta clase veremos aspectos básicos de optimización. En específico, veremos cómo obtener máximos y mínimos de una función escalar de una variable (como en cálculo diferencial).

___
## 0. Librerías que usaremos

Como ya dijimos en la primer clase `python` es el lenguaje de programación (el cual es de alto nivel). Sin embargo, `python` solo tiene unos pocos comandos primitivos y para hacer más fácil su uso en nuestras actividades de simulación en ingeniería, otras personas ya han escrito ciertas librerías por nosotros.

### 0.1 `NumPy`

`NumPy` (Numerical Python) es la librería fundamental para computación científica (numérica) con `Python`. Contiene, entre otras cosas:
- un objeto tipo arreglo N-dimensional muy poderoso
- funciones sofisticadas
- funciones de álgebra lineal, transformada de Fourier y números aleatorios.

Por lo anterior, `NumPy` es de amplio uso entre la comunidad científica e ingenieril (por su manejo de cantidades vectoriales). De la misma manera, se usa para guardar datos. Para nuestros propósitos, se puede usar libremente.

**Referencia:**
- http://www.numpy.org/

`NumPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería numpy
import numpy as np
```

### 0.2 `SymPy`

`SymPy` (Symbolic Python) es una librería de `Python` para matemáticas simbólicas. Su objetivo es convertirse en un sistema de álgebra computacional con las mejores características, manteniendo el código lo más simple posible para que sea comprensible.

**Referencia:**
- http://www.sympy.org/en/index.html

`SymPy` ya viene incluido en la instalación estándar de Anaconda por defecto. Para comenzar a usarlo, solo debemos de importarlo:

```python
# importar la librería sympy
import sympy as sym
# imprimir en formato latex
sym.init_printing(use_latex='mathjax')
```

La funcionalidad de imprimir en formato LaTex que nos da `SymPy` mediante el proyecto `mathjax` hace de `SymPy` una herramienta muy atractiva...

Notar que en `SymPy` y en `NumPy` existen funciones con el mismo nombre, pero reciben tipos de datos diferentes...

```python
sym.var('x')
```

$$x$$

```python
# diferencias de funciones de sympy y numpy
sym.sin(x), np.sin(3)
```

$$\left ( \sin{\left (x \right )}, \quad 0.14112000806\right )$$

Explicar el uso de la sintaxis `from numpy import *` y sus peligros (no recomendable).

```python
# importar con * y ver que pasa
# from numpy import *
# from sympy import *
# No recomendado
```

### 0.3 `PyPlot` de `matplotlib`

El módulo `PyPlot` de la librería `matplotlib` contiene funciones que nos permite generar una gran cantidad de gráficas rápidamente. Las funciones de este módulo están escritas con el mismo nombre que las funciones para graficar en `Matlab`.

**Referencia:**
- https://matplotlib.org/api/pyplot_summary.html

```python
# importar matplotlib.pyplot
import matplotlib.pyplot as plt
# comando para que las gráficas salgan en la misma ventana
%matplotlib inline
```

Ya que revisamos todas las librerías que usaremos, empecemos con la clase como tal...

___
Basamos todos los resultados en los siguientes teoremas:

## 1. Teorema de Fermat (análisis)

Si una función $f(x)$ alcanza un máximo o mínimo local en $x=c$, y si la derivada $f'(c)$ existe en el punto $c$, entonces $f'(c) = 0$.

### Ejemplo

Sabemos que la función $f(x)=x^2$ tiene un mínimo global en $x=0$, pues

$$f(x)=x^2\geq0,\qquad\text{y}\qquad f(x)=x^2=0 \qquad\text{si y solo si}\qquad x=0.$$

```python
# declarar la variable real x
sym.var('x', real=True)
```

$$x$$

```python
# declarar ahora f=x^2 y mostrar
f = x**2
f
```

$$x^{2}$$

```python
# derivar f respecto a x y mostrar
df = sym.diff(f, x)
df
```

$$2 x$$

```python
# resolver f'(x)=0 y mostrar soluciones
xc = sym.solve(df, x)
xc
```

$$\left [ 0\right ]$$

Veamos la gráfica...

```python
# convertir f e una función que se pueda evaluar numéricamente (función lambdify de la librería sympy)
fnum = sym.lambdify(x, f, modules='numpy')
```

```python
#Coordenadas x (abscisas)
#x = np.array([0, 0.1, 0.2, ])...
xnum = np.linspace(-5,5,100)
ynum = fnum(xnum)
```

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(8,6))
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(xnum, ynum, 'k', label='$y=x^2$')
plt.plot([0], [0], '*y', ms=10, label='$(0,0)$')
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$f(x)=x^2$')
# Sirve para poner las etiquetas de las graficas
plt.legend(loc='best')
# Sirve para poner la cuadricula
plt.grid()
```

Ver diferencias entre f y f_num

```python
# intentar evaluar f y f_num
fnum(np.linspace(0, 5, 4))
```

    array([ 0.        ,  2.77777778, 11.11111111, 25.        ])

**Otra manera de hacer lo anterior**

Concepto de función...

```python
def funcion_de_clase(x):
    return x**2
```

```python
x
```

$$x$$

```python
funcion_de_clase(x)
```

$$x^{2}$$

```python
df = sym.diff(funcion_de_clase(x), x)
df
```

$$2 x$$

```python
xc = sym.solve(df, x)
xc
```

$$\left [ 0\right ]$$

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(8,6))
# Sirve para hacer el grafico y determinar sus caracteristicas
#plt.plot(xnum, funcion_de_clase(xnum), 'k', label='$y=x^2$')
plt.plot(xnum, xnum**2, 'k', label='$y=x^2$')
plt.plot([0], [0], '*y', ms=10, label='$(0,0)$')
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$f(x)=x^2$')
# Sirve para poner las etiquetas de las graficas
plt.legend(loc='best')
# Sirve para poner la cuadricula
plt.grid()
```

```python
A = np.array([[1, 2],
              [0, 1]])
B = np.array([[2, 1],
              [1, 0]])
A.dot(B)
```

    array([[4, 1],
           [1, 0]])

El converso del teorema anterior no es cierto.

### Actividad
Considere $g(x)=x^3$.
- Usando `sympy`, muestre que $g'(0)=0$.
- Sin embargo, descartar que $x=0$ es un extremo de $g(x)$ viendo su **gráfica**.

```python
# Declarar la variable simbolica x
sym.var('x')
```

$$x$$

```python
# Definimos funcion g(x)
def g(x):
    return x**3
```

```python
# Derivamos g(x)
dg = sym.diff(g(x), x)
dg
```

$$3 x^{2}$$

```python
# Puntos criticos
xc = sym.solve(dg, x)
xc
```

$$\left [ 0\right ]$$

```python
# graficar
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(8,6))
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(xnum, g(xnum), 'k', label='$y=x^3$')
plt.plot([0], [0], '*y', ms=10, label='$(0,0)$')
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$y$')
# Sirve para poner las etiquetas de las graficas
plt.legend(loc='best')
# Sirve para poner la cuadricula
plt.grid()
```

## 2. Criterio de la segunda derivada

Sea $f(x)$ una función tal que $f’(c)=0$ y cuya segunda derivada existe en un intervalo abierto que contiene a $c$.
- Si $f’’(c)>0$, entonces $f(c)$ es un mínimo relativo.
- Si $f’’(c)<0$, entonces $f(c)$ es un máximo relativo.
- Si $f’’(c)=0$, entonces el criterio no decide.

### Ejemplo

Mostrar, usando `sympy`, que la función $f(x)=x^2$ tiene un mínimo relativo en $x=0$.

Ya vimos que $f'(0)=0$. Notemos que:

```python
def f(x):
    return x**2
```

```python
# Sacamos la segunda derivada
#d2f = sym.diff(f(x), x, x)
d2f = sym.diff(f(x), x, 2)
d2f
```

$$2$$

```python
d2f>0
```

$$\mathrm{True}$$

Por tanto, por el criterio de la segunda derivada, $f(0)=0$ es un mínimo relativo (en efecto, el mínimo global).

### Ejemplo
¿Qué pasa con $g(x)=x^3$ al intentar utilizar el criterio de la segunda derivada? (usar `sympy`).

```python
g(x)
```

$$x^{3}$$

```python
d2g = sym.diff(g(x), x, 2)
d2g
```

$$6 x$$

```python
d2g.subs(x, 0)
```

$$0$$

Como $g''(0)=0$ entonces el criterio de la segunda derivada no concluye.

### Actividad

¿Qué pasa con $h(x)=x^4$ al intentar utilizar el criterio de la segunda derivada?.

```python

```

```python

```

```python

```

```python

```

```python

```

## 3. Método para determinar extremos absolutos de una función continua y=f(x) en [a,b]

- Determinar todos los valores críticos $c_1, c_2, c_3, \dots, c_n$ en $(a,b)$.
- Evaluar $f$ en todos los valores críticos y en los extremos $x=a$ y $x=b$.
- El más grande y el más pequeño de los valores de la lista $f(a), f(b), f(c_1), f(c_2), \dots, f(c_n)$ son el máximo absoluto y el mínimo absoluto, respectivamente, de f en el intervalo [a,b].

### Ejemplo

Determinar los extremos absolutos de $f(x)=x^2-6x$ en $\left[0,5\right]$.

Obtenemos los puntos críticos de $f$ en $\left[0,5\right]$:

```python
def f(x):
    return x**2-6*x
```

```python
# Derivamos f
df = sym.diff(f(x), x)
df
```

$$2 x - 6$$

```python
xc = sym.solve(df, x)
xc
```

$$\left [ 3\right ]$$

Evaluamos $f$ en los extremos y en los puntos críticos:

```python
f(0), f(xc[0]), f(5)
```

$$\left ( 0, \quad -9, \quad -5\right )$$

Concluimos que el máximo absoluto de $f$ en $\left[0,5\right]$ es $0$ y se alcanza en $x=0$, y que el mínimo absoluto es $-9$ y se alcanza en $x=3$.

```python
# graficar
xnum = np.linspace(0, 5, 100)
# Crear ventana de graficos y damos medidas de la ventana
plt.figure(figsize=(8,6))
# Sirve para hacer el grafico y determinar sus caracteristicas
plt.plot(xnum, f(xnum), 'k', label='$y=x^2-6x$')
plt.plot([0], [0], '*y', ms=10, label='$(0,0)=max$')
plt.plot([3], [-9], '*m', ms=10, label='$(3,-9)=min$')
# Nombre del eje x de la grafica
plt.xlabel('$x$')
# Nombre del eje y
plt.ylabel('$y$')
# Sirve para poner las etiquetas de las graficas
plt.legend(loc='best')
# Sirve para poner la cuadricula
plt.grid()
```

### Actividad
Determinar los valores extremos absolutos de $h(x)=x^3-3x$ en $\left[-2.2,1.8\right]$, usando `sympy`. Mostrar en una gráfica.

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

```python

```

### En varias variables...

El procedimiento es análogo.

Si una función $f:\mathbb{R}^n\to\mathbb{R}$ alcanza un máximo o mínimo local en $\boldsymbol{x}=\boldsymbol{c}\in\mathbb{R}^n$, y $f$ es diferenciable en el punto $\boldsymbol{x}=\boldsymbol{c}$, entonces $\left.\frac{\partial f}{\partial \boldsymbol{x}}\right|_{\boldsymbol{x}=\boldsymbol{c}}=\boldsymbol{0}$ (todas las derivadas parciales en el punto $\boldsymbol{x}=\boldsymbol{c}$ son cero).

**Criterio de la segunda derivada:** para ver si es máximo o mínimo, se toma la segunda derivada (matriz jacobiana) y se verifica definición negativa o positiva, respectivamente.

Si se restringe a cierta región, hay ciertas técnicas. La más general, pero también la más compleja es la de **multiplicadores de Lagrange**.

**Ejemplo:** hacer a mano a la vez para corroborar...

```python
sym.var('x y')
x, y
```

$$\left ( x, \quad y\right )$$

```python
def f(x, y):
    return x**2 + y**2
```

```python
dfx = sym.diff(f(x,y), x)
dfy = sym.diff(f(x,y), y)
dfx, dfy
```

$$\left ( 2 x, \quad 2 y\right )$$

```python
xy_c = sym.solve([dfx, dfy], [x, y])
xy_c
```

$$\left \{ x : 0, \quad y : 0\right \}$$

```python
x_c, y_c = xy_c[x], xy_c[y]
x_c, y_c
```

$$\left ( 0, \quad 0\right )$$

```python
d2fx = sym.diff(f(x,y), x, 2)
d2fy = sym.diff(f(x,y), y, 2)
dfxy = sym.diff(f(x,y), x, y)

Jf = sym.Matrix([[d2fx, dfxy], [dfxy, d2fy]])
Jf.eigenvals()
```

$$\left \{ 2 : 2\right \}$$

```python
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

x = np.linspace(-2, 2, 100)
y = x
X, Y = np.meshgrid(x, y)

ax.plot_surface(X, Y, f(X, Y))
ax.plot([x_c], [y_c], [f(x_c,y_c)], '*r')
```

### Tareas.

1. Recordar tarea de colaborativo.

2. Introducción a numpy. Obtener máximos y mínimos absolutos, usando `sympy`, de las funciones dadas en los intervalos dados, y graficar la función en dicho intervalo señalando los puntos máximo y mínimo absolutos.

<footer id="attribution" style="float:right; color:#808080; background:#fff;">
Created with Jupyter by Esteban Jiménez Rodríguez.
</footer>