Computing the Riemann Constant Vector
=====================================

We use the Python software package [`abelfunctions`](http://abelfunctions.cswiercz.info) to compute with Riemann surfaces obtained via the desingularization and compactification of complex plane algebraic curves. `abelfunctions` is written by Chris Swierczewski ([cswiercz@uw.edu](mailto:cswiercz@uw.edu)).

Throughout this notebook we use the genus 4 curve

$$
    f(x,y) = x^2y^3 - x^4 + 1
$$

as an example.

The primary object in `abelfunctions` is "`RiemannSurface`".

```
%matplotlib inline

from sympy.abc import x,y
from abelfunctions import (RiemannSurface, RiemannTheta, Jacobian,
                           RiemannConstantVector, puiseux, AbelMap)

f = x**2*y**3 - x**4 + 1
X = RiemannSurface(f,x,y)

print X
```

Demo - Homology Basis
=====================

A canonical basis of cycles
$$
    H_1(X,\mathbb{Z}) = \{a_1, \ldots, a_g, b_1, \ldots, b_g\}
$$
is computed using

    X.a_cycles()

and

    X.b_cycles().

```
a = X.a_cycles()
b = X.b_cycles()
```

```
# plot the x- and y-projections of the first a-cycle using
# 256 interpolating points
figx = a[0].plot_x(256, color='b', linestyle='dashed', linewidth=2)
figy = a[0].plot_y(256, color='g', linewidth=2)
```

```
# plot in 3d with the path parameter, t, on the z-axis
#
# note that there are no self-intersections in the path
elev = 50     # 50,-130
azim = -130

figx_3d = a[0].plot3d_x(256, color='blue', linewidth=2)
ax = figx_3d.gca()
ax.view_init(elev=elev, azim=azim)

figy_3d = a[0].plot3d_y(256, color='green', linewidth=2)
ax = figy_3d.gca()
ax.view_init(elev=elev, azim=azim)
```

Demo - Basis of Abelian Differentials of the First Kind
=======================================================

**Note**: `abelfunctions` does not necessarily compute a *normalized* basis of holomorphic one-forms.

```
omega = X.holomorphic_differentials()
for omegai in omega:
    print omegai
```

We can plot the value of the differentials along Riemann surface paths.
* blue: real part
* red: imaginary part

Here we plot

$$
\omega_1 = \frac{1}{3x^2y^2} \; dx
$$

along the first $a$-cycle.

```
fig = omega[0].plot(a[0],256)
```

Demo - Riemann / Period Matrices
================================

Integrating each Abelian differential of the first kind basis element around each of the a- and b- cycles produces the *period matrix* of the Riemann surface

$$
A_{ij} = \oint_{a_j} \omega_i, \quad B_{ij} = \oint_{b_j} \omega_i.
$$

The *Riemann matrix* of the surface is then determined by

$$
\Omega = A^{-1}B \in \mathbb{C}^{g \times g}.
$$

The Riemann matrix is symmetric and its imaginary part is positive definite.

```
# for brevity, we only print the first four significant digits
import numpy
numpy.set_printoptions(precision=4, suppress=True)
```

```
Omega = X.riemann_matrix()
print Omega
```

```
symmetric_error = numpy.linalg.norm(Omega - Omega.T)
print symmetric_error
```

```
imag_part_evals = numpy.linalg.eigvals(Omega.imag)
print imag_part_evals
```

Demo - Places and Divisors
==========================

Places on a Riemann surface are given in terms of their Puiseux series. In this example, there is only one place on the surface "over" $x=0$ and three places "over" $x=2$. The places above two are "regular" in the sense that they lie above a non-discriminant point of the curve.

To declare places on a Riemann surface use the notation

```
places = RiemannSurface(xprojection)
```

where `xprojection` is the x-projection of the places you want to construct. That is, we compute all places above a given $x =$ `xprojection`.

```
places_above_zero = X(0)
for P in places_above_zero:
    print P
```

```
P.puiseux_series.extend(10)
```

```
print 'Places:'
places_above_two = X(2)
for P in places_above_two:
    print P

print '\nPuiseux series:'
series_at_two = puiseux(f,x,y,2)
for p in series_at_two:
    print p
```

**Divisors:** Once you have some places you can constuct divisors using the usual operations / notation.

```
P = places_above_zero[0]
Q = places_above_two[0]

D = 3*P + Q

print 'Divisor:', D
print 'Degree:', D.degree
```

Demo - The Abel Map
===================

The Abel map $A:X \to J(X)$ is a map from a Riemann surface to its Jacobian defined component-wise by

$$
A_j(P,Q) = \int_P^Q \omega_j.
$$

In `abelfunctions` this is computed using

    AbelMap(P,Q).

A single argument $P$ given to `AbelMap` will compute $A(P_0,P)$ where $P_0$ is a pre-determined "base place" of the Riemann surface. All paths on the Riemann surface (including the homology basis) begin at this base place.

```
J = Jacobian(X)
z1 = AbelMap(P)
z2 = AbelMap(Q)
z3 = AbelMap(P,Q)

print z1
print z2
print z3
```

```
print X.base_place()
```

Numerically verify that $A(P,Q) = A(P_0,Q) - A(P_0,P)$:

```
print numpy.linalg.norm( J((z2-z1) - z3) )
```

Demo - Localizing Differentials at Places
=========================================

```
nu = omega[0]
print nu
```

```
P
```

```
nu.localize(P)
```

```
P_oo = X('oo')[0]
P_oo.puiseux_series.extend(10)
print P_oo
```

```
nu.localize(P_oo)
```

Demo - Canonical Diviors
========================

Given an Abelian differential of the first kind you can compute its valuation divisor using the syntax

    Differential.valuation_divisor()

```
C0 = omega[0].valuation_divisor()

for place, multiplicity in C0:
    print '\nplace:       ', place
    print 'multiplicity:', multiplicity

print '\nDegree:', C0.degree
```

```
C1 = omega[1].valuation_divisor()

for place, multiplicity in C1:
    print '\nplace:       ', place
    print 'multiplicity:', multiplicity

print '\nDegree:', C1.degree
```

```
C2 = omega[2].valuation_divisor()

for place, multiplicity in C2:
    print '\nplace:       ', place
    print 'multiplicity:', multiplicity

print '\nDegree:', C0.degree
```

```
C3 = omega[3].valuation_divisor()

for place, multiplicity in C3:
    print '\nplace:       ', place
    print 'multiplicity:', multiplicity

print '\nDegree:', C3.degree
```

Demo - Riemann Constant Vector (RCV)
====================================

The function `RiemannConstantVector` is used to compute the Riemann constant vector on a Riemann surface. We make use of the fact

$$
    K(P) = K(P_0) + (g-1)A(P_0,P)
$$

to compute the RCV at various points. Most of the work is done in evaluating $K$ at the base place $P_0$ of the Riemann surface.

```
K = RiemannConstantVector # alias the function for brevity
```

```
P0 = X.base_place()
print K(P0)
```

**Theorem:** Given a canonical divisor $\mathcal{C}$

$$
2 K(P_0) + A(P_0, \mathcal{C}) \equiv 0.
$$

*Computational verification:*

```
C = omega[3].valuation_divisor()
print 'Divisor:'
print C

print '\nExpr:'
z = J(2*K(P0) + AbelMap(C))
print z
```

**Theorem:** Let $\theta : J(X) \times \mathfrak{h}_g$ denote the Riemann theta function. Then

$$
\theta(W,\Omega) = 0
$$

if and only if

$$
W = K(P_0) + A(P_0, \mathcal{D}).
$$

where $\mathcal{D}$ is a degree $g-1$ divisor.

*Computational verification:* we separate the "exponential" and "oscillatory" parts of the Riemann theta function (see DLMF)

$$
\theta(W,\Omega) = e^u v.
$$

Since $e^u$ never vanishes we only check for the vanishing of $v$ using the function `RiemannTheta.oscillatory_part()`.

```
W = K(P0)
v = RiemannTheta.oscillatory_part(W,Omega)
print abs(v)
```

```
D = sum(places_above_two)  # divisor of degree g-1 = 3
W = J(AbelMap(D) + K(P0))
v = RiemannTheta.oscillatory_part(W,Omega)
print abs(v)
```

**Verifying output:** we evaluate the oscillatory part using each half-lattice vector and plot the resulting magnitues in ascending order.

```
from abelfunctions.riemann_constant_vector import initialize_half_lattice_vectors

# list every half-lattice vector
h = initialize_half_lattice_vectors(X)

# evaluate the abel map at the valuation divisor C
AC = AbelMap(C)

# evaluate the oscillatory part of theta at
# each half-lattice vector minus the abel shift
W = numpy.array([J(hi.T - 0.5*AC).flatten() for hi in h])
v = RiemannTheta.oscillatory_part(W, Omega)
v = numpy.array(v, dtype=numpy.complex)

# compute the magnitues of the results and sort
magnitudes = sorted(abs(v))
print magnitudes[:3]
```

```
%matplotlib inline

import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.semilogy(magnitudes, marker='d', markersize=16, color='gray')
ax.axis([-32,256+32,1e-10,10])
fig.set_figwidth(16)
fig.set_figheight(8)
```

```

```

Comments / Suggestions for `abelfunctions`?
===========================================

Please send an email to Chris Swierczewski ([cswiercz@uw.edu](mailto:cswiercz@uw.edu)) or leave an "Issue" on the `abelfunctions` Github page.

> [http://github.com/cswiercz/abelfunctions](http://github.com/cswiercz/abelfunctions)

```

```

notebooks/Riemann Constant Vector - Math Methods Talk.ipynb


# Homework 2

**Instructions:** Complete the notebook below. Download the completed notebook in HTML format. Upload assignment using Canvas.

**Due:** Jan. 19 at **2pm**.

## Exercise: NumPy Arrays

Follow the instructions in the following cells.

```python
# Import numpy
import numpy as np
```

```python
# Use the 'np.arange()' function to create a variable called 'numbers1' that stores the integers
# 1 through (and including) 10
numbers1 = np.arange(1,11)

# Print the value of 'numbers1'
print(numbers1)
```

    [ 1  2  3  4  5  6  7  8  9 10]

```python
# Use the 'np.arange()' function to create a variable called 'numbers2' that stores the numbers
# 0 through (and including) 1 with a step increment of 0.01
step =0.01
numbers2 = np.arange(0,1+step,step)

# Print the value of 'numbers2'
print(numbers2)
```

    [0.   0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1  0.11 0.12 0.13
     0.14 0.15 0.16 0.17 0.18 0.19 0.2  0.21 0.22 0.23 0.24 0.25 0.26 0.27
     0.28 0.29 0.3  0.31 0.32 0.33 0.34 0.35 0.36 0.37 0.38 0.39 0.4  0.41
     0.42 0.43 0.44 0.45 0.46 0.47 0.48 0.49 0.5  0.51 0.52 0.53 0.54 0.55
     0.56 0.57 0.58 0.59 0.6  0.61 0.62 0.63 0.64 0.65 0.66 0.67 0.68 0.69
     0.7  0.71 0.72 0.73 0.74 0.75 0.76 0.77 0.78 0.79 0.8  0.81 0.82 0.83
     0.84 0.85 0.86 0.87 0.88 0.89 0.9  0.91 0.92 0.93 0.94 0.95 0.96 0.97
     0.98 0.99 1.  ]

```python
# Print the 5th value of 'numbers2'. (Remember that the index starts counting at 0)
print(numbers2[4])
```

    0.04

```python
# Print the last value of 'numbers2'.
print(numbers2[-1])
```

    1.0

```python
# Print the first 12 values of 'numbers2'.
print(numbers2[:12])
```

    [0.   0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09 0.1  0.11]

```python
# Print the last 12 values of 'numbers2'.
print(numbers2[-12:])
```

    [0.89 0.9  0.91 0.92 0.93 0.94 0.95 0.96 0.97 0.98 0.99 1.  ]

```python
# Use the 'np.zeros()' function to create a variable called 'zeros' that is an array of 20 zeros
zeros = np.zeros(20)

# Print the value of 'zeros'
print(zeros)
```

    [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

```python
# Change the second value of 'zeros' to 1 and print
zeros[1] = 1

# Print the value of 'zeros'
print(zeros)
```

    [0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

## Exercise: Random Numbers

Follow the instructions in the following cells.

```python
# Set the seed of NumPy's random number generator to 126
np.random.seed(126)

# Create a variable called 'epsilon' that is an array containing 25 draws from
# a normal distribution with mean 4 and standard deviation 2
epsilon = np.random.normal(loc=4,scale=2,size=25)

# Print the value of epsilon
print(epsilon)
```

    [ 2.67538919  6.80573472  2.82000233  4.20989426  4.16082178 -1.647651
      5.03812875  5.01159741  4.39492279  3.85680303  6.91478091  0.0854267
      7.02367775  3.94735082  2.48401748  7.02291824  6.25058482  5.57263063
      1.67321674  4.54716569  6.58060317  5.30662456  1.16478298  3.95415024
      3.56110405]

```python
# Print the mean of 'epsilon'
print(np.mean(epsilon))
```

    4.136587121846035

```python
# Print the standard deviation of 'epsilon'
print(np.std(epsilon))
```

    2.182193135563433

## Exercise: The Cobb-Douglas Production Function

The Cobb-Douglas production function can be written in per worker terms as :
  \begin{align}
  y & = A k^{\alpha},
  \end{align}
where $y$ denotes output per worker, $k$ denotes capital per worker, and $A$ denotes total factor productivity or technology.

### Part (a)

On a single axis: plot the Cobb-Douglas production for $A$ = 0.8, 1, 1.2, and 1.4 with $\alpha$ = 0.35 and $k$ ranging from 0 to 10. Each line should have a different color. Your plot must have a title and axis labels. The plot should also contain a legend that clearly indicates which line is associated with which value of $A$ and does not cover the plotted lines.

```python
# Import the pyplot module from Matplotlib as plt
import matplotlib.pyplot as plt

# Select the Matlplotlib style sheet to use (Optional)
plt.style.use('classic')

# Use the '%matplotlib inline' magic command to ensure that Matplotlib plots are displayed in the Notebook
%matplotlib inline
```

```python
# Set capital share (alpha)
alpha = 0.35

# Create an array of capital values
k = np.arange(0,10,0.001)

# Plot production function for each of the given values for A

def cobbDouglas(A,k,alpha):
    '''Returns the value of the production function A*k**alpha

    Args:
        A (float):          TFP or productivity value
        k (NumPy ndarray):  Array of capital values
        alpha (float):      Capital share in production

     Returns:
         NumPy series
     '''
    return A*k**alpha

for A in [0.8,1,1.2,1.4]:
    plt.plot(k,cobbDouglas(A,k,alpha),lw=3,alpha = 0.65,label='$A='+str(A)+'$')

# Add x- and y-axis labels
plt.xlabel('capital')
plt.ylabel('output')
# Add a title to the plot
plt.title('Cobb-Douglas production function')
# Create a legend
plt.legend(loc='lower right')
# Add a grid
plt.grid(True)
```

**Question**

1. *Briefly* explain in words how increasing $A$ affects the shape of the production function.

**Answer**

1. Increasing $A$ increases the height of the the production function at every value of capital except for 0 so increasing $A$ increases the steepness of the production function. <!-- answer -->

### Part (b)

On a single axis: plot the Cobb-Douglas production for $\alpha$ = 0.1, 0.2, 0.3, 0.4, and 0.5 with $A$ = 1 and $k$ ranging from 0 to 10. Each line should have a different color. Your plot must have a title and axis labels. The plot should also contain a legend that clearly indicates which line is associated with which value of $\alpha$ and does not cover the plotted lines.

```python
# Set TFP (A)
A = 1

# Plot production function for each of the given values for alpha
for alpha in [0.1,0.2,0.3,0.4,0.5]:
    plt.plot(k,cobbDouglas(A,k,alpha),lw=3,alpha = 0.65,label='$\\alpha='+str(alpha)+'$')

# Add x- and y-axis labels
plt.xlabel('capital')
plt.ylabel('output')
# Add a title to the plot
plt.title('Cobb-Douglas production function')
# Create a legend
plt.legend(ncol=3,loc='lower right')
# Add a grid
plt.grid(True)
```

**Question**

1. *Briefly* explain in words how increasing $\alpha$ affects the shape of the production function.

**Answer**

1. Increasing $\alpha$ reduces the curvature of the production function for capital between 0 and 1 and increases the steepness of the production function for capital greater than 1. <!-- answer -->

### Exercise: The Cardioid

The cardioid is a shape described by the parametric equations:

  \begin{align}
  x & = a(2\cos \theta - \cos 2\theta), \\
  y & = a(2\sin \theta - \sin 2\theta).
  \end{align}

Construct a well-labeled graph of the cardiod for $a=4$ and $\theta$ in $[0,2\pi]$. Your plot must have a title and axis labels.

```python
# Construct data for x and y
a = 4
theta = np.arange(0,2*np.pi,0.001)
x = a*(2*np.cos(theta) - np.cos(2*theta))
y = a*(2*np.sin(theta) - np.sin(2*theta))

# Plot y against x
plt.plot(x,y,lw=3,alpha=0.65)

# Create x-axis label
plt.xlabel('x')

# Create y-axis label
plt.ylabel('y')

# Create title for plot
plt.title('Cardioid')

# Add a grid to the plot
plt.grid()
```

## Exercise: Unconstrained optimization

Consider the quadratic function:

\begin{align}
f(x) & = -7x^2 + 930x + 30
\end{align}

You will use analytic (i.e., pencil and paper) and numerical methods to find the the value of $x$ that maximizes $f(x)$. Another name for $x$ that maximizes $f(x)$ is the *argument of the maximum* value $f(x)$.

### Part (a): Analytic solution

Use standard calculus methods to solve for the value of $x$ that maximizes $f(x)$ to **five decimal places**. Use your answer to complete the sentence in the next cell.

 The value of $x$ that maximizes $f(x)$ is:

### Part (b):  Numerical solution

In the cells below, you will use NumPy to try to compute the argument of the maximum of $f(x)$.

```python
# Use np.arange to create a variable called 'x' that is equal to the numbers 0 through 100
# with a spacing between numbers of 0.1
x = np.arange(0,100,0.1)

# Create a variable called 'f' that equals f(x) at each value of the array 'x' just defined
f = -7*x**2 + 930*x + 30

# Use np.argmax to create a variable called xstar equal to the value of 'x' that maximizes the function f(x).
xstar = x[np.argmax(f)]

# Print the value of xstar
print(xstar)
```

    66.4

```python
# Use np.arange to create a variable called 'x' that is equal to the numbers 0 through 100
# with a spacing between numbers of 0.001
x = np.arange(0,100,0.001)

# Create a variable called 'f' that equals f(x) at each value of the array 'x' just defined
f = -7*x**2 + 930*x + 30

# Use np.argmax to create a variable called xstar equal to the value of 'x' that maximizes the function f(x).
xstar = x[np.argmax(f)]

# Print the value of xstar
print(xstar)
```

    66.429

```python
# Use np.arange to create a variable called 'x' that is equal to the numbers 0 through *50*
# with a spacing between numbers of 0.001
x = np.arange(0,50,0.001)

# Create a variable called 'f' that equals f(x) at each value of the array 'x' just defined
f = -7*x**2 + 930*x + 30

# Use np.argmax to create a variable called xstar equal to the value of 'x' that maximizes the function f(x).
xstar = x[np.argmax(f)]

# Print the value of xstar
print(xstar)
```

    49.999

### Part (c): Evaluation

Provide answers to the follow questions in the next cell.

**Questions**

1. How did the choice of step size in the array `x` affect the accuracy of the computed results in the first two cells of Part (b)?

2. What do you think is the drawback to decreasing the stepsize in `x`?

3. In the previous cell, why did NumPy return value for `xstar` that is so different from the solution you derived in Part (a)?

**Answers**

1. Choosing a smaller step size creates a finer grid of points for which the function $f$ is calculated and therefore the accuracy is improved with a smaller step size. <!-- answer -->
2. A smaller step size means that the variable `x` will have more elements and so storing the variable and using it in computations will use more memory and will slow and/or crash the program. <!-- answer -->
3. The range of values in the variable `x` doesn't include $x^*$. <!-- answer -->

## Exercise: Utility Maximization

Recall the two good utility maximization problem from microeconomics. Let $x$ and $y$ denote the amount of two goods that a person consumes. The person receives utility from consumption given by:
  \begin{align}
  u(x,y) & = x^{\alpha}y^{\beta}
  \end{align}
The person has income $M$ to spend on the two goods and the price of the goods are $p_x$ and $p_y$. The consumer's budget constraint is:
  \begin{align}
  M & = p_x x + p_y y
  \end{align}
Suppose that $M = 100$, $\alpha=0.25$, $\beta=0.75$, $p_x = 1$. and $p_y = 0.5$. The consumer's problem is to maximize their utility subject to the budget constraint. While this problem can easily be solved by hand, we're going to use a computational approach. You can also solve the problem by hand to verify your solution.

### Part (a)

Use the budget constraint to solve for $y$ in terms of $x$, $p_x$, $p_y$, and $M$. Use the result to write the consumer's utility as a function of $x$ only. Create a variable called `x` equal to an array of values from 0 to 80 with step size equal to 0.001 and a variable called `utility` equal to the consumer's utility. Plot the consumer's utility against $x$.

```python
# Assign values to the constants alpha, beta, M, px, py
alpha = 0.25
beta = 0.75
M = 100
px=1
py=0.5

# Create an array of x values
x = np.arange(0,80,0.001)

# Create an array of utility values
utility = x**alpha*((M-px*x)/py)**beta

# Plot utility against x.
plt.plot(x,utility,lw=3,alpha=0.65)

# x- and y-axis labels
plt.xlabel('x')
plt.ylabel('utility')

# Title
plt.title('u[x,y(x)]')

# Add grid
plt.grid()
```

### Part (b)

The NumPy function `np.max()` returns the highest value in an array and `np.argmax()` returns the index of the highest value. Print the highest value and index of the highest value of `utility`.

```python
print('highest utility value         :',np.max(utility))
print('index of highest utility value:',np.argmax(utility))
```

    highest utility value         : 95.84146563694088
    index of highest utility value: 25000

### Part (c)

Use the index of the highest value of utility to find the value in `x` with the same index and store value in a new variable called `xstar`. Print the value of `xstar`.

```python
# Create variable 'xstar' equal to value in 'x' that maximizes utility
xstar = x[np.argmax(utility)]

# Print value of 'xstar'
print('xstar:',xstar)
```

    xstar: 25.0

### Part (d)

Use the budget constraint to the find the implied utility-maximizing vaue of $y$ and store this in a variable called `ystar`. Print `ystar`.

```python
# Create variable 'ystar' equal to value in 'y' that maximizes utility
ystar = M/py - px*xstar/py

# Print value of 'xstar'
print('ystar:',ystar)
```

    ystar: 150.0


# Switchpoint example

You can install PyMC3 with

```conda install pymc3```

```python
import os,sys
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc3 as pm
import scipy.stats as st

%matplotlib inline
%precision 4
plt.style.use('bmh')
```

## Switchpoint analysis

This example comes from [Cam Davidson-Pilon's book](https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers)

We will be looking at count data---specifically at the frequency of text messages recieved over a period of time.  We will use the Poisson distribution to help use investigate this series of events.

```python
from IPython.display import Image
Image(filename='./images/poisson.png')
```

```python
fig = plt.figure(figsize=(12.5, 3.5))
ax = fig.add_subplot(111)
count_data = np.loadtxt("data/txtdata.csv")
n_count_data = len(count_data)
ax.bar(np.arange(n_count_data), count_data, color="#348ABD")
ax.set_xlabel("Time (days)")
ax.set_ylabel("count of text-msgs received")
ax.set_title("Did the user's texting habits change over time?")
ax.set_xlim(0, n_count_data);
```

These are time-course data.  Can we infer when a behavioral change occurred?

Lets denote the day with $i$ and text-message count by $C_i$,

$$ C_i \sim \text{Poisson}(\lambda) $$

   * Does the rate $\lambda$ change?
   * Is there a day (call it $\tau$) where the parameter $\lambda$ suddenly jumps to a higher value?
   * We are looking for a _switchpoint_ s.t.

$$
\lambda =
\begin{cases}
\lambda_1  & \text{if } t \lt \tau \cr
\lambda_2 & \text{if } t \ge \tau
\end{cases}
$$

If no sudden change occurred and indeed $\lambda_1 = \lambda_2$, then the $\lambda$s posterior distributions should look about equal.

We are interested in inferring the unknown $\lambda$s. We are using Bayesian inference so we need priors. The *exponential* distribution provides a continuous density function for positive numbers.

\begin{align}
&\lambda_1 \sim \text{Exp}( \alpha ) \\\
&\lambda_2 \sim \text{Exp}( \alpha )
\end{align}

$\alpha$ is a *hyper-parameter*. In literal terms, it is a parameter that influences other parameters.

$$\frac{1}{N}\sum_{i=0}^N \;C_i \approx E[\; \lambda \; |\; \alpha ] = \frac{1}{\alpha}$$

What about $\tau$? Because of the noisiness of the data, it's difficult to pick out a priori when $\tau$ might have occurred. Instead, we can assign a *uniform prior belief* to every possible day. This is equivalent to saying

\begin{align}
& \tau \sim \text{DiscreteUniform(1,70) }\\\\
& \Rightarrow P( \tau = k ) = \frac{1}{70}
\end{align}

So what does this look like in PyMC3?

```python
import pymc3 as pm
import theano.tensor as tt

## assign lambdas and tau to stochastic variables
with pm.Model() as model:
    alpha = 1.0/count_data.mean()  # Recall count_data is the
                                   # variable that holds our txt counts
    lambda_1 = pm.Exponential("lambda_1", alpha)
    lambda_2 = pm.Exponential("lambda_2", alpha)
    tau = pm.DiscreteUniform("tau", lower=0, upper=n_count_data)

## create a combined function for lambda (it is still a RV)
with model:
    idx = np.arange(n_count_data) # Index
    lambda_ = pm.math.switch(tau >= idx, lambda_1, lambda_2)

## combine the data with our proposed data generation scheme
with model:
    observation = pm.Poisson("obs", lambda_, observed=count_data)

## inference
with model:
    step = pm.Metropolis()
    trace = pm.sample(10000, tune=5000,step=step)
```

    Multiprocess sampling (4 chains in 4 jobs)
    CompoundStep
    >Metropolis: [tau]
    >Metropolis: [lambda_2]
    >Metropolis: [lambda_1]
    Sampling 4 chains, 0 divergences: 100%|██████████| 60000/60000 [00:09<00:00, 6275.31draws/s]
    The number of effective samples is smaller than 25% for some parameters.

```python
## get the variables we want to plot from our trace
lambda_1_samples = trace['lambda_1']
lambda_2_samples = trace['lambda_2']
tau_samples = trace['tau']

# draw histogram of the samples:
fig = plt.figure(figsize=(12.5,10))
ax1 = fig.add_subplot(311)
ax2 = fig.add_subplot(312)
ax3 = fig.add_subplot(313)

for ax in [ax1,ax2]:
    ax.set_autoscaley_on(False)

## axis 1
ax1.hist(lambda_1_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_1$", color="#A60628", density=True)
ax1.legend(loc="upper left")
ax1.set_title(r"""Posterior distributions of the variables
    $\lambda_1,\;\lambda_2,\;\tau$""")
ax1.set_xlim([15, 30])
ax1.set_xlabel("$\lambda_1$ value")

## axis 2
ax2.hist(lambda_2_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_2$", color="#7A68A6", density=True)
ax2.legend(loc="upper left")
ax2.set_xlim([15, 30])
ax2.set_xlabel("$\lambda_2$ value")

## axis 3
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
ax3.hist(tau_samples, bins=n_count_data, alpha=1,
         label=r"posterior of $\tau$",
         color="#467821", weights=w, rwidth=2.)
ax3.set_xticks(np.arange(n_count_data))

ax3.legend(loc="upper left")
ax3.set_ylim([0, .75])
ax3.set_xlim([35, len(count_data)-20])
ax3.set_xlabel(r"$\tau$ (in days)")
ax3.set_ylabel("probability");
plt.subplots_adjust(hspace=0.4)
```

### How do we plot the expection distribution of this model?

```python
fig = plt.figure(figsize=(12.5,5))
ax = fig.add_subplot(111)

N = tau_samples.shape[0]
expected_texts_per_day = np.zeros(n_count_data)
for day in range(0, n_count_data):
    ix = day < tau_samples
    expected_texts_per_day[day] = (lambda_1_samples[ix].sum()
                                   + lambda_2_samples[~ix].sum()) / N

ax.plot(range(n_count_data), expected_texts_per_day, lw=4, color="#E24A33",
         label="expected number of text-messages received")
ax.set_xlim(0, n_count_data)
ax.set_xlabel("Day")
ax.set_ylabel("Expected # text-messages")
ax.set_title("Expected number of text-messages received")
ax.set_ylim(0, 60)
ax.bar(np.arange(len(count_data)), count_data, color="#348ABD", alpha=0.65,label="observed texts per day")

ax.legend(loc="upper left");
```

A distribution allows us to see the uncertainty in our estimates.  The two distribution for $\lambda$ are distinct.  Near day 45, there was a 50% chance that the user's behavior changed.

> In fact, the 45th day corresponds to Christmas, and I moved away to Toronto the next month, leaving a girlfriend behind

      -Cam Davidson Pilon

```python

```


# Assignment Problem

A fundamental combinatorial optimization problem.

- $n$ tasks to be done
- $n$ workers to do the tasks
- Each worker has a certain cost for each task ($c_{ij}$)

Problem: Find the best assignment of all tasks that minimizes the total cost.

Constraints:
* Each worker can do at most one task
* All tasks need to be done

# Mathematical Formulation

\begin{align}
\text{minimize} & \sum_{i = 1}^{n}\sum_{j = 1}^{n} c_{ij}x_{ij}, \\
\text{subject to} & \\
& \sum_{i = 1}^{n} x_{ij} \leq 1 & \forall j = 1, ..., n & \ \ \textit{ (workload)},\\
& \sum_{j = 1}^{n} x_{ij} = 1 & \forall i = 1, ..., n & \ \ \textit{ (task completion)}, \\
& x_{ij} \in \{0,1\} & \forall i,j = 1, ..., n
\end{align}

where $x_{ij}$ is 1 if worker $i$ performs task $j$, 0 otherwise.

We can replace the integrality constraints with continuous constraints. The solution will not change as the constraint matrix is totally unimodular.

# Coding in Python using docplex
## Step 1: Creating input data (cost matrix)

```python
import numpy as np
cost = np.random.randint(1, 10, (4,4))
```

## Step 2: Importing docplex package

```python
from docplex.mp.model import Model
```

## Step 3: Creating the model

```python
assignment_model = Model('Assignment')
```

## Step 4: Creating decision variables

```python
x = assignment_model.binary_var_matrix(cost.shape[0],
                                       cost.shape[1],
                                       name="x")
```

## Step 5: Adding the constraints
\begin{align}
\sum_{i = 1}^{n} x_{ij} \leq 1 &\qquad \forall j = 1, ..., n & \ \ \textit{ (workload)},\\
\sum_{j = 1}^{n} x_{ij} = 1 & \qquad \forall i = 1, ..., n & \ \ \textit{ (task completion)}.
\end{align}

```python
# sum_{i = 1}^{n} x_{ij} <= 1 for all j
assignment_model.add_constraints((sum(x[i,j] for i in range(cost.shape[0])) <= 1
                                  for j in range(cost.shape[1])),
                                 names = 'work_load')

# sum_{j = 1}^{n} x_{ij}  = 1 for all i
assignment_model.add_constraints((sum(x[i,j] for j in range(cost.shape[1])) == 1
                                  for i in range(cost.shape[0])),
                                names = 'task_completion')
```

    [docplex.mp.LinearConstraint[task_completion1](x_0_0+x_0_1+x_0_2+x_0_3,EQ,1),
     docplex.mp.LinearConstraint[task_completion2](x_1_0+x_1_1+x_1_2+x_1_3,EQ,1),
     docplex.mp.LinearConstraint[task_completion3](x_2_0+x_2_1+x_2_2+x_2_3,EQ,1),
     docplex.mp.LinearConstraint[task_completion4](x_3_0+x_3_1+x_3_2+x_3_3,EQ,1)]

## Step 6: Defining the objective function
$$\sum_{i = 1}^{n}\sum_{j = 1}^{n} c_{ij}x_{ij}$$

```python
obj_fn = sum(cost[i,j]*x[i,j] for i in range(cost.shape[0]) for j in range(cost.shape[1]))

assignment_model.set_objective('min',obj_fn)

assignment_model.print_information()
```

    Model: Assignment
     - number of variables: 16
       - binary=16, integer=0, continuous=0
     - number of constraints: 8
       - linear=8
     - parameters: defaults
     - objective: minimize
     - problem type is: MILP

## Inspect model by printing

```python
print(assignment_model.export_as_lp_string())
```

    \ This file has been generated by DOcplex
    \ ENCODING=ISO-8859-1
    \Problem name: Assignment

    Minimize
     obj: 6 x_0_0 + 7 x_0_1 + 5 x_0_2 + 8 x_0_3 + 6 x_1_0 + 7 x_1_1 + 2 x_1_2
          + 2 x_1_3 + 2 x_2_0 + x_2_1 + 2 x_2_2 + 7 x_2_3 + x_3_0 + x_3_1 + 8 x_3_2
          + x_3_3
    Subject To
     work_load1: x_0_0 + x_1_0 + x_2_0 + x_3_0 <= 1
     work_load2: x_0_1 + x_1_1 + x_2_1 + x_3_1 <= 1
     work_load3: x_0_2 + x_1_2 + x_2_2 + x_3_2 <= 1
     work_load4: x_0_3 + x_1_3 + x_2_3 + x_3_3 <= 1
     task_completion1: x_0_0 + x_0_1 + x_0_2 + x_0_3 = 1
     task_completion2: x_1_0 + x_1_1 + x_1_2 + x_1_3 = 1
     task_completion3: x_2_0 + x_2_1 + x_2_2 + x_2_3 = 1
     task_completion4: x_3_0 + x_3_1 + x_3_2 + x_3_3 = 1

    Bounds
     0 <= x_0_0 <= 1
     0 <= x_0_1 <= 1
     0 <= x_0_2 <= 1
     0 <= x_0_3 <= 1
     0 <= x_1_0 <= 1
     0 <= x_1_1 <= 1
     0 <= x_1_2 <= 1
     0 <= x_1_3 <= 1
     0 <= x_2_0 <= 1
     0 <= x_2_1 <= 1
     0 <= x_2_2 <= 1
     0 <= x_2_3 <= 1
     0 <= x_3_0 <= 1
     0 <= x_3_1 <= 1
     0 <= x_3_2 <= 1
     0 <= x_3_3 <= 1

    Binaries
     x_0_0 x_0_1 x_0_2 x_0_3 x_1_0 x_1_1 x_1_2 x_1_3 x_2_0 x_2_1 x_2_2 x_2_3 x_3_0
     x_3_1 x_3_2 x_3_3
    End

## Step 7: Solve the model and output the solution

```python
assignment_model.solve()

print('Optimization is done. Objective Function Value: %.2f' % assignment_model.objective_value)

# Get values of the decision variables
assignment_model.print_solution()
```

    Optimization is done. Objective Function Value: 9.00
    objective: 9
      x_0_2=1
      x_1_3=1
      x_2_1=1
      x_3_0=1

# Relaxing the binary constraint

```python
assignment_model_2 = Model('Assignment_2')

y = assignment_model_2.continuous_var_matrix(cost.shape[0],
                                             cost.shape[1],
                                             lb = 0, ub = 1,
                                             name="x")

# sum_{i = 1}^{n} y_{ij} <= 1 for all j
assignment_model_2.add_constraints((sum(y[i,j] for i in range(cost.shape[0])) == 1
                                    for j in range(cost.shape[1])),
                                   names = 'work_load')

# sum_{j = 1}^{n} y_{ij}  = 1 for all i
assignment_model_2.add_constraints((sum(y[i,j] for j in range(cost.shape[1])) == 1
                                    for i in range(cost.shape[0])),
                                  names = 'task_completion')

obj_fn = sum(cost[i,j]*y[i,j] for i in range(cost.shape[0]) for j in range(cost.shape[1]))
assignment_model_2.set_objective('min', obj_fn)

assignment_model_2.solve()

print('Optimization is done. Objective Function Value: %.2f' % assignment_model.objective_value)

# Get values of the decision variables
assignment_model_2.print_solution()
```

    Optimization is done. Objective Function Value: 9.00
    objective: 9.000
      x_0_2=1.000
      x_1_3=1.000
      x_2_1=1.000
      x_3_0=1.000

```python

```


# Lotka-Volterra Introduction

The Lotka-Volterra model is a basic dynamic model named after two biomathematicians, Alfred Lotka and Vito Volterra,
who developed the system of equations independent of each other in the first half of the twentieth century.
Lotka had developed the model to explain the dynamics of predator and prey populations, expanding on his previous model
of autocatalytic chemical reactions. Volterra had developed the system of equations to model the population of predator
fish in the Adriatic Sea.

The basic system consists of two linear ordinary differential equations (ODE). One equation represents the change in population of
the prey over time and is dependent on the population of the predator. The other equation represents the same but
for the predator population.

As an example, we let the prey be sheep and the predators be wolves.
If sheep were to exist without a predator and without a limitation of resources, the population would grow
exponentially:

> $ \frac{dx}{dt}=\alpha*x , $

where $\alpha$ is a constant rate of growth and $x$ is the number of sheep.

If wolves were added to the environment, then the sheep population would depend on how
often they met with the wolves. If there are more wolves, then there is more likelihood that they would meet sheep and
eat one. If there are more sheep, then they would have more likelihood of meeting wolves. If a wolf eats a sheep anytime
it finds one, then the death of the sheep is proportional to how many wolves there are. This relationship is the same
as the law of mass action: the rate of a chemical reaction is proportional to the concentration of the reactants.
In this case, the death of the sheep is then represented by a constant multiplied by the population of the sheep and the
population of the wolves. The sheep population equation is represented as:

> $ \frac{dx}{dt}=\alpha*x-\beta*x*y $

The equation for the population of the wolves is given as:

> $ \frac{dy}{dt} = \delta x y - \gamma  y, $

where $y$ is the wolf population, $\delta$ is the growth rate of the wolves that is proportional to number of sheep and
wolves and $\gamma$ is the mortality rate of the wolves.

There are many assumptions with this model and some are stated here:
- The sheep population grows exponentially without the presence of wolves.
- The wolves will always eat a sheep when it meets one.
- The wolves only eat sheep.

To see the interaction between the wolf and sheep populations, let $\alpha= 1.1, \beta = 0.4, \delta = 0.1$, and $\gamma = 0.4$.
If we then solve this system, assuming time is in weeks, with initial population of 10 thousand sheep
and 1 thousand wolves, the following plot shows the solutions on the interval [0,100] (100 weeks or a little under 2 years).

```python
import matplotlib.pyplot as plt
import numpy as np
import random
from scipy.integrate import odeint as ode
from tabulate import tabulate
```

```python
init_pop = [10,1] # initial population levels [prey, predator]

t_steps = 1000 # time steps
t_end = 100 # end of time interval
time = np.linspace(0, t_end, num=t_steps) # array to store time

alpha = 1.1 # prey birthrate
beta = 0.4 # prey death rate
delta = 0.1 # predator birthrate
gamma = 0.4 # predator death rate

k = 150 # prey carrying capacity
k_y = 100 # predator carrying capacity
# Array to track coefficients
coef = [alpha, beta, delta, gamma]
```

```python
# Define the simulation function
def run_simu(tmp_pop, tmp_time, tmp_coef):

    tmp_x = tmp_pop[0] # Prey population value
    tmp_y = tmp_pop[1] # Predator population value

    tmp_alpha = tmp_coef[0]
    tmp_beta = tmp_coef[1]
    tmp_delta = tmp_coef[2]
    tmp_gamma = tmp_coef[3]

    dxdt = tmp_alpha * tmp_x - tmp_beta * tmp_x * tmp_y
    dydt = tmp_delta * tmp_x * tmp_y - tmp_gamma * tmp_y

    return[dxdt, dydt]

# Call the ode solver function
output = ode(run_simu, init_pop, time, args = (coef,))

plt.plot(time, output[:,0], color = "green", label = "Prey Population") # Prey output
plt.plot(time, output[:,1], color = "red", label = "Predator Population") # Prey output

plt.xlabel("Time")
plt.ylabel("Population Level (in thousands)")

plt.title("Predator / Prey Dynamics")
plt.legend()
plt.grid()

plt.show()
```

It makes sense that we see oscillations in the populations as seen in the plot because we have negative
feedback. As the predator population approaches its maximum, the prey population decreases rapidly. Then
after the predator population has decreased, the prey population begins to increase and eventually the predator population
will begin to increase again.

The steady state of the model is when there is equilibrium between the populations.

To find the steady state we need to find the equilibrium points.
In other words, we want to know when the rate of change (derivative) is equal to zero.

The not-so-meaningful solution is when the prey and the predator populations are zero.

The more meaningful solutions can be found as:

> $ \begin{align}
>0 &= \alpha  x - \beta x  y \\
>0 &= x ( \alpha - \beta  y ) \\
> y &= \frac{\alpha}{\beta} \text{ or }x = 0
> \end{align} $

> $ \begin{align}
>0 &= \delta x y - \gamma  y \\
>0 &= y ( \delta x - \gamma ) \\
> x &= \frac{\gamma}{\delta} \text{ or }y = 0
> \end{align} $

The non-zero equilibrium point for this system is $ (\frac{\gamma}{\delta}, \frac{\alpha}{\beta}) $

If we change our birth and death rate parameters to visualize this, we see that it is in fact an equilibrium state.

```python
alpha_eq = 1.1 # prey birthrate
beta_eq = 1.1 # prey death rate
delta_eq = 0.4 # predator birthrate
gamma_eq = 4 # predator death rate

# Array to track coefficients
coef_eq = [alpha_eq, beta_eq, delta_eq, gamma_eq]

# Call the ode solver function
output_eq = ode(run_simu, init_pop, time, args = (coef_eq,))

plt.plot(time, output_eq[:,0], color = "green", label = "Prey Population") # Prey output
plt.plot(time, output_eq[:,1], color = "red", label = "Predator Population") # Prey output

plt.xlabel("Time")
plt.ylabel("Population Level (in thousands)")

plt.title("Predator / Prey Dynamics")
plt.legend()
plt.grid()

plt.show()

```

It would be interesting to see how the change in the parameters will change the dynamics of the populations.
To visualize this, we can make a phase space plot with varying parameter values.

```python
def run_range_ode(lrange, urange, tmp_run_simu, tmp_init_pop, tmp_time, coef_base):

    output_range = [np.zeros((t_steps,2)), np.zeros((t_steps,2)),
                    np.zeros((t_steps,2)), np.zeros((t_steps,2)),
                    np.zeros((t_steps,2)), np.zeros((t_steps,2)),
                    np.zeros((t_steps,2)), np.zeros((t_steps,2))]

    idx = 0 # index to track output array storage

    for num in [0,1,2,3]:
    # for i in range(len(coef_base)):
        coef_new = coef_base[:]
        for j in [lrange,urange]:
            coef_new[num] = round(coef_base[num] + j, 2)
            output_temp = ode(tmp_run_simu, tmp_init_pop, tmp_time, args = (coef_new,))
            output_range[idx] = output_temp
            idx = idx + 1

    return output_range

output_vary = run_range_ode(-0.2, 0.2, run_simu, init_pop, time, coef)
```

```python
# original parameters
plt.plot(output[:,0], output[:,1], color = "black", label = "a: 1.1, b: 0.4, d:0.1, g: 0.4")

# alpha changes
plt.plot(output_vary[0][:,0], output_vary[0][:,1], color = "mediumvioletred", label = "alpha: 0.9")
plt.plot(output_vary[1][:,0], output_vary[1][:,1], color = "hotpink", label = "alpha: 1.3")

# beta changes
#plt.plot(output_vary[2][:,0], output_vary[2][:,1], color = "gold", label = "beta: 0.2")
#plt.plot(output_vary[3][:,0], output_vary[3][:,1], color = "goldenrod", label = "beta: 0.8")

# delta changes
#plt.plot(output_vary[4][:,0], output_vary[4][:,1], color = "springgreen", label = "delta: 0.05")
#plt.plot(output_vary[5][:,0], output_vary[5][:,1], color = "green", label = "delta: 0.2")

# gamma changes
plt.plot(output_vary[6][:,0], output_vary[6][:,1], color = "aqua", label = "gamma: 0.2")
plt.plot(output_vary[7][:,0], output_vary[7][:,1], color = "darkcyan", label = "gamma: 0.8")

#plt.plot(output)
plt.xlabel("Prey Population (in thousands)")
plt.ylabel("Predator Population (in thousands)")

plt.title("Predator / Prey Phase Space Plot")
plt.legend()
plt.grid()

plt.show()
```

The black line is the phase plot with the original parameters. It is a closed orbit that oscillates around the
fixed point $ (\frac{\gamma}{\delta}, \frac{\alpha}{\beta}) = (4, 2.75) $ (one of the equilibrium points).

When keeping all other parameters constant and changing only $\alpha$, the ellipse would stretch or shrink vertically.
Adjusting the values of only $\gamma$, the ellipse would stretch or shrink horizontally. This is because the closed
orbit oscillates around the fixed point and when we change $\gamma$, we change the x-value of the fixed point and when
we change $\alpha$ we change the y value.

These dynamics can be proven algebraically using concepts from linear algebra (calculating the eigenvalue and
eigenvector of the Jacobian matrix).

The assumption that the prey grow exponentially in the absence of predators is not realistic.
A more realistic approach is to assume there is a carrying capacity. When the sheep population is below the environmental
carrying capacity, the growth rate is large. When the sheep population is equal to it's carrying capacity, there is no
growth and when the sheep population is above the carrying capacity, there is negative growth.

Incorporating the carrying capacity into the model would alter the sheep population ODE:

> $ \frac{dx}{dt} = \alpha x (1-\frac{x}{K}), $

where $K$ is carrying capacity.

Now we need to add the predators to the environment:

> $ \frac{dx}{dt} = \alpha x (1-\frac{x}{K}) - \beta x  y $

And the predator equation remains the same:

>$ \frac{dy}{dt} = \delta x y - \gamma  y $
>

Using these updated ODEs, we can rerun our system and visualize the dynamics.

```python
# Define the simulation function
def run_simu_carry_cap(tmp_pop, tmp_time, tmp_coef):

    tmp_x = tmp_pop[0] # Prey population value
    tmp_y = tmp_pop[1] # Predator population value

    tmp_alpha = tmp_coef[0]
    tmp_beta = tmp_coef[1]
    tmp_delta = tmp_coef[2]
    tmp_gamma = tmp_coef[3]

    dxdt = tmp_alpha * tmp_x * (1 - (tmp_x/k)) - tmp_beta * tmp_x * tmp_y
    dydt = tmp_delta * tmp_x * tmp_y - tmp_gamma * tmp_y
    #dxdt = tmp_alpha * x * (1 - ((x+tmp_beta*y)/k))
    #dydt = tmp_delta * x * y - tmp_gamma * y

    return[dxdt, dydt]

# Call the ode solver function
output_carry_cap = ode(run_simu_carry_cap, init_pop, time, args = (coef,))

plt.plot(time, output_carry_cap[:,0], color = "green", label = "Prey Population") # Prey output
plt.plot(time, output_carry_cap[:,1], color = "red", label = "Predator Population") # Prey output

plt.xlabel("Time")
plt.ylabel("Population Level (in thousands)")

plt.title("Predator / Prey Dynamics with Prey Carrying Capacity")
plt.legend()
plt.grid()

plt.show()
```

There is still an oscillation between the predator and prey populations, but the difference between the maximum and
minimum values is decreasing. The non-zero steady state of this system has changed. Previously, it was
$ (\frac{\gamma}{\delta}, \frac{\alpha}{\beta}) $, but now that has changed.

> $ \begin{align}
> 0 &= \alpha  x (1-\frac{x}{k}) - \beta x  y
>\end{align} $

substitute $x =\frac{\gamma}{\delta}$ and solve for $y$
> $ \begin{align}
> 0 &= \alpha  \frac{\gamma}{\delta} (1-\frac{\frac{\gamma}{\delta}}{k}) - \beta\frac{\gamma}{\delta}  y \\
> 0 &= \alpha (1-\frac{\gamma}{\delta k}) - \beta  y \\
> y &= \frac{\alpha}{\beta}(1-\frac{\gamma}{\delta k})  \\
> \end{align} $

The non-zero new steady state is $(\frac{\gamma}{\delta}, \frac{\alpha}{\beta}(1-\frac{\gamma}{\delta k}))$

With the narrowing of the prey and predator population, we know that our phase plot will spiral instead of have an
ellipse.

```python
# original parameters
plt.plot(output_carry_cap[:,0], output_carry_cap[:,1], color = "black", label = "original")

#plt.plot(output)
plt.xlabel("Prey Population (in thousands)")
plt.ylabel("Predator Population (in thousands)")

plt.title("Predator / Prey  with Carrying Capacity Phase Plot")
plt.legend()
plt.grid()

plt.show()

```

To add stochastic behavior to our system, we can use an algorithm to randomly choose events
and event times from probability distributions.
We can use the Gillespie algorithm to do this.

We keep track of the population of the predator and prey. An event occurs when birth or death
occurs in either the prey or predator population. The propensity for each event to occur at a given
time is shown in the table below.

```python
data = [['Prey + 1', 'alpha * x * (1 - x/k)'],
['Prey - 1', 'beta * x * y'],
['Predator + 1', 'delta * x * y'],
['Predator - 1','gamma * y']]

print(tabulate(data, headers=["Events", "Propensity"]))

```

    Events        Propensity
    ------------  ---------------------
    Prey + 1      alpha * x * (1 - x/k)
    Prey - 1      beta * x * y
    Predator + 1  delta * x * y
    Predator - 1  gamma * y

```python
# Initial conditions
# Original init_pop = [10,1]. Too likely for prey/predator to die out with 10 and 1
x = [10] # track prey
y = [1] # track predator
t = [0] # to keep track of time
end = 100

x_all = [] # track results of prey from all model runs
y_all = [] # track results of predator from all model runs
t_all = []
# Keep same rates of coefficients as above
#alpha = 10 # prey birthrate
#beta = 0.1 # prey death rate
#delta = 0.3 # predator birthrate
#gamma = 30 # predator death rate

#k = 150
#k_y = 100
#r  = 0.05
#K = 100
for i in range(0,5): # simulate the model 100 times
    t = [0] # reset time to rerun
    x = [10] # reset time to rerun
    y = [1] # reset time to rerun
    i = i+1
    while t[-1] < end: # keep running until last item in t is after end
        #current_x = x[-1]
        props = [alpha * x[-1] * (1- x[-1]/k),
                      beta*x[-1]*y[-1],
                      delta*x[-1]*y[-1],
                      gamma * y[-1]]
        prop_sum = sum(props)
        #print("x: ", x[-1], "; y: ", y[-1])
        if prop_sum == 0: # can not divide by zero so break out of while loop
            break

        # choose next time increment randomly from exponential distribution with mean = 1/prop_sum
        #tau = np.random.exponential(scale=1/prop_sum)
        tau = np.random.uniform(0.5, 4.5)

        # add the randomly chosen tau to the current time
        t.append(t[-1]+tau)

        # randomly choose number to later weight with probability of events
        # to randomly choose which event that will occur at the next time point
        rand = random.uniform(0,1)

        if rand * prop_sum <= props[0]: # growth of prey
            x.append(x[-1] + 1)
            y.append(y[-1])
        elif rand * prop_sum > props[0] and rand * prop_sum <= props[0] + props[1]: # death of prey
            x.append(x[-1] - 1)
            y.append(y[-1])
        elif rand * prop_sum > props[0] and rand * prop_sum <= props[0] + props[1] + props[2]: # death of predator
            x.append(x[-1])
            y.append(y[-1] + 1)
        else:
            x.append(x[-1])
            y.append(y[-1] - 1)

    x_all.append(x)
    y_all.append(y)
    t_all.append(t)

# plot results
for i in range(len(x_all)):
    plt.plot(t_all[i],x_all[i], color = "green")
    plt.plot(t_all[i],y_all[i], color = "red")

plt.legend(['Prey', 'Predator'])
plt.xlabel("Time")
plt.ylabel("Population (in thousands)")
plt.show()
```

In the stochastic model, there are several simulations in which the predator and/or prey population become extinct.
This could be a more realistic situation, especially given that the original models showed the populations had fallen
to very low numbers and could likely become extinct.

If we change the predator-prey model dynamics such that the two species are competing for the same resource(s), then we
can use the competitive Lotka-Volterra Equations which are slightly different:

> $ \frac{dx}{dt} = \alpha x (1-\frac{x+\beta y}{K_x}) $
> $ \frac{dy}{dt} = \delta y (1-\frac{y+\gamma x}{K_y}) $
>
Same as before, $\alpha$ and $\delta$ represent the growth of x and y populations, respectively. $\beta$ and $\gamma$
are the effect that y has on x and x has on y, respectively. In this situation, both x and y populations have carrying
capacities.

This system (as with the others) is not restricted to two populations, it can be generalized to many more populations
interacting in the form:

> $ \frac{dx_i}{dt}=r_ix_i(1-\frac{\sum_{j=1}^{N} a_{ij}x_j}{K_i}) $,
>

where $r_i$ is the growth rate of $x_i$, N is the number of populations that interact with $x_i$ and $K_i$ is the
carrying capacity for $x_i$.


## Neural Networks
Mathematically this looks like:

$$
\begin{align}
y &= f(w_1 x_1 + w_2 x_2 + b) \\
y &= f\left(\sum_i w_i x_i +b \right)
\end{align}
$$
With vectors this is the dot/inner product of two vectors:

$$
h = \begin{bmatrix}
x_1 \, x_2 \cdots  x_n
\end{bmatrix}
\cdot
\begin{bmatrix}
           w_1 \\
           w_2 \\
           \vdots \\
           w_n
\end{bmatrix}
$$

## Tensors
It turns out neural network computations are just a bunch of linear algebra operations on tensors, a generalization of matrices. A vector is a 1-dimensional tensor, a matrix is a 2-dimensional tensor, an array with three indices is a 3-dimensional tensor (RGB color images for example). The fundamental data structure for neural networks are tensors and PyTorch (as well as pretty much every other deep learning framework) is built around tensors.

### Reshaping Tensors
There are a few options here: weights.reshape(), weights.resize_(), and weights.view().

- weights.reshape(a, b) will return a new tensor with the same data as weights with size (a, b) sometimes, and sometimes a clone, as in it copies the data to another part of memory.

- weights.resize_(a, b) returns the same tensor with a different shape. However, if the new shape results in fewer elements than the original tensor, some elements will be removed from the tensor (but not from memory). If the new shape results in more elements than the original tensor, new elements will be uninitialized in memory. Here I should note that the underscore at the end of the method denotes that this method is performed in-place. Here is a great forum thread to read more about in-place operations in PyTorch.

- weights.view(a, b) will return a new tensor with the same data as weights with size (a, b).

```python
#import pyTorch
import torch
```

```python
def activation(x):
    return 1/(1+ torch.exp(-x))
```

```python
#Generate some data
torch.manual_seed(7) #set the random seeds so things are predictible

#Features are 5 random normal varables , 1 row 5 columns
features = torch.randn((1,5))
#True weights for our data , random normal variables again , weights created of same shape as features
weights = torch.rand_like(features)
#Bias term
bias = torch.randn((1,1))
```

```python
#Output of network
y = activation(torch.sum(weights*features) + bias)
print(y)
# y = activation((features*weights).sum + bias)
```

    tensor([[0.6140]])

```python
#Methods for matrix multiplication
    # torch.mm() - strict about shape of tensors
    # torch.matmul() - provides output even if shape not appropriate
'''
torch.mm(weights,features)
provides output---  size mismatch, m1: [1 x 5], m2: [1 x 5]
'''
#To find shape of a tensor - tensor.shape
print(weights.shape)

```

    torch.Size([1, 5])

```python
#Reshaping Tensors
# tensor.reshape(), tensor.view() , tensor.resize_()- underscore means it is inplace operation
# features.view(5,1) - creates a new tensor have to be saved

```

```python
#Output using matrix multiplication
y = activation(torch.mm(weights,features.view(5,1)) + bias)
print(y)
```

    tensor([[0.6140]])

2_Simple Neural Network and Random Functions.ipynb