# Fourier Series: Bézier Curves
This notebook shows how to compute the Fourier series of Bézier curves.

### Content
1. [Prerequisites](#prerequisites)
2. [Representing Bézier curves](#representation)
3. [Fourier Series of Bézier curves](#fourier_computation)
    1. [Proof](#proof)
    2. [Implementation](#implementation)
4. [Example](#example)

```python
%matplotlib inline

# Initial imports
import numpy as np
import matplotlib.pyplot as plt

from scipy.special import comb, perm
```

```javascript
%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});

MathJax.Hub.Queue(
  ["resetEquationNumbers", MathJax.InputJax.TeX],
  ["PreProcess", MathJax.Hub],
  ["Reprocess", MathJax.Hub]
);
```

    <IPython.core.display.Javascript object>

## Prerequisites
This notebook shows how to compute the Fourier series of straight lines and polygones. For this, you should be familiar with:
- The general computation of [Fourier series of curves](Fourier-Series-of-Curves-Background.ipynb) and especially the case of piecewiese defines curves.
- Complex numbers, especially the representation of 2-dimensional points as complex numbers
- Computation of integrals of curves

## Representing Bézier curves
Bézier curves are often used in vector graphics (such as svg images) due to their intuitive representation and some nice properties. A Bézier curve of degree $n$ is defined by a sequence of $n+1$ *control points* $p_0,\ldots,p_n$ , which provide a rough intuition about the path of the curve.

Mathematically, a Bézier curve $g:[0,1]\to\mathbb{C}$ with control points $p_0,\ldots,p_n\in\mathbb{C}$ is defined as:
$$
\begin{equation}
    g(t) = \sum_{i=0}^n b^n_i(t) p_i
    \label{eqn:bezier}
\end{equation}
$$

with the Bernstein polynomial
$$
\begin{equation}
    b^n_i(t) = {n \choose i}t^i(1-t)^{n-i}
\end{equation}
$$

In some cases, we might need to define a Bézier curve over an arbitrary interval $[a,b]$. In this case, we use $g_{a,b}:[a,b]\to\mathbb{C}$ with

$$
\begin{equation}
    g_{a,b}(t) = g_{0,1}\left(\frac{t-a}{b-a}\right) = g\left(\frac{t-a}{b-a}\right)
\end{equation}
$$

#### Properties
Some properties of Bézier curves are:
- $p_0 = g(0)$ is the start and $p_n = g(1)$ is the end point of the curve
- $p_1 - p_0$ is the derivate in point $p_0$ and $p_n - p_{n-1}$ is the derivate in point $p_n$
- The derivative is given by a Bézier curve of degree $n-1$ with control points $q_i = n(p_{i+1}-p_{i})$
- More general: the $r$-th derivative is a Bézier curve of degree $n-1$ with control points $q_i = \frac{n!}{(n-r)!}\sum_{j=0}^r {r\choose j}(-1)^{r-j}p_{i+j}$

## Fourier Series of Bézier curves
As described in the [introduction of Fourier series of curves](Fourier-Series-of-Curves-Background.ipynb) we need to solve the integral $\int_a^b g_{a,b}(t)e^{-i\lambda t}dt$ for an arbitrary Bézier curve $g_{a,b}$ over an interval $[a,b]$ and any $\lambda\in\mathbb{R}$ in order to compute the Fourier coefficients.

--------------------------

_Case 1:_ $\lambda=0$:
$$
\begin{equation}
    \int_a^b g(t)e^{-i\lambda t}dt = \frac{b-a}{n+1} \sum_{j=0}^n p_j
    \label{eqn:integral_lambda0}
\end{equation}
$$

_Case 2:_ $\lambda\neq 0$:
$$
\begin{equation}
    \int_a^b g(t)e^{-i\lambda t}dt = -(b-a) e^{-i\lambda a}\sum_{r=0}^n\frac{1}{(i(b-a)\lambda)^{r+1}}\frac{n!}{(n-r)!}\sum_{j=0}^r{r \choose j}(-1)^{r-j}\left(p_{n-r+j}e^{-i(b-a)\lambda} - p_{j}\right)
    \label{eqn:integral_lambda_not0}
\end{equation}
$$

### Proof:
At the beginning, we see that

$$
\begin{align}
    \int_a^b g_{a,b}(t)e^{-i\lambda t}dt ={}& \int_a^b g\left(\frac{t-a}{b-a}\right)e^{-i\lambda t}dt \nonumber\\
        \stackrel{t=(b-a)u+a}={}& \int_0^1 g(u)e^{-i\lambda ((b-a)u+a)}\cdot(b-a)du \nonumber\\
        ={}& (b-a) e^{-i\lambda a}\int_0^1 g(u)e^{-i\lambda(b-a)u}du \label{eqn:from_ab_to_01}
\end{align}
$$

This means, that we only need to solve the integral for $g=g_{0,1}$ and can use $(\ref{eqn:from_ab_to_01})$ to generalize to $g_{a,b}$.

_Case 1:_ $\lambda=0$:
The derivative of a Bézier curve $G:[0,1]\to\mathbb{C}$ of degree $n+1$ with control points $q_0,\ldots,q_{n+1}$ is given by:
$$
\begin{equation}
    G'(t) = (n+1)\sum_{i=0}^n b^n_i(t) (q_{i+1}-q_i)
    \label{eqn:antiderivative}
\end{equation}
$$

Comparing $(\ref{eqn:antiderivative})$ with $(\ref{eqn:bezier})$ show that in case of $q_i := \frac{1}{n+1}\sum_{j=0}^{i-1} p_j$ the Bézier curve $G$ is the antiderivate of Béziercurve $g$.

Thus:

$$
\begin{align*}
    \int_0^1 g(t)e^{-i\lambda t}dt = \int_0^1 g(t)dt = \left[G(t)\right]_{t=0}^{1} = G(1) - G(0) = q_{n+1}-q{0} = \frac{1}{n+1}\sum_{j=0}^n p_j
\end{align*}
$$

_Case 2:_ $\lambda\neq 0$:
Given a Bézier curve $g$ of degree $n$, we get:

$$
\begin{align*}
    \int_0^1 g(t)e^{-i\lambda t}dt ={}& -\left[g(t)\frac{e^{i\lambda t}}{-i\lambda}\right]_{t=0}^1 + \frac{1}{i\lambda}\int_0^1 g'(t)e^{-i\lambda t}dt\\
    ={}& -\left[g(t)\frac{e^{-i\lambda t}}{i\lambda}\right]_{t=0}^1 -\left[g'(t)\frac{e^{-i\lambda t}}{(i\lambda)^2}\right]_{t=0}^1 + \frac{1}{(i\lambda)^2}\int_0^1 g''(t)e^{-i\lambda t}dt\\
    ={}& \ldots\\
    ={}& -\sum_{r=0}^n\left[g^{(r)}(t)\frac{e^{-i\lambda t}}{(i\lambda)^{r+1}}\right]_{t=0}^1 + \frac{1}{(i\lambda)^{n+1}}\int_0^1 g^{(n+1)}(t)e^{-i\lambda t}dt
    \label{eqn:integral_step_1}
\end{align*}
$$

Since the $(n+1)$-th derivation of a Bézier curve of degree $n$ is constantly 0, we get:

$$
\begin{align}
    \int_0^1 g(t)e^{-i\lambda t}dt ={}& -\sum_{r=0}^n\left[g^{(r)}(t)\frac{e^{-i\lambda t}}{(i\lambda)^{r+1}}\right]_{t=0}^1 \nonumber\\
    ={}& -\sum_{r=0}^n\frac{1}{(i\lambda)^{r+1}}\left(g^{(r)}(1)e^{-i\lambda}-g^{(r)}(0)\right)
    \label{eqn:integral_step_2}
\end{align}
$$

Here, the $r$-th derivatives of $g$ are given by:
$$
\begin{equation*}
    g^{(r)}(t) = \frac{n!}{(n-r)!}\sum_{k=0}^{n-r} b^{n-r}_k(t)\sum_{j=0}^r{r \choose j}(-1)^{r-j}p_{k+j}
\end{equation*}
$$

which simplifies for $t=0$ to:
$$
\begin{equation}
    g^{(r)}(0) = \frac{n!}{(n-r)!}\sum_{j=0}^r{r \choose j}(-1)^{r-j}p_{j}
    \label{eqn:derivative_0}
\end{equation}
$$
and for $t=1$ to
$$
\begin{equation}
    g^{(r)}(1) = \frac{n!}{(n-r)!}\sum_{j=0}^r{r \choose j}(-1)^{r-j}p_{n-r+j}
    \label{eqn:derivative_1}
\end{equation}
$$

Combining $(\ref{eqn:integral_step_2})$, $(\ref{eqn:derivative_0})$ and $(\ref{eqn:derivative_1})$ leads to:
$$
\begin{equation}
    \int_0^1 g(t)e^{-i\lambda t}dt = -\sum_{r=0}^n\frac{1}{(i\lambda)^{r+1}}\frac{n!}{(n-r)!}\sum_{j=0}^r{r \choose j}(-1)^{r-j}\left(p_{n-r+j}e^{-i\lambda} - p_{j}\right)
    \label{eqn:integral_step_3}
\end{equation}
$$

### Implementation
We begin with evaluation a Bézir curve a point $t$:

```python
def eval_bezier(p, t):
    t = np.asarray(t)
    s = t.shape

    t = np.reshape(t, (-1,1))

    p = np.reshape(p, (1,-1))
    n = p.shape[1] - 1

    j = np.arange(n+1)
    j = np.reshape(j, p.shape)

    q = np.sum(
        comb(n,j)*np.power(t,j)*np.power(1-t,n-j) * p,
        axis = 1
    )

    q = np.reshape(q, s)

    return q
```

Example curve:

```python
p = [0, 1+5j, 6-3j ,10+5j]

plt.figure(figsize=(6,6))
plt.plot(np.real(p),np.imag(p), marker='s', ms=20, markerfacecolor='red')

q = eval_bezier(p, np.arange(101,dtype=float) / 100)
plt.plot(np.real(q),np.imag(q))

plt.show()
```

Implementing equations $(\ref{eqn:integral_lambda0})$ and $(\ref{eqn:integral_lambda_not0})$:

```python
def transform_bezier(p, l, a, b):
    i = 1j   # Just for shorter notations

    l = np.asarray(l)
    s = l.shape
    l = np.reshape(l, (-1))
    result = np.zeros(shape=l.shape, dtype=np.complex)

    p = np.asarray(p)
    p = np.reshape(p, (-1))
    n = np.prod(p.shape) - 1

    # Handle Case k != 0
    l_ = np.reshape(l[l!=0], (-1))

    # Iterate over r:
    for r in range(n+1):
        j = np.reshape(np.arange(r+1, dtype=int),(1,-1))

        u = comb(r, j) * np.power(-1, r - j) * (p[n-r+j] * np.exp(-i * (b-a) * np.reshape(l_,(-1,1))) - p[j])
        result[l!=0] = result[l!=0] + np.sum(u, axis=1) * perm(n,r) / np.power(i * (b-a)*l_, r+1)

    # Add factor
    result[l!=0] = -(b-a)*np.exp(-i*l_*a)*result[l!=0]

    # Handle case k=0
    result[l==0] = (b-a) * np.sum(p) / (n+1)

    result = np.reshape(result, s)

    # Return results
    return result
```

Extending this to closed sequences of lines. Here we scale the period so that the length $T=2\pi$.

_Note:_ There are different ways to determine the segment-borders $t_0,\ldots,t_m$. Here we use the length of the curves. For this, we need to approximate the length:

```python
def bezier_len(p, m=100):
    t = np.arange(m+1,dtype=float) / m
    t = np.reshape(t, (-1,1))

    eval_bezier(p, t)

    l = np.sum(np.abs(q[1:] - q[:-1]))

    return l

```

And then transform the spline segments:

```python
def transform_bezier_spline(segments, n):
    m = len(segments)   # Number of Segments

    # Length of the segments
    l = [bezier_len(p) for p in segments]
    l = np.asarray(l)

    # Compute t_0 to t_m based on the lengths with t_0 = 0 and t_m = 2\pi
    t = l.cumsum() / l.sum()
    t = 2 * np.pi * np.concatenate([[0], t])

    # get vector of k
    k = np.arange(-n,n+1)

    c = sum([transform_bezier(segments[i], k, t[i], t[i+1]) for i in range(m)]) / (2 * np.pi)

    return c, k

```

Get the fourier approximation as a function.
This will be used to plot the fourier approximation.

```python
def get_fourier_fct(c, k):
    # Reshape the fourier coefficients row vectors
    c = np.reshape(c, (1,-1))
    k = np.reshape(k, (1,-1))

    def fct(t):
        # Reshape the input values into a column vector
        t = np.reshape(t, (-1,1))

        return np.sum(c * np.exp(1j * k * t), axis=1)

    return fct
```

## Example: Bézier Spline
Works also with other polygons, just add or modify points.

```python
# Corner points of the triangle as complex numbers
segments = [
    [0, 1+5j, 7-3j ,10+5j],
    [10+5j, 15, 6-4j],
    [6-4j, 6, 0]
]
```

```python
q = [eval_bezier(segment, np.arange(101,dtype=float) / 100) for segment in segments]
q = np.concatenate(q)

p = np.concatenate(segments)

plt.figure(figsize=(8,8))
plt.plot(np.real(p),np.imag(p), marker='s', ms=20, markerfacecolor='red')
plt.plot(np.real(q),np.imag(q))

plt.show()
```

```python
N = 12

# Compute coefficients c_{-N} to c_N
c,k = transform_bezier_spline(segments, N)

# Get approximations (limited to different values n=1,...,N)
fcts = [get_fourier_fct(c[np.abs(k) <= n], k[np.abs(k) <= n]) for n in range(1,N+1)]
```

Plotting the results

```python
# plot closed curve of complex
def plotcc(p, *args, **kwargs):
    # close curve
    m = len(p)
    p = np.reshape(p, (-1))
    p = p[list(range(m)) + [0]]     # Closing the curve

    # Complex to real x and y vectors
    x, y = np.real(p), np.imag(p)

    plt.plot(x, y, *args, **kwargs)
```

```python
plt.figure(figsize=(30,20))

# Number of points
nT = 1000

t = 2 * np.pi * np.arange(0,1, 1/nT)

for n in range(1, N+1):
    pf = fcts[n-1](t)

    plt.subplot(3,4,n)
    plotcc(q)
    plotcc(pf)
    plt.title(f"n={n}")

plt.show()
```


# Calculating the time dependent solution for $\eta$ and $\theta$ domain
\begin{equation}\label{p1introhpa} \Psi _{\eta \eta \tau }  = \frac{1}{{R_s }}\Psi _{\eta \eta \eta \eta }  + \Psi _{\eta \eta \eta } \Psi _\theta   - \Psi _{\eta \eta \theta } \Psi _\eta\\
-1 \le \eta \le1, \quad 0 \le \theta \le \pi
\end{equation}

The numerical solution found in this notebook is only stable for $0 < R_s \lesssim 55$ an attempt is made to include a cancelling dispersive term to help remove the stiffness. However the code still fails for $R_s \ge 55$

```julia
using DifferentialEquations
using Plots
using BenchmarkTools
using DelimitedFiles
using LaTeXStrings
using SparseArrays
using LinearAlgebra
```

In this code the vector $\Psi_j$ is used to store the solution at the current time step and $\Psi_{jnext}$ is psi value at the next time step

```julia
pyplot()
function Ψ_smallr(θ::Float64, η::Float64 ,r::Float64)
    return(-η*(η^2-1)*sin(θ)/2.0 + r*η*(η^2-1)^2.0*(2.0+η^2)*sin(2*θ)/560.0 +
        r^2.0*η*(η^2-1)^2*((-591 -2294*η^2 + 161*η^4 + 1428*η^6)*sin(θ) + 3.0*(423 + 166*η^2 - 553*η^4 - 84*η^6)sin(3*θ))/62092800)
end

function Ψ_smallr_d1(θ::Float64, η::Float64 ,r::Float64)
    return(-(3.0*η^2-1)*sin(θ)/2.0 + r*(7*η^6-9*η^2+2)*sin(2*θ)/560.0 +
        r^2.0*(3*(-197 - 1112*η^2 + 6930*η^4 - 2772*η^6 - 8085*η^8 + 5236*η^10)*sin(θ) + (1269 - 6120*η^2 - 6930*η^4 + 24948*η^6 - 10395*η^8 - 2772*η^10)*sin(3*θ))/62092800.0)
end

function  Ψ_smallr_d2(θ::Float64, η::Float64 ,r::Float64)
    return(-3.0*η*sin(θ) + r*(42*η^5-18*η)*sin(2*θ)/560.0 +
        r^2.0*(3*( - 2224*η + 27720*η^3 - 16632*η^5 - 64680*η^7 + 52360*η^9)*sin(θ) + (- 12240*η - 27720*η^3 + 149688*η^5 - 83160*η^7 - 27720*η^9)*sin(3*θ))/62092800.0)
end

function  Ψ_smallr_d3(θ::Float64, η::Float64 ,r::Float64)
    return(-3.0*sin(θ) + r*(210*η^4-18)*sin(2*θ)/560.0 +
        r^2.0*(3*( - 2224 + 83160*η^2 - 83160*η^4 - 452760*η^6 + 471240*η^8)*sin(θ) + (- 12240 - 83160*η^2 + 748440*η^4 - 582120*η^6 - 249480*η^8)*sin(3*θ))/62092800.0)
end

function Ψ_bigr(θ::Float64, η::Float64)
    return(sin(pi*η)*sin(θ)/pi)
end

function Ψ_bigr_d1(θ::Float64, η::Float64)
    return(cos(pi*η)*sin(θ))
end

function  Ψ_bigr_d2(θ::Float64, η::Float64)
    return(-sin(pi*η)*sin(θ)*pi)
end

function  Ψ_bigr_d3(θ::Float64, η::Float64)
     return(-cos(pi*η)*sin(θ)*pi^2)
end

```

    Ψ_bigr_d3 (generic function with 1 method)

```julia
#Change these to constants after
const R = Float64(60.0) # Reynolds number
const N = 200  # Total number of intervals (steps-1) in the eta direction
const M = 256  # Total number of intervals (steps-1) theta direction
const J = 20000    # Total number of time steps
ψ_current = zeros(N+1,M+1)
ψ_next =zeros(N+1,M+1)
ψ_j = ψ_current
ψ_jnext = ψ_next
const λ = 10.0
const Δη = 2.0/N
const Δθ = pi/M
const Δt = 0.001
const α = 0.5
const c = Δt/Δη^2
const d3 =      -1.0/12.0 -   α*c/R -   λ*c
const d2 =      4.0/3.0 + 4*α*c/R   + 4*λ*c
const d1 =     -5.0/2.0  -6*α*c/R   - 6*λ*c

A =spdiagm(-2=>ones(N-3)*d3 ,-1 => ones(N-2)*d2, 0 => ones(N-1)*d1, 1 => ones(N-2)*d2,2 => ones(N-3)*d3)

A[1,1] = d1 + d3 #Boundary condition psi@i=2 = 0, psi_eta@i=2 = -sin(theta) add -2d3\delta\eta\sine(theta) to RHS
A[end,end] = d1 + d3 #Boundary condition psi@i=N+1 = 0, psi_eta@i=N+1 = -sin(theta) add 2d3\delta\eta\sine(theta) to RHS
const LUA = lu(A)
varinfo(r"A")
```

| name |       size | summary                                       |
|:---- | ----------:|:--------------------------------------------- |
| A    | 17.172 KiB | 199×199 SparseMatrixCSC{Float64, Int64}       |
| LUA  | 17.195 KiB | SuiteSparse.UMFPACK.UmfpackLU{Float64, Int64} |

```julia
ψ_current[:,1] .=0.0
ψ_current[:,M+1] .=0.0
ψ_current[1,:] .= 0.0
ψ_current[N+1,:] .= 0.0
for n in 2:N
    for m in 2:M
        ψ_current[n,m] = Ψ_smallr(Δθ*(m-1),-1.0+Δη*(n-1),R)
    end
end
#rhs =ones(N-1)
ψ_current
#n=1 is ficticous point at eta=-1-deltaeta, n=2 is at eta=-1 and n=N+2 and n=N+3 is ficticious at eta=1+deltaeta

```

    201×257 Matrix{Float64}:
     0.0   0.0           0.0          …   0.0           0.0          0.0
     0.0  -0.000123517  -0.000247015     -0.00023473   -0.000117373  0.0
     0.0  -0.00024855   -0.000497057     -0.000449221  -0.000224624  0.0
     0.0  -0.000374954  -0.000749839     -0.000645074  -0.000322556  0.0
     0.0  -0.000502574  -0.00100505      -0.000823785  -0.000411914  0.0
     0.0  -0.000631242  -0.00126235   …  -0.000986745  -0.000493396  0.0
     0.0  -0.000760782  -0.0015214       -0.00113525   -0.000567648  0.0
     0.0  -0.000891008  -0.00178181      -0.00127049   -0.000635271  0.0
     0.0  -0.00102173   -0.00204321      -0.00139359   -0.000696822  0.0
     0.0  -0.00115274   -0.00230519      -0.00150558   -0.000752816  0.0
     0.0  -0.00128384   -0.00256735   …  -0.00160741   -0.000803728  0.0
     0.0  -0.00141483   -0.00282927      -0.00169995   -0.000849999  0.0
     0.0  -0.00154549   -0.00309054      -0.00178402   -0.000892032  0.0
     ⋮                                ⋱                 ⋮
     0.0   0.00141483    0.00282927       0.00169995    0.000849999  0.0
     0.0   0.00128384    0.00256735   …   0.00160741    0.000803728  0.0
     0.0   0.00115274    0.00230519       0.00150558    0.000752816  0.0
     0.0   0.00102173    0.00204321       0.00139359    0.000696822  0.0
     0.0   0.000891008   0.00178181       0.00127049    0.000635271  0.0
     0.0   0.000760782   0.0015214        0.00113525    0.000567648  0.0
     0.0   0.000631242   0.00126235   …   0.000986745   0.000493396  0.0
     0.0   0.000502574   0.00100505       0.000823785   0.000411914  0.0
     0.0   0.000374954   0.000749839      0.000645074   0.000322556  0.0
     0.0   0.00024855    0.000497057      0.000449221   0.000224624  0.0
     0.0   0.000123517   0.000247015      0.00023473    0.000117373  0.0
     0.0   0.0           0.0          …   0.0           0.0          0.0

```julia
#wireframe(range(0,stop = pi,length = M+1),range(-1,stop = 1,length = N+1),ψ_j)

```

```julia
function make_ψ_jnext_on_boundaries(ψ_j, ψ_jnext, LUA, parameters)
    (Δθ, Δη, R, N, M, rc, rd1, rd2, rd3, d1, d2, d3) = parameters
    for m = 2:M-2:M #For the first theta profile and last profile use second order difference formula for theta near boundaries

    #at n=1 psi[1,:] = 0 and we are only calculating the interior points from n=2 to n=N
    n=2
    bc=2*Δη*sin(Δθ*(m-1))
    bcp1=2*Δη*sin(Δθ*m)
    bcm1=2*Δη*sin(Δθ*(m-2))

    rhs = @view ψ_jnext[2:N,m]
    rhs[1] = rd3*(ψ_j[2,m]+bc)    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*ψ_j[n+2,m]+
        rc*(-(ψ_j[2,m]+bc)/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + ψ_j[n+2,m]/2)*
            (-ψ_j[n,m-1]/2 + ψ_j[n,m+1]/2)-
        rc*(-(-(ψ_j[2,m-1]+bcm1)/12 + 4*ψ_j[n-1,m-1]/3 -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -ψ_j[n+2,m-1]/12)/2+
             (-(ψ_j[2,m+1]+bcp1)/12 + 4*ψ_j[n-1,m+1]/3 -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -ψ_j[n+2,m+1]/12)/2)*
                ((ψ_j[2,m]+bc)/12 -  2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - ψ_j[n+2,m]/12)-d3*bc

        for n = 3:N-1
        rhs[n-1] = rd3*ψ_j[n-2,m]    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*ψ_j[n+2,m]+
                rc*(-ψ_j[n-2,m]/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + ψ_j[n+2,m]/2)*
                                       (-ψ_j[n,m-1]/2 + ψ_j[n,m+1]/2)-
        rc*(-(-ψ_j[n-2,m-1]/12 + 4*ψ_j[n-1,m-1]/3  -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -ψ_j[n+2,m-1]/12)/2+
            (-ψ_j[n-2,m+1]/12 +  4*ψ_j[n-1,m+1]/3  -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -ψ_j[n+2,m+1]/12)/2)*
                    (ψ_j[n-2,m]/12 -  2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - ψ_j[n+2,m]/12)

        end#End of the n loop
    #at n=N psi[N+1,:] = 0
    n=N
    rhs[N-1] = rd3*ψ_j[n-2,m]    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*(ψ_j[N,m]-bc)+
            rc*(-ψ_j[n-2,m]/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + (ψ_j[N,m]-bc)/2)*
                                       (-ψ_j[n,m-1]/2 + ψ_j[n,m+1]/2)-
            rc*(-(-ψ_j[n-2,m-1]/12 + 4*ψ_j[n-1,m-1]/3 -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -(ψ_j[N,m-1]-bcm1)/12)/2+
                 (-ψ_j[n-2,m+1]/12 + 4*ψ_j[n-1,m+1]/3 -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -(ψ_j[N,m+1]-bcp1)/12)/2)*
                    (ψ_j[n-2,m]/12 - 2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - (ψ_j[N,m]-bc)/12) +d3*bc

    #ψ_jnext[2:N,m] = LUA\rhs
    ldiv!(LUA,rhs)
    end#End of the m loop
end

function make_ψ_jnext_in_core_region(ψ_j, ψ_jnext, LUA, parameters)
    (Δθ, Δη, R, N, M, rc, rd1, rd2, rd3, d1, d2, d3) = parameters
    for m = 3:M-1

    #at n=1 psi[1,:] = 0 and we are only calculating the interior points from n=2 to n=N
    n=2
    bc=2*Δη*sin(Δθ*(m-1))
    bcp1=2*Δη*sin(Δθ*m)
    bcp2=2*Δη*sin(Δθ*(m+1))
    bcm1=2*Δη*sin(Δθ*(m-2))
    bcm2=2*Δη*sin(Δθ*(m-3))
    rhs = @view ψ_jnext[2:N,m]

    rhs[1] = rd3*(ψ_j[2,m]+bc)    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*ψ_j[n+2,m]+
            rc*(-(ψ_j[2,m]+bc)/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + ψ_j[n+2,m]/2)*
                ( ψ_j[n,m-2]/12 -  2*ψ_j[n,m-1]/3  +2*ψ_j[n,m+1]/3 - ψ_j[n,m+2]/12)-
        rc*((-(ψ_j[2,m-2]+bcm2)/12 + 4*ψ_j[n-1,m-2]/3 -5*ψ_j[n,m-2]/2 + 4*ψ_j[n+1,m-2]/3 -ψ_j[n+2,m-2]/12)/12-
          2*(-(ψ_j[2,m-1]+bcm1)/12 + 4*ψ_j[n-1,m-1]/3 -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -ψ_j[n+2,m-1]/12)/3+
          2*(-(ψ_j[2,m+1]+bcp1)/12 + 4*ψ_j[n-1,m+1]/3 -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -ψ_j[n+2,m+1]/12)/3-
            (-(ψ_j[2,m+2]+bcp2)/12 + 4*ψ_j[n-1,m+2]/3 -5*ψ_j[n,m+2]/2 + 4*ψ_j[n+1,m+2]/3 -ψ_j[n+2,m+2]/12)/12)*
                    (  (ψ_j[2,m]+bc)/12 -  2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - ψ_j[n+2,m]/12) -d3*bc

        for n = 3:N-1
            rhs[n-1] = rd3*ψ_j[n-2,m]    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*ψ_j[n+2,m]+
            rc*(-ψ_j[n-2,m]/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + ψ_j[n+2,m]/2)*
              ( ψ_j[n,m-2]/12 -  2*ψ_j[n,m-1]/3  +2*ψ_j[n,m+1]/3 - ψ_j[n,m+2]/12)-
            rc*((-ψ_j[n-2,m-2]/12 +  4*ψ_j[n-1,m-2]/3 -5*ψ_j[n,m-2]/2 + 4*ψ_j[n+1,m-2]/3 -ψ_j[n+2,m-2]/12)/12-
              2*(-ψ_j[n-2,m-1]/12 +  4*ψ_j[n-1,m-1]/3 -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -ψ_j[n+2,m-1]/12)/3+
              2*(-ψ_j[n-2,m+1]/12 +  4*ψ_j[n-1,m+1]/3 -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -ψ_j[n+2,m+1]/12)/3-
                (-ψ_j[n-2,m+2]/12 +  4*ψ_j[n-1,m+2]/3 -5*ψ_j[n,m+2]/2 + 4*ψ_j[n+1,m+2]/3 -ψ_j[n+2,m+2]/12)/12)*
                   (ψ_j[n-2,m]/12 -  2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - ψ_j[n+2,m]/12)
        end#End of the n loop

    #at n=N psi[N+1,:] = 0
    n=N
    rhs[N-1] = rd3*ψ_j[n-2,m]    +    rd2*ψ_j[n-1,m] + rd1*ψ_j[n,m]+rd2*ψ_j[n+1,m] + rd3*(ψ_j[N,m]-bc)+
            rc*(-ψ_j[n-2,m]/2 +     ψ_j[n-1,m]      -ψ_j[n+1,m]   + (ψ_j[N,m]-bc)/2)*
                ( ψ_j[n,m-2]/12 -  2*ψ_j[n,m-1]/3  +2*ψ_j[n,m+1]/3 - ψ_j[n,m+2]/12)-
            rc*((-ψ_j[n-2,m-2]/12 + 4*ψ_j[n-1,m-2]/3 -5*ψ_j[n,m-2]/2 + 4*ψ_j[n+1,m-2]/3 -(ψ_j[N,m-2]-bcm2)/12)/12-
              2*(-ψ_j[n-2,m-1]/12 + 4*ψ_j[n-1,m-1]/3 -5*ψ_j[n,m-1]/2 + 4*ψ_j[n+1,m-1]/3 -(ψ_j[N,m-1]-bcm1)/12)/3+
              2*(-ψ_j[n-2,m+1]/12 + 4*ψ_j[n-1,m+1]/3 -5*ψ_j[n,m+1]/2 + 4*ψ_j[n+1,m+1]/3 -(ψ_j[N,m+1]-bcp1)/12)/3-
                (-ψ_j[n-2,m+2]/12 + 4*ψ_j[n-1,m+2]/3 -5*ψ_j[n,m+2]/2 + 4*ψ_j[n+1,m+2]/3 -(ψ_j[N,m+2]-bcp2)/12)/12)*
                (ψ_j[n-2,m]/12 -  2*ψ_j[n-1,m]/3  +2*ψ_j[n+1,m]/3 - (ψ_j[N,m]-bc)/12) +d3*bc

    #ψ_jnext[2:N,m] = LUA\rhs
    ldiv!(LUA,rhs)
    end#End of the m loop
end
```

    make_ψ_jnext_in_core_region (generic function with 1 method)

```julia
#Forming the righthand side vector
const rd3 =      -1.0/12.0 + (1-α)*c/R    -λ*c
const rd2 =      4.0/3.0 - 4*(1-α)*c/R + 4*λ*c
const rd1 =     -5.0/2.0 + 6*(1-α)*c/R - 6*λ*c
const rc    =  Δt/(Δη*Δθ)

parameters =  (Δθ, Δη, R, N, M, rc, rd1, rd2, rd3, d1, d2, d3)
percent = 10.0

#Only theta profiles from m =2 to m = M need to be calculated with one sided theta derivatives second order.
#11111111111111111111111111111111111111111111111111111111
@time for j =1:J

    make_ψ_jnext_on_boundaries(ψ_j, ψ_jnext, LUA, parameters)
    make_ψ_jnext_in_core_region(ψ_j,ψ_jnext, LUA, parameters)

    if( mod(j,2) == 1)
        ψ_j    = ψ_next
        ψ_jnext=ψ_current
    else
        ψ_j    = ψ_current
        ψ_jnext=ψ_next
        ψ_jnext .= 2 .* ψ_jnext .- ψ_j
    end #End of if statement

    if( j> round(percent*J/100))
        #Calculate a simple norm of psi
        psi_norm = norm(ψ_j[:,4])
        println(string(percent)*"% compelete T="*string(round(j*Δt; digits=2))*" Core flow norm = "*string(round(psi_norm; digits=6)))
        percent+=10
        if isnan(psi_norm )
           println("Psi is no longer a number, exiting... ")
           exit()
       end
    end

end #End of time loop j
println("Reynolds number= "*string(R))
#At this point inverse of A should be applied to the rhs vector to produce the next iterate of psi at the j+1 time step

```

    10.0% compelete T=2.0 Core flow norm = 0.103741
    20.0% compelete T=4.0 Core flow norm = 0.102673

```julia
open("psi_solution.txt", "w") do io
           writedlm(io, ψ_current)
       end

```

```julia
m=20
MatrixPsi=readdlm("psifin.m",Float64,);
xinterval=range(-1,stop=1,length=101)
size(MatrixPsi)
eta=range(-1, stop=1,length= N+1)
plot(eta,ψ_j[:,m],linecolor=:orange, label="Julia", linestyle=:solid)
plot!(x->Ψ_smallr(Δθ*(m-1),x,R),-1,1, label="Small R analytical", linestyle=:dot, linecolor=:blue,xlabel = L"\eta", ylabel = L"\Psi",size=(800,400))
plot!(xinterval,MatrixPsi[2:102,2*m-1], label="Spectral", linestyle=:dash, linecolor=:black,xlabel = L"\eta", ylabel = L"\Psi")
plot!(x->Ψ_bigr((m-1)*Δθ,x),-1,1, label="Large R analytical",  linestyle=:dash, linecolor=:red,xlabel = L"\eta", ylabel = L"\Psi",size=(800,400),yguidefontrotation=-90, legend=:outertopright)
```

```julia
eta_profile =0.6
theta=range(0, stop=pi,length= M+1)
plot(theta,ψ_j[Int((eta_profile+1)/Δη),:],linecolor=:orange, label="Julia", linestyle=:solid)
plot!(x->Ψ_smallr(x,eta_profile,R),0,pi, label="Small R analytical", linestyle=:dot, linecolor=:blue,xlabel = L"\eta", ylabel = L"\Psi",size=(800,400))
plot!(theta,MatrixPsi[Int((eta_profile+1)/(2*Δη)),1:2:255])
```

```julia

```


To do:
 * Generate and output sympy code
 * Generate and output C++ code
 * Loop over components

***Note:*** At the moment, this notebook computes radiated spherical harmonic modes from *source* multipole tensors.  It is designed only for the relatively low-order requirements of the spin parts of the waveform.  Thus, it generally ignores terms like $\hat{n} \cdot \vec{v}$.  (That is, the velocity is assumed to be orthogonal to the separation between the black holes.)  Also, it ignores the intermediate multipoles needed to translate between source and radiated multipoles; they enter at somewhat higher order than we need.

It would be easy enough to modify this notebook to account for the unaccounted-for terms, but this has not been done yet.

We have a couple formulas at lowest order that can guide us in our choice of terms to keep:

$dv/dt = -\mathcal{L}/(dE/dv) \approx -(2v^{10}/5)/(-v/8) = v^9/20$

$d^2v/dt^2 = 9v^8/20\, dv/dt = 9 v^{17}/400$

\begin{align*}
\hat{n} \cdot \vec{v} &\approx v\, \frac{dr}{dt} \\
&= v\, \frac{d(1/v^2)}{dt} \\
&= -\frac{2}{v^2}\, \frac{dv}{dt} \\
&= -\frac{v^7}{10}
\end{align*}

Clearing the cache is not strictly necessary, but I have found the cache can sometimes cause trouble.
from sympy.core.cache import clear_cache
clear_cache()

```python
from __future__ import division, print_function
import sympy
from sympy import *
from sympy.printing import ccode
from sympy import Rational as frac
import re
import simpletensors
from simpletensors import Vector, xHat, yHat, zHat
from simpletensors import TensorProduct, SymmetricTensorProduct, Tensor, ReduceExpr
import sphericalharmonictensors
init_printing()

var('mu,ell,A,B,C,epsilon')
var('jk,jkl,jklm,jklmn,jklmno,jklmnop,jklmnopq') # Index sets Blanchet uses
var('i,j,k,l,m,n,o') # Indices Bohé et al. use
#var('Subscript', cls=Function); # Mathematica will return this, so let's just pretend it exists
var('vartheta, varphi')
var('nu, m, delta, c, t, v0')

# These are related scalar functions of time
var('v, gamma, r', cls=Function)
v = v(t)
x = v**2
Omega = v**3
# gamma = v**2*(1 + (1-nu/3)*v**2 + (1-65*nu/12)*v**4) # APPROXIMATELY!!!  Change this later
#r = 1/gamma
gamma = gamma(t)
r = r(t)
#r = 1/v**2 # APPROXIMATELY!!!  Just for testing.  Change this later

# These get redefined momentarily, but have to exist first
var('nHat, lambdaHat, ellHat', cls=Function)
# And now we define them as vector functions of time
nHat = Vector('nHat', r'\hat{n}', [cos(Omega*t),sin(Omega*t),0,])(t)
lambdaHat = Vector('lambdaHat', r'\hat{\lambda}', [-sin(Omega*t),cos(Omega*t),0,])(t)
ellHat = Vector('ellHat', r'\hat{\ell}', [0,0,1,])(t)

# # These are the spin functions -- first, the individual components as regular sympy.Function objects; then the vectors themselves
# var('S_n, S_lambda, S_ell', cls=Function)
# var('Sigma_n, Sigma_lambda, Sigma_ell', cls=Function)
# SigmaVec = Vector('SigmaVec', r'\vec{\Sigma}', [Sigma_n(t), Sigma_lambda(t), Sigma_ell(t)])(t)
# SVec = Vector('S', r'\vec{S}', [S_n(t), S_lambda(t), S_ell(t)])(t)

# These are the spin vector functions, treated as constant at this order
# (note that this is w.r.t. the x,y,z basis, rather than n,lambda,ell)
var('S_n, S_lambda, S_ell')
var('Sigma_n, Sigma_lambda, Sigma_ell')
SVec = Vector('S', r'\vec{S}', [S_n, S_lambda, S_ell])(t)
SigmaVec = Vector('SigmaVec', r'\vec{\Sigma}', [Sigma_n, Sigma_lambda, Sigma_ell])(t)

# These are some cross products that appear in the spin terms, assuming $\vec{v} = v\hat{\lambda}$
SVectimeslambdaHat = Vector('SVectimeslambdaHat', r'\vec{S} \times \hat{\lambda}',
                            [-S_ell*cos(Omega*t), -S_ell*sin(Omega*t), S_lambda*sin(Omega*t) + S_n*cos(Omega*t)])(t)
SigmaVectimeslambdaHat = Vector('SigmaVectimeslambdaHat', r'\vec{\Sigma} \times \hat{\lambda}',
                                [-Sigma_ell*cos(Omega*t), -Sigma_ell*sin(Omega*t), Sigma_lambda*sin(Omega*t) + Sigma_n*cos(Omega*t)])(t)
nHattimesSVec = Vector('nHattimesSVec', r'\hat{n} \times \vec{S}',
                       [S_ell*sin(Omega*t), -S_ell*cos(Omega*t), S_lambda*cos(Omega*t) - S_n*sin(Omega*t)])(t)
nHattimesSigmaVec = Vector('nHattimesSigmaVec', r'\hat{n} \times \vec{\Sigma}',
                           [Sigma_ell*sin(Omega*t), -Sigma_ell*cos(Omega*t), Sigma_lambda*cos(Omega*t) - Sigma_n*sin(Omega*t)])(t)
nHattimeslambdaHat = ellHat
nHatdotSVecdotlambdaHat = -S_ell # Symbol(r'\left[ \hat{n} \cdot \left( \vec{S} \times \hat{\lambda} \right) \right]')
nHatdotSigmaVecdotlambdaHat = -Sigma_ell # Symbol(r'\left[ \hat{n} \cdot \left( \vec{\Sigma} \times \hat{\lambda} \right) \right]')

nHatdotv = diff(r,t,1) # Symbol(r'\hat{n} \cdot \hat{v}')

# These ignore the v^7 contribution from v along nHat
SVecdotv = v*(-S_n*sin(Omega*t)+S_lambda*cos(Omega*t)) # Symbol(r'\vec{S} \cdot \vec{v}')
SigmaVecdotv = v*(-Sigma_n*sin(Omega*t)+Sigma_lambda*cos(Omega*t)) # Symbol(r'\vec{\Sigma} \cdot \vec{v}')

nHatdotSVec = S_n*cos(Omega*t)+S_lambda*sin(Omega*t) # Symbol(r'\hat{n} \cdot \vec{S}')
nHatdotSigmaVec = Sigma_n*cos(Omega*t)+Sigma_lambda*sin(Omega*t) # Symbol(r'\hat{n} \cdot \vec{\Sigma}')
```

```python
SimplifyVectorDerivatives = {diff(nHat,t):Omega*lambdaHat, diff(lambdaHat,t):-Omega*nHat}
DropvDerivatives = {diff(v,t,vDerivs)**vDerivPower:0
                    for vDerivs in range(1,3)
                    for vDerivPower in range(1,5)
                    if vDerivs+vDerivPower>=2} # dv/dt \approx v^9 / 20
DropvDerivatives.update({diff(gamma,t,gammaDerivs)**gammaDerivPower:0
                         for gammaDerivs in range(1,3)
                         for gammaDerivPower in range(1,5)
                         if gammaDerivs+gammaDerivPower>=2})
DropvDerivatives.update({diff(r,t,rDerivs)**rDerivPower:0
                         for rDerivs in range(1,3)
                         for rDerivPower in range(1,5)
                         if rDerivs+rDerivPower>=2})
DropvDerivatives.update({'nHatdotv':0}) # n dot v \approx - v^7 / 10
ApproxvDerivatives = {diff(v,t): v**9/20,
                      diff(gamma,t): v**10/10,
                      diff(r,t): -v**6/10,
                      }
SpinComponents = {'SVecdotv':v0*S_lambda, 'SigmaVecdotv':v0*Sigma_lambda,
                  'nHatdotSVec':S_n, 'nHatdotSigmaVec':Sigma_n, }
# powers.get(v,0) + 2*powers.get(x,0) + 3*powers.get(Omega,0) + 2*powers.get(gamma,0) - 2*powers.get(r,0)
```

```python
def Subscript(vec, indices):
    #print("Subscript({0}, {1})".format(vec, indices))
    try:
        indices_str = indices.name
    except AttributeError:
        indices_str = indices
    #print("About to construct TensorProduct([{0},]*{1}, coefficient=1, symmetric=True)".format(vec, len(indices_str)))
    return TensorProduct([vec,]*len(indices_str), coefficient=1, symmetric=True)
```

```python
Math={}
Sym={}
ns = locals()
```

```python
def CollectivePower(expr):
    """Given some product, return 2 * total PN order

    This treats v as 1, x**2 as 2, r as -2, etc., not accounting
    for any contribution to PN order from spins
    """
    powers = expr.subs({v.subs(t,0):v,r.subs(t,0):r,}).as_powers_dict()
    return powers.get(v,0) + 2*powers.get(x,0) + 3*powers.get(Omega,0) + 2*powers.get(gamma,0) - 2*powers.get(r,0) \
        + 9*powers.get(diff(v,t,1), 0) + 17*powers.get(diff(v,t,2), 0) + 25*powers.get(diff(v,t,3), 0) \
        + 10*powers.get(diff(gamma,t,1), 0) + 18*powers.get(diff(gamma,t,2), 0) + 26*powers.get(diff(gamma,t,3), 0) \
        + 6*powers.get(diff(r,t,1), 0) + 14*powers.get(diff(r,t,2), 0) + 22*powers.get(diff(r,t,3), 0)

def DropHigherRelativePowers(expression, relative_order=3):
    try:
        expression = sympy.expand(expression)
        if(expression.func==sympy.Add):
            lowest_order = min( [ CollectivePower(arg) for arg in expression.args ] )
            expression = sum( [ arg for arg in expression.args if CollectivePower(arg)<=relative_order+lowest_order ] )
            try:
                return expression.simplify()
            except:
                if(expression==0.0) :
                    return sympy.sympify(0)
                return expression
        else:
            return expression
    except:
        return expression

def DropHigherAbsolutePowers(expression, absolute_order=3):
    try:
        expression = sympy.expand(expression)
        if(expression.func==sympy.Add):
            expression = sum( [ arg for arg in expression.args if CollectivePower(arg)<=absolute_order ] )
            try:
                return expression.simplify()
            except:
                if(expression==0.0) :
                    return sympy.sympify(0)
                return expression
        else:
            if(CollectivePower(expression)<=absolute_order) :
                return expression
            else:
                return sympy.sympify(0)
    except:
        return expression

def DropHigherAbsolutePowersOfTensorProduct(tp, absolute_order=3):
    return simpletensors.TensorProduct([c for c in tp],
                                       coefficient=DropHigherAbsolutePowers(tp.coefficient, absolute_order), symmetric=tp.symmetric)

def DropHigherRelativePowersOfTensorProduct(tp, relative_order=3):
    return simpletensors.TensorProduct([c for c in tp],
                                       coefficient=DropHigherRelativePowers(tp.coefficient, relative_order), symmetric=tp.symmetric)

def DropHigherAbsolutePowersOfTensor(t, absolute_order=3):
    return sum( [DropHigherAbsolutePowersOfTensorProduct(tp, absolute_order) for tp in t ] )

def DropHigherRelativePowersOfTensor(t, relative_order=3):
    lowest_order = sys.maxint
    for term in t:
        expression = sympy.expand(term.coefficient)
        try:
            if(expression.func==sympy.Add):
                for arg in expression.args:
                    lowest_order = min(lowest_order, CollectivePower(arg))
            else:
                lowest_order = min(lowest_order, CollectivePower(expression))
        except:
            pass
    return sum( [DropHigherAbsolutePowersOfTensorProduct(tp, relative_order+lowest_order) for tp in t ] )

```

```python
%%time

MathKernel='/Applications/Local/Mathematica.app/Contents/MacOS/MathKernel'

# This little `math` script does the conversion we need.
# It has to output to a temporary file because I know of no
# better way to get the correct format out of Mathematica.
# This whole thing
MathCommand = r"""
    \[Nu] = nu;
    \[Mu] = mu;
    \[CapitalDelta] = delta;
    \[Delta] = delta;
    \[Lambda] = lambda;
    \[CapitalSigma] = Sigma;
    \[Gamma] = gamma;
    \[Epsilon] = epsilon;
    \[Gamma]\[Nu] = gamma*nu;
    \[ScriptL] = ell;
    (*Print[InputForm[ToExpression["{0}", TeXForm]]];*)
    ToExpression["{0}", TeXForm] >> /tmp/Math.cpp;
    Exit[];
"""

# # I believe all the results from this group:
# SrcDir = "source/BlanchetLivRev/"
# MultipoleNames = ["I_ij", "I_ij_A", "I_ij_B", "I_ij_C", "J_ij", "I_ijk", "J_ijk", "I_ijkl", "J_ijkl"]

# # I believe all the results from this group:
# SrcDir = "source/BoheEtAl/eqs/"
# MultipoleNames = ["JijCMS", "IijklCMS", "JijklCMS","JijkCMS",]

# # I mostly believe the results from this group, but
# # I don't have the patience to check them all:
# SrcDir = "source/BoheEtAl/eqs/"
# MultipoleNames = ["IijCMS", "IijkCMS", "JijklCMS",]

SrcDir = "source/BoheEtAl/eqs/"
MultipoleNames = ["IijklCMS", "JijklCMS","IijkCMS", "JijkCMS", "IijCMS", "JijCMS",]

multiplication = r'\,'

def TensorCreator(coefficient, vector):
    "Used in the regex wars (below)"
    def func(match):
        indices = list(match.group(1))
        if not coefficient:
            return vector + '_{%s}'%match.group(1)
        return vector + '_{%s}'%match.group(1) + multiplication + coefficient + '^{%s}'%len(indices)
    return func

prereplacements = [
    [r'\\\\',r''], # Line breask are meaningless
    [r'&',r''], # Alignments are meaningless
    [r'\\nonumber',r''], # I wasn't gonna anyway
    [r'~',r''], # Explicit spacing is useless
    [r'\\q*uad',r''], # Explicit spacing is useless
    [r'.*?= *',r''],
    [r' *\+ *\\mathcal{O}.*',r''],
    [r'\(n,\\*(Sigma|S) *,v\)',
     r'v\,\mathrm{ndot\1dotlambdaHat}'], # Catch Bohé et al.'s use of (n,S,v), (n,Sigma,v), for the triple product
    [r'\(\\*(n|v|S|Sigma) *\\*(n|v|S|Sigma) *\)',
     r'\mathrm{\1dot\2}'], # Catch Bohé et al.'s use of (nv), etc., for the dot product
    #[r'\\,', multiplication], # This gets us a good chunk of the way, if we're lucky
    [r'(\\langle|\\rangle|<|>)',r''], # The YlmTensors are already STF
    [r'\\epsilon_\{ab *i\}(.*?)(\{*.\}*)(_.*?)a(.*?)(\{*.\}*)(_.*?)b',
     r'r\,v\,\ell_i\, \1\2\3 \4\5\6 '], # Do the cross product
    [r'\{*x\}*_(\{\}| )', ''], # Remove stubs left from above
    [r'\{*v\}*_(\{\}| )', ''], # Remove stubs left from above
    [r'\\left(\(|\[|\\{)',r'('],
    [r'\\right(\)|\]|\\})',r')'],
    [r'\\(B|b)ig+(\(|\[|\\{)',r'('],
    [r'\\(B|b)ig+(\)|\]|\\})',r')'],
    [r'\\left\.',r''], # Do this BEFORE replacing \left
    [r'\\right\.',r''], # Do this BEFORE replacing \right
    [r'\\left',r''],
    [r'\\right',r''],
    [r'\(\\mathbf{\\*(.*?)}\\times\\mathbf{\\*(.*?)}\)',
     r'\mathrm{\1times\2}'],
    [r'\^\{([ijkl])\}',r'_{\1}'],
    [r'n_{ *(.*?) *}',TensorCreator('', 'n')],
    [r'x_{ *(.*?) *}',TensorCreator('r', 'n')],
    [r'\{x\}_{ *(.*?) *}',TensorCreator('r', 'n')],
    [r'v_{ *(.*?) *}',TensorCreator('v', r'\lambda')],
    [r'\{v\}_{ *(.*?) *}',TensorCreator('v', r'\lambda')],
    [r'  *',r' '], # Tighten up spacing
    [r'\(',r'\,('], # Doesn't hurt to assume multiplication
    [r'([^\\]),',r'\1'], # Remove comma at the end of an equation
    [r'([^\\])\.',r'\1'], # Remove period at the end of an equation
    [r'\\ln *\\*,*',r'\log'],
    ['\\\\',r'\\\\'], # Escape backslashes for Mathematica
    [r"'",r"Prm"],
    #[r"'",r"'\''"], # Pull the ugly command-line single-quote hack AFTER escaping backslashes
]

postreplacements = [
    [r'\^',r'**'],
    [r'lambda(?!Hat)',r'lambdaHat'],
    [r'\[',r'('],
    [r'\]',r')'],
    [r'Sigma',r'SigmaVec'],
    [r'S(?!igma|ubscript)',r'SVec'],
    [r'n(?!u)',r'nHat'],
    [r'ell',r'ellHat'],
    [r'Subscript\(((SigmaVec|SVec|nHat)times)v', r'v*Subscript(\1lambdaHat'],
    #[r'Subscript',r'TensorProductCreator'],
]

for MultipoleName in MultipoleNames:
    print("\n"+MultipoleName)
    with open(SrcDir+MultipoleName+".tex", "r") as myfile:
        data=myfile.read().replace('\n', ' ')
    #print("Raw content of {0}:\n".format(MultipoleName)); print(data)
    for replacement in prereplacements:
        data = re.sub(replacement[0], replacement[1], data)
    #print("\nbecomes\n"); print("""'{0}'""".format(data))
    #! echo '{MathCommand.format(data)}'
    #continue # Skip Mathematica stuff for now
    ! {MathKernel} -run '{MathCommand.format(data)}' >& /dev/null
    MathTmp = !cat /tmp/Math.cpp
    Math[MultipoleName] = ''.join(MathTmp)
    for replacement in postreplacements:
        Math[MultipoleName] = re.sub(replacement[0], replacement[1], Math[MultipoleName])
    #print("\nbecomes\n"); print(Math[MultipoleName]); print("\nbecomes\n")
    Sym[MultipoleName] = sympify(Math[MultipoleName], ns)
    #display(Sym[MultipoleName])
    with open('results/raw/'+MultipoleName+'.tex', 'w') as the_file:
        the_file.write(latex(Sym[MultipoleName]))
    #print("  reduces to")
    Sym[MultipoleName] = ReduceExpr(Sym[MultipoleName]).compress()
    #display(Sym[MultipoleName])
    with open('results/'+MultipoleName+'.tex', 'w') as the_file:
        the_file.write(latex(Sym[MultipoleName]))
    with open('results/'+MultipoleName+'.py', 'w') as the_file:
       the_file.write(repr(Sym[MultipoleName]))
    #print("\n\n")
```

    IijklCMS

    JijklCMS

    IijkCMS

    JijkCMS

    IijCMS

    JijCMS
    CPU times: user 7min 15s, sys: 3.02 s, total: 7min 18s
    Wall time: 8min 36s

```python
%%time
AsymmetricModeTerms = {}
for I_L,J_L in [["IijklCMS", "JijklCMS"], ["IijkCMS", "JijkCMS"], ["IijCMS", "JijCMS"]]:
    U_L,V_L = [Sym[I_L],Sym[J_L]]
    ell = U_L.rank
    print('ell={0}'.format(ell)); sys.stdout.flush()
    AsymmetricModeTerms[ell] = {}
    for m in range(0,ell+1):
        print('(ell,m)=({0},{1})'.format(ell,m)); sys.stdout.flush()
        if((ell+m)%2==0) :
            tmp = sympy.simplify(sympy.I*sphericalharmonictensors.Vlm(V_L, m)/sympy.sqrt(2))
        else :
            tmp = sympy.simplify(-sphericalharmonictensors.Ulm(U_L, m)/sympy.sqrt(2))
        for i_order in range(ell):
            tmp = diff(tmp, t)
            tmp = tmp.xreplace(SimplifyVectorDerivatives).subs(dict(DropvDerivatives.items()+ApproxvDerivatives.items())).subs(SpinComponents)
            tmp = DropHigherRelativePowers(tmp, 3)
        AsymmetricModeTerms[ell][m] = sympy.simplify(sympy.simplify(tmp).doit().subs(t,0))
        display(AsymmetricModeTerms[ell][m])
        with open('results/hAsymmetric_{0}_{1}.tex'.format(ell,m), 'w') as the_file:
            the_file.write(latex(AsymmetricModeTerms[ell][m]))
        with open('results/hAsymmetric_{0}_{1}.py'.format(ell,m), 'w') as the_file:
            the_file.write(repr(AsymmetricModeTerms[ell][m]))
        with open('results/hAsymmetric_{0}_{1}.c'.format(ell,m), 'w') as the_file:
            the_file.write(ccode(N(AsymmetricModeTerms[ell][m])))
```

```python
-272*S_lambda*delta + 250*i*S_n*delta + 319*Sigma_lambda*nu - 125*Sigma_lambda -239*i*Sigma_n*nu + 19*i*Sigma_n - 293*i*Sigma_n*nu
```

```python
LeadingOrder22 = (2*nu*v.subs(t,0)**2)*sqrt(16*pi/5)
( DropHigherRelativePowers(AsymmetricModeTerms[2][2],0) ) / ( LeadingOrder22 )
```

```python
LeadingOrder44 = (2*nu*v.subs(t,0)**2)*sqrt(16*pi/5)*v.subs(t,0)**2*8*sqrt(frac(5,7))*(-1 + 3*nu)/9
( DropHigherRelativePowers(AsymmetricModeTerms[4][4],0) ) / ( LeadingOrder44 )
```

$$
-\frac{81}{32} \frac{v^2} {1-3\nu} \left[ (S_\lambda + i\, S_n)\delta + (1-3\nu)(\Sigma_\lambda + i\, \Sigma_n) \right]
$$

```python

```

```python

```


# Feature request for NBVAL
For mathematical computation, it would be useful to compare $\LaTeX$ output. Here is an example:

```python
import sympy

sympy.init_printing()

x, y = sympy.symbols(["x", "y"])
```

Should pass:

```python
x**2
```

Should fail (in most cases):

```python
import random
n = int(random.uniform(1, 10000000000000))
```

```python
x**n
```


# Partial Differential Equation and Gaussian Process

# 1 The Spatial Temporal Model

A model of post-transcriptional processing is formulated to describe the spatio-temporal Drosophila
protein expression data(Becker 2012). Protein production is considered to be linearly dependent on the
concentration of mRNA at an earlier time point. The model also allows for diffusion of protein between
nuclei and linear protein decay. These processes are dependent on the diffusion parameter and the degradation
rate of protein respectively.

\begin{equation}
 a \frac{\partial ^2 y_{x,t}}{\partial x^2} + b \frac{\partial y_{x,t}}{\partial t} + c y_{x,t}=  f_{x,t}
\end{equation}

The coefficients a, b and c are unknown. In this study, we use Gaussian process with RBF kernel as a prior over $y_{x,t}$. The multi-output Gaussian process are developed by applying the partial differential operator on the spatial-temporal RBF kernel.

```
# Copyright (c) 2012, GPy authors (see AUTHORS.txt).
# Licensed under the BSD 3-clause license (see LICENSE.txt)
# model : a*d^2y/dx^2  + b * dy/dt + c * y = f
#  lengthscale for Yt :
#  lengthscale for Yx :
# variance for Yt  :
# variance for Yx  :

%pylab inline
%config InlineBackend.figure_format = 'svg'
import numpy as np
import pylab as pb
import GPy
```

    Populating the interactive namespace from numpy and matplotlib

The spatiol-temporal multi-output partial differential equation kernel are developed with kernel name "GPy.kern.ODE_st" in GPy. The inputs are one dimension spatial data, one dimension temporal data and one dimension index which is used to indicate f and Y.

```
KY = GPy.kern.ODE_st(input_dim=3,a=1.,b=1,c=1,variance_Yx=1.,variance_Yt=1.,
    lengthscale_Yx=1.,lengthscale_Yt=1.)

nd =10
t1 = 10*np.random.rand(nd)[:,None]
x1 = 10*np.random.rand(nd)[:,None]
inx = np.zeros(nd**2)[:,None]

T = np.kron(t1,np.ones(nd)[:,None])
S = np.kron(np.ones(nd)[:,None],x1)

inx[nd**2/2:nd**2]=1
X = np.hstack([T,S,inx])

#Y=np.sin(X[:,0:1])
#Y=np.sin(X[:,0:1])
Y = np.sin(X[:,0:1]) + np.cos(X[:,1:2])#*(1-X[:,1:2]) #+ np.random.randn(16,1)*0.1

Y[nd**2/2:nd**2] =  np.sin(X[nd**2/2:nd**2,0:1]) + np.cos(X[nd**2/2:nd**2,0:1]) + 2*np.cos(X[nd**2/2:nd**2,1:2])
```

```
m = GPy.models.GPRegression(X,Y,KY)
```

# 2 Model Output

In order to test the kernel, we defined

$y_{x,t} = cos(x) + sin(t)$

$f_{x,t} = sin(t) + cos(t) + 2cos(x)$

and a, b and c are equal to 1. With some arbitray choices of kernel parameters we plot the random field of f and y separatly.
The spatial variable can be fixed to certain value. It become easier to compare the true curve and our estimation.

```
fixX = 5.
xplot = np.linspace(-2,12,200)
m.plot(fixed_inputs=[(2,0)], which_data_rows = slice(0,nd**2/2))
m.plot(fixed_inputs=[(1,fixX),(2,0)], which_data_rows = slice(0,nd**2/2))
pb.plot(xplot,np.cos(fixX) + np.sin(xplot))

m.plot(fixed_inputs=[(2,1)], which_data_rows = slice(nd**2/2,nd**2))
m.plot(fixed_inputs=[(1,fixX),(2,1)], which_data_rows = slice(nd**2/2,nd**2))
pb.plot(xplot,np.cos(xplot) + np.sin(xplot) + 2*np.cos(fixX))
```

    [<matplotlib.lines.Line2D at 0x48d6f10>]

```
m.optimize()
```

    Warning: adding jitter of 2.3116375706e+39
    Warning: adding jitter of 8.3803679219e+35
    Warning: adding jitter of 3.4977999999e-03
    Warning: adding jitter of 1.1815217911e+32

```
print m
```

      GP_regression.           |        Value        |  Constraint  |  Prior  |  Tied to
      ode_st.a                 |      1.00220487586  |     +ve      |         |
      ode_st.b                 |      1.00058344853  |     +ve      |         |
      ode_st.c                 |       0.9977523428  |     +ve      |         |
      ode_st.variance_Yt       |      1.80961000578  |     +ve      |         |
      ode_st.variance_Yx       |      1.80961000578  |     +ve      |         |
      ode_st.lengthscale_Yt    |      7.30175140468  |     +ve      |         |
      ode_st.lengthscale_Yx    |      7.92072957612  |     +ve      |         |
      Gaussian_noise.variance  |  1.12525055876e-09  |     +ve      |         |

After optimization, the estimated value of a, b and c are printed. It is quite close to the true parameters we used to generate the testing data. The plot of the random fields are plotted below.

```
fixX = 5.
xplot = np.linspace(-2,12,200)
m.plot(fixed_inputs=[(2,0)], which_data_rows = slice(0,nd**2/2))
m.plot(fixed_inputs=[(1,fixX),(2,0)], which_data_rows = slice(0,nd**2/2))
pb.plot(xplot,np.cos(fixX) + np.sin(xplot))

m.plot(fixed_inputs=[(2,1)], which_data_rows = slice(nd**2/2,nd**2))
m.plot(fixed_inputs=[(1,fixX),(2,1)], which_data_rows = slice(nd**2/2,nd**2))
pb.plot(xplot,np.cos(xplot) + np.sin(xplot) + 2*np.cos(fixX))
```

    /home/mu/GPy/GPy/likelihoods/gaussian.py:96: RuntimeWarning: invalid value encountered in sqrt
      return  [stats.norm.ppf(q/100.)*np.sqrt(var) + mu for q in quantiles]

    [<matplotlib.lines.Line2D at 0x471aa50>]

The true random field is plotted below

```
xplot = np.linspace(-1,10,200)
#pb.plot(xplot,np.sin(xplot))
PP = np.zeros((xplot.shape[0], xplot.shape[0]))
QQ = np.zeros((xplot.shape[0], xplot.shape[0]))

for i in range(0,200):
    for j in range(0,200):
        PP[i,j] = np.sin(xplot[i]) + np.cos(xplot[j])
        QQ[i,j] = np.sin(xplot[i]) + np.cos(xplot[i]) + 2*np.cos(xplot[j])

pb.figure()
pb.imshow(PP)
```

    <matplotlib.image.AxesImage at 0x4ea5550>

```

```


# Landau-Zener transition

```python
import numpy as np
import qutip as qt
import matplotlib.pyplot as plt
%matplotlib inline
```

## Hamiltonian

$$
\begin{equation}
H = 4E_{c}\delta N_{G}\hat{\sigma}_{z}-\frac{E_{J}}{2}\hat{\sigma}_{x}
\end{equation}
$$

but recall that we are centered at $\delta N_{G}=0$, so

$$
\begin{equation}
H = -\frac{E_{J}}{2}\hat{\sigma}_{x}
\end{equation}
$$

Time-dependent variation of $\delta N_{G}$ via Hamiltonian term

$$
\begin{equation}
H = \frac{At}{2}\hat{\sigma}_{z}
\end{equation}
$$

```python
Ej = 0.5 * 2 * np.pi   # qubit sigma_x coefficient
A  = 2.0 * 2 * np.pi   # sweep rate

# Hamiltonian
sx = qt.sigmax()
sy = qt.sigmay()
sz = qt.sigmaz()
sm = qt.sigmam()

H0 = - Ej/2.0 * sx
H1 = A/2.0 * sz
H = [H0, [H1, 't']]

psi0 = qt.basis(2,1) # initial state
tlist = np.linspace(-20.0, 20.0, 5000)
```

```python
# Solve
output = qt.mesolve(H, psi0, tlist, [], [sm.dag() * sm],
                   progress_bar=True)

p_ex = output.expect[0]
```

    10.0%. Run time:   0.38s. Est. time left: 00:00:00:03
    20.0%. Run time:   0.64s. Est. time left: 00:00:00:02
    30.0%. Run time:   0.86s. Est. time left: 00:00:00:02
    40.0%. Run time:   1.07s. Est. time left: 00:00:00:01
    50.0%. Run time:   1.25s. Est. time left: 00:00:00:01
    60.0%. Run time:   1.44s. Est. time left: 00:00:00:00
    70.0%. Run time:   1.67s. Est. time left: 00:00:00:00
    80.0%. Run time:   1.89s. Est. time left: 00:00:00:00
    90.0%. Run time:   2.15s. Est. time left: 00:00:00:00
    Total run time:   2.38s

```python
fig = plt.figure(figsize=(8,6))
plt.plot(tlist, p_ex, tlist, 1-p_ex,lw=2)
plt.plot(tlist, 1 - np.exp(-np.pi * Ej **2 / (2 * A)) * np.ones_like(tlist), 'k--',lw=2)
plt.xlabel('Time',fontsize=16)
plt.ylabel('Occupation probability',fontsize=16)
plt.title('Landau-Zener transition')
plt.legend(("Excited state", "Ground state", "Landau-Zener formula"), loc=0,fontsize=12);
```

## Plot dynamics on Bloch sphere

```python
# Solve
output = qt.mesolve(H, psi0, tlist, [], [sx,sy,sz],
                   progress_bar=True)
```

    10.0%. Run time:   0.41s. Est. time left: 00:00:00:03
    20.0%. Run time:   0.94s. Est. time left: 00:00:00:03
    30.0%. Run time:   1.21s. Est. time left: 00:00:00:02
    40.0%. Run time:   1.44s. Est. time left: 00:00:00:02
    50.0%. Run time:   1.70s. Est. time left: 00:00:00:01
    60.0%. Run time:   1.91s. Est. time left: 00:00:00:01
    70.0%. Run time:   2.16s. Est. time left: 00:00:00:00
    80.0%. Run time:   2.42s. Est. time left: 00:00:00:00
    90.0%. Run time:   2.66s. Est. time left: 00:00:00:00
    Total run time:   2.94s

```python
import matplotlib as mpl
nrm=mpl.colors.Normalize(-1,20)
colors=mpl.cm.magma(nrm(tlist))
```

```python
B = qt.Bloch()
B.add_points(output.expect,'m')
B.point_color=list(colors)
B.show()
```

```python

```

```python

```


```python
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Homework 3

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

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

## Exercise: Income and Growth Across Countries

The data file `cross_country_gdp_pc.csv` contains annual data on GDP per capita for over 100 countries and is available here: https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/cross_country_gdp_pc.csv. The data are was constructed from the Penn World Table (https://www.rug.nl/ggdc/productivity/pwt/).

In this exercise, you will:

1. Analyze some basic facts about GDP per capita across the world.
2. Compute the average annual growth rate of GDP per capita for each country in the data.
3. Compute a linear regression of average GDP per capita growth on GDP per capita in the initial year.

Follow the instructions in the following cells.

### Part (a)

```python
# Create a vhttp://localhost:8889/notebooks/Homework%20Notebooks/Econ126_Winter2021_Homework_03.ipynb#Part-(a)ariable called 'data' that stores the data in the file 'cross_country_gdp_pc.csv' in a DataFrame
# Make sure that 'Year', the left-most column of the data, is set to be the index column
data = pd.read_csv('https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/cross_country_gdp_pc.csv',index_col=0,parse_dates = True)

# Print the first 5 rows of data
data.head()
```

      Argentina - ARG
      Australia - AUS
      Austria - AUT
      Burundi - BDI
      Belgium - BEL
      Benin - BEN
      Burkina Faso - BFA
      Bangladesh - BGD
      Bolivia (Plurinational State of) - BOL
      Brazil - BRA
      ...
      Turkey - TUR
      Taiwan - TWN
      U.R. of Tanzania: Mainland - TZA
      Uganda - UGA
      Uruguay - URY
      United States - USA
      Venezuela (Bolivarian Republic of) - VEN
      South Africa - ZAF
      Zambia - ZMB
      Zimbabwe - ZWE

      Year

      1960-01-01
      2851.49
      14223.37
      8984.20
      676.57
      9100.09
      1616.84
      724.34
      1429.30
      1771.04
      2114.20
      ...
      4691.59
      2376.92
      1173.90
      789.13
      6731.78
      17462.63
      6749.85
      5913.81
      1665.98
      1990.92

      1961-01-01
      2897.14
      14303.40
      9427.85
      570.27
      9550.57
      1601.02
      681.06
      1391.08
      1785.90
      2338.23
      ...
      4649.28
      2442.46
      1187.66
      782.75
      6739.92
      17616.14
      6721.12
      6008.94
      1670.34
      2040.24

      1962-01-01
      2903.85
      14820.87
      9630.59
      607.52
      10008.56
      1522.49
      724.86
      1445.78
      1816.33
      2442.88
      ...
      4820.57
      2550.53
      1267.04
      778.84
      6725.06
      18395.60
      6972.68
      6119.67
      1581.47
      1960.41

      1963-01-01
      2758.01
      15573.81
      9960.43
      623.15
      10322.03
      1572.27
      698.74
      1505.08
      1889.88
      2533.68
      ...
      5163.09
      2725.46
      1330.11
      774.15
      6514.72
      18943.34
      7222.31
      6466.65
      1614.93
      1944.13

      1964-01-01
      2853.73
      16042.42
      10375.80
      611.82
      10947.25
      1622.50
      718.58
      1466.38
      1961.70
      2572.07
      ...
      5211.92
      2947.73
      1492.33
      816.41
      6683.54
      19808.61
      7757.37
      6738.99
      1792.59
      1836.15

5 rows × 111 columns

```python
# Each column contains data for a specific country. Print the number of countries (e.g., the length of data.columns)
print("Number of countries: ",len(data.columns))
```

    Number of countries:  111

```python
# Each column contains data for a specific year. Print the number of years (e.g., the length of data.index)
print("Number of years of observations: ",len(data.index))
```

    Number of years of observations:  58

```python
# Print the values of the initial year and the final year in the data
print("Initial year: ",data.index[0])
print("Final year:   ",data.index[-1])
```

    Initial year:  1960-01-01 00:00:00
    Final year:    2017-01-01 00:00:00

```python
# Create a variable called 'data_initial' that is equal to the first row of the data DataFrame
data_initial = data.iloc[0]

# Create a variable called 'data_final' that is equal to the last row of the data DataFrame
data_final = data.iloc[-1]
```

```python
# Use the sort_values() method of data_initial to sort the Series in ascending order.
data_initial=data_initial.sort_values()

# Print the name and GDP per capita of the countries with the 10 lowest GDPs per capita in the initial year of the data
print('Countries with lowest GDP per capita in '+str(data.index[0])+':\n')
print(data_initial.head(10))
```

    Countries with lowest GDP per capita in 1960-01-01 00:00:00:

    Botswana - BWA         404.68
    Mozambique - MOZ       493.70
    Ethiopia - ETH         522.52
    Burundi - BDI          676.57
    Egypt - EGY            685.77
    Nepal - NPL            701.33
    Mali - MLI             703.97
    Malta - MLT            718.99
    Burkina Faso - BFA     724.34
    Guinea-Bissau - GNB    763.46
    Name: 1960-01-01 00:00:00, dtype: float64

```python
# Use the sort_values() method of data_final to sort the Series in ascending order.
data_final=data_final.sort_values()

# Print the name and GDP per capita of the countries with the 10 lowest GDPs per capita in the final year of the data
print('Countries with lowest GDP per capita in '+str(data.index[-1])+':\n')
print(data_final.head(10))
```

    Countries with lowest GDP per capita in 2017-01-01 00:00:00:

    Central African Republic - CAF     727.29
    D.R. of the Congo - COD            760.47
    Burundi - BDI                      799.61
    Niger - NER                        908.65
    Malawi - MWI                      1038.32
    Chad - TCD                        1307.97
    Mozambique - MOZ                  1376.55
    Togo - TGO                        1566.15
    Ethiopia - ETH                    1591.49
    Madagascar - MDG                  1591.55
    Name: 2017-01-01 00:00:00, dtype: float64

```python
# Print the names of the countries that are among the 10 with lowest GDPs per capita in BOTH the initial year and
# the final year.
print('Countries among lowest 10 GDPs per capita in '+str(data.index[0])+' and '+str(data.index[-1])+':\n')
for country in data_initial.head(10).index:
    if country in data_final.head(10).index:
        print(country.split("-")[0])
```

    Countries among lowest 10 GDPs per capita in 1960-01-01 00:00:00 and 2017-01-01 00:00:00:

    Mozambique
    Ethiopia
    Burundi

```python
# Print the name and GDP per capita of the countries with the 10 highest GDPs per capita in the initial year of the data
print('Countries with highest GDP per capita in both '+str(data.index[0])+':\n')
print(data_initial.tail(10))
```

    Countries with highest GDP per capita in both 1960-01-01 00:00:00:

    Netherlands - NLD       11461.35
    Sweden - SWE            11766.14
    United Kingdom - GBR    11909.77
    Denmark - DNK           12026.71
    New Zealand - NZL       12835.86
    Canada - CAN            13360.91
    Australia - AUS         14223.37
    Luxembourg - LUX        15757.45
    United States - USA     17462.63
    Switzerland - CHE       20086.09
    Name: 1960-01-01 00:00:00, dtype: float64

```python
# Print the name and GDP per capita of the countries with the 10 highest GDPs per capita in the final year of the data
print('Countries with highest GDP per capita in '+str(data.index[-1])+':\n')
print(data_final.tail(10))
```

    Countries with highest GDP per capita in 2017-01-01 00:00:00:

    Austria - AUT          46103.27
    Netherlands - NLD      46601.56
    Australia - AUS        47392.62
    Germany - DEU          47556.00
    United States - USA    54794.76
    Norway - NOR           60590.25
    Switzerland - CHE      64219.30
    Singapore - SGP        67138.11
    Luxembourg - LUX       74605.23
    Ireland - IRL          75916.08
    Name: 2017-01-01 00:00:00, dtype: float64

```python
# Print the names of the countries that are among the 10 with highest GDPs per capita in BOTH the initial year
# and the final year
print('Countries among highest 10 GDPs per capita in both '+str(data.index[0])+' and '+str(data.index[-1])+':\n')
for country in data_initial.tail(10).index:
    if country in data_final.tail(10).index:
        print(country.split("-")[0])
```

    Countries among highest 10 GDPs per capita in both 1960-01-01 00:00:00 and 2017-01-01 00:00:00:

    Netherlands
    Australia
    Luxembourg
    United States
    Switzerland

### Part (b)

Let $y_t$ denotes GDP per capita for some country in some year $t$ and let $g$ denotes the average annual growth in GDP per capita between years 0 and $T$. $g$ is defined by:
\begin{align}
y_T & = (1+g)^T y_0
\end{align}
which implies:
\begin{align}
g & = \left(\frac{y_T}{y_0}\right)^{1/T} - 1
\end{align}
We can use this equation to compute the aveage growth rate of GDP per capita for each country in our data. Note that for our data, $T$ is equal to `len(data.index)-1`.

```python
# Create a variable called 'growth_rates' that contains the average growth rate of each country in the data.
# NOTE: You do not need to re-sort the variables data_initial and data_final. If their indices don't align,
# Pandas will automatically sort their indices alphabetically.
T = len(data.index)-1
growth_rates = (data_final/data_initial)**(1/T)-1

# Use the sort_values() method of growth_rates to sort the Series in ascending order.
growth_rates=growth_rates.sort_values()
```

```python
# Print the average growth rate of GDP per capita for all countries rounded to 4 decimal places
print('Average growth rate of GDP per capita for all countries: ',round(growth_rates.mean(),4))
```

    Average growth rate of GDP per capita for all countries:  0.0224

```python
# Print the standard deviation of the growth rate of GDP per capita for all countries rounded to 4 decimal places
print('Standard deviation of the growth rate of GDP per capita for all countries: ',round(growth_rates.std(),4))
```

    Standard deviation of the growth rate of GDP per capita for all countries:  0.0159

```python
# Print the name and growth rates of the countries with the 10 lowest growth rates of GDP per capita
print('Countries with lowest growth rates of GDP per capita:\n')
print(growth_rates.head(10))
```

    Countries with lowest growth rates of GDP per capita:

    D.R. of the Congo - COD          -0.022039
    Central African Republic - CAF   -0.011405
    Niger - NER                      -0.007151
    Guinea - GIN                     -0.004706
    Zimbabwe - ZWE                   -0.001082
    Chad - TCD                       -0.000086
    Gambia - GMB                      0.000916
    Nigeria - NGA                     0.001056
    Madagascar - MDG                  0.001781
    Barbados - BRB                    0.001924
    dtype: float64

```python
# Print the name and growth rates of the countries with the 10 highest growth rates of GDP per capita
print('Countries with highest growth rates of GDP per capita:\n')
print(growth_rates.tail(10))
```

    Countries with highest growth rates of GDP per capita:

    China - CHN                0.045830
    Thailand - THA             0.048224
    Equatorial Guinea - GNQ    0.048863
    Egypt - EGY                0.050227
    Romania - ROU              0.051858
    Taiwan - TWN               0.052199
    Singapore - SGP            0.058379
    Republic of Korea - KOR    0.063021
    Botswana - BWA             0.065429
    Malta - MLT                0.068160
    dtype: float64

```python
# Print the names of the countries that were BOTH among the 10 with lowest GDPs per capita in the initial year and
# among the 10 with the highest growth of GDP per capita.
print('Countries among highest 10 GDPs per capita in both '+str(data.index[0])+' and '+str(data.index[-1])+':\n')
for country in growth_rates.tail(10).index:
    if country in data_initial.head(10).index:
        print(country.split("-")[0])
```

    Countries among highest 10 GDPs per capita in both 1960-01-01 00:00:00 and 2017-01-01 00:00:00:

    Egypt
    Botswana
    Malta

```python
# Use the sort_index() method of data_initial to alphabetize the index [e.g., data_initial = data_initial.sort_index()]
data_initial = data_initial.sort_index()

# Use the sort_index() method of growth_rates to alphabetize the index
growth_rates = growth_rates.sort_index()

# Construct a scatter plot with:
#     1. GDP per capita in the initial year on the horizontal axis
#     2. Average growth rate of GDP per capita on the vertical axis
#     3. Size of scatter plot markers at least 50
#     4. Opacity of scatter plot markers no greater than 0.5
#     5. x-axis limit: [0,20000]
#     6. Suitable title and labels for the axes

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.scatter(data_initial,growth_rates,s=100,alpha=0.2)
ax.set_title('Income versus Growth for '+str(len(data.columns))+' Countries')
ax.set_xlabel('GDP per capita in '+str(data.index[0])+'\n(Thousands of $)')
ax.set_ylabel('Growth rate of GDP per capita\nfrom '+str(data.index[0])+' to '+str(data.index[-1]))
ax.set_xlim([0,20000])
ax.grid()
```

**Question**

1. Describe in words the relationship between GDP per capita in the initial year and average growth of GDP per capita. How is the relationship different for low income countries and high income countries?

**Answer**

1. There is a right-pointing triangle-shaped relationship between GDP per capita in the initial year and average growth of GDP per capita. Countries with high GDP per capita in the initial year had average growth rates that were almost uniformly close to the world average of about 2 percent. Countries with lower GDP per capita in the initial year had much more variation across the average growth rates of their GDP per capita. The countries with the highest growth rates of GDP per capita were among the lowest income initially and the countries with the lowest growth rates of GDP per capita were also among the lowest income initially.

### Part (c)

In this part you will use OLS to estimate the relationship between GDP per capita in the initial year and average growth of GDP per capita.

\begin{align}
g_i & = \beta_0 + \beta_{\text{1}}y_i + \epsilon_i,
\end{align}

where $g_i$ is the average annual growth rate of country $i$, $y_i$ is country $i$'s initial GDP per capita, and $\epsilon_i$ is the residual of the regression.

```python
growth_rates = growth_rates.sort_index()
data_initial = data_initial.sort_index()

# Create a variable called 'y' that is equal to the average growth rates of the countries
y = growth_rates

# Create a variable called x' that is equal to the GDP per capita of the countries in the initial year
x = data_initial

# Use the Statsmodels function add_constant() to add a constant column to the variable x
x = sm.add_constant(data_initial)

# Create a variable called 'model' equal to the output of the Statsmodels OLS function regressing y upon x
model = sm.OLS(y,x)

# Create a variable called 'results' that stores the fitted model (i.e., the output of the fit() method of model)
results = model.fit()

# Print the summary of results generated by the summary() method of results
print(results.summary2())
```

                      Results: Ordinary least squares
    ===================================================================
    Model:              OLS              Adj. R-squared:     -0.009
    Dependent Variable: y                AIC:                -601.5469
    Date:               2021-02-09 13:27 BIC:                -596.1278
    No. Observations:   111              Log-Likelihood:     302.77
    Df Model:           1                F-statistic:        0.01452
    Df Residuals:       109              Prob (F-statistic): 0.904
    R-squared:          0.000            Scale:              0.00025479
    -------------------------------------------------------------------
                          Coef.  Std.Err.    t    P>|t|   [0.025 0.975]
    -------------------------------------------------------------------
    const                 0.0226   0.0022 10.3406 0.0000  0.0182 0.0269
    1960-01-01 00:00:00  -0.0000   0.0000 -0.1205 0.9043 -0.0000 0.0000
    -------------------------------------------------------------------
    Omnibus:                4.381        Durbin-Watson:           1.666
    Prob(Omnibus):          0.112        Jarque-Bera (JB):        3.995
    Skew:                   0.318        Prob(JB):                0.136
    Kurtosis:               3.678        Condition No.:           8515
    ===================================================================
    * The condition number is large (9e+03). This might indicate
    strong multicollinearity or other numerical problems.

**Questions**

1. Interpret the estimate for the coefficient on $y_i$.
2. Interpret the estimate for the constant. Where did we see this number earlier?
3. Interpret the R-squared of the regression.

**Answers**

1. The estimated coefficient on $y_i$ is both very close to zero and is not statistically different from zero.
2. The estimated constant is equal to the average growth rate of GDP per capita across all countries and we computed that in the second code cell of Part (b).
3. The R-squared implies that the linear regression model explains essentially zero percent of the variation in average GDP per capita growth across countries.

## Exercise: Income and Growth Across US States

The data file `state_income_data.csv` contains annual data on income per capita for the 48 continental states, Washington D.C., and the United States as a whole and is available here: https://raw.githubusercontent.com/letsgoexploring/economic-data/master/us-convergence/csv/state_income_data.csv.

In this exercise, you will:

1. Analyze some basic facts about income per capita among US states.
2. Compute the average annual growth rate of income per capita for each state in the data.
3. Compute a linear regression of average income per capita growth on income per capita in the initial year.

Follow the instructions in the following cells.

### Part (a)

```python
# Create a variable called 'state_df' that stores the data in the file 'cross_country_gdp_pc.csv' in a DataFrame
# Make sure that 'Year', the left-most column of the data, is set to be the index column
state_df= pd.read_csv('https://raw.githubusercontent.com/letsgoexploring/economic-data/master/us-convergence/csv/state_income_data.csv',index_col=0)

# Print the first 5 rows of data
print(state_df.head())
```

          United States           AL           AZ           AR            CA  \
    1840    1179.435484   961.693548          NaN  1233.870968           NaN
    1880    1915.322581  1129.032258  4778.225806  1250.000000   3911.290323
    1900    2460.483871  1350.000000  4354.838710  1371.774194   3941.129032
    1929    7417.230000  3384.970000  6345.500000  3215.200000  10547.540000
    1930    6839.960000  2896.790000  5705.470000  2467.230000   9791.830000

                   CO            CT            DE        DC           FL  ...  \
    1840          NaN   1651.209677   1233.870968       NaN  1252.016129  ...
    1880  4455.645161   2943.548387   2177.419355       NaN   967.741935  ...
    1900  3810.483871   3396.774194   2721.774194       NaN  1458.870968  ...
    1929  6695.670000  10918.930000  10940.150000  14049.24  5507.220000  ...
    1930  6322.280000  10210.380000   9450.380000  14472.96  5154.750000  ...

                   SD           TN           TX           UT           VT  \
    1840          NaN   852.822581          NaN          NaN  1179.435484
    1880  2116.935484  1048.387097  1209.677419  1633.064516  2096.774194
    1900  1513.306452  1328.225806  1829.032258  2482.258065  2569.354839
    1929  4403.650000  3957.980000  5029.710000  5814.940000  6685.060000
    1930  3932.150000  3535.630000  4482.870000  5441.130000  6300.250000

                   VA           WA           WV           WI           WY
    1840   979.838710          NaN   979.838710  1451.612903          NaN
    1880  1028.225806  2822.580645  1088.709677  1895.161290  3104.838710
    1900  1437.096774  3941.129032  1720.161290  2329.838710  5160.483871
    1929  4615.870000  7852.290000  4870.540000  7109.510000  7130.730000
    1930  4262.580000  7236.480000  4482.870000  6443.440000  6410.400000

    [5 rows x 50 columns]

The DataFrame method `dropna()` returns a DataFrame that omits rows that contain missing (NaN means *not a number*) values. The following command will remove the rows with missing values from the DataFrame `df`:

    df =  df.dropna()

The DataFrame method `drop()` returns a DataFrame that omits rows or columns with a given label. The following command will remove the column named `'LABEL'` from the DataFrame `df`:

    df = df.drop('LABEL',axis=1)

The argument `axis=1` means to look among the columns for `'LABEL'`. If we had wanted to remove a row with the index value `'LABEL'`, then the axis argument would have been: `axis=0`.

```python
# Remove the rows with missing data from state_df
state_df = state_df.dropna()

# Remove the column named 'United States' from state_df
state_df = state_df.drop('United States',axis=1)

# Set state_df equal to itself divided by 1000 so that its units are in thousands of dollars
state_df = state_df/1000

# Print the first 5 rows of data
print(state_df.head())
```

               AL       AZ       AR        CA       CO        CT        DE  \
    1929  3.38497  6.34550  3.21520  10.54754  6.69567  10.91893  10.94015
    1930  2.89679  5.70547  2.46723   9.79183  6.32228  10.21038   9.45038
    1931  2.70071  5.27866  2.54112   9.24380  5.79425   9.90670   9.52615
    1932  2.21048  4.47657  2.11317   8.11900  4.96316   8.71681   8.21632
    1933  2.34520  4.41870  2.18790   7.86501  5.04791   8.45131   8.09381

                DC       FL       GA  ...       SD       TN       TX       UT  \
    1929  14.04924  5.50722  3.63964  ...  4.40365  3.95798  5.02971  5.81494
    1930  14.47296  5.15475  3.33737  ...  3.93215  3.53563  4.48287  5.44113
    1931  15.38178  4.87356  3.11809  ...  2.97078  3.35134  4.23521  4.56666
    1932  15.38996  4.40706  2.73877  ...  2.62755  2.69707  3.64243  4.22633
    1933  13.51351  4.13270  2.88860  ...  1.94480  2.87430  3.63220  4.24710

               VT       VA       WA       WV       WI       WY
    1929  6.68506  4.61587  7.85229  4.87054  7.10951  7.13073
    1930  6.30025  4.26258  7.23648  4.48287  6.44344  6.41040
    1931  5.78198  4.55438  6.55536  4.34569  5.75743  5.85563
    1932  5.04657  3.97609  5.58877  3.57292  5.03267  5.21340
    1933  4.81910  4.08980  5.37681  3.70370  4.77620  5.31961

    [5 rows x 49 columns]

```python
# Create a variable called 'state_growth_rates' that contains the average growth rate of each state in the data.
T = len(state_df)-1
state_growth_rates = (state_df.iloc[-1]/state_df.iloc[0])**(1/T)-1
```

```python
# Construct a scatter plot with:
#     1. Income per capita in the initial year on the horizontal axis
#     2. Average growth rate of income per capita on the vertical axis
#     3. Size of scatter plot markers at least 50
#     4. Opacity of scatter plot markers no greater than 0.5
#     5. y-axis limit: [0.015,0.035]
#     6. Suitable title and labels for the axes

fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.scatter(state_df.iloc[0],state_growth_rates,s=100,alpha=0.2)
ax.set_title('Income versus Growth for '+str(len(state_df.columns))+' States incl. DC')
ax.set_xlabel('Income per capita in '+str(state_df.index[0])+'\n(Thousands of $)')
ax.set_ylabel('Growth rate of income per capita\nfrom '+str(state_df.index[0])+' to '+str(state_df.index[-1]))
ax.set_ylim([0.015,0.035])
ax.grid()
```

### Part (b)

Like you did for the country data above, use OLS to estimate the relationship between income per capita in the initial year and average growth of income per capita.

\begin{align}
g_i & = \beta_0 + \beta_{\text{1}}y_i + \epsilon_i,
\end{align}

where $g_i$ is the average annual growth rate of country $i$, $y_i$ is state $i$'s initial income per capita, and $\epsilon_i$ is the residual of the regression.

```python
# Create a variable called 'y' that is equal to the average growth rates of the states
y = state_growth_rates

# Create a variable called x' that is equal to the income per capita of the states in the initial year
x = state_df.iloc[0]

# Use the Statsmodels function add_constant() to add a constant column to the variable x
x = sm.add_constant(x)

# Create a variable called 'model' equal to the output of the Statsmodels OLS function regressing y upon x
model = sm.OLS(y,x)

# Create a variable called 'results' that stores the fitted model (i.e., the output of the fit() method of model)
results = model.fit()

# Print the summary of results generated by the summary() method of results
print(results.summary2())
```

                      Results: Ordinary least squares
    ===================================================================
    Model:              OLS              Adj. R-squared:     0.769
    Dependent Variable: y                AIC:                -491.9873
    Date:               2021-02-09 13:27 BIC:                -488.2036
    No. Observations:   49               Log-Likelihood:     247.99
    Df Model:           1                F-statistic:        161.1
    Df Residuals:       47               Prob (F-statistic): 8.57e-17
    R-squared:          0.774            Scale:              2.4525e-06
    ---------------------------------------------------------------------
                Coef.    Std.Err.      t       P>|t|     [0.025    0.975]
    ---------------------------------------------------------------------
    const       0.0304     0.0006    48.7986   0.0000    0.0291    0.0317
    1929       -0.0011     0.0001   -12.6915   0.0000   -0.0013   -0.0009
    -------------------------------------------------------------------
    Omnibus:                1.862        Durbin-Watson:           2.164
    Prob(Omnibus):          0.394        Jarque-Bera (JB):        1.597
    Skew:                   0.300        Prob(JB):                0.450
    Kurtosis:               2.351        Condition No.:           20
    ===================================================================

**Questions**

1. Interpret the estimate for the coefficient on $y_i$.
2. Interpret the R-squared of the regression.
3. Why do you think that initial income and average growth are so strongly correlated in the state data but not so in the country data? (If you're not sure, referesh your knowledge about the difference between *conditional* and *unconditional* convergence: https://en.wikipedia.org/wiki/Convergence_(economics)#Types_of_Convergence)

**Answers**

1. The estimated coefficient on $y_i$ implies that a \$1,000 increase in income per captia reduces a states average growth rate of income per capita by 1.2 percent. I.e., states with lower initial income had greater income growth than higher income states.
2. The R-squared implies that the linear regression model explains about 76 percent of the variation in average income per capita growth across states.
3. Lower-income US states are "economically similar" to higher-income states in that they have the same currency, federal regulation, legal traditions, and language, and they have similar laws, poltical institutions. Therefore, it's not surprising to observe *catch-up growth* among the lower-income states. By contrast, lower-income countries are a diverse group with broad differences in policies and institutions and so it is not at all guaranteed that low income countries will enjoy catch-up growth simply by virtue of their low income.


```python
import numpy as np
import sympy as sym
import json
import sympy as sym
import pydae.build as db
from pydae.grid_b_pu import bal_pu
```

```python
file_path = 'milano_8p1_vsg_1.json'
grid = bal_pu(file_path)
```

```python
params_dict = grid['params']

g_list = grid['g']
h_dict = grid['h']
f_list = grid['f']
x_list = grid['x']

sys = {'name':'milano_8p1_vsg_1',
       'params_dict':params_dict,
       'f_list':f_list,
       'g_list':g_list,
       'x_list':x_list,
       'y_ini_list':grid['y'],
       'y_run_list':grid['y'],
       'u_run_dict':grid['u'],
       'u_ini_dict':grid['u'],
       'h_dict':h_dict}

sys = db.system(sys)
db.sys2num(sys)
```

```python

```

```python
soc = 1.1
p_soc_ref = 1.0
p_sto = 0.1
p_soc = sym.Piecewise((0.0,(soc<0.0) and (p_sto>0.0)),(0.0,(soc>1.0) and (p_sto<0.0)), (p_soc_ref, True))
p_soc
```

$\displaystyle 1.0$

```python

```