# Detailed Workflow of Ripley K Applications to Subsurface Modeling in Python

### The University of Texas at Austin, PGE 2020 SURI, Undergraduation Research
### Alan Scherman, Undergraduate Student, Rice University, UT PGE SURI 2020
### Supervised by:
### Michael Pyrcz, Associate Professor, University of Texas at Austin
***
### Introduction
Ripley's K function is a second moment measure of stationary point patterns (SPP) first formalized by B.D. Ripley in 1976 [[1](https://www.jstor.org/stable/3212829?read-now=1&seq=1#page_scan_tab_contents)]. It is defined as the product of the inverse of an event's intensity ($\lambda$) and the expectation of additional events within a fixed distance ($E[r]$). That is:

\begin{equation}
K(r) = \frac{E[r]}{\lambda}
\end{equation}

Ripley's K gained importance within spatial point pattern analysis for being a distance-based measure which indicates spatial clustering or dispersion. The user can identify a pattern's spatial tendency by graphically comparing its K function to the K evaluation of complete spatial randomness (CSR) patterns. Within the realm of subsurface modeling, Ripley's K can indicate the spatial distribution of samples with desired properties, such as porosity and permeability, from a known sample.

***
### Theory
The procedure to calculate the $K$ function for a SPP has been illustrated in several formats (see references section for more detail), but it can be synthesized in the following steps:

#### Steps to calculate the Ripley K function of a SPP

1. Determine an area of study $A$ containing a finite number of events $N$;

2. Pick an event from collection of $N$ events possible (e.g. $n_0$) and draw a circle of radius $r$ around it;

3. Count the number of events other than $n_0$ occurring within the circle of radius $r$ AND average this sum by $N$;

4. Repeat steps 2 and 3 for the circles of radius $2r$, $3r$ and so forth until $r$ equals $1/4$ of the smaller side (for rectangular areas by convention);

5. Repeat steps 2 through 4 for all other $N-1$ events within $A$.

Note that for datasets, Step 3 is formalized as a indicator function which returns True (1) for an event within $r$ and False (0) if otherwise. Additionally, there is no rule of thumb for $r_max$ of more complex $A$ geometries. However, it must be stated that the greater $r$, the greater the variance in the SPP and the more biased the $K$ will become.

See below a non-rigorous demonstration of how to calculate the Ripley K function for an event $n_0$ within $A$:

The white circles represent the consecutive circles inside which the number of events are counted. The counted events are surrounded by a green ring. Note as well that the events outside $A$ are disregarded (thus the red cross).

#### Edge correction

One pitfall for Ripley's K is the necessity of an edge correction approach for events whose circles exceed the boundaries of $A$. If no edge correction approach is adopted (in which case $K$ is called the *naive estimator*), the $K$ for events closer to the boundaries of $A$ will be biased since it would undercount the number of neighbors for these events (i.e. events outside $A$ are disregarded). The edge correction term is applied by multiplying a weighting term to the expectation for every possible value of $r$. Several edge correction approaches have been proposed, including the border (or guard) zone, Ripley's Isotropic Corrector, and Besag's Edge Correction. Please consult the references section for more detail.

#### Confidence envelopes

However, in order to validate that the clustering or dispersion of a SPP suggested by the $K$ function is relevant, it is necessary to compare it to the $K$ function for a myriad of other CSR patterns. The most common approach is to generate a large number (e.g. 1000 realizations) of Monte Carlo generated CSR patterns and compute the $P10$ and $P90$ of the set of resulting $K$ functions. Graphically speaking, these percentiles provide upper and lower fences which indicate where significant clustering or dispersion begins (i.e. a confidence envelope for typical Poisson distribution behavior). See below an example of a Ripley K graph with confidence envelope:

The Ripley K function is shown in black. The blue line represents the upper fence of the confidence envelope (the $P975$ of 1000 realizations). If the *Ripley K is above the blue line, then there is significant clustering in the SPP*. The red line represents the lower fence of the confidence envelope (the $P25$ of the 1000 realizations). If the *Ripley K is below the red line, then there is significant dispersion (or inhibition) in the SPP*.

#### $L$ and $H$ Transforms

Lastly, it is common to represent the $K$ function by one of its transforms ($L$ and $H$ functions) for better visualization. The transform equations are given below:

\begin{equation}
L(r) = \sqrt{\frac{K(r)}{\pi}}
\end{equation}

\begin{equation}
H(r) = \sqrt{\frac{K(r)}{\pi}} - r
\end{equation}

Where $r$ is the search radius and $K(r)$ the Ripley K function.

***
### Objective
For this exercise, we will compute the $K$ function of a subsurface sample with porosity and permeability data. First, we will calculate the regular Ripley K function which will indicate potential clustering or dispersion of our sample events. Then, we will sort our subsurface data for low and high permeability and calculate the cross Ripley K of the transformed data (i.e. we will determine potential clustering or dispersion of low permeability events from high permeability events).

Note that the **_spatstat_** library on **_R_** provides great Ripley K and more distance-based pattern utilities widely used for spatial pattern analysis. Python equivalents can be found under the AstroPy, PySAL, and GeostatsPy packages (which we will utilize in this demonstration).
***
### Load required libraries
In order to find the Ripley K function for our subsurface sample, we will need some Python libraries to help import, manage, and visualize our data. These should be previously installed if you have Anaconda or other similar environment software.

```python
import numpy as np                                      # To manage NumPy arrays
import scipy.stats as sts                               # For use in CSR pattern generation
from pandas import read_csv                             # To import data and obtain summary statistics
from matplotlib import pyplot as plt                    # For plotting
from ripley_k_function import ripley_K                  # Python utility to compute K - more info on later section
```

If you get a package import error, you may have to first install some of these packages. This can usually be accomplished by opening up a command window on Windows and then typing 'python -m pip install [package-name]'. More assistance is available with the respective package docs.
***
### Visualize data
Let's import our subsurface sample data. For now let us only worry about our events' coordinates. Later, when we do cross Ripley K computations, we will also account for differences in properties (specifically permeability).

```python
sample1 = read_csv('sample_data_biased_1.csv', usecols = ['X','Y'])
sample1.head()
```

      X
      Y

      0
      100
      900

      1
      100
      800

      2
      100
      600

      3
      100
      500

      4
      100
      100

Since this data set refers to Cartesian coordinates, let us view a scatter plot of the events by their locations:

```python
sample1 = sample1.to_numpy()
X = sample1[:,0]; Y = sample1[:,1]
plt.style.use('fivethirtyeight')
figure = plt.figure(figsize=(14,10))
plt.scatter(X,Y,s=3,c='black')
plt.title('Scatter Plot of Sample Event Locations',size=30); plt.xlabel('X (m)',size=20); plt.ylabel('Y (m)',size=20);
```

At a first glance, we can spot some areas of potential clustering (e.g. at ($x = 400 m$,$y = 900 m$)) and dispersion (e.g. at ($x = 900 m$,$y = 800 m$)). However, we must first apply some SPP distance measure, for instance Ripley's K, to affirm that the SPP above demonstrates significant clustering or dispersion behavior.

***
### Regular Ripley K
In our first analysis, we will calculate the Ripley K function of the entire *sample1* dataset. All events are treated equally, with no distinction due to property values. The resulting curve will indicate potential clustering or dispersion of all events.

#### Determine area of study
Usually, the area of study encompasses the surveyed parts of the subsurface (i.e. all events are mapped within $A$). From the scatter plot produced previously, we can trace a rectangular area of study where $x$ and $y$ each range in $[0 m , 1000 m]$. However, it is also possible to scale down the area of study or adopt other shapes to bound $A$. For the sake of keeping this demonstration as objective as possible, we will limit $A$ to the square formed by the vertices $(0 m, 0 m)$, $(0 m, 1000 m)$, $(1000 m, 1000 m)$, and $(1000 m, 0 m)$:

```python
x_min = 0; x_max = 1000
y_min = 0; y_max = 1000
area = (x_max - x_min)*(y_max - y_min)
```

#### Calculate search radii
The next step is to determine the size and number of search circles which will be drawn around each event $n$. As mentioned before, the radius of the largest circle is by convention taken as:

\begin{equation}
r_{max} = \sqrt{A/2}
\end{equation}

However, in this demonstration we will consider $r_max$ to be equal to the side length of $A$. Even though this choice will add much bias to the $K$ for larger search radii, it will also allow a complete picture of this SPP's behavior.

Also, because the dimensions of $A$ range $[0 m, 1000 m]$, we will take 50 circles about each event to get enough detail but not overwhelm our calculation. Hence:

```python
radii = np.linspace(0,x_max-x_min,50).reshape(50,1)               # reshape method to get row vector format
```

#### Compute pairwise differences
Now that we have our area of study and search circles determined, we can start calculating the Ripley K function of our SPP. Manually, this could be done by drawing the sequence of circles and counting the events inside each shape. A beginner's program could easily achieve this with nested for-loops and other iterative resources. Nevertheless, because *sample1* is a large dataset, we must think of a more efficient procedure to compute the Ripley K.

Instead of drawing the circles first, let us compute all possible distances within $A$ and break those down into $x$ and $y$ displacements since we are using Cartesian coordinates:

Note that we can find the total number of distances in a SPP if we know $N$. That is:

\begin{equation}
D = \frac{N*(N-1)}{2}
\end{equation}

With the distances decomposed into $x$ and $y$ coordinates in matrix form, we can implement a single for-loop to span the set of events in $N$. By adding some other counters, we can obtain the decomposed distances matrix(*diff*) by:

```python
npts = np.shape(sample1)[0]                                 # Number of events in A

diff = np.zeros(shape = (npts*(npts-1)//2,2))               # Decomposed distances matrix
k = 0
for i in range(npts - 1):
    size = npts - i - 1
    diff[k:k + size] = abs(sample1[i] - sample1[i+1:])
    k += size
```

In order to emphasize the importance of edge adjustment techniques in the calculation of a SPP's Ripley K, we will compute and plot two Ripley K functions: one without edge adjustment and another with Ripley's Isotropic Edge Adjustment (because it is the better option for polygonal areas).

#### Evaluate Ripley K (without edge adjustment)
In this first example, we will not include an edge adjustment term. Then, all we need to do is to compute the distances from the pairwise differences (as simple as applying the Pythagorean Theorem to every row of the *diff* matrix) and determine whether these distances are less than a given $r$. If yes, the indicator function returns 1 (true). Otherwise, the indicator function returns 0 (false). Finally, we sum the true instances to obtain the expectation of events within $r$, and divide it by the intensity (which is the total number of distances divided by $A$). Hence:

```python
n_ripley = np.zeros(len(radii))
distances = np.hypot(diff[:,0], diff[:,1])                      # Pythagorean Theorem (a^2+b^2=c^2)

for r in range(len(radii)):
    n_ripley[r] = (distances<radii[r]).sum()                    # Indicator function and summation term

n_ripley = area * 2. * n_ripley / (npts * (npts - 1))           # Expectation vector element-wise divided by scalar intensity
```

#### Evaluate Ripley K (with Ripley's Isotropic Correction)
By applying Ripley's Isotropic Correction, we create a weighting term which is multiplied by the expectation at a given search radius $r$. In its essence, this edge correction method scales the naive estimator by the ratio of the radii of search circles extending outside $A$ by the radii of search circles completely inside $A$. Find more theoretical background on Ripley's Isotropic Correction on the References section. Hence:

```python
ripley = np.zeros(len(radii))

hor_dist = np.zeros(shape=(npts * (npts - 1)) // 2, dtype=np.double)
ver_dist = np.zeros(shape=(npts * (npts - 1)) // 2, dtype=np.double)

for k in range(npts - 1):                           # Finds horizontal and vertical distances from every event to nearest egde
    min_hor_dist = min(x_max - sample1[k][0], sample1[k][0] - x_min)
    min_ver_dist = min(y_max - sample1[k][1], sample1[k][1] - y_min)
    start = (k * (2 * (npts - 1) - (k - 1))) // 2
    end = ((k + 1) * (2 * (npts - 1) - k)) // 2
    hor_dist[start: end] = min_hor_dist * np.ones(npts - 1 - k)
    ver_dist[start: end] = min_ver_dist * np.ones(npts - 1 - k)

dist = np.hypot(diff[:, 0], diff[:, 1])
dist_ind = dist <= np.hypot(hor_dist, ver_dist)     # True if distance between events is less than or equal to distance to edge

w1 = (1 - (np.arccos(np.minimum(ver_dist, dist) / dist) + np.arccos(np.minimum(hor_dist, dist) / dist)) / np.pi)
w2 = (3 / 4 - 0.5 * (np.arccos(ver_dist / dist * ~dist_ind) + np.arccos(hor_dist / dist * ~dist_ind)) / np.pi)

weight = dist_ind * w1 + ~dist_ind * w2              # Weighting term

for r in range(len(radii)):
    ripley[r] = ((dist < radii[r]) / weight).sum()   # Indicator function with weighting term

ripley = area * 2. * ripley / (npts * (npts - 1))
```

#### Create confidence envelopes
Confidence envelopes are created by collecting high and low percentiles of sorted Ripley K for CSR patterns. Usually, $P10$ and $P90$ are good choices. If the Ripley K function exceeds the top threshold of the confidence envelope, then there is evidence for statistically significant clustering. Conversely, if the $K$ function sits below the lower end of the confidence envelope, then there is statistically significant inhibition.

The confidence envelopes are generated by the bootstrap method:
1. Generate a large number of CSR patterns by the Monte Carlo method;

2. Calculate the Ripley K function for each pattern;

3. Sort the $K$ values for each radius (each row) by increasing order;

4. Take the $P10$ and $P90$ of $K$ for each radius $r$ to form the top and bottom fences of the confidence envelope.

Observe that step 3 requires several iterations of the previous steps. To keep this demonstration as succint as possible, we will utilize a function which implements Ripley's K. More information on that function can be found in a later section.

First, let us find the confidence envelope without edge correction:

```python
Ks = np.zeros(shape=(len(radii),100))
xDel = (x_max-x_min); yDel = (y_max-y_min)
edge = np.array([[0,0],[0,1000],[1000,1000],[1000,0]])                            # Boundary coordinates

for i in range(100):                                                              # 100 realizations
    x_coords = xDel*sts.uniform.rvs(0,1,((npts,1))) + x_min
    y_coords = yDel*sts.uniform.rvs(0,1,((npts,1))) + y_min

    Ks[:,i] = ripley_K(data_x=x_coords, data_y=y_coords, edge_x=edge[:,0],\
                       edge_y=edge[:,1], radii=radii, mode='none', cv=False)      # Compute Ripley K for pattern

n_k10 = np.zeros((len(radii),1))
n_k90 = np.zeros((len(radii),1))

for j in range(len(radii)):
    Ks[j,:] = np.sort(Ks[j,:])                                                    # Sort row of K values at fixed radius
    n_k10[j] = Ks[j,:][9]                                                         # Take P10
    n_k90[j] = Ks[j,:][89]                                                        # Take P90
```

Now, repeat the same procedure but with Ripley's Isotropic Correction:

```python
for i in range(100):                                                              # 100 realizations
    x_coords = xDel*sts.uniform.rvs(0,1,((npts,1))) + x_min
    y_coords = yDel*sts.uniform.rvs(0,1,((npts,1))) + y_min

    Ks[:,i] = ripley_K(data_x=x_coords, data_y=y_coords, edge_x=edge[:,0],\
                       edge_y=edge[:,1], radii=radii, mode='ripley', cv=False)    # Compute Ripley K for pattern

k10 = np.zeros((len(radii),1))
k90 = np.zeros((len(radii),1))

for j in range(len(radii)):
    Ks[j,:] = np.sort(Ks[j,:])                                                    # Sort row of K values at fixed radius
    k10[j] = Ks[j,:][9]                                                           # Take P10
    k90[j] = Ks[j,:][89]                                                          # Take P90
```

#### Plot the Ripley K function
With the regular Ripley K functions and the corresponding confidence envelopes calculated, we can proceed to plot the resulting curves. Recall that we have computed two Ripley K functions for the same dataset: one without edge adjustment, and another with Ripley's Isotropic Correction:

```python
fig, [fig1,fig2] = plt.subplots(2,1,figsize=(13,24))

fig1.plot(radii,n_ripley,color='black',linewidth=2,label='K')
fig1.plot(radii,n_k10,color='red',linewidth=2,label='Lower Confidence')
fig1.plot(radii,n_k90,color='blue',linewidth=2,label='Upper Confidence')
fig1.fill_between(radii[:,0],n_k10[:,0],n_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig1.set_title('K Function without edge adjustment',size=20)
fig1.set_xlabel('Radii (m)',size=15); fig1.set_ylabel('K',size=15)
fig1.legend();

fig2.plot(radii,ripley,color='black',linewidth=2,label='K')
fig2.plot(radii,k10,color='red',linewidth=2,label='Lower Confidence')
fig2.plot(radii,k90,color='blue',linewidth=2,label='Upper Confidence')
fig2.fill_between(radii[:,0],k10[:,0],k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig2.set_title("K Function with Isotropic Correction",size=20)
fig2.set_xlabel('Radii (m)',size=15); fig2.set_ylabel('K',size=15)
fig2.legend();
```

#### Plot the $L$ and $H$ transforms
The K function can also be processed in order to improve visualization. Using the $L$ and $H$ transform functions provided previously, we obtain:

```python
L_none = np.sqrt(n_ripley/np.pi)                                              # L without edge adjustment
L_none_k10 = np.sqrt(n_k10/np.pi); L_none_k90 = np.sqrt(n_k90/np.pi)          # L conf. env. without edge adjustment
L_ripley = np.sqrt(ripley/np.pi)                                              # L with isotropic correction
L_ripley_k10 = np.sqrt(k10/np.pi); L_ripley_k90 = np.sqrt(k90/np.pi)          # L conf. env. with isotropic correction

H_none = L_none - radii[:,0]                                                  # H without edge adjustment
H_none_k10 = L_none_k10 - radii; H_none_k90 = L_none_k90 - radii              # H conf. env. without edge adjustment
H_ripley = L_ripley - radii[:,0]                                              # H with isotropic correction
H_ripley_k10 = L_ripley_k10 - radii; H_ripley_k90 = L_ripley_k90 - radii      # H conf. env. with isotropic correction

plt.style.use('fivethirtyeight')
fig, [[fig1,fig2],[fig3,fig4]] = plt.subplots(2,2,figsize=(13,20))

fig1.plot(radii,L_none,color='black',linewidth=2,label='L')                   # L without edge adjustment plot settings
fig1.plot(radii,L_none_k10,color='red',linewidth=2,label='Lower Confidence')
fig1.plot(radii,L_none_k90,color='blue',linewidth=2,label='Upper Confidence')
fig1.fill_between(radii[:,0],L_none_k10[:,0],L_none_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig1.set_title('L Function without edge adjustment')
fig1.set_xlabel('Radii (m)'); fig1.set_ylabel('L')
fig1.legend();

fig2.plot(radii,L_ripley,color='black',linewidth=2,label='L')                 # L with isotropic correction plot settings
fig2.plot(radii,L_ripley_k10,color='red',linewidth=2,label='Lower Confidence')
fig2.plot(radii,L_ripley_k90,color='blue',linewidth=2,label='Upper Confidence')
fig2.fill_between(radii[:,0],L_ripley_k10[:,0],L_ripley_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig2.set_title("L Function with Isotropic Correction")
fig2.set_xlabel('Radii (m)'); fig2.set_ylabel('L')
fig2.legend();

fig3.plot(radii,H_none,color='black',linewidth=2,label='H')                   # H without edge adjustment plot settings
fig3.plot(radii,H_none_k10,color='red',linewidth=2,label='Lower Confidence')
fig3.plot(radii,H_none_k90,color='blue',linewidth=2,label='Upper Confidence')
fig3.fill_between(radii[:,0],H_none_k10[:,0],H_none_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig3.set_title('H Function without edge adjustment')
fig3.set_xlabel('Radii (m)'); fig3.set_ylabel('H')
fig3.legend();

fig4.plot(radii,H_ripley,color='black',linewidth=2,label='H')                # H with isotropic correction plot settings
fig4.plot(radii,H_ripley_k10,color='red',linewidth=2,label='Lower Confidence')
fig4.plot(radii,H_ripley_k90,color='blue',linewidth=2,label='Upper Confidence')
fig4.fill_between(radii[:,0],H_ripley_k10[:,0],H_ripley_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig4.set_title("H Function with Isotropic Correction")
fig4.set_xlabel('Radii (m)'); fig4.set_ylabel('H')
fig4.legend();
```

The $H$ functions provide the best visualization of the regular Ripley K for this subsurface sample. Both the uncorrected and edge adjusted plots suggest siginificant event clustering for search radii less than or equal to $400 m$. For larger search radii, the edge correction approach factors in (because variance is greater for larger search radii) and while the no edge correction model suggests typical CSR behavior, the isotropically-corrected model indicates some event inhibition. Note that the uncorrected Ripley K curves (especially the confidence envelopes) are "smoother" because they tend to follow the theoretical K curve of a Poisson distribution.

***
### Cross Ripley K
Even though the regular Ripley K function is a powerful statistic to describe the density of events in a SPP, it is often the case where events inside $A$ have distinct properties. In subsurface analysis, it is useful to know, for instance, if events of low permeability cluster or disperse about events of high permeability. Therefore, the cross Ripley K function operates exactly like the regular K but also considers different types of events contained by $A$.

#### Distinguish events
Let us filter the events in *sample1* by their permeabilities. First, we will import the dataset again and obtain summary statistics of the events' permeabilities:

```python
sample1 = read_csv('sample_data_biased_1.csv', usecols = ['X','Y','Perm'])
sample1['Perm'].describe()
```

    count     279.000000
    mean      212.546761
    std       568.481338
    min         0.075819
    25%         3.666729
    50%        14.908970
    75%        64.608670
    max      5308.842566
    Name: Perm, dtype: float64

Now, let us convert this dataframe to NumPy arrays and filter the events for high and low permeability. We will place the high permeability events in matrix called *high_perm* and the events with low permeability in another matrix called *low_perm*:

```python
sample1 = sample1.to_numpy()

low_perm_thresh = 20                                                     # Low threshold (20 mD)
high_perm_thresh = 70                                                    # High threshold (70 mD)

high_perm = np.zeros(shape=(1,2))
low_perm = np.zeros(shape=(1,2))

for i in range(len(sample1)):                                            # Filter by row (by event) and store coordinates
    if sample1[i,-1] <= low_perm_thresh:
        low_perm = np.row_stack((low_perm,sample1[i,0:-1]))
    elif sample1[i,-1] >= high_perm_thresh:
        high_perm = np.row_stack((high_perm,sample1[i,0:-1]))

high_perm = high_perm[1:]; low_perm = low_perm[1:]                       # Eliminate zero rows (from preallocating high_perm and low_perm)

print('Number of events with low permeability is:', len(low_perm))
print('Number of events with high permeability is:', len(high_perm))
```

    Number of events with low permeability is: 162
    Number of events with high permeability is: 69

Finally, let us create a scatter plot of the filtered data that will help us to validate our cross Ripley K graphic results:

```python
plt.figure(figsize=(14,10))
plt.title('Scatter Plot of High and Low Permeability Events',size=30)
plt.xlabel('X (m)',size=20); plt.ylabel('Y (m)',size=20)
plt.scatter(low_perm[:,0],low_perm[:,1],marker='^',s=15,label='Low Perm')
plt.scatter(high_perm[:,0],high_perm[:,1],marker='o',s=15,label='High Perm')
plt.legend()
plt.show()
```

We can see a clear cluster of high permeability events on $x = [200 m, 600 m]$ and $y = [600 m, 1000 m]$. Therefore, we can hypothesize that the cross $K$ function will initially indicate strong dispersion of low permeability events from the high permeablity events and then clustering for larger search radii.

#### Determine area of study and calculate search radii
Because we are using the same dataset *sample1* from before, the area of study $A$ and the search radii are the same from the previous regular K exercise.

#### Compute pairwise differences
We will use the same procedure of breaking up the distances between two events in $x$ and $y$ coordinates. However, because cross Ripley K accounts for different types of events, the dimensions of the *diff* matrix are different. For instance, consider the following example:

So if we decide to compute the cross Ripley K of the circle events relative to the star events, the only relevant distances are those between a star and a circle. Then, the number of relevant distances (i.e. the number of rows of *diff*) is given by:

\begin{equation}
D = N_a * N_b
\end{equation}

where $N_a$ is the number of events of type $a$ (e.g . stars) and $N_b$ is the number of events of type $b$ (e.g. circles).

Then, the *diff* matrix for *sample1* concerning low and high permeability events can be computed by:

```python
npts = len(low_perm)                                         # Number of events with low perm
cross_npts = len(high_perm)                                  # Number of events with high perm

diff = np.zeros(shape = (npts*cross_npts,2))                 # Decomposed distances matrix

for i in range(cross_npts):
    sub_data = np.zeros(shape=(npts,2))
    sub_data[:] = high_perm[i]
    diff[npts*i:npts*(i+1)] = abs(sub_data - low_perm)
```

Including edge correction terms is also very important for cross Ripley K. As such, we will evaluate the cross K function twice for the dataset (without correction and with Ripley's Isotropic Correction) to compare results.

#### Evaluate cross Ripley K (without edge adjustment)
The procedure to obtain the cross Ripley K of datasets is nearly identical to that of regular K functions. Without an edge correction approach, the only noticeable difference is in the intensity term. For instance, when calculating the cross K of low permeability events relative to high permeability events, the intensity is the quotient of the number of distances between high and low permeability events (not all possible distances) and the area of study $A$. Then:

```python
n_ripley = np.zeros(len(radii))

distances = np.hypot(diff[:,0], diff[:,1])                                       # Pythagorean theorem

for r in range(len(radii)):
    n_ripley[r] = (distances<radii[r]).sum()

n_ripley = area * n_ripley / (npts * cross_npts)
```

#### Evaluate cross Ripley K (with Ripley's Isotropic Correction)
Similar to the evaluation without edge adjustment, the calculation of the cross K with isotropic correction features few changes from its regular procedure. The main modifications are in the for-loop which calculates the distances of select events to the border and in the intensity of the select events. So, the number of rows of *min_hor_dist* and *min_ver_dist* is equivalent to $npts*cross_npts$, and the intensity $\lambda$ equals $(npts*cross_npts)/A$.

```python
ripley = np.zeros(len(radii))

hor_dist = np.zeros(shape=(npts*cross_npts), dtype=np.double)
ver_dist = np.zeros(shape=(npts*cross_npts), dtype=np.double)

for k in range(cross_npts - 1):         # Finds horizontal and vertical distances from every high_perm event to nearest egde
    min_hor_dist = min(x_max - high_perm[k][0], high_perm[k][0] - x_min)
    min_ver_dist = min(y_max - high_perm[k][1], high_perm[k][1] - y_min)
    start = k*npts
    end = (k+1)*npts
    hor_dist[start: end] = min_hor_dist * np.ones(npts)
    ver_dist[start: end] = min_ver_dist * np.ones(npts)

dist = np.hypot(diff[:, 0], diff[:, 1])
dist_ind = dist <= np.hypot(hor_dist, ver_dist) # True if distance between events is less than or equal to distance to edge

w1 = (1 - (np.arccos(np.minimum(ver_dist, dist) / dist) + np.arccos(np.minimum(hor_dist, dist) / dist)) / np.pi)
w2 = (3 / 4 - 0.5 * (np.arccos(ver_dist / dist * ~dist_ind) + np.arccos(hor_dist / dist * ~dist_ind)) / np.pi)

weight = dist_ind * w1 + ~dist_ind * w2                                        # Weighting term

for r in range(len(radii)):
    ripley[r] = ((dist < radii[r]) / weight).sum()                             # Indicator function with summation term

ripley = area * ripley / (npts * cross_npts)
```

#### Create confidence envelope
Again, the process to create a confidence envelope is nearly identical to that of regular K confidence envelopes. However, note that the Monte Carlo simulations need to create $x$ and $y$ coordinates for both sets of events (e.g. for low and high permeability events). We will use the *ripley_K* function again for conciseness. First, the confidence envelope without edge correction:

```python
Ks = np.zeros(shape=(len(radii),100))
xDel = x_max - x_min; yDel = y_max - y_min
edge = np.array([[0,0],[0,1000],[1000,1000],[1000,0]])                         # Boundary coordinates

for i in range(100):                                                           # 100 realizations
    x_coords = xDel*sts.uniform.rvs(0,1,((npts,1))) + x_min
    y_coords = yDel*sts.uniform.rvs(0,1,((npts,1))) + y_min
    cross_x_coords = xDel*sts.uniform.rvs(0,1,((cross_npts,1))) + x_min
    cross_y_coords = yDel*sts.uniform.rvs(0,1,((cross_npts,1))) + y_min

    Ks[:,i] = ripley_K(data_x=x_coords, data_y=y_coords, edge_x=edge[:,0],\
                       edge_y=edge[:,1], cross_data_x=cross_x_coords,\
                       cross_data_y=cross_y_coords, radii=radii, mode='none', cv=False)  # Compute Ripley K for pattern

n_k10 = np.zeros((len(radii),1))
n_k90 = np.zeros((len(radii),1))

for j in range(len(radii)):
    Ks[j,:] = np.sort(Ks[j,:])                                                 # Sort row of K values at fixed radius
    n_k10[j] = Ks[j,:][9]                                                      # Take P10 - lower fence
    n_k90[j] = Ks[j,:][89]                                                     # Take P90 - upper fence
```

And the confidence envelope with isotropic correction:

```python
for i in range(100):                                                             # 100 realizations
    x_coords = xDel*sts.uniform.rvs(0,1,((npts,1))) + x_min
    y_coords = yDel*sts.uniform.rvs(0,1,((npts,1))) + y_min
    cross_x_coords = xDel*sts.uniform.rvs(0,1,((cross_npts,1))) + x_min
    cross_y_coords = yDel*sts.uniform.rvs(0,1,((cross_npts,1))) + y_min
    spp = np.column_stack((x_coords,y_coords))                                   # Assemble pattern coordinates
    cross_spp = np.column_stack((cross_x_coords,cross_y_coords))

    Ks[:,i] = ripley_K(data_x=x_coords, data_y=y_coords, edge_x=edge[:,0],\
                       edge_y=edge[:,1], cross_data_x=cross_x_coords,\
                       cross_data_y=cross_y_coords, radii=radii, mode='ripley', cv=False)  # Compute Ripley K for pattern

k10 = np.zeros((len(radii),1))
k90 = np.zeros((len(radii),1))

for j in range(len(radii)):
    Ks[j,:] = np.sort(Ks[j,:])                                                   # Sort row of K values at fixed radius
    k10[j] = Ks[j,:][9]                                                          # Take P10 - lower fence
    k90[j] = Ks[j,:][89]                                                         # Take P90 - upper fence
```

#### Plot the cross Ripley K function
It is now time to plot the resulting cross Ripley K functions:

```python
plt.style.use('fivethirtyeight')
fig, [fig1,fig2] = plt.subplots(2,1,figsize=(13,24))

fig1.plot(radii,n_ripley,color='black',linewidth=2,label='K')
fig1.plot(radii,n_k10,color='red',linewidth=2,label='Lower Confidence')
fig1.plot(radii,n_k90,color='blue',linewidth=2,label='Upper Confidence')
fig1.fill_between(radii[:,0],n_k10[:,0],n_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig1.set_title('Cross K Function without edge adjustment',size=20)
fig1.set_xlabel('Radii (m)',size=15); fig1.set_ylabel('K',size=15)
fig1.legend();

fig2.plot(radii,ripley,color='black',linewidth=2,label='K')
fig2.plot(radii,k10,color='red',linewidth=2,label='Lower Confidence')
fig2.plot(radii,k90,color='blue',linewidth=2,label='Upper Confidence')
fig2.fill_between(radii[:,0],k10[:,0],k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig2.set_title("Cross K Function with Isotropic Correction",size=20)
fig2.set_xlabel('Radii (m)',size=15); fig2.set_ylabel('K',size=15)
fig2.legend();
```

#### Plot the $L$ and $H$ transforms

```python
L_none = np.sqrt(n_ripley/np.pi)                                              # L without edge adjustment
L_none_k10 = np.sqrt(n_k10/np.pi); L_none_k90 = np.sqrt(n_k90/np.pi)          # L conf. env. without edge adjustment
L_ripley = np.sqrt(ripley/np.pi)                                              # L with isotropic correction
L_ripley_k10 = np.sqrt(k10/np.pi); L_ripley_k90 = np.sqrt(k90/np.pi)          # L conf. env. with isotropic correction

H_none = L_none - radii[:,0]                                                  # H without edge adjustment
H_none_k10 = L_none_k10 - radii; H_none_k90 = L_none_k90 - radii              # H conf. env. without edge adjustment
H_ripley = L_ripley - radii[:,0]                                              # H with isotropic correction
H_ripley_k10 = L_ripley_k10 - radii; H_ripley_k90 = L_ripley_k90 - radii      # H conf. env. with isotropic correction

plt.style.use('fivethirtyeight')
fig, [[fig1,fig2],[fig3,fig4]] = plt.subplots(2,2,figsize=(13,20))

fig1.plot(radii,L_none,color='black',linewidth=2,label='L')                   # L without edge adjustment plot settings
fig1.plot(radii,L_none_k10,color='red',linewidth=2,label='Lower Confidence')
fig1.plot(radii,L_none_k90,color='blue',linewidth=2,label='Upper Confidence')
fig1.fill_between(radii[:,0],L_none_k10[:,0],L_none_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig1.set_title('Cross L Function without edge adjustment')
fig1.set_xlabel('Radii (m)'); fig1.set_ylabel('L')
fig1.legend();

fig2.plot(radii,L_ripley,color='black',linewidth=2,label='L')                 # L with isotropic correction plot settings
fig2.plot(radii,L_ripley_k10,color='red',linewidth=2,label='Lower Confidence')
fig2.plot(radii,L_ripley_k90,color='blue',linewidth=2,label='Upper Confidence')
fig2.fill_between(radii[:,0],L_ripley_k10[:,0],L_ripley_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig2.set_title("Cross L Function with Isotropic Correction")
fig2.set_xlabel('Radii (m)'); fig2.set_ylabel('L')
fig2.legend();

fig3.plot(radii,H_none,color='black',linewidth=2,label='H')                   # H without edge adjustment plot settings
fig3.plot(radii,H_none_k10,color='red',linewidth=2,label='Lower Confidence')
fig3.plot(radii,H_none_k90,color='blue',linewidth=2,label='Upper Confidence')
fig3.fill_between(radii[:,0],H_none_k10[:,0],H_none_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig3.set_title('Cross H Function without edge adjustment')
fig3.set_xlabel('Radii (m)'); fig3.set_ylabel('H')
fig3.legend();

fig4.plot(radii,H_ripley,color='black',linewidth=2,label='H')                # H with isotropic correction plot settings
fig4.plot(radii,H_ripley_k10,color='red',linewidth=2,label='Lower Confidence')
fig4.plot(radii,H_ripley_k90,color='blue',linewidth=2,label='Upper Confidence')
fig4.fill_between(radii[:,0],H_ripley_k10[:,0],H_ripley_k90[:,0],color='yellow',alpha=0.5,label='Conf. Envelope')
fig4.set_title("Cross H Function with Isotropic Correction")
fig4.set_xlabel('Radii (m)'); fig4.set_ylabel('H')
fig4.legend();
```

Again, the $H$ transforms provide the most detectable evidence that initially events of low permeability are more dispersed from events of high permeability and then more clustered than would be expected in a Poisson distribution. Recall that if the $K$ curve sits below the confidence envelope then there is dispersion, and if it sits above then there is clustering. Both the uncorrected and isotropically corrected functions suggest the most meaningful inhibition of low permeability events about high permeability events for search radii of $200 m$, but the isotropic correction shows the degree of dispersion with greater magnitude (i.e. $K$ curve dips further away from confidence envelope).

***
### *ripley_K* function
In order to generate confidence envelopes, this workflow relied on the *ripley_K* function. This Python utility synthesizes what this demonstration aims to show in a more efficient and functional format. *ripley_K* can be found on the GeostatsPy package and most of its code comes from the *class RipleysKEstimator* from the *AstroPy* package and from the *Kest* and *Kcross* functions in the **R** library *spatstat*.

```python
print(ripley_K.__doc__)
```

    Calculates and plots regular or cross Ripley K curve for subsurface data.
           Plots K curve with confidence envelopes defined by P25 and P975. Includes options for L or H transform functions.
           Regular Ripley K calculates the behavior of events defined by data_x and data_y about themselves.
           Cross Ripley K calculates the behavior of events defined by data_x and data_y about events
           defined by cross_data_x and cross_data_y.

        :param data_x: events x-coordinates
        :type: pandas.DataFrame
        :param data_y: events y-coordinates
        :type: pandas.DataFrame
        :param edge_x: boundary point x-coordinates
        :type: pandas.DataFrame
        :param edge_y: boundary point y-coordinates
        :type: pandas.DataFrame
        :param cross_data_x: default empty array for regular K;
                             for cross K, pandas.DataFrame w/ x-coordinates of events from which to calculate K
        :param cross_data_y: default empty array for regular K;
                             for cross K, pandas.DataFrame w/ y-coordinates of events from which to calculate K
        :param radii: default empty array;
                      pandas.DataFrame for custom search radii
        :param mode: default 'none' for no edge correction;
                     adjustments available: 'translation','ohser','ripley'
        :param transform: default None for plot of K curve;
                          'L' for L-transform and 'H' for H-transform
        :param cv: default True used for recursive call;
                   if False then confidence interval is not calculated
        :return:K_plot: plot of Ripley K curve with confidence intervals
               :type: figure

        Note: Source code for regular K calculation and edge corrections taken from Astropy v4.0.1 class RipleysKEstimator,
              created by the Astropy Developers, last updated on 02 Apr 2020, available at:
              "https://docs.astropy.org/en/stable/_modules/astropy/stats/spatial.html#RipleysKEstimator".

***
### References
Find below some useful resources to learn more about the theory behind Ripley's K function, confidence envelopes, the AstroPy class utility which served as base code for the *ripley_K* function, and the *Kest* and *Kcross* functions in **R**:

1. [B.D.Ripley's original article on K function](https://www.jstor.org/stable/3212829?read-now=1&seq=1#page_scan_tab_contents)
2. [GeoDa tutorial - K function](https://www.youtube.com/watch?v=oT_jMTkkld8)
3. [Ripley K interactive](https://blog.jlevente.com/understanding-the-cross-k-function/)
4. [AstroPy - RipleysKEstimator](https://docs.astropy.org/en/stable/api/astropy.stats.RipleysKEstimator.html#astropy.stats.RipleysKEstimator.evaluate)
5. [Ripley's K theory and edge correction overview](https://lib.dr.iastate.edu/cgi/viewcontent.cgi?article=1048&context=stat_las_preprints)
6. [spatstat R documentation](https://cran.r-project.org/web/packages/spatstat/spatstat.pdf)
7. [Confidence envelopes](https://hpaulkeeler.com/poisson-point-process-simulation/)

```python

```


```python
import numpy as np
import scipy as sp
import scipy.stats
import matplotlib.pyplot as plt
import math as mt
import scipy.special
import seaborn as sns
plt.style.use('fivethirtyeight')
from statsmodels.graphics.tsaplots import plot_acf
import pandas as pd
```

#  Markov Chain Monte Carlo

The **Markov Chain Monte Carlo** (**MCMC**) is a class of algorithm to simulate a distribution that has no closed-form expression. To illustrate the mechanism of MCMC, we resort to the example of Gamma-Poisson conjugate.

Though it has a closed-form expression of posterior, we can still simulate the posterior for demonstrative purpose.

To use MCMC, commonly the Bayes' Theorem is modified without affecting the final result.
$$
P(\lambda \mid y) \propto P(y \mid \lambda)  P(\lambda)
$$
where $\propto$ means proportional to, the integration in the denominator can be safely omitted since it is a constant.

Here we recap the example of hurricanes in the last chapter. The prior elicitation uses

$$
E(\lambda) = \frac{\alpha}{\beta}\\
\text{Var}(\lambda) = \frac{\alpha}{\beta^2}
$$

```python
x = np.linspace(0, 10, 100)
params = [10, 2]
gamma_pdf = sp.stats.gamma.pdf(x, a=params[0], scale=1/params[1])

fig, ax = plt.subplots(figsize=(7, 7))
ax.plot(x, gamma_pdf, lw = 3, label = r'$\alpha = %.1f, \beta =  %.1f$' % (params[0], params[1]))
ax.set_title('Prior')
mean = params[0]/params[1]
mode = (params[0]-1)/params[1]
ax.axvline(mean, color = 'tomato', ls='--', label='mean: {}'.format(mean))
ax.axvline(mode, color = 'red', ls='--', label='mode: {}'.format(mode))
ax.legend()
plt.show()
```

1. Because posterior will also be Gamma distribution, we start from proposing a value drawn from posterior
$$
\lambda = 8
$$
This is an arbitrary value, which is called the **initial value**.

2. Calculate the likelihood of observing $k=3$ hurricanes given $\lambda=8$.
$$
\mathcal{L}(3 ; 8)=\frac{\lambda^{k} e^{-\lambda}}{k !}=\frac{8^{3} e^{-8}}{3 !}=0.1075
$$

```python
def pois_lh(k, lamda):
    lh = lamda**k*np.exp(-lamda)/mt.factorial(k)
    return lh
```

```python
lamda_init = 8
k = 3
pois_lh(k = k, lamda = lamda_init)
```

    0.02862614424768101

3. Calculate prior
$$
g(\lambda ; \alpha, \beta)=\frac{\beta^{\alpha} \lambda^{\alpha-1} e^{-\beta \lambda}}{\Gamma(\alpha)}
$$

```python
def gamma_prior(alpha, beta, lamda):
    prior = (beta**alpha*lamda**(alpha-1)*np.exp(-beta*lamda))/sp.special.gamma(alpha)
    return prior
```

```python
lamda_current = lamda_init
alpha=10
beta=2
gamma_prior(alpha=alpha, beta=beta, lamda=lamda_current)
```

    0.0426221247856141

4. Calculate the posterior with the first guess $\lambda=8$ and we denote it as $\lambda_{current}$

```python
k=3
posterior_current = pois_lh(k=k, lamda=lamda_current) * gamma_prior(alpha=10, beta=2, lamda=lamda_current)
posterior_current
```

    0.0012201070922556493

5. Draw a second value $\lambda_{proposed}$ from a **proposal distribution** with $\mu=\lambda_{current}$ and $\sigma = .5$. The $\sigma$ here is called **tuning parameter**, which will be clearer in following demonstrations.

```python
tuning_param = .5
lamda_prop = sp.stats.norm(loc=lamda_current, scale=tuning_param).rvs()
lamda_prop
```

    8.48325129448904

6. Calculate posterior based on the $\lambda_{proposed}$.

```python
posterior_prop = gamma_prior(alpha, beta, lamda=lamda_prop)*pois_lh(k, lamda=lamda_prop)
posterior_prop
```

    0.0005786900039113245

7. Now we have two posteriors. To proceed, we need to make some rules to throw one away. Here we introduce the **Metropolis Algorithm**. The probability threshold for accepting $\lambda_{proposed}$ is
$$
P_{\text {accept }}=\min \left(\frac{P\left(\lambda_{\text {proposed }} \mid \text { data }\right)}{P\left(\lambda_{\text {current }} \mid \text { data }\right)}, 1\right)
$$

```python
print(posterior_current)
print(posterior_prop)
```

    0.0012201070922556493
    0.0005786900039113245

```python
prob_threshold = np.min([posterior_prop/posterior_current, 1])
prob_threshold
```

    0.4742944349593793

It means the probability of accepting $\lambda_{proposed}$ is $1$. What if the smaller value is $\frac{\text{posterior proposed}}{\text{posterior current}}$, let's say $\text{prob_threshold}=.768$. The algorithm requires a draw from a uniform distribution, if the draw is smaller than $.768$, go for $\lambda_{proposed}$ if larger then stay with $\lambda_{current}$.

8. The demonstrative algorithm will be

```python
if sp.stats.uniform.rvs() > .768:
    print('stay with current lambda')
else:
    print('accept next lambda')
```

    accept next lambda

9. If we accept $\lambda_{proposed}$, redenote it as $\lambda_{current}$, then repeat from step $2$ for thousands of times.

##  Combine All Steps

We will join all the steps in a loop for thousands of times (the number of repetition depends on your time constraint and your computer's capacity).

```python
def gamma_poisson_mcmc(lamda_init = 2, k = 3, alpha = 10, beta= 2, tuning_param = 1, chain_size = 10000):
    np.random.seed(123)
    lamda_current = lamda_init

    lamda_mcmc = []
    pass_rate = []
    post_ratio_list = []

    for i in range(chain_size):
        lh_current = pois_lh(k = k, lamda = lamda_current)
        prior_current = gamma_prior(alpha=alpha, beta=beta, lamda=lamda_current)
        posterior_current = lh_current*prior_current

        lamda_proposal = sp.stats.norm(loc=lamda_current, scale=tuning_param).rvs()
        prior_next = gamma_prior(alpha=alpha, beta=beta, lamda=lamda_proposal)
        lh_next = pois_lh(k, lamda=lamda_proposal)

        posterior_proposal = lh_next*prior_next
        post_ratio = posterior_proposal/posterior_current

        prob_next = np.min([post_ratio, 1])
        unif_draw = sp.stats.uniform.rvs()
        post_ratio_list.append(post_ratio)

        if unif_draw < prob_next:
            lamda_current = lamda_proposal
            lamda_mcmc.append(lamda_current)
            pass_rate.append('Y')
        else:
            lamda_mcmc.append(lamda_current)
            pass_rate.append('N')
    return lamda_mcmc, pass_rate
```

The proposal distribution must be symmetrical otherwise the Markov chain won't reach an equilibrium distribution. Also the tuning parameter should be set at a value which maintains $30\%\sim50\%$ acceptance.

```python
lamda_mcmc, pass_rate = gamma_poisson_mcmc(chain_size = 10000)
```

```python
yes = ['Pass','Not Pass']
counts = [pass_rate.count('Y'), pass_rate.count('N')]

x = np.linspace(0, 10, 100)
params_prior = [10, 2]
gamma_pdf_prior = sp.stats.gamma.pdf(x, a=params_prior[0], scale=1/params_prior[1])
```

We assume $1$ year, the data records in total $3$ hurricanes. Obtain the analytical posterior, therefore we can compare the  simulation and the analytical distribution.
\begin{align}
\alpha_{\text {posterior }}&=\alpha_{0}+\sum_{i=1}^{n} x_{i} = 10+3=13\\
\beta_{\text {posterior }}&=\beta_{0}+n = 2+1=3
\end{align}
Prepare the analytical Gamma distribution

```python
params_post = [13, 3]
gamma_pdf_post = sp.stats.gamma.pdf(x, a=params_post[0], scale=1/params_post[1])
```

Because initial sampling might not converge to the equilibrium, so the first $1/10$ values of the Markov chain can be safely dropped. This $1/10$ period is termed as **burn-in** period. Also in order to minimize the _autocorrelation_ issue, we can perform **pruning** process to drop every other (or even five) observation(s).

That is why we use ```lamda_mcmc[1000::2]``` in codes below.

```python
fig, ax = plt.subplots(figsize = (12, 12), nrows = 3, ncols = 1)
ax[0].hist(lamda_mcmc[1000::2], bins=100, density=True)
ax[0].set_title(r'Posterior Frequency Distribution of $\lambda$')
ax[0].plot(x, gamma_pdf_prior, label='Prior')
ax[0].plot(x, gamma_pdf_post, label='Posterior')
ax[0].legend()
ax[1].plot(np.arange(len(lamda_mcmc)), lamda_mcmc, lw=1)
ax[1].set_title('Trace')
ax[2].barh(yes, counts, color=['green', 'blue'], alpha=.7)
plt.show()
```

#  Diagnostics of MCMC

The demonstration above has been fine-tuned deliberately to circumvent potential errors, which are common in MCMC algorithm designing. We will demonstrate how it happens, what probable remedies we might possess.

##  Invalid Proposal

Following the Gamma-Poisson example, if we have a proposal distribution with $\mu=1$, the random draw from this proposal distribution might be a negative number, however the posterior is a Gamma distribution which only resides in positive domain.

The remedy of this type of invalid proposal is straightforward, multiply $-1$ onto all negative draws.

```python
x_gamma = np.linspace(-3, 12, 100)
x_norm = np.linspace(-3, 6, 100)
params_gamma = [10, 2]
gamma_pdf = sp.stats.gamma.pdf(x, a=params_gamma[0], scale=1/params_gamma[1])

mu = 1
sigma = 1
normal_pdf = sp.stats.norm.pdf(x, loc=mu, scale=sigma)

fig, ax = plt.subplots(figsize=(14, 7))
ax.plot(x, gamma_pdf, lw = 3, label = r'Prior $\alpha = %.1f, \beta =  %.1f$' % (params[0], params[1]), color='#FF6B1A')
ax.plot(x, normal_pdf, lw = 3, label = r'Proposal $\mu=%.1f , \sigma= %.1f$' % (mu, sigma), color='#662400')
ax.text(4, .27, 'Gamma Prior', color ='#FF6B1A')
ax.text(1.7, .37, 'Normal Proposal', color ='#662400')
ax.text(.2, -.04, r'$\lambda_{current}=1$', color ='tomato')

x_fill = np.linspace(-3, 0, 30)
y_fill = sp.stats.norm.pdf(x_fill, loc=mu, scale=sigma)
ax.fill_between(x_fill, y_fill, color ='#B33F00')

ax.axvline(mu, color = 'red', ls='--', label=r'$\mu=${}'.format(mu), alpha=.4)
ax.legend()
plt.show()
```

Two lines of codes will solve this issue
```
        if lamda_proposal < 0:
            lamda_proposal *= -1
```

```python
def gamma_poisson_mcmc_1(lamda_init = 2, k = 3, alpha = 10, beta= 2, tuning_param = 1, chain_size = 10000):
    np.random.seed(123)
    lamda_current = lamda_init

    lamda_mcmc = []
    pass_rate = []
    post_ratio_list = []

    for i in range(chain_size):
        lh_current = pois_lh(k = k, lamda = lamda_current)
        prior_current = gamma_prior(alpha=alpha, beta=beta, lamda=lamda_current)
        posterior_current = lh_current*prior_current

        lamda_proposal = sp.stats.norm(loc=lamda_current, scale=tuning_param).rvs()
        if lamda_proposal < 0:
            lamda_proposal *= -1
        prior_next = gamma_prior(alpha=alpha, beta=beta, lamda=lamda_proposal)
        lh_next = pois_lh(k, lamda=lamda_proposal)

        posterior_proposal = lh_next*prior_next
        post_ratio = posterior_proposal/posterior_current

        prob_next = np.min([post_ratio, 1])
        unif_draw = sp.stats.uniform.rvs()
        post_ratio_list.append(post_ratio)

        if unif_draw < prob_next:
            lamda_current = lamda_proposal
            lamda_mcmc.append(lamda_current)
            pass_rate.append('Y')
        else:
            lamda_mcmc.append(lamda_current)
            pass_rate.append('N')
    return lamda_mcmc, pass_rate
```

This time we can set chain size much larger.

```python
lamda_mcmc, pass_rate = gamma_poisson_mcmc_1(chain_size = 100000, tuning_param = 1)
```

As you can see the frequency distribution is also much smoother.

```python
y_rate = pass_rate.count('Y')/len(pass_rate)
n_rate = pass_rate.count('N')/len(pass_rate)
yes = ['Pass','Not Pass']
counts = [pass_rate.count('Y'), pass_rate.count('N')]

fig, ax = plt.subplots(figsize = (12, 12), nrows = 3, ncols = 1)
ax[0].hist(lamda_mcmc[int(len(lamda_mcmc)/10)::2], bins=100, density=True)
ax[0].set_title(r'Posterior Frequency Distribution of $\lambda$')
ax[0].plot(x, gamma_pdf_prior, label='Prior')
ax[0].plot(x, gamma_pdf_post, label='Posterior')
ax[0].legend()
ax[1].plot(np.arange(len(lamda_mcmc)), lamda_mcmc, lw=1)
ax[1].set_title('Trace')

ax[2].barh(yes, counts, color=['green', 'blue'], alpha=.7)
ax[2].text(counts[1]*.4, 'Not Pass', r'${}\%$'.format(np.round(n_rate*100,2)), color ='tomato', size = 28)
ax[2].text(counts[0]*.4, 'Pass', r'${}\%$'.format(np.round(y_rate*100,2)), color ='tomato', size = 28)
plt.show()
```

##  Numerical Overflow

If prior and likelihood are extremely close to $0$, the product would be even closer to $0$. This would cause storage error in computer due to the binary system.

The remedy is to use the log version of Bayes' Theorem, i.e.

$$
\ln{P(\lambda \mid y)} \propto \ln{P(y \mid \lambda)}+  \ln{P(\lambda)}
$$

Also the acceptance rule can be converted into log version

$$
\ln{ \left(\frac{P\left(\lambda_{proposed } \mid  y \right)}{P\left(\lambda_{current} \mid y \right)}\right)}
=\ln{P\left(\lambda_{proposed } \mid  y \right)} - \ln{P\left(\lambda_{current } \mid  y \right)}
$$

```python
def gamma_poisson_mcmc_2(lamda_init = 2, k = 3, alpha = 10, beta= 2, tuning_param = 1, chain_size = 10000):
    np.random.seed(123)
    lamda_current = lamda_init

    lamda_mcmc = []
    pass_rate = []
    post_ratio_list = []

    for i in range(chain_size):
        log_lh_current = np.log(pois_lh(k = k, lamda = lamda_current))
        log_prior_current = np.log(gamma_prior(alpha=alpha, beta=beta, lamda=lamda_current))
        log_posterior_current = log_lh_current + log_prior_current

        lamda_proposal = sp.stats.norm(loc=lamda_current, scale=tuning_param).rvs()
        if lamda_proposal < 0:
            lamda_proposal *= -1
        log_prior_next = np.log(gamma_prior(alpha=alpha, beta=beta, lamda=lamda_proposal))
        log_lh_next = np.log(pois_lh(k, lamda=lamda_proposal))

        log_posterior_proposal = log_lh_next + log_prior_next
        log_post_ratio = log_posterior_proposal - log_posterior_current
        post_ratio = np.exp(log_post_ratio)

        prob_next = np.min([post_ratio, 1])
        unif_draw = sp.stats.uniform.rvs()
        post_ratio_list.append(post_ratio)

        if unif_draw < prob_next:
            lamda_current = lamda_proposal
            lamda_mcmc.append(lamda_current)
            pass_rate.append('Y')
        else:
            lamda_mcmc.append(lamda_current)
            pass_rate.append('N')
    return lamda_mcmc, pass_rate
```

With the use of log posterior and acceptance rule, the numerical overflow is unlikely to happen anymore, which means we can set a much longer Markov chain and also a larger tuning parameter.

```python
lamda_mcmc, pass_rate = gamma_poisson_mcmc_2(chain_size = 100000, tuning_param = 3)
```

```python
y_rate = pass_rate.count('Y')/len(pass_rate)
n_rate = pass_rate.count('N')/len(pass_rate)
yes = ['Pass','Not Pass']
counts = [pass_rate.count('Y'), pass_rate.count('N')]

fig, ax = plt.subplots(figsize = (12, 12), nrows = 3, ncols = 1)
ax[0].hist(lamda_mcmc[int(len(lamda_mcmc)/10)::2], bins=100, density=True)
ax[0].set_title(r'Posterior Frequency Distribution of $\lambda$')
ax[0].plot(x, gamma_pdf_prior, label='Prior')
ax[0].plot(x, gamma_pdf_post, label='Posterior')
ax[0].legend()
ax[1].plot(np.arange(len(lamda_mcmc)), lamda_mcmc, lw=1)
ax[1].set_title('Trace')

ax[2].barh(yes, counts, color=['green', 'blue'], alpha=.7)
ax[2].text(counts[1]*.4, 'Not Pass', r'${}\%$'.format(np.round(n_rate*100,2)), color ='tomato', size = 28)
ax[2].text(counts[0]*.4, 'Pass', r'${}\%$'.format(np.round(y_rate*100,2)), color ='tomato', size = 28)
plt.show()
```

Larger tuning parameter yields a lower pass ($30\%\sim50\%$) rate that is exactly what we are seeking for.

##  Pruning

```python
lamda_mcmc = np.array(lamda_mcmc)
n = 4
fig, ax = plt.subplots(ncols = 1, nrows = n ,figsize=(12, 10))
for i in range(1, n+1):
    g = plot_acf(lamda_mcmc[::i], ax=ax[i-1], title='', label=r'$lag={}$'.format(i), lags=30)
fig.suptitle('Markov chain Autocorrelation')

plt.show()
```

#  Gibbs Sampling Algorithm

The **Gibbs sampler** is a special case of the Metropolis sampler in which the proposal distributions exactly match the posterior conditional distributions and naturally proposals are accepted 100% of the time.

However a specialty of Gibbs Sampler is that can allow one to estimate multiple parameters.

In this section, we will use Normal-Normal conjugate priors to demonstrate the algorithm of Gibbs sampler.

Suppose you want to know the average height of female in your city, in the current setting, we assume $\mu$ and $\tau$ are our parameters of interest for estimation. Note that in conjugate prior section we assumed $\tau$ to be known, however in Gibbs sampling, both can be estimated.

A prior of _normal distribution_ will be assumed for $\mu$ with hyperparameters
$$
\text{inverse of }\sigma_0:\tau_0 = .15\\
\text{mean}:\mu_0 = 170
$$

A prior of _gamma distribution_ will be assumed for $\tau$ since it can't be negative.
$$
\text{shape}: \alpha_0 = 2\\
\text{rate}:\beta_0 = 1
$$

The priors graphically are

```python
mu_0, tau_0 = 170, .35
x_mu = np.linspace(150, 190, 100)
y_mu = sp.stats.norm(loc=mu_0, scale=1/tau_0).pdf(x_mu)

alpha_0, beta_0 = 2, 1
x_tau = np.linspace(0, 8, 100)
y_tau = sp.stats.gamma(a=alpha_0, scale=1/beta_0).pdf(x_tau)

fig, ax = plt.subplots(figsize=(15,5), nrows=1, ncols=2)
ax[0].plot(x_mu, y_mu)
ax[0].set_title(r'Prior of $\mu$')
ax[1].plot(x_tau, y_tau)
ax[1].set_title(r'Prior of $\tau$')
plt.show()
```

Choose an initial value of proposal of $\tau$, denoted as $\tau_{\text{proposal},0}$, the $0$ subscript represents the time period, since this is the initial value.

Say
$$
\tau_{\text{proposal},0} = 7
$$

Next step is to obtain
$$
\mu_{\text{proposal},0}|\tau_{\text{proposal},0}
$$
where $\mu_{\text{proposal},0}$ is the first value of proposal $\mu$ conditional on $\tau_{\text{proposal},0}$.

Now go collect some data, for instance you measured $10$ random women's heights, here's the data.

```python
heights = np.array([156, 167, 178, 182, 169, 174, 175, 164, 181, 170])
np.sum(heights)
```

    1716

Recall we have a sets of analytical solution derived in chapter 2

\begin{align}
\mu_{\text {posterior }} &=\frac{\tau_{0} \mu_{0}+\tau \sum x_{i}}{\tau_{0}+n \tau}\\
\tau_{\text {posterior }} &=\tau_{0}+n \tau
\end{align}

Substitute $\tau_{\text{proposal},0}$ into both formula.

$$
\mu_{\text {posterior},1} =\frac{\tau_{0} \mu_{0}+\tau_{\text{proposal},0} \sum_{i=0}^{100} x_{i}}{\tau_{0}+n \tau_{\text{proposal},0}}=\frac{.15\times170+7\times 1716}{.15+10\times7}\\
\tau_{\text {posterior}, 1} =\tau_{0}+n \tau_{\text{proposal},0} = .15 + 10\times 7
$$

```python
mu_post = [0]
tau_post = [0] # 0 is placeholder, there isn't 0th eletment, according to algo

tau_proposal = [7]
mu_proposal = [0] # 0 is placeholder

mu_post.append((.15*170+tau_proposal[0]*1716)/(.15+10*tau_proposal[0]))
tau_post.append(.15+10*tau_proposal[0])
```

Draw a proposal from updated distribution for $\mu$, that is $\mu_{\text{posterior}, 1}$ and $\tau_{\text{posterior}, 1}$

```python
mu_proposal_draw = sp.stats.norm(loc=mu_post[1], scale=1/tau_post[1]).rvs()
mu_proposal.append(mu_proposal_draw)
```

Turn to $\tau$ for proposal

```python

```

Chapter 4 - Markov Chain Monte Carlo.ipynb


- - - -
# Mechpy Tutorials
a mechanical engineering toolbox

source code - https://github.com/nagordon/mechpy
documentation - https://nagordon.github.io/mechpy/web/

- - - -

Neal Gordon
2017-02-20

- - - -

## Plotting

to plot inline in the ipython console or jupyter notebook, otherwise plots will be in the a seperate window

to turn on inline plotting
```python
from IPython import get_ipython
get_ipython().magic('matplotlib inline')
#or
%matplotlib inline
```

to turn off inline plotting
```python
get_ipython().magic('matplotlib')
#or
%matplotlib
#or
%matplotlib qt
```

```python
%matplotlib inline

# set figure size and font size for notebook
import matplotlib as mpl
mpl.rcParams['figure.figsize'] = (12,8)
mpl.rcParams['font.size'] = 14
mpl.rcParams['legend.fontsize'] = 14
```

```python
# one way to plot using symbolic math
x = sp.var('x')
f =  sp.sin(6*x)*sp.exp(-x)
df = f.diff()
w = np.array([f.subs(x,k) for k in z])
dw = np.array([df.subs(x,k) for k in z])

plt.plot(z,w,z,dw);
```

```python
#%% Sympy Plotting

# shows two plots together
from sympy import symbols
from sympy.plotting import plot
x = symbols('x')
p1 = plot(x*x)
p2 = plot(x)
p1.extend(p2)
p1.show()
```

```python
#single plot with range
plot(x**2, (x,-5,5));
```

```python
#multiple plots with range
plot(x,x**2,x**3, (x,-5,5))
```

```python
#multiple plots with different ranges
plot( (x, (x,-5,5)), (x**2, (x,-2,2)), (x**3, (x,-3,3)),
     title='testing multiple plots',
     xlabel='x-label',
     ylabel='ylabel')
```

```python
# parametric plots
from sympy import symbols, cos,sin
from sympy.plotting import plot_parametric
u = symbols('x')
plot_parametric(cos(u), sin(u), (u,-5,5))
```

```python
#multiple parametric plots with a single range
plot_parametric((cos(u), sin(u)), (u,cos(u)))
```

```python
# multiple parametric plots with multiple ranges
plot_parametric((cos(u), sin(u), (u,-5,5)), (cos(u), u, (u,-10,10)))
```

```python
# 3d plots
from sympy import symbols
from sympy.plotting import plot3d
x,y = symbols('x,y')
plot3d(x*y, (x,-5,5), (y,-5,5))
```

```python
# multiple plots with multiple ranges
plot3d((x**2+y**2, (x,-5,5), (y,-5,5)) , (x*y, (x,-3,3), (y,-3,3)))
```

```python
# 3d parametric plots
from sympy import symbols, sin,cos
from sympy.plotting import plot3d_parametric_line
u = symbols('u')
plot3d_parametric_line(cos(u), sin(u), u, (u,-5,5))
```

```python
#plotting regions
p6 = plot_implicit(y>x**2)
```

```python
#plotting using boolean
p7 = plot_implicit(And(y>x, y >=-x))
```

```python
from numpy import pi, sin
I = np.arange(0, 2*pi+0.1, 0.1)
plt.plot(I,sin(I), label='sin(I)')
plt.title('y=sin(x)')
plt.xlabel('x [rad]')
plt.ylabel(' Function y = sin(x)')
plt.text(pi/2,1, 'Max value', ha = 'center', va='bottom')
plt.text(3*pi/2,-1, 'Min value', ha = 'center', va='top')
plt.xticks(np.arange(0, 2*pi, pi/2),
       ('0', r'$\frac{\pi}{2}$', r'$\pi$',r'$\frac{3\pi}{2}$'))
plt.xlim([0, 2*pi])
plt.ylim([-1.2, 1.2])
plt.grid()
```

### Drawing and Diagrams
[index](#Mechpy)

to install pysketcher run these commands for a windows machine

check out tutorials
http://hplgit.github.io/pysketcher/doc/pub/tutorial/._pysketcher002.html

```bash
pip install future
choco install imagemagick -y # make sure to run as admin
git clone https://github.com/hplgit/pysketcher
cd pysketcher/
python setup.py install
```

```python
get_ipython().magic('matplotlib')  # seperate window

from pysketcher import *

L = 8.0
H = 1.0
xpos = 2.0
ypos = 3.0

drawing_tool.set_coordinate_system(xmin=0, xmax=xpos+1.2*L,
                                   ymin=0, ymax=ypos+5*H,
                                   axis=True)
drawing_tool.set_linecolor('blue')
drawing_tool.set_grid(True)
drawing_tool.set_fontsize(22)

P0 = point(xpos,ypos)
main = Rectangle(P0, L, H)
h = L/16  # size of support, clamped wall etc
support = SimplySupportedBeam(P0, h)
clamped = Rectangle(P0 + point(L, 0) - point(0,2*h), h, 6*h).set_filled_curves(pattern='/')
F_pt = point(P0[0]+L/2, P0[1]+H)
force = Force(F_pt + point(0,2*H), F_pt, '$F$').set_linewidth(3)
L_dim = Distance_wText((xpos,P0[1]-3*h), (xpos+L,P0[1]-3*h), '$L$')
beam = Composition({'main': main, 'simply supported end': support,
                    'clamped end': clamped, 'force': force,
                    'L': L_dim})
beam.draw()
beam.draw_dimensions()
drawing_tool.display()

get_ipython().magic('matplotlib inline') # inline plotting
```

```python
get_ipython().magic('matplotlib')  # seperate window

from pysketcher import *

L = 8.0
H = 1.0
xpos = 2.0
ypos = 3.0

drawing_tool.set_coordinate_system(xmin=0, xmax=xpos+1.2*L,
                                   ymin=0, ymax=ypos+5*H,
                                   axis=True)
drawing_tool.set_linecolor('blue')
drawing_tool.set_grid(True)
drawing_tool.set_fontsize(22)

P0 = point(xpos,ypos)
main = Rectangle(P0, L, H)
h = L/16  # size of support, clamped wall etc
support = SimplySupportedBeam(P0, h)
clamped = Rectangle(P0 + point(L, 0) - point(0,2*h), h, 6*h).set_filled_curves(pattern='/')
F_pt = point(P0[0]+L/2, P0[1]+H)
force = Force(F_pt + point(0,2*H), F_pt, '$F$').set_linewidth(3)
L_dim = Distance_wText((xpos,P0[1]-3*h), (xpos+L,P0[1]-3*h), '$L$')
beam = Composition({'main': main, 'simply supported end': support,
                    'clamped end': clamped, 'force': force,
                    'L': L_dim})
beam.draw()
beam.draw_dimensions()
drawing_tool.display()

get_ipython().magic('matplotlib inline') # inline plotting
```

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

```python
%matplotlib inline
```

```python
fig, ax = plt.subplots(figsize=(12, 3))

ax.set_yticks([])
ax.set_xticks([])
ax.set_xlim(-0.5, 3.5)
ax.set_ylim(-0.05, 0.25)
ax.axhline(0)

ax.text(0, 0.1, "Text label", fontsize=14, family="serif")

ax.plot(1, 0, 'o')
ax.annotate("Annotation",
            fontsize=14, family="serif",
            xy=(1, 0), xycoords='data',
            xytext=(+20, +50), textcoords='offset points',
            arrowprops=dict(arrowstyle="->", connectionstyle="arc3, rad=1.5"))

ax.text(2, 0.1, r"Equation: $i\hbar\partial_t \Psi = \hat{H}\Psi$", fontsize=14, family="serif")

ax.annotate('arc3', xy=(0.5, -1), xycoords='data',
            xytext=(-30, -30), textcoords='offset points',
            arrowprops=dict(arrowstyle="->",
                            connectionstyle="arc3,rad=.2")
            )

ax.annotate('arc', xy=(1., 1), xycoords='data',
            xytext=(-40, 30), textcoords='offset points',
            arrowprops=dict(arrowstyle="->",
                            connectionstyle="arc,angleA=0,armA=30,rad=10"),
            )

ax.annotate('arc', xy=(1.5, -1), xycoords='data',
            xytext=(-40, -30), textcoords='offset points',
            arrowprops=dict(arrowstyle="->",
                            connectionstyle="arc,angleA=0,armA=20,angleB=-90,armB=15,rad=7"),
            )
plt.show()
```

```python
fig, ax = plt.subplots(figsize=(12, 3))

ax.set_yticks([])
ax.set_xticks([])
ax.set_xlim(-1, 3.5)
ax.set_ylim(-0.1, 1)
ax.axhline(0)

ax.text(0, 0.1, "Text label", fontsize=14, family="serif")

ax.text(2, 0.1, r"Equation: $i\hbar\partial_t \Psi = \hat{H}\Psi$", fontsize=14, family="serif")

plt.annotate("Important Value", (.5,.5), xycoords='data',xytext=(1,.2), arrowprops=dict(arrowstyle='->'))

ax.plot([1,0],marker=r'$\circlearrowleft$',ms=50)
ax.plot([0,.5],marker=r'$\downarrow$',ms=100)

ax.plot(1, 0, 'o')
```

```python
def test_Spring():
    L = 5.0
    W = 2.0

    drawing_tool.set_coordinate_system(
        xmin=0, xmax=7*W, ymin=-L/2, ymax=1.5*L,
        axis=True, instruction_file='tmp_Spring.py')
    drawing_tool.set_linecolor('blue')
    drawing_tool.set_grid(True)

    xpos = W
    s1 = Spring((W,0), L, teeth=True)
    s1_title = Text('Default Spring',
                    s1.geometric_features()['end'] + point(0,L/10))
    s1.draw()
    s1_title.draw()
    #s1.draw_dimensions()
    xpos += 3*W
    s2 = Spring(start=(xpos,0), length=L, width=W/2.,
                bar_length=L/6., teeth=False)
    s2.draw()
    s2.draw_dimensions()
    drawing_tool.display('Spring')
test_Spring()
```

```python
def test_Dashpot():
    L = 5.0
    W = 2.0
    xpos = 0

    drawing_tool.set_coordinate_system(
        xmin=xpos, xmax=xpos+5.5*W, ymin=-L/2, ymax=1.5*L,
        axis=True, instruction_file='tmp_Dashpot.py')
    drawing_tool.set_linecolor('blue')
    drawing_tool.set_grid(True)

    # Default (simple) dashpot
    xpos = 1.5
    d1 = Dashpot(start=(xpos,0), total_length=L)
    d1_title = Text('Dashpot (default)',
                    d1.geometric_features()['end'] + point(0,L/10))
    d1.draw()
    d1_title.draw()

    # Dashpot for animation with fixed bar_length, dashpot_length and
    # prescribed piston_pos
    xpos += 2.5*W
    d2 = Dashpot(start=(xpos,0), total_length=1.2*L, width=W/2,
                 bar_length=W, dashpot_length=L/2, piston_pos=2*W)
    d2.draw()
    d2.draw_dimensions()

    drawing_tool.display('Dashpot')
test_Dashpot()
```

```python
## https://github.com/hplgit/pysketcher/blob/master/examples/beam2.py

"""A more sophisticated beam than in beam1.py."""
from pysketcher import *

def beam():
    L = 8.0
    a = 3*L/4
    b = L - a
    H = 1.0
    xpos = 0.0
    ypos = 3.0

    drawing_tool.set_coordinate_system(
        xmin=-3, xmax=xpos+1.5*L,
        ymin=0, ymax=ypos+5*H,
        axis=False)
    drawing_tool.set_linecolor('blue')
    #drawing_tool.set_grid(True)
    drawing_tool.set_fontsize(16)

    A = point(xpos,ypos)

    beam = Rectangle(A, L, H)

    h = L/16  # size of support, clamped wall etc

    clamped = Rectangle(A - point(h,0) - point(0,2*h), h,
                        6*h).set_filled_curves(pattern='/')

    load = ConstantBeamLoad(A + point(0,H), L, H)
    load.set_linewidth(1).set_linecolor('black')
    load_text = Text('$w$',
                     load.geometric_features()['mid_top'] +
                     point(0,h/2.))

    B = A + point(a, 0)
    C = B + point(b, 0)

    support = SimplySupportedBeam(B, h)  # pt B is simply supported

    R1 = Force(A-point(0,2*H), A, '$R_1$', text_spacing=1./50)
    R1.set_linewidth(3).set_linecolor('black')
    R2 = Force(B-point(0,2*H),
               support.geometric_features()['mid_support'],
               '$R_2$', text_spacing=1./50)
    R2.set_linewidth(3).set_linecolor('black')
    M1 = Moment('$M_1$', center=A + point(-H, H/2), radius=H/2,
                left=True, text_spacing=1/30.)
    M1.set_linecolor('black')

    ab_level = point(0, 3*h)
    a_dim = Distance_wText(A - ab_level, B - ab_level, '$a$')
    b_dim = Distance_wText(B - ab_level, C - ab_level, '$b$')
    dims = Composition({'a': a_dim, 'b': b_dim})
    symbols = Composition(
        {'R1': R1, 'R2': R2, 'M1': M1,
         'w': load, 'w text': load_text,
         'A': Text('$A$', A+point(0.7*h,-0.9*h)),
         'B': Text('$B$',
                   support.geometric_features()['mid_support']-
                   point(1.25*h,0)),
         'C': Text('$C$', C+point(h/2,-h/2))})

    x_axis = Axis(A + point(L+h, H/2), 2*H, '$x$',).\
             set_linecolor('black')
    y_axis = Axis(A + point(0,H/2), 3.5*H, '$y$',
                  label_alignment='left',
                  rotation_angle=90).set_linecolor('black')
    axes = Composition({'x axis': x_axis, 'y axis': y_axis})

    annotations = Composition({'dims': dims, 'symbols': symbols,
                               'axes': axes})
    beam = Composition({'beam': beam, 'support': support,
                        'clamped end': clamped, 'load': load})

    def deflection(x, a, b, w):
        import numpy as np
        R1 = 5./8*w*a - 3*w*b**2/(4*a)
        R2 = 3./8*w*a + w*b + 3*w*b**2/(4*a)
        M1 = R1*a/3 - w*a**2/12
        y = -(M1/2.)*x**2 + 1./6*R1*x**3 - w/24.*x**4 + \
            1./6*R2*np.where(x > a, 1, 0)*(x-a)**3
        return y

    x = linspace(0, L, 101)
    y = deflection(x, a, b, w=1.0)
    y /= abs(y.max() - y.min())
    y += ypos + H/2

    elastic_line = Curve(x, y).\
                   set_linecolor('red').\
                   set_linestyle('dashed').\
                   set_linewidth(3)

    beam.draw()
    drawing_tool.display()
    drawing_tool.savefig('tmp_beam2_1')

    import time
    time.sleep(1.5)

    annotations.draw()
    drawing_tool.display()
    drawing_tool.savefig('tmp_beam2_2')
    time.sleep(1.5)

    elastic_line.draw()
    drawing_tool.display()
    drawing_tool.savefig('tmp_beam2_3')
    #beam.draw_dimensions()
    #test_Dashpot(xpos+2*W)

beam()
input()
```


# The Solow model with human capital

In this model project we solve the Solow model with human capital. This is done with the usual Cobb-Douglas function but we also further extend the model with the CES production function.

# The model

Consider the following Solow model with human capital:

\begin{align}
Y_t & = K_t^{\alpha} H_t ^\varphi (A_t L_t)^{1-\alpha}, &0<\alpha,\varphi<1, \ \alpha+\varphi < 1 \\
I_t^K & = s_K Y_t, &0<s_H<1 \\
I_t^H & = s_H Y_t, &0<s_K<1 \\
K_{t+1} & = I_t^K + (1-\delta)K_t, &H_0 \\
H_{t+1} & = I_t^H + (1-\delta)K_t, &K_0 \\
L_{t+1} & = (1+n) L_t, &L_0 \\
A_{t+1} & = (1+g) A_t, &A_0
\end{align}
where $s_h + s_k < 1$.

It can be shown that the model can be translated into the following Solow equations:

\begin{align}
\tilde{k}_{t+1} - \tilde{k}_{t} &= \frac{1}{(1+n)(1+g)}(s_K\tilde{k}_t^{\alpha}\tilde{h}_t^{\varphi}-(n+g+\delta + ng)\tilde{k}_t) \\
\tilde{h}_{t+1} - \tilde{h}_{t} &= \frac{1}{(1+n)(1+g)}(s_H\tilde{k}_t^{\alpha}\tilde{h}_t^{\varphi}-(n+g+\delta + ng)\tilde{h}_t),
\end{align}

where $\tilde{k}_t \equiv \frac{K_t}{A_t L_t}$ and $\tilde{h}_t \equiv \frac{H_t}{A_t L_t}$. In steady state it must hold that $\tilde{k}_{t+1}-\tilde k_t = \tilde{h}_{t+1} - \tilde h_t = 0$.
We utilize this to solve for the steady state values of real and human capital per effective worker, $\tilde{k}^*$ and $\tilde{h}^*$.

To solve the model in Python, we first import the relevant packages.

```python
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from scipy import optimize
import sympy as sm
sm.init_printing(use_unicode=True)
from IPython.display import display, Latex, Markdown
import ipywidgets as widgets
from ipywidgets import interact
```

## Steady state in the Solow model with human capital

We can now solve the model analytically using Sympy. First we define the symbols which are to be used. Then we define the two equations $\tilde{k}_{t+1}=\tilde k_t = \tilde k^*$ and $\tilde{h}_{t+1} = \tilde h_t = \tilde h^*$ which are the steady state conditions. Finally, we solve for $\tilde k_t^*$ and $\tilde h_t^*$, respectively.

```python
# Defining the symbols used
sk, sh, alpha, g, delta, n, phi, tilkt, tilkt1, tilht, tilht1 = sm.symbols('s_K s_H alpha g delta n \\varphi \\tilde{k}_t \\tilde{k}_{t+1} \\tilde{h}_t \\tilde{h}_{t+1}')

# Defining the equation for physical capital per effective worker to be solved
ss_k = sm.Eq(1/((1+n)*(1+g))*(sk*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilkt))

# Defining the equation for human capital per effective worker to be solved
ss_h = sm.Eq(1/((1+n)*(1+g))*(sh*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilht))

# Solution for the steady steady state value for ss_k and ss_h
ss_sol = sm.solve((ss_k,ss_h),(tilkt,tilht))
# Steady state value for ss_k
ktilstar = ss_sol[0][0]

# Steady state value for ss_h
htilstar = ss_sol[0][1]

# Latex print of ktilstar
display(
        Markdown(
            r'The steady state value of physical capital per effective worker is $\ \tilde k^*= $ {}.'.format(
                sm.latex(ktilstar, mode='inline'))))

# Latex print of htilstar
display(
    Markdown(
        r'The steady state value of human capital per effective worker is $\ \tilde h^*= $ {}.'.format(
            sm.latex(htilstar, mode='inline'))))
```

The steady state value of physical capital per effective worker is $\ \tilde k^*= $ $s_{H}^{- \frac{\varphi}{\varphi + \alpha - 1}} \left(s_{K}^{\varphi - 1} \left(\delta + g n + g + n\right)\right)^{\frac{1}{\varphi + \alpha - 1}}$.

The steady state value of human capital per effective worker is $\ \tilde h^*= $ $\left(\left(s_{H}^{- \frac{\varphi}{\varphi + \alpha - 1}} \left(s_{K}^{\varphi - 1} \left(\delta + g n + g + n\right)\right)^{\frac{1}{\varphi + \alpha - 1}}\right)^{- \alpha} \left(\delta + g n + g + n\right) / s_{H}\right)^{\frac{1}{\varphi - 1}}$.

### Parameter calibration

In the following section we insert parameters in the steady state solutions, thus giving a numerical solution. By default we will set the paramaters in the derived steady state values as, $\varphi = \alpha = \frac{1}{3}$, $\delta = 0.05$, $g = n = 0.02$, $s_K = 0.1$ and $s_H = 0.15$. These values can be changed by the user below (given that the parameter restrictions are respected), such that one can see how different parameter values change the steady state values.

```python
# Turn symbols into variables with SymPy's lambdify function
_htilstar_sol = sm.lambdify((sh,phi,alpha,sk,delta,g,n),htilstar)
_ktilstar_sol = sm.lambdify((sh,phi,alpha,sk,delta,g,n),ktilstar)

# Define widget for steady state value
def ss_sol_int(sh,phi,alpha,sk,delta,g,n):

    # The sum of the elasticities of human and physical capital AND the sum of the savings rates must be strictly less than 1
    if alpha+phi >= 1.0 and sh+sk >= 1.0:
        result = display(Latex(
            r'$\alpha + \varphi$ and $s_H+s_K$ have to be stricly smaller than 1.0')) # print warning

    # The sum of the savings' rates must be strictly less than 1
    elif sh+sk >= 1.0:
        result = display(Latex(
            r'$s_H+s_K$ has to be stricly smaller than 1.0')) # print error

    # The elasticities of human and physical capital must som to be strictly less than 1
    elif alpha+phi >= 1.0:
        result = display(Latex(
            r'$\alpha + \varphi$ has to be stricly smaller than 1.0')) # print error

    # Stability constraint
    elif n+g+delta+n*g <= 0:
        result = display(Latex(
        r'$n+g+\delta+ng$ has to be strictly greater than $0$.')) # print error

    # If all the above conditions are not violated, print result
    else:
        result = display(Markdown(
            r'The steady state value of human capital per effective worker is $\ \tilde k^*= $ {}.'.format(
            sm.latex(_ktilstar_sol(sh,phi,alpha,sk,delta,g,n), mode='inline'))))
        result = display(Markdown(
            r'The steady state value of human capital per effective worker is $\ \tilde h^*= $ {}.'.format(
            sm.latex(_htilstar_sol(sh,phi,alpha,sk,delta,g,n), mode='inline'))))
    return result

# Make the solution interactive with widget controlling the parameters
widgets.interact(ss_sol_int,
                 sh = widgets.FloatSlider(description='$s_H$', min=0.01, max=0.99, value=0.4, step=0.01, continuous_update=False),
                 phi = widgets.FloatSlider(description='$\\varphi$', min=0.01, max=0.99, value=1/3, step=0.01, continuous_update=False),
                 alpha = widgets.FloatSlider(description='$\\alpha$', min=0.01, max=0.99, value=1/3, step=0.01, continuous_update=False),
                 sk = widgets.FloatSlider(description='$s_K$', min=0.01, max=0.99, value=0.4, step=0.01, continuous_update=False),
                 delta = widgets.FloatSlider(description='$\delta$', min=0.0, max=1.0, value=0.05, step=0.01, continuous_update=False),
                 g = widgets.FloatSlider(description='$g$', min=-1.0, max=1.0, value=0.02, step=0.01, continuous_update=False),
                 n = widgets.FloatSlider(description='$n$', min=-1.0, max=1.0, value=0.02, step=0.01, continuous_update=False)
                )
```

    interactive(children=(FloatSlider(value=0.4, continuous_update=False, description='$s_H$', max=0.99, min=0.01,…

    <function __main__.ss_sol_int(sh, phi, alpha, sk, delta, g, n)>

## Transition equations and phase diagram

In the following section we will derive the transition equations for the physical capital, $\tilde k_{t+1}$ and human capital, $\tilde h_{t+1}$.

```python
# Define transition equations for physical and human capital
ktplus1 = (1/((1+n)*(1+g))*(sk*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilkt) + tilkt)
htplus1 = (1/((1+n)*(1+g))*(sh*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilht) + tilht)

# Turn into function with inputs using lambdify function from SymPy
ktplus1lamb = sm.lambdify((tilkt, tilht, alpha, n, g, sh, sk, delta, phi),ktplus1)
htplus1lamb = sm.lambdify((tilkt, tilht, alpha, n, g, sh, sk, delta, phi),htplus1)

# Turn lambdify function into Python function with standard parameterization
def kt_plus1_func(tilkt,tilht,alpha=1/3,n=0.02,g=0.02,sh=0.4,sk=0.4,delta=0.05,phi=1/3):
    return ktplus1lamb(tilkt, tilht, alpha, n, g, sh, sk, delta, phi)
def ht_plus1_func(tilkt,tilht,alpha=1/3,n=0.02,g=0.02,sh=0.4,sk=0.4,delta=0.05,phi=1/3):
    return htplus1lamb(tilkt, tilht, alpha, n, g, sh, sk, delta, phi)
```

To create the paths where either $\Delta h_t = h_{t+1}-h_t = 0$ or $\Delta k_t = k_{t+1}-k_t = 0$, we isolate $h_t$ in both capital accumulation equations. These are the phase equations, i.e. when the Solow equations are set equal to 0.

```python
# Define Solow equations for physical and human capital
solow_kt = (1/((1+n)*(1+g))*(sk*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilkt))
solow_ht = (1/((1+n)*(1+g))*(sh*tilkt**alpha*tilht**phi-(n+g+delta+n*g)*tilht))

# Solve for physical and human capital when Solow equations are equal to 0
Delta_kt0 = sm.solve(solow_kt,tilht)
Delta_ht0 = sm.solve(solow_ht,tilht)

# Turn into function with inputs using lambdify function from SymPy
Delta_kt0 = sm.lambdify((tilkt, alpha, n, g, sh, sk, delta, phi),Delta_kt0[0])
Delta_ht0 = sm.lambdify((tilkt, alpha, n, g, sh, sk, delta, phi),Delta_ht0[0])

# Turn lambdify function into Python function with standard parameterization
def Delta_kt0_func(tilkt,alpha=1/3,n=0.02,g=0.02,sh=0.4,sk=0.4,delta=0.05,phi=1/3):
    return Delta_kt0(tilkt, alpha, n, g, sh, sk, delta, phi)
def Delta_ht0_func(tilkt,alpha=1/3,n=0.02,g=0.02,sh=0.4,sk=0.4,delta=0.05,phi=1/3):
    return Delta_ht0(tilkt, alpha, n, g, sh, sk, delta, phi)
```

We create the paths by iterating through a linearly spaced vector (```np.linspace```) from $0.0001$ to $1000$, creating $100000$ evenly spaced numbers.
These values are our given $k_t$, and thus we can find both paths.

```python
# Create plot of convergence to steady state
def create_cd_plot(k0cd_list,h0cd_list,alpha, n, g, sh, sk, delta, phi): # Inputs: Lists of k_t and h_t's paths,
                                                                         # and parameter values

    # Plot both
    fig,ax = plt.subplots() # Create plot
    plt.plot(k0cd_list,h0cd_list) #plot lists with paths
    plt.plot(k0cd_list[-1],h0cd_list[-1], 'bo') #plot point with steady state

    # set limits, if the initial values are above steady state, choose these as those who guide the limits
    # if the initial values are below steady state, choose steady state values to guide the limits
    # (last element in list)

    ax.set_xlim([0,max(k0cd_list[-1]*1.1,h0cd_list[-1]*1.1,k0cd_list[0]*1.1,h0cd_list[0]*1.1)]) # set xlim depending on the largest value
    ax.set_ylim([0,max(k0cd_list[-1]*1.1,h0cd_list[-1]*1.1,k0cd_list[0]*1.1,h0cd_list[0]*1.1)]) # set ylim depending on the largest value
    ax.set_title('Convergence in the Solow model with CD production function', pad=15) # Set title
    ax.set_xlabel('$k_t$') # set xlabel
    ax.set_ylabel('$h_t$') # set ylabel

    # Define the linearly spaced vector from 0.0001 to 10000 and make 100000 iterations
    x = np.linspace(0.0001,10000,num=100000)
    Delta_kt0_list = []
    Delta_ht0_list = []
    for i in range(0,x.shape[0]):
        Delta_kt0_list.append(Delta_kt0_func(x[i],alpha, n, g, sh, sk, delta, phi)) # list where Delta k_t = 0
        Delta_ht0_list.append(Delta_ht0_func(x[i],alpha, n, g, sh, sk, delta, phi)) # list where Delta k_t = 0

    plt.plot(x,Delta_kt0_list) # plot loci
    plt.plot(x,Delta_ht0_list) # plot loci
    return plt.show() # return plot
```

To make the plot interactive we now define a function, ```interact_CD```, where the user can define the paramters which are to be used. The plot then shows the convergence path.

```python
# Define interactive part
def interact_CD(k0=10, h0=10,sk=0.2,sh=0.1,alpha=1/3,phi=1/3,n=0.02,g=0.02,delta=0.05):

    # Make same conditions as with steady state values
    if sk + sh >= 1 and alpha + phi >=  1:
        result = display(Latex(r'$\alpha + \varphi$ and $s_H + s_K$ has to be strictly less than $1$.')) #print warning
    elif alpha + phi >= 1:
        result = display(Latex(r'$\alpha + \varphi$ has to be stricly less than $1$.')) #print warning
    elif sk + sh >= 1:
        result = display(Latex(r'$s_H+s_K$ has to be stricly less than $1$.')) #print warning
    elif (n + g + delta+n*g) <= 0:
        result = display(Latex(r'$n+g+\delta$ has to be greater than $0$.')) #print warning

    # Else create plot
    else:

        # Initialize lists that will hold all the values of k_t and h_t, starting with k_0 and h_0
        k0cd_list = [k0]
        h0cd_list = [h0]

        # Append k_1 and h_1 to their respective lists. They are calculated using the functions defined earlier, here hardreferencing to the initial values
        k0cd_list.append(kt_plus1_func(k0cd_list[0],h0cd_list[0],alpha, n, g, sh, sk, delta, phi))
        h0cd_list.append(ht_plus1_func(k0cd_list[0],h0cd_list[0],alpha, n, g, sh, sk, delta, phi))

        #initialize i for while loop
        i = 1

        # while loop that calculates the next values...
        while i<10000:

            #... unless the error margin has been reach
            if ((np.abs(h0cd_list[-1]-h0cd_list[-2])<0.00001) & (np.abs(k0cd_list[-1]-k0cd_list[-2])<0.00001)):
                display(Latex('Convergence archieved!')) #Inform of convergence
                result = create_cd_plot(k0cd_list,h0cd_list,alpha, n, g, sh, sk, delta, phi) # As convergence has been reached, create plot
                break

            #... or the max iterations have been reached
            elif i==9999:
                result = display(Latex('No convergence archieved')) #Inform of no convergence
                break

            # If error margin not satisfied and max iter not reached, append new values calculated using
            # function and the last value of the lists, repeat while loop
            else:
                k0cd_list.append(kt_plus1_func(k0cd_list[i],h0cd_list[i],alpha, n, g, sh, sk, delta, phi))
                h0cd_list.append(ht_plus1_func(k0cd_list[i],h0cd_list[i],alpha, n, g, sh, sk, delta, phi))

                #increase such that new values are used
                i += 1
    return result

# Let plot and sliders interact
widgets.interact(interact_CD,
            k0    = widgets.FloatSlider(description='$k_0$', min=1, max=10,step=0.25,value=4,continuous_update=False),
            h0    = widgets.FloatSlider(description='$h_0$', min=1, max=15,step=0.25,value=6,continuous_update=False),
            sk    = widgets.FloatSlider(description='$s_K$', min=0.01, max=0.99,step=0.01,value=0.1,continuous_update=False),
            sh    = widgets.FloatSlider(description='$s_H$', min=0.01, max=0.99,step=0.01,value=0.15,continuous_update=False),
            alpha = widgets.FloatSlider(description='$\\alpha$', min=0, max=0.99,step=0.01,value=1/3,continuous_update=False),
            phi   = widgets.FloatSlider(description='$\\varphi$', min=0, max=0.99,step=0.01,value=1/3,continuous_update=False),
            n     = widgets.FloatSlider(description='$n$', min=-1, max=1,step=0.01,value=0.02,continuous_update=False),
            g     = widgets.FloatSlider(description='$g$', min=-1, max=1,step=0.01,value=0.02,continuous_update=False),
            delta = widgets.FloatSlider(description='$\delta$', min=0, max=1,step=0.01,value=0.05,continuous_update=False)
                )
```

    interactive(children=(FloatSlider(value=4.0, continuous_update=False, description='$k_0$', max=10.0, min=1.0, …

    <function __main__.interact_CD(k0=10, h0=10, sk=0.2, sh=0.1, alpha=0.3333333333333333, phi=0.3333333333333333, n=0.02, g=0.02, delta=0.05)>

# The Solow model with human capital and a CES production function

In the following section we will extend the Solow model by changing the Cobb-Douglas production function with a CES (constant elasticity of substitution) function. We then have the following model setup.

\begin{align}
Y_t & = \left[\alpha K_t^{\frac{\sigma-1}{\sigma}}+ \varphi H_t^{\frac{\sigma-1}{\sigma}} + (1-\alpha-\varphi)(A_tL_t)^{\frac{\sigma-1}{\sigma}}\right]^{\frac{\sigma}{\sigma-1}}, \ &\alpha+\varphi < 1, \ \sigma \geq 0 \\
I_t^K & = s_K Y_t, &0<s_H<1 \\
I_t^H & = s_H Y_t, &0<s_K<1 \\
K_{t+1} & = I_t^K + (1-\delta)K_t, &H_0 \\
H_{t+1} & = I_t^H + (1-\delta)K_t, &K_0 \\
L_{t+1} & = (1+n) L_t, &L_0 \\
A_{t+1} & = (1+g) A_t, &A_0
\end{align}
where $s_h + s_k < 1$.

This is done to consider the more general case for the model as the Cobb-Douglas function is merely a special case of the CES function (that is, for $\sigma \to 1$ the CES production function corresponds to a Cobb-Douglas production function).

**NB:** SymPy cannot solve the model analytically with the CES production function. Hence, the model will only be solved numerically in this section.

Firstly, we define the two transition equations used:

\begin{align}
\tilde k_{t+1} &= \frac{1}{(1+n)(1+g)} \left(\left(s_K \left(\alpha \tilde k_t^{\frac{\sigma-1}{\sigma}} + \varphi \tilde h_t^{\frac{\sigma-1}{\sigma}} + (1-\alpha-\varphi) \right)\right)^{\frac{\sigma}{\sigma-1}}-(n+g+\delta+ng)\tilde k_t \right) + \tilde k_t \\
\tilde h_{t+1} &= \frac{1}{(1+n)(1+g)} \left(\left(s_H \left(\alpha \tilde k_t^{\frac{\sigma-1}{\sigma}} + \varphi \tilde h_t^{\frac{\sigma-1}{\sigma}} + (1-\alpha-\varphi) \right)\right)^{\frac{\sigma}{\sigma-1}}-(n+g+\delta+ng)\tilde h_t \right) + \tilde h_t
\end{align}

The equations are defined in Python below.

```python
# Define transition equation for physical capital per effective worker
def kt1(tilkt,tilht,sk=0.4,alpha=1/3,phi=1/3,sigma=1.5,n=0.02,g=0.02,delta=0.05):
    return (1/((1+n)*(1+g)))*(sk*(alpha*tilkt**((sigma-1)/sigma)+phi*tilht**((sigma-1)/sigma)+(1-alpha-phi))**(sigma/(sigma-1))-(n+g+delta+n*g)*tilkt+tilkt)

# Define transition equation for human capital per effective worker
def ht1(tilkt,tilht,sh=0.2,alpha=1/3,phi=1/3,sigma=1.5,n=0.02,g=0.02,delta=0.05):
    return (1/((1+n)*(1+g)))*(sh*(alpha*tilkt**((sigma-1)/sigma)+phi*tilht**((sigma-1)/sigma)+(1-alpha-phi))**(sigma/(sigma-1))-(n+g+delta+n*g)*tilht)+tilht
```

To make the convergence process interactive, we define the function for making exactly this below.

```python
# Define interactive part
def interact_CES(k0=10, h0=10,sk=0.2,sh=0.1,alpha=1/3,phi=1/3,sigma=1.5,n=0.02,g=0.02,delta=0.05):

    # sum of savings rates and sum of income shares must not exceed 1
    if sk + sh >= 1 and alpha + phi >=  1:
        result = display(Latex(r'$\alpha + \varphi$ and $s_H + s_K$ has to be strictly less than $1$.')) #print warning

    # sum of income shares must not exceed 1
    elif alpha + phi >= 1:
        result = display(Latex(r'$\alpha + \varphi$ has to be stricly less than $1$.')) #print warning

    # sum of savings rates must not exceed 1
    elif sk + sh >= 1:
        result = display(Latex(r'$s_H+s_K$ have to be stricly less than $1$.')) #print warning

    # stability constraint
    elif (n + g + delta) == 0:
        result = display(Latex(r'$n+g+\delta$ has to be greater than $0$.')) #print warning

    else:
        # Initialize lists that will hold all the values of k_t and h_t, starting with k_0 and h_0
        k0ces_list = [k0]
        h0ces_list = [h0]
        # Append k_1 and h_1 to their respective lists. They are calculated using the functions defined earlier, here hardreferencing to the initial values
        k0ces_list.append(kt1(k0ces_list[0],h0ces_list[0],sk,alpha,phi,sigma,n,g,delta))
        h0ces_list.append(ht1(k0ces_list[0],h0ces_list[0],sh,alpha,phi,sigma,n,g,delta))
        i = 1 #initialize i for while loop
        while i<10000: #while loop that calculates the next values...
            if ((np.abs(h0ces_list[-1]-h0ces_list[-2])<0.00001) & (np.abs(k0ces_list[-1]-k0ces_list[-2])<0.00001)): #... unless the error margin has been reached
                display(Latex('Convergence archieved!')) #Inform of convergence
                result = create_ces_plot(k0ces_list,h0ces_list) # As convergence has been reached, create plot
                break
            elif i==9999: #... unless the max iter has been reached
                result = display(Latex('No convergence archieved')) #Inform of no converge :(
                break
            else: #If error margin not satisfied and max iter not reached, append new values calculated using function and the last value of the lists, repeat while loop
                k0ces_list.append(kt1(k0ces_list[i],h0ces_list[i],sk,alpha,phi,sigma,n,g,delta))
                h0ces_list.append(ht1(k0ces_list[i],h0ces_list[i],sh,alpha,phi,sigma,n,g,delta))
                i += 1 #increase i for while loop
    return result
```

Finally, we create the plot and make it interact with the widgets for controlling the parameters.

```python
# Create plot of convergence to steady state
def create_ces_plot(k0ces_list,h0ces_list): # Inputs: List of paths of k_t and h_t
    fig,ax = plt.subplots() # create plot
    plt.plot(k0ces_list,h0ces_list) # plot the lists of k_t and h_t
    plt.plot(k0ces_list[-1],h0ces_list[-1], 'bo') # plot a point with the steady state values

    # set limits, if the initial values are above steady state, choose these as those who guide the limits
    # if the initial values are below steady state, choose steady state values to guide the limits (last element
    # in list)
    ax.set_xlim([0,max(k0ces_list[-1]*1.1,h0ces_list[-1]*1.1,k0ces_list[0]*1.1,h0ces_list[0]*1.1)])
    ax.set_ylim([0,max(k0ces_list[-1]*1.1,h0ces_list[-1]*1.1,k0ces_list[0]*1.1,h0ces_list[0]*1.1)])
    ax.set_title('Convergence in the Solow model with CES production function', pad=15) #Set title
    ax.set_xlabel('$k_t$') #Set xlabel
    ax.set_ylabel('$h_t$') #Set ylabel

    return plt.show()

#Let plot and sliders interact
widgets.interact(interact_CES,
            k0    = widgets.FloatSlider(description='$k_0$', min=1, max=10,step=0.25,value=4,continuous_update=False),
            h0    = widgets.FloatSlider(description='$h_0$', min=1, max=15,step=0.25,value=6,continuous_update=False),
            sk    = widgets.FloatSlider(description='$s_K$', min=0.01, max=0.99,step=0.01,value=0.1,continuous_update=False),
            sh    = widgets.FloatSlider(description='$s_H$', min=0.01, max=0.99,step=0.01,value=0.15,continuous_update=False),
            alpha = widgets.FloatSlider(description='$\\alpha$', min=0.01, max=0.99,step=0.01,value=1/3,continuous_update=False),
            phi   = widgets.FloatSlider(description='$\\varphi$', min=0.01, max=0.99,step=0.01,value=1/3,continuous_update=False),
            sigma = widgets.FloatSlider(description='$\sigma$', min=0, max=10,step=0.05,value=1.5,continuous_update=False),
            n     = widgets.FloatSlider(description='$n$', min=-1, max=1,step=0.01,value=0.01,continuous_update=False),
            g     = widgets.FloatSlider(description='$g$', min=-1, max=1,step=0.01,value=0.01,continuous_update=False),
            delta = widgets.FloatSlider(description='$\delta$', min=0, max=1,step=0.01,value=0.05,continuous_update=False)
                )
```

    interactive(children=(FloatSlider(value=4.0, continuous_update=False, description='$k_0$', max=10.0, min=1.0, …

    <function __main__.interact_CES(k0=10, h0=10, sk=0.2, sh=0.1, alpha=0.3333333333333333, phi=0.3333333333333333, sigma=1.5, n=0.02, g=0.02, delta=0.05)>


```python
# (Partial) Implementation of Barmatz and Collas 1985,
# "Acoustic radiation potential on a sphere in plane, cylindrical, and spherical standing wave fields"
```

```python
import sympy as sp
import sympy.physics.vector as vec
from sympy.physics.vector import ReferenceFrame
from sympy import symbols
from numpy import linspace
from sympy import lambdify
from sympy import I as j
from sympy import re, im
from sympy import sin, cos, tan, pi, Pow
from sympy.plotting import plot3d
import matplotlib.pyplot as mpl

from sympy.physics.mechanics import *
init_vprinting()
```

```python
from sympy.physics.vector import ReferenceFrame
W = ReferenceFrame('W')
t, rho, R, c = symbols('t rho R c')
f_1, f_2 = symbols('f_1 f_2')
v_0 = symbols('v_0')
p_in, v_in = symbols('p_in v_in')

rho_s, c_s = symbols('rho_s c_s')
#phi = symbols('phi')

f_1 = (1 - rho*c**2) / (rho_s*c_s**2)
f_2 = (2*(rho_s - rho)) / (2*rho_s + rho)

#Gorkov
#p_in = -rho*sp.diff(phi, t)
#v_in = vec.gradient(phi, W)
#U = (2*pi*R**3)*( (p_in**2 / (3*rho*c**2))*f_1 - (rho*v_in**2 / 2)*f_2 )
#U
```

```python
x, y, z = symbols('x y z')
n_x, n_y, n_z = symbols('n_x n_y n_z')
l_x, l_y, l_z = symbols('l_x l_y l_z')
k_x = pi*n_x / l_x
k_y = pi*n_y / l_y
k_z = pi*n_z / l_z
k = (k_x + k_y + k_z)**(1/2)

k
# k = (w/c), therefore w = k*c
```

```python
phi = -(v_0 / k)*cos(k_x*W[0])*cos(k_y*W[1])*cos(k_z*W[2])*sin(k*c*t)
phi
```

```python
p_in = -rho*sp.diff(phi, t)
p_in
```

```python
v_in = vec.gradient(phi, W)
v_in
```

```python
#v_in.subs({W[0]: x, W[1]: y, W[2]: z})
v_in.magnitude()
```

```python
#Pow(p_in, 2) Pow(v_in, 2)
U = ( (2*pi*R**3)*( (Pow(p_in, 2) / (3*rho*c**2))*f_1 - (rho*Pow(v_in.magnitude(), 2) / 2)*f_2 ) )
U = U.subs({W[0]: x, W[1]: y, W[2]: z})
U
```

```python
U_221 = U.subs({n_x:2, n_y:2, n_z:1})
U_221
```

```python
U_221.free_symbols
```

```python
plug_in = {
    R:0.01,
    c:1500,
    c_s:150,
    l_x:1,
    l_y:1,
    l_z:1,
    rho:200,
    rho_s:5,
    t:0,
    v_0:500,
    z:1
}
plot3d(U_221.subs(plug_in), (x, 0, 1), (y, 0, 1))
```

```python
plug_in = {
    R:0.01,
    c:1500,
    c_s:150,
    l_x:1,
    l_y:1,
    l_z:1,
    rho:200,
    rho_s:5,
    t:0,
    v_0:500,
    y:1
}
plot3d(U_221.subs(plug_in), (x, 0, 1), (z, 0, 1))
```

```python

```

```python

```