# Hamilton-Jacobi Reachibility

In the following we will analyse the Hamilton-Jacobi Reachibility for a 1-dimensional coupled space, with state $\vec{x}$ and dynamics $\frac{d}{dt} \vec{x}$:

\begin{equation}
\vec{x} = \begin{pmatrix}x\\v_x\end{pmatrix} = \begin{pmatrix}x_{robot} - x_{pedestrian}\\v_x\end{pmatrix}
\end{equation}

\begin{equation}
\frac{d}{dt}\vec{x} = \begin{pmatrix}v_x - d\\u\end{pmatrix}
\end{equation}

with $u$ being the control input applied by the robot and $d$ the disturbance, the pedestrian with single integrator dynamics. In order to be provably safe we look for the tube in that the robot's state must be in at every point in time so that no matter what the pedestrian does they both will not collide (collision is modelled here as being less or equal than one meter apart from each other).

The Hamilton-Jacobi Reachibility computes the values of a min-max game, in which the robot chooses its actions in order to avoid some set (here a tube around $\Delta x = 0$) as best as possible while the the disturbance does the oppositie, i.e. the disturbance (aka the pedestrian) tries to push the system in the avoid set.

```python
import matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
plt.ioff()
from IPython.display import HTML
import seaborn as sns
```

```python
import numpy as np
import torch

from mantrap.agents import DoubleIntegratorDTAgent
from mantrap.environment import PotentialFieldEnvironment
from mantrap.modules import HJReachabilityModule
from mantrap.solver import SGradSolver

env = PotentialFieldEnvironment(DoubleIntegratorDTAgent, ego_position=torch.zeros(2), dt=0.4)
env.add_ado(position=torch.tensor([8, 0]), num_modes=1)
env.add_ado(position=torch.tensor([-8, 0]), num_modes=1)

module = HJReachabilityModule(env=env, t_horizon=3, weight=1.0)
```

### Value Function and Gradient Plot (2D slices)

```python
N = 100

plt.close('all')
fig = plt.figure(figsize=(13, 5))
ticks = np.arange(0, N, step=N//10)

# Draw x-vx two-dimensional slice of value function at y = vy = 0.
x_linspace = np.linspace(*env.x_axis, num=N)
v_linspace = np.linspace(*env.ego.speed_limits, num=N)
x_grid, v_grid = np.meshgrid(x_linspace, v_linspace)
x_grid = x_grid.flatten()
v_grid = v_grid.flatten()
xyv_grid = np.stack((x_grid, np.zeros(N**2), v_grid, np.zeros(N**2))).T
value_function = module.value_function(xyv_grid)

plt.subplot(1, 2, 1)
im = plt.imshow(value_function.reshape(N, N))
plt.xlabel("dx[m]")
plt.ylabel("vx[m/s]")
plt.xticks(ticks, np.round(x_linspace, 1)[ticks])
plt.yticks(ticks, np.round(v_linspace, 1)[ticks])
plt.title("value function")
plt.colorbar(im)

# Draw x-y two-dimensional slice of value function at vx = vy = 0.
y_linspace = np.linspace(*env.y_axis, num=N)
x_grid, y_grid = np.meshgrid(x_linspace, y_linspace)
x_grid = x_grid.flatten()
y_grid = y_grid.flatten()
xyv_grid = np.stack((x_grid, y_grid, np.zeros(N**2), np.zeros(N**2))).T
value_function = module.value_function(xyv_grid)

plt.subplot(1, 2, 2)
im = plt.imshow(value_function.reshape(N, N))
plt.xlabel("dx[m]")
plt.ylabel("dy[m]")
plt.xticks(ticks, np.round(x_linspace, 1)[ticks])
plt.yticks(ticks, np.round(y_linspace, 1)[ticks])
plt.title("value function")
plt.colorbar(im)

plt.show()
```

### HJ-Constraint in Optimization

```python
env = PotentialFieldEnvironment(DoubleIntegratorDTAgent, ego_position=torch.zeros(2), dt=0.4)
env.add_ado(position=torch.tensor([3, 0]), num_modes=1)
env.add_ado(position=torch.tensor([-3, 0]), num_modes=1)

solver = SGradSolver(env, goal=torch.rand(2), t_planning=3, modules=[HJReachabilityModule])
HTML(solver.visualize_heat_map(propagation="constant"))
```

### Value Function Approximation

```python
import os

import mantrap
import numpy as np
import pandas as pd
import torch

t_horizon = 5

env = mantrap.environment.Trajectron(mantrap.agents.DoubleIntegratorDTAgent,
                                     ego_position=torch.zeros(2))
_  = env.add_ado(position=torch.rand(2) * 4, velocity=torch.rand(2))
```

```python
file_baseline = "2D_large.mat"
module_baseline = mantrap.modules.HJReachabilityModule(env, t_horizon=t_horizon, data_file=file_baseline)

# Loading accuartely computed value function and compute meshgrid containing every computed grid coordinate.
_, _, grid_size_by_dim, _, (grid_min, grid_max) = mantrap.modules.HJReachabilityModule.unpack_mat_file(
    mat_file_path=os.path.join(mantrap.utility.io.build_os_path("third_party/reachability"), file_baseline)
)
x_grid, y_grid, vx_grid, vy_grid = np.meshgrid(*(np.linspace(grid_min[i], grid_max[i], num=grid_size_by_dim[i]) \
                                                 for i in range(4)))
mesh = np.stack((x_grid.flatten(), y_grid.flatten(), vx_grid.flatten(), vy_grid.flatten())).T

# In order to reduce the number of evaluation points, sample 1000 random points.
idx = np.random.randint(low=0, high=mesh.shape[0] - 1, size=1000)
mesh = mesh[idx, :]
print(f"Meshgrid with {mesh.shape[0]} elements ready for evaluation !")
```

```python
# Evaluate value function error of approximation.
models = {"small": "2D_small.mat", "interior": "2D_interior.mat", "medium": "2D_medium.mat"}

results_df = pd.DataFrame(columns=["model", "error", "x", "y", "vx", "vy"])
for label, file_name in models.items():
    print(label, file_name)
    module = mantrap.modules.HJReachabilityModule(env, t_horizon=t_horizon, data_file=file_name)

    for x_rel in mesh:
        error = module.value_function(x_rel) - module_baseline.value_function(x_rel)
        results_df = results_df.append({"model": label, "error": float(error),
                                        "x": x_rel[0], "y": x_rel[1], "vx": x_rel[2], "vy": x_rel[3]},
                                        ignore_index=True)
```

```python
results_df["log_error"] = np.log(np.abs(results_df["error"]) + 1e-10)  # avoiding nan values (log(0)/log(-x))
results_df
```

```python
# Plot results as bar plot.
ax = sns.boxplot(x="log_error", y="model", data=results_df)
plt.show()
```

```python
ax = sns.pairplot(data=results_df, hue="model", y_vars=["error"], x_vars=["x", "y", "vx", "vy"], diag_kind="kde")
plt.show()
```

ax = sns.pairplot(data=results_df, hue="model", y_vars=["error"], x_vars=["x", "y", "vx", "vy"], diag_kind="kde")
plt.show()

```python
import os

import mantrap
import numpy as np
import pandas as pd
import torch

t_horizon = 5

env = mantrap.environment.Trajectron(mantrap.agents.DoubleIntegratorDTAgent,
                                     ego_position=torch.zeros(2))
_  = env.add_ado(position=torch.rand(2) * 4, velocity=torch.rand(2))
```

```python
file_baseline = "2D_large.mat"
module_baseline = mantrap.modules.HJReachabilityModule(env, t_horizon=t_horizon, data_file=file_baseline)

# Loading accuartely computed value function and compute meshgrid containing every computed grid coordinate.
_, _, grid_size_by_dim, _, (grid_min, grid_max) = mantrap.modules.HJReachabilityModule.unpack_mat_file(
    mat_file_path=os.path.join(mantrap.utility.io.build_os_path("third_party/reachability"), file_baseline)
)
x_grid, y_grid, vx_grid, vy_grid = np.meshgrid(*(np.linspace(grid_min[i], grid_max[i], num=grid_size_by_dim[i]) \
                                                 for i in range(4)))
mesh = np.stack((x_grid.flatten(), y_grid.flatten(), vx_grid.flatten(), vy_grid.flatten())).T

# In order to reduce the number of evaluation points, sample 1000 random points.
idx = np.random.randint(low=0, high=mesh.shape[0] - 1, size=1000)
mesh = mesh[idx, :]
print(f"Meshgrid with {mesh.shape[0]} elements ready for evaluation !")
```

    Meshgrid with 1000 elements ready for evaluation !

```python
# Evaluate value function error of approximation.
models = {"small": "2D_small.mat", "interior": "2D_interior.mat", "medium": "2D_medium.mat"}

results_df = pd.DataFrame(columns=["model", "error", "x", "y", "vx", "vy"])
for label, file_name in models.items():
    print(label, file_name)
    module = mantrap.modules.HJReachabilityModule(env, t_horizon=t_horizon, data_file=file_name)

    for x_rel in mesh:
        error = module.value_function(x_rel) - module_baseline.value_function(x_rel)
        results_df = results_df.append({"model": label, "error": float(error),
                                        "x": x_rel[0], "y": x_rel[1], "vx": x_rel[2], "vy": x_rel[3]},
                                        ignore_index=True)
```

    small 2D_small.mat
    interior 2D_interior.mat
    medium 2D_medium.mat

```python
results_df["log_error"] = np.log(np.abs(results_df["error"]) + 1e-10)  # avoiding nan values (log(0)/log(-x))
results_df
```

      model
      error
      x
      y
      vx
      vy
      log_error

      0
      small
      0.119561
      -4.5
      -24.0
      1.5
      -0.5
      -2.123932

      1
      small
      2.141991
      3.0
      1.5
      -1.8
      -2.0
      0.761736

      2
      small
      0.060316
      -22.5
      18.0
      0.9
      1.8
      -2.808151

      3
      small
      0.145727
      30.0
      -10.5
      -1.6
      -0.2
      -1.926019

      4
      small
      0.283705
      9.0
      18.0
      -1.3
      -1.8
      -1.259819

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      2995
      medium
      -0.002578
      -15.0
      -27.0
      -2.0
      1.6
      -5.960809

      2996
      medium
      0.017864
      22.5
      30.0
      1.9
      1.7
      -4.024951

      2997
      medium
      0.025322
      -9.0
      13.5
      1.0
      1.4
      -3.676078

      2998
      medium
      0.039885
      16.5
      -28.5
      -0.6
      -1.1
      -3.221748

      2999
      medium
      0.029276
      16.5
      24.0
      1.7
      -0.8
      -3.530993

3000 rows × 7 columns

```python
# Plot results as bar plot.
ax = sns.boxplot(x="log_error", y="model", data=results_df)
plt.show()
```

```python
ax = sns.pairplot(data=results_df, hue="model", y_vars=["error"], x_vars=["x", "y", "vx", "vy"], diag_kind="kde")
plt.show()
```


```python
import matplotlib.pyplot as plt
import numpy as np
import sympy as sym
import pandas as pd
from google.colab import drive
drive.mount('/content/drive')
```

    Mounted at /content/drive

En este cuaderno se calcularán numéricamente los campos producidos por una carga en movimiento en dos sistemas de referencia: el de la carga y el de un observador en reposo.

Primero buscamos el tensor de Faraday en el sistema de la carga. Podemos hacerlo directamente pues conocemos la forma del campo eléctrico producido por una carga puntual.

```python
def Ex(x,y,z):
  return q*x/(np.sqrt(x**2+y**2+z**2)**3)

def Ey(x,y,z):
  return q*y/(np.sqrt(x**2+y**2+z**2)**3)

def Ez(x,y,z):
  return q*z/(np.sqrt(x**2+y**2+z**2)**3)
c=299792458
q=300
x=1
y=1
z=1
Fprima=np.array([[0, Ex(x,y,z)/c, Ey(x,y,z)/c, Ez(x,y,z)/c],[-Ex(x,y,z)/c, 0, 0, 0],[-Ey(x,y,z)/c, 0, 0, 0], [-Ez(x,y,z)/c, 0, 0, 0]], dtype=np.float128)
print(Fprima)
```

    [[ 0.0000000e+00  1.9258332e-07  1.9258332e-07  1.9258332e-07]
     [-1.9258332e-07  0.0000000e+00  0.0000000e+00  0.0000000e+00]
     [-1.9258332e-07  0.0000000e+00  0.0000000e+00  0.0000000e+00]
     [-1.9258332e-07  0.0000000e+00  0.0000000e+00  0.0000000e+00]]

Ahora que tenemos el tensor de Faraday, podemos transformar al frame en reposo con una transformación de Lorentz.

```python
vx = input('Ingrese la velocidad para el boost en x')
vy = input('Ingrese la velocidad para el boost en y')
vz = input('Ingrese la velocidad para el boost en z')
vx = float(vx)
betax=vx/c
vy = float(vy)
betay=vy/c
vz = float(vz)
betaz=vz/c
v2=vx**2+vy**2+vz**2
beta2=betax**2+betay**2+betaz**2
if v2>c**2:
  print('Este valor de velocidad no es posible, está por encima de c, los cálculos NO SON CORRECTOS')
```

    Ingrese la velocidad para el boost en x300
    Ingrese la velocidad para el boost en y0
    Ingrese la velocidad para el boost en z0

Ahora se calcula directamente la T. de Lorentz con la forma general

```python
Mgeneral=np.array([[gamma, -gamma*betax, -gamma*betay, -gamma*betaz],[-gamma*betax, 1+(gamma-1)*betax**2/beta2, (gamma-1)*betax*betay/beta2,(gamma-1)*betax*betaz/beta2],[-gamma*betay, (gamma-1)*betay*betax/beta2 ,1+(gamma-1)*betay**2/beta2, (gamma-1)*betay*betaz/beta2],[-gamma*betaz, (gamma-1)*betaz*betax/beta2, (gamma-1)*betay*betaz/beta2, 1+(gamma-1)*betaz**2/beta2]], dtype=np.float128)
print(Mgeneral)
```

    [[ 1.09108945e+00 -1.09184480e-06 -0.00000000e+00 -0.00000000e+00]
     [-1.09184480e-06  1.09108945e+00  0.00000000e+00  0.00000000e+00]
     [-0.00000000e+00  0.00000000e+00  1.00000000e+00  0.00000000e+00]
     [-0.00000000e+00  0.00000000e+00  0.00000000e+00  1.00000000e+00]]

Con la T. de Lorentz calculada, podemos transformar al frame en el que nos interesa el movimiento.

```python
F=Mgeneral.dot(Fprima.dot(Mgeneral))
print(F)
```

    [[ 0.00000000e+00  7.64219524e-10  7.00418764e-10  7.00418764e-10]
     [-7.64219524e-10  0.00000000e+00 -7.00903653e-16 -7.00903653e-16]
     [-7.00418764e-10  7.00903653e-16  0.00000000e+00  0.00000000e+00]
     [-7.00418764e-10  7.00903653e-16  0.00000000e+00  0.00000000e+00]]

datos/colabs/Campo de una carga en movimiento.ipynb


# Chapter 10: Beam Theory: Architecture for Cells and Skeletons

(c) 2019 Manuel Razo. This work is licensed under a [Creative Commons Attribution License CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). All code contained herein is licensed under an [MIT license](https://opensource.org/licenses/MIT).

---

```python
# Our numerical workhorses
import numpy as np
import scipy as sp
import pandas as pd
import sympy

# Import matplotlib stuff for plotting
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib as mpl

# Seaborn, useful for graphics
import seaborn as sns

# Import the default plotting style
import plotting_style as pstyle

# Function to import and display images from the Garland website
from IPython.display import Image

# This enables SVG graphics inline.
%config InlineBackend.figure_formats = {'png', 'retina'}

# Define directories
figdir = '../fig/'
```

```python
# Set plotting style
pstyle.set_plotting_style()
# Set dpi for inline figures
mpl.rcParams['figure.dpi'] = 110
```

## 10.1 D Persistence length and Fourier analysis

d) Make use of the identity $\left\langle \cos X \right\rangle = e^{−\left\langle X^2 \right\rangle/2}$, which holds for a Gaussian distributed random variable $X$, to obtain the equation for the tangent–tangent correlation function, $\left\langle t(s) \cdot t(0)\right\rangle = e^{−\theta(s)^2 / 2}$. Then compute $\left\langle \theta(s)^2\right\rangle$ by using the Fourier series representation of $\theta (s)$ and the average values of the Fourier amplitudes $\tilde \theta$ obtained in (c). Convince yourself either by plotting or Fourier analysis that on the interval $0 < s < L$, $\left\langle \theta(s)^2\right\rangle = {s \over \xi_p}$.

### Solution

We arrived to the solution that the average value of the angle squared $\left\langle \theta(s)^2\right\rangle$ is of the form
$$
\left\langle \theta(s)^2\right\rangle = {L \over \xi_p}
\sum_{n = 1}^\infty {8 \over [(2n -1)\pi]^2}
\sin^2\left( {(2n - 1)\pi \over 2L}s \right).
$$
Let's define a function that performs this sum up to a specific $n$ and see to which value it converges as we add more and more terms.

```python
def avg_theta_sqr(n, s, L=1, xi_p=0.1):
    '''
    Computes the average value of the angle squared
    '''
    # Initialize array to save each entry for each n and s value
    theta_sq = np.zeros([n, len(s)])

    # Loop through n and compute s
    for i in range(1, n + 1):
        theta_sq[i-1, :] = (L / xi_p) * (8 / ((2 * i - 1) * np.pi)**2) *\
                         np.sin((2 * i - 1) * np.pi / (2 * L) * s)**2

    return theta_sq
```

```python
# Define range of s values
L = 1
s_array = np.linspace(0, L, 101)
xi_p = L / 1000

# Compute sum up to 100
theta_sq = avg_theta_sqr(150, s_array, L, xi_p)

##   Plot partial sums

# Define partial sums to plot
n_plot = [5, 10, 50, 100, 150]

# Define colors
colors = sns.color_palette('Blues_r', n_colors=len(n_plot)+2)

# Loop through partial plots
for i, n in enumerate(n_plot):
    # Plot partial sum
    plt.plot(s_array, theta_sq[0:n, :].sum(axis=0) - s_array / xi_p,
             label=str(n), color=colors[i])

# Label axis
plt.xlabel(r'$s / L$')
plt.ylabel(r'$\left\langle \theta(s)^2 \right\rangle - s / \xi_p$')

# Add legend
plt.legend(title='$n$', ncol=2)

# Save figure
plt.savefig(figdir + 'problem_10_01_04.png', bbox_inches='tight',
            dpi=300)
```

## 10.5 Packing free energy for a spherical virus

Repeat the calculations of the energy of DNA packing for a spherical capsid. Contrast this result with that obtained in the chapter where it was assumed that the capsid is a cylinder. Use the experimental data provided on the book’s website and compare the model with the data shown in Figure 10.19(B).

### Solution

The free energy of assembly for a spherical virus is divided into two components:
$$
G_{virus} = G_{bend} + G_{charge},
$$
where $G_{bend}$ is the energy cost for bending the DNA and $G_{charge}$ is the energy associated with bringing charges close together.

For the problem we derived the bending energy to be of the form
$$
G_{bend} = {4 \pi \xi_p k_BT \over \sqrt{3} d_s^2} R_{out}
\left[ {1 \over 2} \log\left( {1 + x \over 1 - x} \right) - x\right],
$$
where
$$
x \equiv \left( {3\sqrt{3} d_s^2 L \over 8 \pi R_{out}^3} \right)^{1/3},
$$
is a dimensionless quantity that relates the volume of the packed DNA to the volume of the capsid. $\xi_p$ is the persistence length, $d_s$ is the spacing between DNA strands, $L$ is the length of the packed genome, and $R_{out}$ is the radius of the spherical capsid.

The free energy of charge interaction is very difficult to calculate from "first principles". Therefore the prophets take an empirical approach based on some data that shows that as a function of the interspacing between strands the osmotic pressure $p(d_s)$ can be fit by an exponential curve of the form
$$
p(d_s) = F_0 e^{-d_s / c},
$$
where $F_0$ and $c$ are constants to be obtained from data. From this form of the osmotic pressure we can build the energy of charge interaction that takes the form
$$
G_{charge} = \sqrt{3} F_0 \left(c^2 + c d_s  \right)L e^{- d_s / c}.
$$

To make progress we need to find the value of the constants $F_0$ and $c$ from the osmotic pressure vs ineraxial spacing. This is shown in the boox in Fig. 6.24. Here I will import the data. I manually turned this data into tidy format because the excel was hard to import as it originally stands.

```python
# Read data into memory
df = pd.read_excel('../data/fig6.24.xls', sheet_name='tidy')

df.head()
```

      salt_M
      spacing_angstroms
      log_pressure_dyne_cm2

      0
      0.1
      38.108328
      6.707046

      1
      0.1
      35.583976
      6.774559

      2
      0.1
      33.595808
      6.970460

      3
      0.1
      32.202357
      7.055454

      4
      0.1
      31.010081
      7.191798

Having read this into memory let's reproduce Fig. 6.24. For this we consider that 10 angstroms = 1 nm and 1 dyne/cm$^2$ = 0.1 N/m$^2$.

```python
# Add column with log pressure in N/m^2
# df['log_pressure_N_m2'] = np.log(np.exp(df.log_pressure_dyne_cm2) * 0.1)
df['log_pressure_N_m2'] = np.log10(10**(df.log_pressure_dyne_cm2) * 0.1)

# group data by salt concentration
df_group = df.groupby('salt_M')

# Define list of markers
markers = ['o', 'v', 's', '+']

# Loop through concentrations to plot data
for i, (group, data) in enumerate(df_group):
    plt.scatter(data.spacing_angstroms / 10,
                data.log_pressure_N_m2,
                label=group, marker=markers[i])

# Label axis
plt.xlabel('interaxial spacing (nm)')
plt.ylabel('log pressure (N/m$^2$)')

# Add legend
_ = plt.legend(title='salt (M)')
```

From this data we can fit the parameters $F_0$ and $c$ by simply performing a linear regression on the log of the pressure. This takes the form
$$
\log p(d_s) = \log F_0 - d_s / c.
$$
A proper extensive analysis would require us to do the fitting not in log scale but in linear scale with the proper estimation of error bars. But we'll leave that for another problem. For now we will simply use numpy to find the slope and intercept that best describes this data.

```python
# Perform simple linear regression on data
linear_fit = np.polyfit(df.spacing_angstroms  * 1E-10,
                        df.log_pressure_N_m2,
                        deg=1)

# Find parameters
Fo = np.exp(linear_fit[1])
c = - 1 / linear_fit[0]

# Print parameters
print('Parameter values:')
print('Fo = {:.1f} N/m^2'.format(Fo))
print('c = {:.1f} nm'.format(c * 1E9))
```

    Parameter values:
    Fo = 7776.8 N/m^2
    c = 1.1 nm

Let's see how well these parameters fit the data

```python
# Define range for linear fit
ds_range = np.linspace(df.spacing_angstroms.min() / 10,
                       df.spacing_angstroms.max() / 10)
log_pressure = linear_fit[1] - ds_range / (c * 1E9)

# group data by salt concentration
df_group = df.groupby('salt_M')

# Define list of markers
markers = ['o', 'v', 's', '+']

# Loop through concentrations to plot data
for i, (group, data) in enumerate(df_group):
    plt.scatter(data.spacing_angstroms / 10,
                data.log_pressure_N_m2,
                label=group, marker=markers[i])

# Plot best fit line
plt.plot(ds_range, log_pressure, label='', color='k')

# Label axis
plt.xlabel('interaxial spacing (nm)')
plt.ylabel('log pressure (N/m$^2$)')

# Add legend
_ = plt.legend(title='salt (M)')
```

There are possibly a lot of sloptes and intercepts that would fit this data. So a more formal analysis would require a Bayesian approach to consider the uncertainty on these fit parameters.

Having found values for this let's now read the relevant data to compare our theoretical model. The data is presented in the book as Fig. 10.19(B) where the percentage of genome packed is plotted against the internal force as measured with optical tweezers. Let's again import the data that I manually fit into a tidy format and plot it to reproduce the figure.

```python
# Import data
df_force = pd.read_excel('../data/fig10.19B.xls', sheet_name='tidy')

# Plot data
plt.plot(df_force.percentage_packed,
         df_force.force_pN, lw=2)

# Fill betwee curve and x axis
plt.fill_between(df_force.percentage_packed,
                 df_force.force_pN,
                 alpha=0.5)

# Label axis
plt.xlabel('percentage of genome packed')
_ = plt.ylabel('internal force (pN)')
```

Now that we imported the data we are ready to compare it with our theoretical result. We know the persistence length $\xi_p$, we know the radius of the phage $\phi$ 29 $R_{out}$, the length of it's genome $L$, and we just determined the parameters for the osmotic pressure $F_o$ and $c$. The only parameter that we do not know is the spacing between DNA strands $d_s$.

The approach we will take to determine this parameter makes the assumption that the DNA that has already being packed reaches an equilibrium inside the capside. What this allows us to do is to numerically find for each value of $L$ the value of $d_s$ that minimizes the free energy. This is equivalent to computing
$$
{\partial G_{virus} \over \partial d_s} \vert_{L} = 0.
$$
We could in principle compute this derivative using the `sympy` symbolic package, but at the end of the day we would still find the value of $d_s$ that minimiazes the free energy numerically. So the approach we will take is to define the free energy function itself and just minimize such function for a given value of L. Let's go ahead and define this function.

```python
def G_virus(ds, L, xi_p, Rout, kbt, Fo, c):
    '''
    Function to compute the free energy of packing the genome of a phage into a
    spherical capsid.
    Parameters
    ----------
    ds : float.
        Distance between strands of DNA inside the capsid.
    L : float.
        Length of the genome
    xi_p : float.
        DNA persistence length
    Rout : float.
        Radius of spherical capsid
    kbt : float.
        Thermal energy scale
    Fo, c : floats.
        Empirical parameters associated with the charge interaction free energy
    '''

    # Define x value for bending free energy
    x = (3 * np.sqrt(3) * ds**2 * L / 8 / np.pi / Rout**3)**(1 / 3)

    # Compute bending free energy
    G_bend = 4 * np.pi * xi_p * kbt / np.sqrt(3) / ds**2 * Rout *\
             (1 / 2 * np.log((1 + x) / (1 - x)) - x)

    # Compute charge interaction free energy
    G_charge = np.sqrt(3) * Fo * (c**2 + c * ds) * L * np.exp(- ds / c)

    # Return total free energy
    return G_bend + G_charge
```

Let's now define the numerical value for our parameters. All of this will be set in SI units.

```python
# Persistence length
xi_p = 150 * (1 / 3) * 1E-9  # bp->nm->m

# Thermal energy
kbt = 4.1E-21  # pN nm -> Joules
# Phage dimensions from bionumbers
# https://bionumbers.hms.harvard.edu/search.aspx?task=searchbytrmorg&log=y&trm=Bacteriophage%20phi%2029
L = 19.3E3 * (1 / 3) * 1E-9  # bp->nm->m
Rout = 50 / 2 * 1E-9  # nm->m

# Option 1 listed in solution
Fo = 7.8 * 1E-9 / (1E-9)**2
c = 0.35E-9

# Option 2 listed in solution
# Fo = 226 * 1E-9 / (1E-9)**2
# c = 0.27E-9
```

```python
# ds = np.linspace(1E-10, 1E-9, 2000)
ds = np.logspace(-9, -5)

G = np.zeros_like(ds)

param = (L, xi_p, Rout, kbt, Fo, c)
for i, d in enumerate(ds):
    G[i] = G_virus(d, *param)

plt.plot(ds, G / kbt)
plt.xscale('log')
```

**NOTE**: I've been trying to get the numerics to work for several days now and I can't get an energy function that has a clear minimum as a function of the distance between DNA strands. In the solution they specify that this is very sensitive to the values of $F_o$ and $c$ that one fits to the data. But even when I input the values they report I don't see a clear minimum. So for the sake of my time I will have to move on and abandon this problem.

Now we can use a minimization routine from `scipy` to systematically find the value of $d_s$ that minimizes the free energy as a function of $L$.

```python
# Define array of values of L
L_array = np.linspace(0.01, 1, 100) * L

# Define array to save values of ds that minimize free energy
ds_array = np.zeros_like(L_array)

# Loop through values of length
for i, l in enumerate(L_array):
    # Set parameter values for this iteration
    param = (l, xi_p, Rout, kbt, Fo, c)

    # Minimize free energy function
    ds_array[i] = sp.optimize.minimize(G_virus, 1E-9, args=param).x
```

    /Users/razo/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:26: RuntimeWarning: invalid value encountered in log

```python
plt.plot(L_array, ds_array)
plt.ylim([ds_array.min(), ds_array.max()])
```

```python
G
```

    array([2.12715421e-14, 2.09939589e-14, 2.07197397e-14, ...,
                      nan,            nan,            nan])

```python

```


# The Implicit Crank-Nicolson Difference Equation for the Heat Equation

#### John S Butler john.s.butler@tudublin.ie   [Course Notes](https://johnsbutler.netlify.com/files/Teaching/Numerical_Analysis_for_Differential_Equations.pdf)    [Github](https://github.com/john-s-butler-dit/Numerical-Analysis-Python)
## Overview
This notebook will illustrate the Crank-Nicolson Difference method for the Heat Equation.
## The Heat Equation
The Heat Equation is the first order in time ($t$) and second order in space ($x$) Partial Differential Equation:
\begin{equation}  \frac{\partial u}{\partial t} = \frac{\partial^2 u}{\partial x^2},\end{equation}
The equation describes heat transfer on a domain
\begin{equation} \Omega = \{ t \geq, 0\leq x \leq 1\}. \end{equation}
with an initial condition at time $t=0$ for all $x$ and boundary condition on the left ($x=0$) and right side

This notebook will illustrate the Crank-Nicolson Difference method for the Heat Equation with the __initial conditions__
\begin{equation} u(x,0)=2x, \ \ 0 \leq x \leq \frac{1}{2}, \end{equation}
\begin{equation} u(x,0)=2(1-x), \ \ \frac{1}{2}  \leq x \leq 1, \end{equation}
and __boundary condition__
\begin{equation} u(0,t)=0,  u(1,t)=0. \end{equation}
## Crank-Nicolson Difference method

The implicit Crank-Nicolson difference equation of the Heat Equation
is derived by discretising the
\begin{equation}  \frac{\partial u_{ij+\frac{1}{2}}}{\partial t} = \frac{\partial^2 u_{ij+\frac{1}{2}}}{\partial x^2},\end{equation}
around $(x_i,t_{j+\frac{1}{2}})$ giving the difference equation
\begin{equation}
\frac{w_{ij+1}-w_{ij}}{k}=\frac{1}{2}\left(\frac{w_{i+1j+1}-2w_{ij+1}+w_{i-1j+1}}{h^2}+\frac{w_{i+1j}-2w_{ij}+w_{i-1j}}{h^2}\right).
\end{equation}
Rearranging give the difference equation
\begin{equation}
-rw_{i-1j+1}+(2+2r)w_{ij+1}-rw_{i+1j+1}=rw_{i-1j}+(2-2r)w_{ij}+rw_{i+1j}
\end{equation}
for $i=1,...9$ where $r=\frac{k}{h^2}$.

```python
# LIBRARY
# vector manipulation
import numpy as np
# math functions
import math

# THIS IS FOR PLOTTING
%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import warnings
warnings.filterwarnings("ignore")

```

## Discete Grid
The region $\Omega$ is discretised into a uniform mesh $\Omega_h$. In the space $x$ direction into $N$ steps giving a stepsize of
\begin{equation} h=\frac{1-0}{N},\end{equation}
resulting in
\begin{equation}x[i]=0+ih, \ \ \  i=0,1,...,N,\end{equation}
and into $N_t$ steps in the time $t$ direction giving a stepsize of
\begin{equation} k=\frac{1-0}{N_t}\end{equation}
resulting in
\begin{equation}t[i]=0+ik, \ \ \ k=0,...,K.\end{equation}
The Figure below shows the discrete grid points for $N=10$ and $N_t=100$ , the red dots are the unknown values, the green dots are the known boundary conditions and the blue dots are the known initial conditions of the Heat Equation.

```python
N=10
Nt=100
h=1/N
k=1/Nt
r=k/(h*h)
time_steps=15
time=np.arange(0,(time_steps+.5)*k,k)
x=np.arange(0,1.0001,h)
X, Y = np.meshgrid(x, time)
fig = plt.figure()
plt.plot(X,Y,'ro');
plt.plot(x,0*x,'bo',label='Initial Condition');
plt.plot(np.ones(time_steps+1),time,'go',label='Boundary Condition');
plt.plot(x,0*x,'bo');
plt.plot(0*time,time,'go');
plt.xlim((-0.02,1.02))
plt.xlabel('x')
plt.ylabel('time (ms)')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.title(r'Discrete Grid $\Omega_h,$ h= %s, k=%s'%(h,k),fontsize=24,y=1.08)
plt.show();
```

## Discrete Initial and Boundary Conditions

The discrete initial conditions are
\begin{equation} w[i,0]=2x[i], \ \ 0 \leq x[i] \leq \frac{1}{2} \end{equation}
\begin{equation} w[i,0]=2(1-x[i]), \ \ \frac{1}{2}  \leq x[i] \leq 1 \end{equation}
and the discete boundary conditions are
\begin{equation} w[0,j]=0,  w[10,j]=0, \end{equation}
where $w[i,j]$ is the numerical approximation of $U(x[i],t[j])$.

The Figure below plots values of $w[i,0]$ for the inital (blue) and boundary (red) conditions for $t[0]=0.$

```python
w=np.zeros((N+1,time_steps+1))
b=np.zeros(N-1)
# Initial Condition
for i in range (1,N):
    w[i,0]=2*x[i]
    if x[i]>0.5:
        w[i,0]=2*(1-x[i])

# Boundary Condition
for k in range (0,time_steps):
    w[0,k]=0
    w[N,k]=0

fig = plt.figure(figsize=(8,4))
plt.plot(x,w[:,0],'o:',label='Initial Condition')
plt.plot(x[[0,N]],w[[0,N],0],'go',label='Boundary Condition t[0]=0')
#plt.plot(x[N],w[N,0],'go')
plt.xlim([-0.1,1.1])
plt.ylim([-0.1,1.1])
plt.title('Intitial and Boundary Condition',fontsize=24)
plt.xlabel('x')
plt.ylabel('w')
plt.legend(loc='best')
plt.show()
```

## The Implicit Crank-Nicolson Difference Equation
The implicit Crank-Nicolson difference equation of the Heat Equation
is derived by discretising the
\begin{equation}  \frac{\partial u_{ij+\frac{1}{2}}}{\partial t} = \frac{\partial^2 u_{ij+\frac{1}{2}}}{\partial x^2},\end{equation}
around $(x_i,t_{j+\frac{1}{2}})$ giving the difference equation
\begin{equation}
\frac{w_{ij+1}-w_{ij}}{k}=\frac{1}{2}\left(\frac{w_{i+1j+1}-2w_{ij+1}+w_{i-1j+1}}{h^2}+\frac{w_{i+1j}-2w_{ij}+w_{i-1j}}{h^2}\right)
\end{equation}
Rearranging the equation we get
\begin{equation}
-rw_{i-1j+1}+(2+2r)w_{ij+1}-rw_{i+1j+1}=rw_{i-1j}+(2-2r)w_{ij}+rw_{i+1j}
\end{equation}
for $i=1,...9$ where $r=\frac{k}{h^2}$.

This gives the formula for the unknown term $w_{ij+1}$ at the $(ij+1)$ mesh points
in terms of $x[i]$ along the jth time row.

Hence we can calculate the unknown pivotal values of $w$ along the first row of $j=1$ in terms of the known boundary conditions.

This can be written in matrix form
\begin{equation} A\mathbf{w}_{j+1}=B\mathbf{w}_{j} +\mathbf{b}_{j}+\mathbf{b}_{j+1} \end{equation}
for which $A$ is a $9\times9$ matrix:
\begin{equation}
\left(\begin{array}{cccc cccc}
2+2r&-r& 0&0&0 &0&0&0\\
-r&2+2r&-r&0&0&0 &0&0&0\\
0&-r&2+2r &-r&0&0& 0&0&0\\
0&0&-r&2+2r &-r&0&0& 0&0\\
0&0&0&-r&2+2r &-r&0&0& 0\\
0&0&0&0&-r&2+2r &-r&0&0\\
0&0&0&0&0&-r&2+2r &-r&0\\
0&0&0&0&0&0&-r&2+2r&-r\\
0&0&0&0&0&0&0&-r&2+2r\\
\end{array}\right)
\left(\begin{array}{c}
w_{1j+1}\\
w_{2j+1}\\
w_{3j+1}\\
w_{4j+1}\\
w_{5j+1}\\
w_{6j+1}\\
w_{7j+1}\\
w_{8j+1}\\
w_{9j+1}\\
\end{array}\right)=
\left(\begin{array}{cccc cccc}
2-2r&r& 0&0&0 &0&0&0\\
r&2-2r&r&0&0&0 &0&0&0\\
0&r&2-2r &r&0&0& 0&0&0\\
0&0&r&2-2r &r&0&0& 0&0\\
0&0&0&r&2-2r &r&0&0& 0\\
0&0&0&0&r&2-2r &r&0&0\\
0&0&0&0&0&r&2-2r &r&0\\
0&0&0&0&0&0&r&2-2r&r\\
0&0&0&0&0&0&0&r&2-2r\\
\end{array}\right)
\left(\begin{array}{c}
w_{1j}\\
w_{2j}\\
w_{3j}\\
w_{4j}\\
w_{5j}\\
w_{6j}\\
w_{7j}\\
w_{8j}\\
w_{9j}\\
\end{array}\right)+
\left(\begin{array}{c}
rw_{0j}\\
0\\
0\\
0\\
0\\
0\\
0\\
0\\
rw_{10j}\\
\end{array}\right)+
\left(\begin{array}{c}
rw_{0j+1}\\
0\\
0\\
0\\
0\\
0\\
0\\
0\\
rw_{10j+1}\\
\end{array}\right).
\end{equation}
It is assumed that the boundary values $w_{0j}$ and $w_{10j}$ are known for $j=1,2,...$, and $w_{i0}$ for $i=0,...,10$ is the initial condition.

The Figure below shows the values of the $9\times 9$  matrix in colour plot form for $r=\frac{k}{h^2}$.

```python
A=np.zeros((N-1,N-1))
B=np.zeros((N-1,N-1))
for i in range (0,N-1):
    A[i,i]=2+2*r
    B[i,i]=2-2*r

for i in range (0,N-2):
    A[i+1,i]=-r
    A[i,i+1]=-r
    B[i+1,i]=r
    B[i,i+1]=r

Ainv=np.linalg.inv(A)
fig = plt.figure(figsize=(12,4));
plt.subplot(121)
plt.imshow(A,interpolation='none');
plt.xticks(np.arange(N-1), np.arange(1,N-0.9,1));
plt.yticks(np.arange(N-1), np.arange(1,N-0.9,1));
clb=plt.colorbar();
clb.set_label('Matrix elements values');
#clb.set_clim((-1,1));
plt.title('Matrix A r=%s'%(np.round(r,3)),fontsize=24)

plt.subplot(122)
plt.imshow(B,interpolation='none');
plt.xticks(np.arange(N-1), np.arange(1,N-0.9,1));
plt.yticks(np.arange(N-1), np.arange(1,N-0.9,1));
clb=plt.colorbar();
clb.set_label('Matrix elements values');
#clb.set_clim((-1,1));
plt.title(r'Matrix $B$ r=%s'%(np.round(r,3)),fontsize=24)
fig.tight_layout()
plt.show();
```

## Results
To numerically approximate the solution at $t[1]$ the matrix equation becomes
\begin{equation} \mathbf{w}_{1}=A^{-1}(B\mathbf{w}_{0} +\mathbf{b}_{1}+\mathbf{b}_{0}) \end{equation}
where all the right hand side is known.
To approximate solution at time $t[2]$ we use the matrix equation
\begin{equation}\mathbf{w}_{2}=A^{-1}(B\mathbf{w}_{1}+\mathbf{b}_{2} +\mathbf{b}_{1}). \end{equation}
Each set of numerical solutions $w[i,j]$ for all $i$ at the previous time step is used to approximate the solution $w[i,j+1]$.
The left and right plot below show the numerical approximation $w[i,j]$ of the Heat Equation using the Crank-Nicolson method for $x[i]$ for $i=0,...,10$ and time steps $t[j]$ for $j=1,...,15$.

```python
fig = plt.figure(figsize=(12,6))
plt.subplot(121)
for j in range (0,time_steps+1):
    b[0]=r*w[0,j-1]+r*w[0,j]
    b[N-2]=r*w[N,j-1]+r*w[N,j]
    v=np.dot(B,w[1:(N),j-1])
    w[1:(N),j]=np.dot(Ainv,v+b)
    plt.plot(x,w[:,j],'o:',label='t[%s]=%s'%(j,time[j]))
plt.xlabel('x')
plt.ylabel('w')
#plt.legend(loc='bottom', bbox_to_anchor=(0.5, -0.1))
plt.legend(bbox_to_anchor=(-.4, 1), loc=2, borderaxespad=0.)

plt.subplot(122)
plt.imshow(w.transpose())
plt.xticks(np.arange(len(x)), x)
plt.yticks(np.arange(len(time)), time)
plt.xlabel('x')
plt.ylabel('time')
clb=plt.colorbar()
clb.set_label('Temperature (w)')
plt.suptitle('Numerical Solution of the  Heat Equation r=%s'%(np.round(r,3)),fontsize=24,y=1.08)
fig.tight_layout()
plt.show()
```

## Local Trunction Error
The local truncation error of the Crank-Nicoloson we approximate
\begin{equation}
\frac{\partial U}{\partial t} - \frac{\partial^2 U}{\partial x^2}=0,
\end{equation}
with

\begin{equation}
F_{ij+\frac{1}{2}}(w)=\frac{w_{ij+1}-w_{ij}}{k}-\frac{1}{2}\left(\frac{w_{i+1j}-2w_{ij}+w_{i-1j}}{h^2}
+\frac{w_{i+1j+1}-2w_{ij+1}+w_{i-1j+1}}{h^2}
\right)=0.\end{equation}
Subbing the exact answer into the difference equation gives the truncation error
\begin{equation}
T_{ij+\frac{1}{2}}=F_{ij+\frac{1}{2}}(U)=\frac{U_{ij+1}-U_{ij}}{k}-\frac{1}{2}\left(\frac{U_{i+1j}-2U_{ij}+U_{i-1j}}{h^2}+\frac{U_{i+1j+1}-2U_{ij+1}+U_{i-1j+1}}{h^2}\right),
\end{equation}
simplify
\begin{equation}
U_{ij+\frac{1}{2}}=\frac{U_{ij+1}+U_{ij}}{2},
\end{equation}
substitute into equation
\begin{equation}
T_{ij+\frac{1}{2}}=F_{ij+\frac{1}{2}}(U)=\frac{U_{ij+1}-U_{ij}}{k}-\frac{U_{i+1j+\frac{1}{2}}-2U_{ij+\frac{1}{2}}+U_{i-1j+\frac{1}{2}}}{h^2},
\end{equation}
By Taylors expansions we have
\begin{eqnarray*}
U_{i+1j+\frac{1}{2}}&=&U((i+1)h,(j+\frac{1}{2})k)=U(x_i+h,t_j+\frac{1}{2}k)\\
&=&U_{ij+\frac{1}{2}}+h\left(\frac{\partial U}{\partial x} \right)_{ij+\frac{1}{2}}+\frac{h^2}{2}\left(\frac{\partial^2 U}{\partial x^2} \right)_{ij+\frac{1}{2}}+\frac{h^3}{6}\left(\frac{\partial^3 U}{\partial x^3} \right)_{ij+\frac{1}{2}} +...\\
U_{i-1j+\frac{1}{2}}&=&U((i-1)h,(j+\frac{1}{2})k)=U(x_i-h,t_j+\frac{1}{2}k)\\
&=&U_{ij+\frac{1}{2}}-h\left(\frac{\partial U}{\partial x} \right)_{ij+\frac{1}{2}}+\frac{h^2}{2}\left(\frac{\partial^2 U}{\partial x^2} \right)_{ij+\frac{1}{2}}-\frac{h^3}{6}\left(\frac{\partial^3 U}{\partial x^3} \right)_{ij+\frac{1}{2}} +...\\
U_{ij+1}&=&U(ih,(j+1)k)=U(x_i,t_j+k)\\
&=&U_{ij+\frac{1}{2}}+\frac{k}{2}\left(\frac{\partial U}{\partial t} \right)_{ij+\frac{1}{2}}+\frac{(\frac{k}{2})^2}{2}\left(\frac{\partial^2 U}{\partial t^2} \right)_{ij+\frac{1}{2}}+\frac{(\frac{k}{2})^3}{6}\left(\frac{\partial^3 U}{\partial t^3} \right)_{ij+\frac{1}{2}} +...\\
U_{ij-1}&=&U(ih,(j-1)k)=U(x_i,t_j-k)\\
&=&U_{ij+\frac{1}{2}}-\frac{k}{2}\left(\frac{\partial U}{\partial t} \right)_{ij+\frac{1}{2}}+\frac{(\frac{k}{2})^2}{2}\left(\frac{\partial^2 U}{\partial t^2} \right)_{ij+\frac{1}{2}}-\frac{(\frac{k}{2})^3}{6}\left(\frac{\partial^3 U}{\partial t^3} \right)_{ij+\frac{1}{2}} +...
\end{eqnarray*}

substitution into the expression for $T_{ij+\frac{1}{2}}$ then gives

\begin{eqnarray*}
T_{ij+\frac{1}{2}}&=&\left(\frac{\partial U}{\partial t} - \frac{\partial^2 U}{\partial x^2} \right)_{ij+\frac{1}{2}}
-\frac{h^2}{12}\left(\frac{\partial^4 U}{\partial x^4} \right)_{ij+\frac{1}{2}}\\
& &	+\frac{k^2}{24}\left(\frac{\partial^3 U}{\partial t^3} \right)_{ij+\frac{1}{2}}
-\frac{h^4}{360}\left(\frac{\partial^6 U}{\partial x^6} \right)_{ij+\frac{1}{2}}+ ...
\end{eqnarray*}
But $U$ is the solution to the differential equation so
\begin{equation} \left(\frac{\partial U}{\partial t} - \frac{\partial^2 U}{\partial x^2} \right)_{ij+\frac{1}{2}}=0,\end{equation}

the principal part of the local truncation error is

\begin{equation}
\frac{k^2}{24}\left(\frac{\partial^3 U}{\partial t^3} \right)_{ij+\frac{1}{2}}-\frac{h^2}{12}\left(\frac{\partial^4 U}{\partial x^4} \right)_{ij+\frac{1}{2}}.
\end{equation}

Hence the truncation error is
\begin{equation}
T_{ij}=O(k^2)+O(h^2).
\end{equation}

## Stability Analyis

To investigating the stability of the fully implicit Crank Nicolson difference method of the Heat Equation, we will use the von Neumann method.
The difference equation is:
\begin{equation}\frac{1}{k}(w_{pq+1}-w_{pq})=\frac{1}{2}\left(\frac{1}{h_x^2}(w_{p-1q+1}-2w_{pq+1}+w_{p+1q+1})+\frac{1}{h_x^2}(w_{p-1q}-2w_{pq}+w_{p+1q})\right),\end{equation}
approximating
\begin{equation}\frac{\partial U}{\partial t}=\frac{\partial^2 U}{\partial x^2}\end{equation}

at $(ph,(q+\frac{1}{2})k)$. Substituting $w_{pq}=e^{i\beta x}\xi^{q}$ into the difference equation gives:
\begin{equation}e^{i\beta ph}\xi^{q+1}-e^{i\beta ph}\xi^{q}=\frac{1}{2}\left(r\{e^{i\beta (p-1)h}\xi^{q+1}-2e^{i\beta ph}\xi^{q+1}+e^{i\beta (p+1)h}\xi^{q+1}+e^{i\beta (p-1)h}\xi^{q}-2e^{i\beta ph}\xi^{q}+e^{i\beta (p+1)h}\xi^{q} \right) \}
\end{equation}

where $r=\frac{k}{h_x^2}$. Divide across by $e^{i\beta (p)h}\xi^{q}$ leads to

\begin{equation} \xi-1=r \xi (e^{i\beta (-1)h} -2+e^{i\beta h})+r (e^{i\beta (-1)h} -2+e^{i\beta h}), \end{equation}

\begin{equation}\xi-\xi r (2\cos(\beta h)-2)= 1+r (2\cos(\beta h)-2),\end{equation}

\begin{equation}\xi(1+4r\sin^2(\beta\frac{h}{2})) =1-4r\sin^2(\beta\frac{h}{2})\end{equation}
Hence
\begin{equation}\xi=\frac{1-4r\sin^2(\beta\frac{h}{2})}{(1+4r\sin^2(\beta\frac{h}{2}))} \leq 1\end{equation}
therefore the equation is unconditionally stable as $0 < \xi \leq 1$ for all $r$ and all $\beta$ .

## References
[1] G D Smith Numerical Solution of Partial Differential Equations: Finite Difference Method Oxford 1992

[2] Butler, J. (2019). John S Butler Numerical Methods for Differential Equations. [online] Maths.dit.ie. Available at: http://www.maths.dit.ie/~johnbutler/Teaching_NumericalMethods.html [Accessed 14 Mar. 2019].

[3] Wikipedia contributors. (2019, February 22). Heat equation. In Wikipedia, The Free Encyclopedia. Available at: https://en.wikipedia.org/w/index.php?title=Heat_equation&oldid=884580138 [Accessed 14 Mar. 2019].

```python

```

Chapter 08 - Heat Equations/803_Heat Equation-Crank_Nicolson.ipynb


```python
import pickle
import sys
sys.path.append('..')
from src.sympy_utilities import *
from src.BinaryEncodings import *

import sympy as sp
h_b = 0.5*(p*p + m*m*q*q)
h_f = -0.5*m#[bdag,b]

from src.BinaryEncodings import *

hamHO = Hamiltonian(h_b, h_f, {m:1, g:1},
                     8, standard_encode)
```

## Harmonic Oscillator from 0000

```python
import sys
sys.path.append('..')
from src.qiskit_utilities import *

trotter_step = pauli_string_to_trotter_step(hamHO.pauliStrings,1)
print(trotter_step)
```

    global phase: -4
         ┌─────────┐ ┌─────────┐
    q_0: ┤ RZ(0.5) ├─┤ RZ(0.5) ├─
         └┬────────┤ └┬────────┤
    q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
          ├────────┤  ├────────┤
    q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
         ┌┴────────┴┐┌┴────────┴┐
    q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
         └──────────┘└──────────┘

```python
from qiskit import QuantumCircuit, transpile
from src.sympy_utilities import *

nqubits = max_sympy_exponent(hamHO.pauliStrings)+1
qc = QuantumCircuit(nqubits, nqubits)
qc.append(trotter_step, [i for i in range(nqubits)])
transpile(qc, basis_gates = ['cx', 'u1', 'u2', 'u3', 'H', 'X', 'Y', 'Z']).draw('mpl')
```

```python
from qiskit import (
    execute,
    Aer)
from qiskit.providers.aer.noise import NoiseModel
from qiskit import IBMQ
from qiskit.visualization import plot_histogram

provider = IBMQ.load_account()
```

    /home/chrisculver/miniconda3/envs/QuantumComputing/lib/python3.9/site-packages/qiskit/providers/ibmq/ibmqfactory.py:192: UserWarning: Timestamps in IBMQ backend properties, jobs, and job results are all now in local time instead of UTC.
      warnings.warn('Timestamps in IBMQ backend properties, jobs, and job results '

```python
noise_backend = provider.get_backend('ibmq_santiago')
noise_model = NoiseModel.from_backend(noise_backend)
```

```python
simulator = Aer.get_backend('qasm_simulator')

qc.measure([i for i in range(nqubits)],[i for i in range(nqubits)])
```

    <qiskit.circuit.instructionset.InstructionSet at 0x7fe1c867f730>

```python
job = execute(qc, simulator, shots=1000, noise_model=noise_model)
result=job.result()
counts = result.get_counts(qc)
plot_histogram(counts)
```

|k>=e^{iHt}|0000>
<k|k>=0.981<0000|0000>^2 + 0.003<0001|0001>^2 + 0.008<0010|0010>^2 + 0.003<0100|0100>^2 + 0.005<1000|1000>^2

Is what is done above.

Lets say we want

<0000|H|0000>=E_0<0000|h>

Set up state
|h>=H|0000>
<h|h>=0.99<0000|0000> + 0.005<0001|0001>
<h|h>=<0000|H^dag H|0000>
<h|h>=E_0^2<0000|0000>

|h>=O|psi>
<h|h>=c_i <psi_i|psi_i>
if(0 is eigenstate of O)
<0|O^dag O|0>=lambda<0|0>

## Harmonic Oscillator from 1000

```python
trotter_step = pauli_string_to_trotter_step(hamHO.pauliStrings,1)

nqubits = max_sympy_exponent(hamHO.pauliStrings)+1
qc = QuantumCircuit(nqubits, nqubits)
qc.x(nqubits-1)
qc.append(trotter_step, [i for i in range(nqubits)])
transpile(qc, basis_gates = ['cx', 'u1', 'u2', 'u3', 'H', 'X', 'Y', 'Z']).draw('mpl')
```

```python
qc.measure([i for i in range(nqubits)],[i for i in range(nqubits)])
job = execute(qc, simulator, shots=1000, noise_model=noise_model)
result=job.result()
counts = result.get_counts(qc)
plot_histogram(counts)
```

## Anharmonic Oscillator from 0000

```python
h_b = 0.5*(p*p + m*m*q*q + 2.*m*g*q*q*q*q + g*g*q*q*q*q*q*q)
h_f = -0.5*(m+3*g*q*q)#[bdag,b]

n=3
hamAHO = Hamiltonian(h_b, h_f, {m:1, g:1},
                    n, standard_encode)

ahoPS = hamAHO.pauliStrings

trotter_step = pauli_string_to_trotter_step(ahoPS,1)
nqubits = max_sympy_exponent(ahoPS)+1
qc = QuantumCircuit(nqubits, nqubits)
qc.append(trotter_step, [i for i in range(nqubits)])
transpile(qc, basis_gates = ['cx', 'u1', 'u2', 'u3', 'H', 'X', 'Y', 'Z']).draw('mpl')
```

```python
qc.measure([i for i in range(nqubits)],[i for i in range(nqubits)])
job = execute(qc, simulator, shots=4096, noise_model=noise_model)
result=job.result()
counts = result.get_counts(qc)
plot_histogram(counts)
```

## Anharmonic oscillator from 1000

```python
trotter_step = pauli_string_to_trotter_step(ahoPS,1)
nqubits = max_sympy_exponent(ahoPS)+1
qc = QuantumCircuit(nqubits, nqubits)

qc.x(nqubits-1)

qc.append(trotter_step, [i for i in range(nqubits)])
transpile(qc, basis_gates = ['cx', 'u1', 'u2', 'u3', 'H', 'X', 'Y', 'Z']).draw('mpl')

qc.measure([i for i in range(nqubits)],[i for i in range(nqubits)])
job = execute(qc, simulator, shots=4096, noise_model=noise_model)
result=job.result()
counts = result.get_counts(qc)
plot_histogram(counts)
```

```python

```


# One-dimensional advection equation

We want to solve the following PDE:

\begin{equation}
    \frac{\partial \phi}{\partial t} + u \frac{\partial \phi}{\partial x} = 0
\end{equation}

The independen variables (i.e, $x$ and $t$) are used as input values for the NN, and the solution (i.e. $\phi$) is the output. In order to find the solution, at each step the NN outputs are derived w.r.t the inputs. Then, a loss function that matches the PDE is built and the weights are updated accordingly. If the loss function goes to zero, we can assume that our NN is indeed the solution to our PDE. We will try to find a general solution for different values of $u$, so it will be set also as an input.

```python
# autoreload nangs
%reload_ext autoreload
%autoreload 2
%matplotlib inline
```

```python
# imports
import math
import numpy as np
import matplotlib.pyplot as plt

import torch
cuda = True
device = "cuda" if torch.cuda.is_available() and cuda else "cpu"
```

## Define PDE

First we define our PDE and set the values for training.

```python
# imports
from nangs.pde import PDE
from nangs.bocos import PeriodicBoco, DirichletBoco
from nangs.solutions import MLP

# define our PDE
class MyPDE(PDE):
    def __init__(self, inputs=None, outputs=None):
        super().__init__(inputs, outputs)
    # the loss of our NN is the PDE !
    def computePDELoss(self, grads, inputs, outputs, params):
        dpdt, dpdx = grads['p']['t'], grads['p']['x']
        u = inputs['u']
        return [dpdt + u*dpdx]

# instanciate pde with keys for inputs/outputs
pde = MyPDE(inputs=['x', 't', 'u'], outputs=['p'])

# define input values for training
x_t = np.linspace(0,1,20)
t_t = np.linspace(0,1,30)
u_t = np.linspace(0,1,10)
pde.setValues({'x': x_t, 't': t_t, 'u': u_t})

# periodic b.c for the space dimension
x1, x2 = np.array([0]), np.array([1])
boco = PeriodicBoco('boco', {'x': x1, 't': t_t, 'u': u_t}, {'x': x2, 't': t_t, 'u': u_t})
pde.addBoco(boco)

# initial condition (dirichlet for temporal dimension)
p00, p0 = np.sin(2.*math.pi*x_t), np.array([])
for i in u_t:
    p0 = np.concatenate((p0,p00)) # one for each value of 'u', keeping the order (x, t, u)
boco = DirichletBoco('initial_condition', {'x': x_t, 't': np.array([0]), 'u': u_t}, {'p': p0})
pde.addBoco(boco)

# define input values for validation
x_v = np.linspace(0,1,25)
t_v = np.linspace(0,1,15)
u_v = np.linspace(0,1,5)
pde.setValues({'x': x_v, 't': t_v, 'u': u_v}, train=False)
```

Now we define a topology for our solution and set the training parameters. Then we can find a solution for our PDE.

```python
# define solution topology
mlp = MLP(pde.n_inputs, pde.n_outputs, 5, 2048)
optimizer = torch.optim.Adam(mlp.parameters(), lr=3e-4)
pde.compile(mlp, optimizer)
```

```python
# find the solution
hist = pde.solve(epochs=50)
```

Epoch 1/50 Losses 0.39949
 PDE  [ 0.03317 ]
 boco 0.04164
 initial_condition 0.32468
 Val [ 0.07423 ]
Epoch 2/50 Losses 0.20675
 PDE  [ 0.04072 ]
 boco 0.04337
 initial_condition 0.12265
 Val [ 0.01944 ]
Epoch 3/50 Losses 0.07403
 PDE  [ 0.01658 ]
 boco 0.02439
 initial_condition 0.03307
 Val [ 0.03006 ]
Epoch 4/50 Losses 0.02846
 PDE  [ 0.00848 ]
 boco 0.01021
 initial_condition 0.00976
 Val [ 0.00333 ]
Epoch 5/50 Losses 0.01836
 PDE  [ 0.00458 ]
 boco 0.00837
 initial_condition 0.00541
 Val [ 0.00862 ]
Epoch 6/50 Losses 0.01901
 PDE  [ 0.00595 ]
 boco 0.00749
 initial_condition 0.00557
 Val [ 0.00521 ]
Epoch 7/50 Losses 0.01683
 PDE  [ 0.00390 ]
 boco 0.00801
 initial_condition 0.00493
 Val [ 0.00600 ]
Epoch 8/50 Losses 0.02357
 PDE  [ 0.00678 ]
 boco 0.00945
 initial_condition 0.00734
 Val [ 0.00382 ]
Epoch 9/50 Losses 0.01394
 PDE  [ 0.00361 ]
 boco 0.00594
 initial_condition 0.00440
 Val [ 0.00293 ]
Epoch 10/50 Losses 0.01103
 PDE  [ 0.00280 ]
 boco 0.00510
 initial_condition 0.00313
 Val [ 0.00313 ]
Epoch 11/50 Losses 0.01071
 PDE  [ 0.00322 ]
 boco 0.00450
 initial_condition 0.00299
 Val [ 0.00394 ]
Epoch 12/50 Losses 0.02089
 PDE  [ 0.00515 ]
 boco 0.00923
 initial_condition 0.00651
 Val [ 0.00347 ]
Epoch 13/50 Losses 0.01311
 PDE  [ 0.00344 ]
 boco 0.00597
 initial_condition 0.00370
 Val [ 0.00678 ]
Epoch 14/50 Losses 0.00898
 PDE  [ 0.00265 ]
 boco 0.00375
 initial_condition 0.00258
 Val [ 0.00286 ]
Epoch 15/50 Losses 0.01491
 PDE  [ 0.00444 ]
 boco 0.00622
 initial_condition 0.00425
 Val [ 0.01150 ]
Epoch 16/50 Losses 0.01701
 PDE  [ 0.00535 ]
 boco 0.00659
 initial_condition 0.00507
 Val [ 0.00427 ]
Epoch 17/50 Losses 0.01294
 PDE  [ 0.00367 ]
 boco 0.00566
 initial_condition 0.00360
 Val [ 0.00391 ]
Epoch 18/50 Losses 0.00776
 PDE  [ 0.00226 ]
 boco 0.00336
 initial_condition 0.00214
 Val [ 0.00174 ]
Epoch 19/50 Losses 0.00909
 PDE  [ 0.00258 ]
 boco 0.00394
 initial_condition 0.00257
 Val [ 0.00359 ]
Epoch 20/50 Losses 0.01046
 PDE  [ 0.00268 ]
 boco 0.00459
 initial_condition 0.00319
 Val [ 0.00431 ]
Epoch 21/50 Losses 0.01084
 PDE  [ 0.00379 ]
 boco 0.00388
 initial_condition 0.00317
 Val [ 0.00216 ]
Epoch 22/50 Losses 0.01509
 PDE  [ 0.00353 ]
 boco 0.00677
 initial_condition 0.00478
 Val [ 0.00307 ]
Epoch 23/50 Losses 0.01632
 PDE  [ 0.00329 ]
 boco 0.00706
 initial_condition 0.00597
 Val [ 0.00333 ]
Epoch 24/50 Losses 0.00865
 PDE  [ 0.00248 ]
 boco 0.00396
 initial_condition 0.00221
 Val [ 0.00262 ]
Epoch 25/50 Losses 0.00918
 PDE  [ 0.00224 ]
 boco 0.00411
 initial_condition 0.00283
 Val [ 0.00392 ]
Epoch 26/50 Losses 0.00998
 PDE  [ 0.00243 ]
 boco 0.00462
 initial_condition 0.00293
 Val [ 0.00274 ]
Epoch 27/50 Losses 0.02604
 PDE  [ 0.00447 ]
 boco 0.01039
 initial_condition 0.01117
 Val [ 0.00342 ]
Epoch 28/50 Losses 0.01921
 PDE  [ 0.00422 ]
 boco 0.00789
 initial_condition 0.00710
 Val [ 0.00378 ]
Epoch 29/50 Losses 0.01376
 PDE  [ 0.00327 ]
 boco 0.00586
 initial_condition 0.00463
 Val [ 0.00168 ]
Epoch 30/50 Losses 0.00627
 PDE  [ 0.00188 ]
 boco 0.00245
 initial_condition 0.00194
 Val [ 0.00467 ]
Epoch 31/50 Losses 0.01165
 PDE  [ 0.00294 ]
 boco 0.00474
 initial_condition 0.00396
 Val [ 0.00302 ]
Epoch 32/50 Losses 0.02776
 PDE  [ 0.00738 ]
 boco 0.01073
 initial_condition 0.00966
 Val [ 0.00243 ]
Epoch 33/50 Losses 0.00955
 PDE  [ 0.00298 ]
 boco 0.00384
 initial_condition 0.00273
 Val [ 0.00775 ]
Epoch 34/50 Losses 0.01129
 PDE  [ 0.00302 ]
 boco 0.00428
 initial_condition 0.00399
 Val [ 0.00228 ]
Epoch 35/50 Losses 0.00927
 PDE  [ 0.00241 ]
 boco 0.00418
 initial_condition 0.00267
 Val [ 0.00177 ]
Epoch 36/50 Losses 0.00856
 PDE  [ 0.00262 ]
 boco 0.00368
 initial_condition 0.00227
 Val [ 0.00375 ]
Epoch 37/50 Losses 0.01694
 PDE  [ 0.00568 ]
 boco 0.00632
 initial_condition 0.00494
 Val [ 0.00356 ]
Epoch 38/50 Losses 0.01008
 PDE  [ 0.00280 ]
 boco 0.00408
 initial_condition 0.00320
 Val [ 0.00156 ]
Epoch 39/50 Losses 0.01129
 PDE  [ 0.00282 ]
 boco 0.00491
 initial_condition 0.00356
 Val [ 0.00234 ]
Epoch 40/50 Losses 0.01286
 PDE  [ 0.00394 ]
 boco 0.00490
 initial_condition 0.00402
 Val [ 0.00296 ]
Epoch 41/50 Losses 0.01104
 PDE  [ 0.00315 ]
 boco 0.00437
 initial_condition 0.00352
 Val [ 0.00349 ]
Epoch 42/50 Losses 0.01064
 PDE  [ 0.00305 ]
 boco 0.00416
 initial_condition 0.00342
 Val [ 0.00285 ]
Epoch 43/50 Losses 0.01263
 PDE  [ 0.00330 ]
 boco 0.00509
 initial_condition 0.00425
 Val [ 0.00732 ]
Epoch 44/50 Losses 0.01184
 PDE  [ 0.00325 ]
 boco 0.00442
 initial_condition 0.00417
 Val [ 0.00332 ]
Epoch 45/50 Losses 0.01118
 PDE  [ 0.00296 ]
 boco 0.00478
 initial_condition 0.00344
 Val [ 0.00434 ]
Epoch 46/50 Losses 0.01478
 PDE  [ 0.00332 ]
 boco 0.00602
 initial_condition 0.00544
 Val [ 0.00187 ]
Epoch 47/50 Losses 0.01488
 PDE  [ 0.00349 ]
 boco 0.00615
 initial_condition 0.00523
 Val [ 0.01453 ]
Epoch 48/50 Losses 0.01409
 PDE  [ 0.00388 ]
 boco 0.00528
 initial_condition 0.00493
 Val [ 0.00431 ]
Epoch 49/50 Losses 0.01371
 PDE  [ 0.00418 ]
 boco 0.00523
 initial_condition 0.00430
 Val [ 0.00372 ]
Epoch 50/50 Losses 0.01060
 PDE  [ 0.00241 ]
 boco 0.00470
 initial_condition 0.00350
 Val [ 0.00191 ]

    /home/sebastianm/.local/lib/python3.7/site-packages/numpy/core/fromnumeric.py:3257: RuntimeWarning: Mean of empty slice.
      out=out, **kwargs)
    /home/sebastianm/.local/lib/python3.7/site-packages/numpy/core/_methods.py:161: RuntimeWarning: invalid value encountered in double_scalars
      ret = ret.dtype.type(ret / rcount)

```python
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15,5))
ax1.plot(hist['train_loss'], label="train_loss")
ax1.plot(hist['val_loss'], label="val_loss")
ax1.grid(True)
ax1.legend()
ax1.set_yscale("log")
for boco in pde.bocos:
    ax2.plot(hist['bocos'][boco.name], label=boco.name)
ax2.legend()
ax2.grid(True)
ax2.set_yscale("log")
plt.show()
```

Finally, we can evaluate our solution. Since we trained it for also the parameter *u* we can obtain a solution for any value of *u* without having to perform additional computations.

```python
# evaluate the solution
x = np.linspace(0,1,50)
t = np.linspace(0,1,50)
u = np.linspace(0,1,5)
p, p0, l2 = [], [], []
for _t in t:
    _p, _p0, _l2 = [], [], []
    for _u in u:
        _p0.append(np.sin(2.*math.pi*(x-_u*_t)))
        pde.evaluate({'x': x, 't': np.array([_t]), 'u': np.array([_u])}, device)
        _p.append(pde.outputs['p'])
        _l2.append(np.mean((pde.outputs['p']-np.sin(2.*math.pi*(x-_u*_t)))**2))
    p.append(_p)
    p0.append(_p0)
    l2.append(_l2)

from matplotlib import animation, rc
rc('animation', html='html5')

def plot(x, p, p0, t, l2, u):
    ax.clear()
    #tit = ax.set_title(f"t = {t:.2f}, l2 = {l2:.5f}", fontsize=14)
    tit = ax.set_title(f"t = {t:.2f}", fontsize=14)
    for i, _u in enumerate(u):
        ax.plot(x, p0[i], label=f"Exact (u = {_u})")
        ax.plot(x, p[i], ".k", label=f"NN (u = {_u}, l2 = {l2[i]:.5f})")
    ax.set_xlabel("x", fontsize=14)
    ax.set_ylabel("p", fontsize=14, rotation=np.pi/2)
    ax.legend(loc="upper right")
    ax.grid(True)
    ax.set_xlim([0, 1])
    ax.set_ylim([-1.2, 1.2])
    return [tit]

def get_anim(fig, ax, x, p, p0, t, l2, u):
    def anim(i):
        return plot(x, p[i], p0[i], t[i], l2[i], u)
    return anim

fig = plt.figure(figsize=(10,5))
ax = fig.add_subplot(111, autoscale_on=False)
animate = get_anim(fig, ax, x, p, p0, t, l2, u)
anim = animation.FuncAnimation(fig, animate, frames=len(t), interval=100, blit=True)
```

```python
anim
```


# draw_flowchart
Dengan menggunakan [Mermaid](https://mermaid-js.github.io/mermaid/#/) dapat dibuat beraneka [diagram alir](https://mermaid-js.github.io/mermaid/#/flowchart).

```python
%load_ext nb_js_diagrammers
```

## sign
Suatu variabel $x$ dapat dapat memiliki tanda positif (+1), nol (0) atau negativ (-1) mengikuti

\begin{equation}\tag{1}
{\rm sign}(x) = \left\{
\begin{array}{rl}
+1, & x > 0\newline
0, & x = 0 \newline
-1, & x < 0.
\end{array}
\right.
\end{equation}

Persamaan ([1](#eqn1)) ini dipergunakan salah satunya untuk menentukan arah dari suatu vektor.

## example
Berikut ini adalah contoh penggunaan Mermaid untuk membuat diagram alir menentukan tanda dari suatu bilangan.

```python
%%mermaid_magic -h 720

flowchart TD
    B([ Mulai ]);
    E([ Selesai ]);
    I[/ x /];
    D1{ x > 0};
    D2{ x = 0};
    S1[ sign = 1];
    S2[ sign = 0];
    S3[ sign = -1];
    O[/ sign /];
    B --> I;
    I --> D1;
    D1 -- Y --> S1;
    D1 -- T --> D2;
    D2 -- T --> S3;
    D2 -- Y --> S2;
    S1 --> O;
    S2 --> O;
    S3 --> O;
    O --> E;
```

Diagram alir di atas merupakan penerapan dari menurut Persamaan ([1](#eqn1)).

Untuk dapat menggambarkan diagram alir di atas secara lokal perlu dinstal [nb-js-diagrammers](https://pypi.org/project/nb-js-diagrammers/0.0.7/). Dan untuk melihatnya di GitHub cukup dibuat suatu berkas Markdown seperti [draw_flowchart_sign.md](draw_flowchart_sign.md).

## comparing two number
Salah satu diagram alir untuk membandingkan dua buah bilangan adalah sebagai berikut.

Koreksi kode berikut untuk mendapatkan hasil di atas.

```python
%%mermaid_magic -h 160

flowchart TD
    B([ Mulai ]);
    E([ Selesai ]);
    B --> E;
```

Hasil kode di atas dapat dilihat pada berkas [draw_flowchart_compare_two_numbers.md](draw_flowchart_compare_two_numbers.md).


# **Practice 3: Least Squares and Parameter identification**

## **Identification in the Mechanical systems**

>Suppose that we have mechanical system with dynamics given linearly to parameters ([revise here](https://colab.research.google.com/drive/1UDpCx13PppfQe4u3g0vdq7tpYxaaTujz#scrollTo=exLMblug8v92) ):
\begin{equation}
\mathbf{Q} =
 \mathbf{Y}(\mathbf{q},\dot{\mathbf{q}},\ddot{\mathbf{q}}) \mathbf{p}
\end{equation}
>where:
>* $\mathbf{Q} \in \mathbb{R}^n $ - generalized forces corresponding to
>* $\mathbf{q} \in \mathbb{R}^{n}$ - vector of generilized coordinates
>* $\mathbf{Y} \in \mathbb{R}^{n\times p}$ is so called **regressor matrix**
>* $\mathbf{p} \in \mathbb{R}^{p}$ - is full vector of parameters.
>

A problem is to find dynamical parameters of this system (masses, motor inertias etc) given just set of measurements from the control system, namely actuator torques/forces $\mathbf{Q}$ and joint coordinates $\mathbf{q}$.

To do so we firstly combine all measurements in augmented regressor and force vector:

>\begin{equation}
\bar{\mathbf{Y}} =
\begin{bmatrix}
\mathbf{Y}(t_1) \\
\mathbf{Y}(t_2) \\
\cdots \\
\mathbf{Y}(t_N)
\end{bmatrix}=
\begin{bmatrix}
\mathbf{Y}(\mathbf{q}_1,\dot{\mathbf{q}}_1,\ddot{\mathbf{q}}_1) \\
\mathbf{Y}(\mathbf{q}_2,\dot{\mathbf{q}}_2,\ddot{\mathbf{q}}_2) \\
\cdots \\
\mathbf{Y}(\mathbf{q}_N,\dot{\mathbf{q}}_N,\ddot{\mathbf{q}}_N)
\end{bmatrix},
\bar{\mathbf{Q}} =
\begin{bmatrix}
\mathbf{Q}(t_1) \\
\mathbf{Q}(t_2) \\
\cdots \\
\mathbf{Q}(t_N)
\end{bmatrix}
\end{equation}

Now we just need to solve $\bar{\mathbf{Q}} = \bar{\mathbf{Y}}\mathbf{p}$ for $\mathbf{p}$, however a number of rows in $\bar{\mathbf{Y}}$ is typically much larger then numer of columns, thus we can siolve this system by means of minimization of some cost, a common one is:
\begin{equation}
\mathcal{J}_c = \| \bar{\mathbf{Q}} - \bar{\mathbf{Y}}\mathbf{p} \|^2_2
\end{equation}

Which you already know how to solve, just using Moore- Penrose pseudo inverse:
\begin{equation}
\hat{\mathbf{p}} = \underset{\mathbf{x}}{{\text{argmin}}}{\mathcal{J_c}} = \bar{\mathbf{Y}}^+\bar{\mathbf{Q}} =\bar({\bar{\mathbf{Y}}}^T\bar{\mathbf{Y}})^{-1}\bar{\mathbf{Y}}^T\bar{\mathbf{Q}}
\end{equation}

Let us now try to use this solution on the particular system.

## **Case Study: Remotely Driven 2DoF Manipulator**

During this practice we will do this on the remotely driven manipulator that we have studied in our previous classes:

To do so we firstly recall regressor of this manipulator:

\begin{equation}
\mathbf{Y} =
\begin{bmatrix}
 \ddot{\alpha_1} l_{1}^2 + l_{1} g \cos \alpha_1 & \ddot{\alpha_1} &
\dot{\alpha_1} &
l_{1} \left(\ddot{\alpha_1} l_{1} + \ddot{\alpha_2} l_{2} \cos{\left (\alpha_1 - \alpha_2 \right )} + \dot{\alpha_2}^{2} l_{2} \sin{\left (\alpha_1 - \alpha_2 \right )} + g \cos{\left (\alpha_1 \right )}\right)
& 0 & 0\\
0 & 0 & 0 &
l_{2} \left(\ddot{\alpha_1} l_{1} \cos{\left (\alpha_1 - \alpha_2 \right )} + \ddot{\alpha_2} l_{2} - \dot{\alpha_1}^{2} l_{1} \sin{\left (\alpha_1 - \alpha_2 \right )} + g \cos{\left (\alpha_2 \right )}\right) &
\ddot{\alpha_2} &
\dot{\alpha_2}\end{bmatrix}
\end{equation}

That is written with respect to following parameters:

\begin{equation}
\mathbf{p}  = [m_1, J_1, b_1, m_2, J_2, b_2]^T
\end{equation}

```
from numpy import sin, cos, zeros

def regressor(q, dq, ddq, l, g):

    alpha_1, alpha_2 = q
    dalpha_1, dalpha_2 = dq
    ddalpha_1, ddalpha_2 = ddq

    Y = zeros((2,6))
    # Joint 1
    Y[0,0] = l[0]**2 * ddalpha_1 + l[0]*g*cos(alpha_1)
    Y[0,1] = ddalpha_1
    Y[0,2] = dalpha_1
    Y[0,3] = l[0]*l[1]*ddalpha_1 + l[0]*l[1]*cos(alpha_1 - alpha_2)*ddalpha_2\
             + l[0]*l[1]*sin(alpha_1 - alpha_2)*dalpha_2**2\
             + g*l[0]*cos(alpha_1)
    # Joint 2
    Y[1,3] = l[0]*l[1]*cos(alpha_1 - alpha_2)*ddalpha_1 + l[1]**2 * ddalpha_2\
             - l[0]*l[1]*sin(alpha_1 - alpha_2)*dalpha_1**2\
             + g*l[1]*cos(alpha_2)
    Y[1,4] = ddalpha_2
    Y[1,5] = dalpha_2

    return Y

```

There are three type of trajectories are given for you:

```
from numpy import loadtxt
from matplotlib.pyplot import plot, xlim, ylabel, xlabel, show
traj_types = ['slow', 'normal', 'fast']

t, q, Q = dict(), dict(), dict()
for traj_type in traj_types:
  data = loadtxt('traj_'+ traj_type + '.csv', delimiter=',')
  t[traj_type] = data[:,0]
  q[traj_type] = data[:,1:3]
  Q[traj_type] = data[:,3:]

  print('Angles and torques in joints for ' + traj_type + ' trajectory')
  plot(t[traj_type],q[traj_type])
  xlim([t[traj_type][0], t[traj_type][-1]])
  grid(color='black', linestyle='--', linewidth=1.0, alpha = 0.7)
  grid(True)
  ylabel(r'Angles ${q}$ (rad)')
  xlabel(r'Time $t$ (s)')
  show()

  plot(t[traj_type],Q[traj_type])
  xlim([t[traj_type][0], t[traj_type][-1]])
  grid(color='black', linestyle='--', linewidth=1.0, alpha = 0.7)
  grid(True)
  ylabel(r'Torques ${Q}$ (N/m)')
  xlabel(r'Time $t$ (s)')
  show()

```

Since regressor contain velocity as well as acceleration we need to defirentiate position in order to obtain them:

```
from numpy import gradient
dq, ddq = dict(), dict()

for traj_type in traj_types:
  dq[traj_type] = gradient(q[traj_type], t[traj_type], axis = 0)
  ddq[traj_type] = gradient(dq[traj_type], t[traj_type], axis = 0)

  print('Velocity and accileration for ' + traj_type + ' trajectory')
  plot(t[traj_type],dq[traj_type])
  xlim([t[traj_type][0], t[traj_type][-1]])
  grid(color='black', linestyle='--', linewidth=1.0, alpha = 0.7)
  grid(True)
  ylabel(r'Velocity $\dot{q}$ (rad/s)')
  xlabel(r'Time $t$ (s)')
  show()

  plot(t[traj_type],ddq[traj_type])
  xlim([t[traj_type][0], t[traj_type][-1]])
  grid(color='black', linestyle='--', linewidth=1.0, alpha = 0.7)
  grid(True)
  ylabel(r'Acceleration $\ddot{q}$ (rad/s$^2$)')
  xlabel(r'Time $t$ (s)')
  show()
```

>**QUESTION**:
* What do you think, can we filter velocity, acceleration and torque before calculating regressor? If not explain how you would reduce noise.

Procceding with regressor calculations and augmenting torques:

```
g = 9.81
l = 0.5, 0.35
Q_bar, Y_bar = dict(), dict()
for traj_type in traj_types:
  Q_bar[traj_type] = zeros(len(t[traj_type])*2)
  Y_bar[traj_type] = zeros((2*len(t[traj_type]),6))
  for i in range(len(t[traj_type])):
    Q_bar[traj_type][2*i:2*(i+1)] = Q[traj_type][i,:]
    Y = regressor(q[traj_type][i,:], dq[traj_type][i,:], ddq[traj_type][i,:], l, g)
    Y_bar[traj_type][2*i:2*(i+1),:] = Y
```

A natural question to ask is to which trajectory is better in terms of system identification.

To answer such question you may think of equation $\bar{\mathbf{Q}} = \bar{\mathbf{Y}}\mathbf{p}$ as linear mapping between parameter and force spaces:

So in order to identify **all parameters** of our system we should be able to somehow inverse this mapping (by using pseudo-inverse for instance), thus one criteria is cruitial
$\sigma_{min} \neq 0$, a 'good' trajectory for identification should in fact minimize the deviation of condition number from one:
\begin{equation}
\mathcal{O}_1 = \kappa - 1 = \frac{\sigma_{max}}{\sigma_{min}} - 1
\end{equation}

```
from numpy.linalg import cond

for traj_type in traj_types:
  print('Condition number k(Y) for ' + traj_type + ' trajectory', cond(Y_bar[traj_type]))
```

    Condition number k(Y) for slow trajectory 12.621985808225547
    Condition number k(Y) for normal trajectory 6.5636486660573965
    Condition number k(Y) for fast trajectory 19.876553635979676

>**HW EXERCISE**:
* Try to explain why is it decireble to minimize $\mathcal{O}_1$
*  Proof that in order to use pseudo-inverse you should have $\sigma_{min} \neq 0$.
*  Is it always possible to identify all parameter of general mechanical system? Provide example where it is not the case.
*  What is physical interpritation of the **Column space** $\mathcal{C}(\bar{\mathbf{Y}})$ and **Null space** $\mathcal{N}(\bar{\mathbf{Y}})$?

>**BONUS**:
>*   Try to find such trajectory (just by trials) that minimize $\mathcal{O}_{1}$ while maximizing $\mathcal{O}_{2} = \prod_i \sigma_i$

So now we are ready to solve our problem:

```
from numpy.linalg import pinv

p_hat = dict()

for traj_type in traj_types:Sure
  p_hat[traj_type] = dot(pinv(Y_bar[traj_type]),Q_bar[traj_type])
  print('Estimated parameters for ' + traj_type + ' trajectory', p_hat[traj_type])
```

    Estimated parameters for slow trajectory [1.10516396 0.01064903 0.01516329 1.50718101 0.00854068 0.02320772]
    Estimated parameters for normal trajectory [1.10255084 0.00963239 0.01955347 1.50960089 0.0109886  0.01746008]
    Estimated parameters for fast trajectory [1.10303712 0.0091489  0.02029594 1.50870946 0.01117695 0.0155727 ]

>**HW EXERCISE**:
* Compare estimated parameters with following:
  * Masses $m_1 = 1.10$ [kg], $m_1 = 1.51$ [kg],
  * Motor inertias: $I_1 = 9.8\cdot10^{-3}$ [kg m$^2$], $I_2 = 11\cdot10^{-3}$ [kg m$^2$]
  * Joint friction: $b_1 = 21\cdot10^{-2}$ [kg m$^2$], $b_2 = 17\cdot10^{-2}$ [kg m$^2$]
* Which one are more closer in terms of norm $\|\mathbf{p} - \hat{\mathbf{p}}\|_2$?
* Compare accuracy of model predictions of different parameter estimates on different trajectories by calculating mean and standart deviation.

### **Quadratic Programming Solution**
---
In the last lecture you have studied how to solve the optimization problems in form:

\begin{equation}
\begin{aligned}
& \underset{\mathbf{x}}{\text{minimize}}
& & \mathbf{x}^T \mathbf{H} \mathbf{x} + \mathbf{g}^T\mathbf{x}, \\
& \text{subject to}
& & \begin{cases}
    \mathbf{A}\mathbf{x} \leq \mathbf{b}, \\
    \mathbf{C}\mathbf{x} = \mathbf{d}.
    \end{cases}
\end{aligned}
\end{equation}

Let us know reformulate our problem as QP and compare the solution with analytical one given by pseudo-inverse:

A cost function $\mathcal{J}_c:
\begin{equation}
\mathcal{J}_c = \| \bar{\mathbf{Q}} - \bar{\mathbf{Y}}\mathbf{p} \|^2_2 = (\bar{\mathbf{Q}} - \bar{\mathbf{Y}}\mathbf{p})^T(\bar{\mathbf{Q}} - \bar{\mathbf{Y}}\mathbf{p}) =  \mathbf{p}^T\bar{\mathbf{Y}}^T\bar{\mathbf{Y}}\mathbf{p} + \bar{\mathbf{Q}}^T\bar{\mathbf{Y}}\mathbf{p} + \bar{\mathbf{Q}}^T\bar{\mathbf{Q}}
\end{equation}

Which is in standart QP form with folowing Hessian and gradient:
\begin{equation}
\mathbf{H} = \bar{\mathbf{Y}}^T\bar{\mathbf{Y}}, \quad \mathbf{g} = {\bar{\mathbf{Y}}^T\bar{\mathbf{Q}}}
\end{equation}

```
!pip install quadprog
from quadprog import *
```

    Requirement already satisfied: quadprog in /usr/local/lib/python3.6/dist-packages (0.1.7)
    Requirement already satisfied: Cython in /usr/local/lib/python3.6/dist-packages (from quadprog) (0.29.21)

```
p_hat_qp = dict()

for traj_type in traj_types:
  H = dot(Y_bar[traj_type].transpose(), Y_bar[traj_type])
  g = dot(Y_bar[traj_type].transpose(), Q_bar[traj_type])
  p_hat_qp[traj_type] = solve_qp(H,g)[0]
  print('Estimated parameters via QP for ' + traj_type + ' trajectory', p_hat_qp[traj_type])

```

    Estimated parameters via QP for slow trajectory [1.10516396 0.01064903 0.01516329 1.50718101 0.00854068 0.02320772]
    Estimated parameters via QP for normal trajectory [1.10255084 0.00963239 0.01955347 1.50960089 0.0109886  0.01746008]
    Estimated parameters via QP for fast trajectory [1.10303712 0.0091489  0.02029594 1.50870946 0.01117695 0.0155727 ]

Which exactly coincides with results obtained by pseudo inverse.

>**HW EXERCISE**:
* Explain why result obtained by pesudo-inverse and quadratic programming coincide.
* What are possible pros and cons of the QP based approach in identification?
* Suppose there is a noise in one of the torques is much higher than in the other one, how would you modify cost function to tackle this issue?
* Explain connection between singular values of augmented regressor $\sigma(\bar{\mathbf{Y}})$ and properties of Hessian $\mathbf{H}$


# NX31 Model Project

___
DELETE BEFORE HANDIN!

Notes, and things to remember before handing in!

- notes in parentheses
- ( lav docstrings til funktionerne! )
- (import packages - erase those we do not use)
- Clear difference between numerical variables and analytical variable for math eg. n_v vs. n_a
___

# Introduction

We will examine the overlapping generations model (OLG), first the general OLG model with consumption and savings, second will expand with one extensions, as the Fully Funded (FF) method where the young generation pay a tax on their wages.
In the OLG model there is typically two generations, a generation working and a generation living on pension.
The FF model is like a state pension where the young generation pays to the current retired generation. When the young generation retires, the new young generation will pay a tax and thus fund the pensionscheme.

```python
#Loads packages
import numpy as np
import scipy as sp
from scipy import linalg
from scipy import optimize
from scipy import interpolate
import sympy as sm

%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
import ipywidgets as widgets
```

# The General OLG model:

As mentioned, we start with our general OLG model with just savings and consumption. This model looks as shown below.

Population growth or Labor growth:

\begin{eqnarray*}
L_t=(1+n) L_{t-1}
\end{eqnarray*}

Utility:

\begin{eqnarray*}
U_t=ln(c_{1,t} )+ \frac{1}{(1+\rho)}ln(c_{2,t+1} )
\end{eqnarray*}

Contraints:

\begin{eqnarray*}
ρ>-1
\end{eqnarray*}
\begin{eqnarray*}
c_{1,t}=w_t-s_t
\end{eqnarray*}
\begin{eqnarray*}
c_{2,t+1}=(1+r_{t+1} ) s_t
\end{eqnarray*}

where

- $L$ is population
- $n$ is the rate of population
- $U$ is lifetime utility
- $c_1$ is consumption as young
- $c_2$ is consumption as old
- $\rho$ is the discount rate on consumption
- $w$ is wage
- $s$ is savings
- $r$ is the interest rate

## The firm problem

The firms optimization problem will be the same in all extensions of the OLG model.

The output function is given by:

\begin{eqnarray*}
F(K_t,L_t)=Y_t=A(K_t)^\alpha(L_t)^{1-\alpha}
\end{eqnarray*}

The profit function is given by:

\begin{eqnarray*}
\Pi(K_t,L_t)=F(K_t,L_t)-w_tL_t-(r_t+\delta)K_t
\end{eqnarray*}

- $Y$ is output
- $A$ is total factor productivity (TFP)
- $K$ is capital
- $L$ is population / labor force
- $\alpha$ is the share of capital
- $\Pi$ is profit
- $w$ is wage
- $r$ is the real interest rate
- $\delta$ is the degree of depreciation

We will calculate our marginal products, r and w,  using the sympy package. We start off by defining variables as symbols.

```python
# Enables sympy to write functions as math
sm.init_printing(use_unicode=True)
```

```python
# Defines variables for sympy
K = sm.symbols('K')
L = sm.symbols('L')
A = sm.symbols('A')
alpha = sm.symbols('alpha')
delta = sm.symbols('delta')
rho = sm.symbols('rho')
w = sm.symbols('w')
r = sm.symbols('r')
y = sm.symbols('y')
n = sm.symbols('n')
k = sm.symbols('k')
Pi = sm.symbols('Pi')
s = sm.symbols('s')
U = sm.symbols('U')
r_plus = sm.symbols('r_t+1')
```

Now that the symbols are defined, we now setup the functions and solve using the sympy package.

```python
# Solves the firm problem and finds r and w
Pi = A*(K**alpha)*(L**(1-alpha))-w*L-(r+delta)*K
print(Pi)
f1 = sm.diff(Pi,K)
f2 = sm.diff(Pi,L)
print(f1)
print(f2)
f1solve = sm.solve(A*K**alpha*L**(-alpha + 1)*alpha/K - delta - r, r)[0]
f2solve = sm.solve(A*K**alpha*L**(-alpha + 1)*(-alpha + 1)/L - w, w)[0]
print(f1solve)
print(f2solve)
print(f'r = (A*K**alpha*L*L**(-alpha)*alpha/K - delta)')
print(f'w = (-A*K**alpha*L**(-alpha)*(alpha - 1))')
```

    A*K**alpha*L**(-alpha + 1) - K*(delta + r) - L*w
    A*K**alpha*L**(-alpha + 1)*alpha/K - delta - r
    A*K**alpha*L**(-alpha + 1)*(-alpha + 1)/L - w
    A*K**alpha*L*L**(-alpha)*alpha/K - delta
    -A*K**alpha*L**(-alpha)*(alpha - 1)
    r = (A*K**alpha*L*L**(-alpha)*alpha/K - delta)
    w = (-A*K**alpha*L**(-alpha)*(alpha - 1))

```python
f1
```

```python
f2
```

```python
f1solve
```

```python
f2solve
```

Now that we have the marginal products of capital and labors. We are interested in having the results giving with capital pr. capita, $k$.

The marginal products of capital (r) and labor (w) can be written as.

\begin{eqnarray*}
r_t = \alpha Ak_t^{\alpha-1}-\delta
\end{eqnarray*}

\begin{eqnarray*}
w_t = (1-\alpha) Ak_t^{\alpha}
\end{eqnarray*}

## The household problem

Next we will have to solve the household problem, by finding a expression for savings, $s$. Below we once again use the sympy package to solve the household problem and thus finds the savings rate.

```python
# Solve the household problem and find s
U = sm.log(w-s)+(1/(1+rho))*sm.log((1+r_plus)*s)
U_d = sm.diff(U,s)
u = sm.Eq(s,U_d)
print(u)
usolve = sm.solve(-1/(-s + w) + 1/(s*(rho + 1)),s)[0]
print(usolve)
print(f's = w/(rho + 2)')
```

    Eq(s, -1/(-s + w) + 1/(s*(rho + 1)))
    w/(rho + 2)
    s = w/(rho + 2)

## Finding steady state

In order to reach a steady state for $k$, we have to setup the transitions curve for capital. Below we use the model and the results we have found, to reach an expression for steady state $k$.

The aggregated solution in the OLG model is defined as
\begin{eqnarray*}
K_{t+1}=K_t (1-δ)+S_{1,t}+S_{2,t+1}, S_{2,t+1}≡-(1-δ) K_t=0
\end{eqnarray*}
Because there are no savings in second period and we have assumed full depreciation we get
\begin{eqnarray*}
K_{t+1}=S_{1,t}=s_t L_t
\end{eqnarray*}
Now we set up the transition curve
\begin{eqnarray*}
\frac{K_{t+1}}{L_{t+1}} =k_{t+1}=s_t  \frac{L_t}{L_{t+1}} =s_t  \frac{L_t}{(L_t (1+n) )}=\frac{s_t}{(1+n)}=\frac{w_t}{(2+ρ)}  \frac{1}{(1+n)}=\frac{(1-α)Ak_t^α}{(2+ρ)(1+n)}
\end{eqnarray*}

Next up we solve it using sympy, and find en expression for steady state $k$

```python
# Solves transition curve to find SS k
ss = sm.Eq(k,(((1-alpha)*A*(k**alpha))/((2+rho)*(1+n))))
kss = sm.solve(ss,k)[0]
kss
```

### Analytical solution

```python
#Creates SS function for k
ss_func = sm.lambdify((alpha,n,rho,A),kss)
```

```python
# Solves SS for k, given input
ss_func(1/3,0.02,0.04,1)
```

### Numerical solution

```python
# Defines global values for parameters
alpha_v = 1/3
n_v = 0.02
rho_v = 0.04
A_v = 1
```

```python
obj_kss = lambda kss: kss - ((1-alpha_v)*A_v*(kss**alpha_v))/((2+rho_v)*(1+n_v))
result = optimize.root_scalar(obj_kss,bracket=[0.1,100],method='brentq')
result
```

          converged: True
               flag: 'converged'
     function_calls: 8
         iterations: 7
               root: 0.18135001783804428

```python
print(f'Analytical solution is: {ss_func(alpha_v,n_v,rho_v,A_v):.3f}')
print(f'Numerical solution is: {result.root:.3f}')
```

    Analytical solution is: 0.181
    Numerical solution is: 0.181

## Visualization

### Interactive Figures

Below we plot the interactive figures showing the transition curve

```python
# Defining functions
def trans_curve(k, alpha, A, rho, n):
    return (1-alpha)*A*k**alpha/(2+rho)*(1+n)

```

```python
def plot1(alpha):
    print(f"The analytic solution is {ss_func(alpha, n_v,rho_v,A_v):.2f}")
    k_start = 0.01
    numbers = [k_start]
    for i in range(1,10):
        numbers.append(trans_curve(numbers[i - 1], alpha, A_v, rho_v, n_v))
    plt.xlabel('k_t')
    plt.ylabel('k_t+1')
    plt.title('General model - change in $\\alpha$')
    plt.plot([i for i in range(len(numbers))], numbers)
    return plt
    return numbers

def plot2(rho):
    print(f"The analytic solution is {ss_func(alpha_v, n_v,rho,A_v):.2f}")
    k_start = 0.01
    numbers = [k_start]
    for i in range(1,10):
        numbers.append(trans_curve(numbers[i - 1], alpha_v, A_v, rho, n_v))
    plt.xlabel('k_t')
    plt.ylabel('k_t+1')
    plt.title('General model - change in $\\rho$')
    plt.plot([i for i in range(len(numbers))], numbers)
    return plt
    return numbers

def plot3(n):
    print(f"The analytic solution is {ss_func(alpha_v, n,rho_v,A_v):.2f}")
    k_start = 0.01
    numbers = [k_start]
    for i in range(1,10):
        numbers.append(trans_curve(numbers[i - 1], alpha_v, A_v, rho_v, n))
    plt.xlabel('k_t')
    plt.ylabel('k_t+1')
    plt.title('General model - change in n')
    plt.plot([i for i in range(len(numbers))], numbers)
    return plt
    return numbers

#widgets.interact(trans_curve, k=widgets.IntSlider(min=0, max=10, step=1, value=0))

#plt.subplot(2, 2, 2)
#plt.xlabel('k')
#plt.ylabel('k_t+1')
#plt.title('Change in population growth')
#plt.plot(k, y)
#
#widgets.interact(
#        trans_curve,
#        alpha_v = widgets.IntSlider(
#            description="$\\alpha$",
#            min=0.01,
#            max=0.99,
#            step=0.01,
#            value=0.33,
#            continuous_update=False,
#        ),
#    )
#
#plt.subplot(2, 2, 3)
#plt.xlabel('k')
#plt.ylabel('k_t+1')
#plt.title('Change in rho')
#plt.plot(k, y)
#plt.subplot(2, 2, 4)
#plt.xlabel('k')
#plt.ylabel('k_t+1')
#plt.title('Change in population alpha')
#plt.plot(k, y)

widgets.interact(
        plot1,
        alpha = widgets.FloatSlider(
            description="$\\alpha$",
            min=0.01,
            max=0.99,
            step=0.01,
            value=alpha_v,
            continuous_update=False,
        ),
    )

widgets.interact(
        plot2,
        rho = widgets.FloatSlider(
            description="$\\rho$",
            min=-.99,
            max=1,
            step=0.1,
            value=rho_v,
            continuous_update=False,
        ),
    )
widgets.interact(
        plot3,
        n = widgets.FloatSlider(
            description="n",
            min=0.00,
            max=0.1,
            step=0.01,
            value=n_v,
            continuous_update=False,
        ),
    )
```

    interactive(children=(FloatSlider(value=0.3333333333333333, continuous_update=False, description='$\\alpha$', …

    interactive(children=(FloatSlider(value=0.04, continuous_update=False, description='$\\rho$', max=1.0, min=-0.…

    interactive(children=(FloatSlider(value=0.02, continuous_update=False, description='n', max=0.1, step=0.01), O…

    <function __main__.plot3(n)>

# NOTES !

a: Guess on a k_ss, could be 1.6, or atleast lets start with that.
b: Calculate r_ss and w_ss
c: Solve the household problem
d: Calculate S (for the young)
e: k_sst (true) = s/1-n
f: Visualize your guesses

The true is the k_ss at the 45 degree line

g: k_sst-k_ss = 0

Tilbage til punkt a hvis nr. g ikke passer.

```python
##a: Guess
#k_ss = 1
#delta_v=1
#r_ss = sm.symbols('r_ss')
#w_ss = sm.symbols('w_ss')
#
#def r(k,alpha,A,delta):
#    return alpha*A*k**(alpha-1)-delta
#def w(k,alpha,A):
#    return (1-alpha)*A*k**alpha
#def c1_gen(w,s):
#    return w-s
#def c2_gen(r,s):
#    return (1+r)*s
#def utility(c1,c2,rho_v):
#    rho_v>-1
#    return np.log(c1) + (1/(1+rho_v))*np.log(c2)
#
##b: Calculate r_ss and w_ss for guess
#r_ss = r(k_ss,alpha_v,A_v,delta_v)
#w_ss = w(k_ss,alpha_v,A_v)
#print('Interest Rate Steady State: ',r_ss)
#print('Wages Steady State: ',w_ss)
#
##c: Solve household problem
#s_ss = w_ss/(2+rho_v)
#
#print('Savings Steady State: ',s_ss)
#
#c1 = c1_gen(w_ss,s_ss)
#c2 = c2_gen(r_ss,s_ss)
#util = utility(c1,c2,rho_v)
#print('Consumption 1 Steady State: ', c1)
#print('Consumption 2 Steady State: ', c2)
#print('Utility Steady State: ', util)
#
#
#k_sst = ((1-alpha_v)/((2+rho_v)*(1+n_v)))**(1/(1-alpha_v))
#print(k_sst)
#
#k_ss-k_sst == 0
```

### functions

```python
def output(K,L,alpha,A):
    return A*(K**alpha)*(L**(1 - alpha))
def profit(output,w,L,r,K,delta=1):
    return output-w*L-(r+delta)*K
```

```python
#Solving the general model gives:
def r_payg(k,alpha,A,delta):
    return alpha*A*k**(alpha-1)-delta
def w_payg(k,alpha,A):
    return (1-alpha)*A*k**alpha
```

PAYG OLG model

Utility...

\begin{eqnarray*}
U_t=ln(c_{1,t} )+ \frac{1}{(1+\rho)}ln(c_{2,t+1} )
\end{eqnarray*}

s.t.

\begin{eqnarray*}
ρ>-1
\end{eqnarray*}
\begin{eqnarray*}
c_{1,t}=w_t(1-d)-s_t
\end{eqnarray*}
\begin{eqnarray*}
c_{2,t+1}=(1+r_{t+1} ) s_t + w_{t+1}d(1+n)
\end{eqnarray*}

where

- $L$ is population
- $n$ is the rate of population
- $U$ is lifetime utility
- $c_1$ is consumption as young
- $c_2$ is consumption as old
- $\rho$ is the discount rate
- $w$ is wage
- $s$ is savings
- $r$ is the interest rate

```python
def c1_payg(w,s,d):
    return w(1-d)-s

def c2_payg(r,s,w,d,n):
    return (1+r)*s+wd(1+n)

d = 0.12
```

FF model

Utility...

\begin{eqnarray*}
U_t=ln(c_{1,t} )+ \frac{1}{(1+\rho)}ln(c_{2,t+1} )
\end{eqnarray*}

s.t.

\begin{eqnarray*}
ρ>-1
\end{eqnarray*}
\begin{eqnarray*}
c_{1,t}=w_t(1-\tau)-s_t
\end{eqnarray*}
\begin{eqnarray*}
c_{2,t+1}=(1+r_{t+1} ) s_t + \tau(1+r_{t+1})w_t
\end{eqnarray*}

where

- $L$ is population
- $n$ is the rate of population
- $U$ is lifetime utility
- $c_1$ is consumption as young
- $c_2$ is consumption as old
- $\rho$ is the discount rate
- $w$ is wage
- $s$ is savings
- $r$ is the interest rate

```python
def c1_ff(w,s,tau):
    return w(1-tau)-s

def c2_ff(r,s,tau,w):
    return (1+r)*s+tau*(1+r)*w

tau = 0.13
```

```python
def c1_gen(w,s):
    return w-s

def c2_gen(r,s):
    return (1+r)*s

def utility(c1,c2,rho):
    rho>-1
    return np.log(c1)+(1/(1+rho))*np.log(c2)
```


# 图(Graph)的引入

数据之间的关系有 3 种，分别是 "一对一"、"一对多" 和 "多对多"，前两种关系的数据可分别用线性表和树结构存储，存储具有"多对多"逻辑关系数据的结构——图存储结构。

1. 在线性结构中，数据元素之间满足唯一的线性关系，每个数据元素(除第一个和最后一个外)只有一个直接前趋和一个直接后继；
2. 在树形结构中，数据元素之间有着明显的层次关系，并且每个数据元素只与上一层中的一个元素(parent node)及下一层的多个元素(孩子节点)相关；
3. 而在图形结构中，节点之间的关系是任意的，图中任意两个数据元素之间都有可能相关。

针对非欧几里得结构化数据表示问题，研究者们引入了图论中抽象意义上的图(Graph)来表示非欧几里得结构化数据, 图(Graph)定义形式：$G = (V, E)$, 其中 $V = \{ v_{i} | i = 1, ..., N \}$ 表示顶点或是节点， 其中N表示节点的个数。 $E = \{e_{ij} | v_i, v_j \in V\}, |E| \leq N^2$ 表示顶点与顶点之间所连接的边。

For example, 如下图所示， $V = \{1, 2, 3, 4, 5, 6\}$, 边集 $E = \{\{1, 2\}, \{1, 5\}, \{2, 3\}, \{2, 5\}, \{3, 4\}, \{4, 5\}, \{4, 6\}\}$

# 图的分类

1. **无向图（Undirected Graph）：** 在无向图中，每个顶点和其他顶点通过相连线连接。
2. **有向图（Directed Graph）：** 有向图中的相连线是有方向的.
3. **权重图（Weighted Graph）：** 在权重图中，每条相连线有各自的权重.
4. **多重图（Multigraph）：** 图中某两个顶点之间的边数多于一条，又允许顶点通过同一条边和自己关联.

根据不同的特征，图又可分为完全图，连通图、稀疏图和稠密图：
1. **完全图**：若图中各个顶点都与除自身外的其他顶点有关系，这样的无向图称为完全图（如图 a)）。同时，满足此条件的有向图则称为有向完全图（图 b)）. 具有 n 个顶点的完全图，图中边的数量为 n(n-1)/2；而对于具有 n 个顶点的有向完全图，图中弧的数量为 n(n-1)。

2. **稀疏图和稠密图：** 这两种图是相对存在的，即如果图中具有很少的边（或弧），此图就称为"稀疏图"；反之，则称此图为"稠密图"。  稀疏和稠密的判断条件是：$e<nlogn$，其中 e 表示图中边（或弧）的数量，n 表示图中顶点的数量。如果式子成立，则为稀疏图；反之为稠密图。

# The Definition of “graph” and Terminologies

“Graph” is a non-linear data structure consisting of vertices and edges. There are a lot of terminologies to describe a graph. If you encounter an unfamiliar term in the following Explore Card, you may look up the definition below.

1. **Vertex:** In Figure 1, nodes such as A, B, and C are called vertices of the graph.

2. **Edge:** The connection between two vertices are the edges of the graph. In Figure 1, the connection between person A and B is an edge of the graph.

3. **Path**: the sequence of vertices to go through from one vertex to another. In Figure 1, a path from A to C is [A, B, C], or [A, G, B, C], or [A, E, F, D, B, C]. **Note**: there can be multiple paths between two vertices.

4. **Path Length:** the number of edges in a path. In Figure 1, the path lengths from person A to C are 2, 3, and 5, respectively.

5. **Cycle:** a path where the starting point and endpoint are the same vertex. In Figure 1, [A, B, D, F, E] forms a cycle. Similarly, [A, G, B] forms another cycle.

6. **Negative Weight Cycle:** In a “weighted graph”, if the sum of the weights of all edges of a cycle is a negative value, it is a negative weight cycle. In Figure 4, the sum of weights is -3.

7. **Connectivity:** if there exists at least one path between two vertices, these two vertices are connected. In Figure 1, A and C are connected because there is at least one path connecting them.

8. **Degree of a Vertex:** the term “degree” applies to unweighted graphs. The degree of a vertex is the number of edges connecting the vertex. In Figure 1, the degree of vertex A is 3 because three edges are connecting it.
- In-Degree: “in-degree” is a concept in directed graphs. If the in-degree of a vertex is d, there are d directional edges incident to the vertex. In Figure 2, A’s indegree is 1, i.e., the edge from F to A.
- Out-Degree: “out-degree” is a concept in directed graphs. If the out-degree of a vertex is d, there are d edges incident from the vertex. In Figure 2, A’s outdegree is 3, i,e, the edges A to B, A to C, and A to G.

# 图的特性

## 图的顶点

1. 对于无向图，顶点V的度（Degree) 表示以该顶点作为一个端点的边的数目, D(V)
2. 对于有向图，顶点V的度分为入度(Indegree, 记为 ID(V))和出度(OutDegree，记为OD(V))。入度表示以该顶点为终点的入边数目，出度是以该顶点为起点的出边数目，该顶点的度等于其入度和出度之和D(V) = ID(V) + OD(V)。无箭头一端的顶点通常被称为"初始点"或"弧尾"，箭头直线的顶点被称为"终端点"或"弧头"。

不管是无向图还是有向图，顶点数n，边数e和顶点的度数有如下关系:
$$e = \frac{1}{2}\sum^{n}_{i=1}D(v_i)$$

## 图的边

**(V1,V2) 和 <V1,V2> 的区别**
1. 无向图中描述两顶点（V1 和 V2）之间的关系可以用 (V1,V2) 来表示，而有向图中描述从 V1 到 V2 的"单向"关系用 <V1,V2> 来表示。
2. 由于图存储结构中顶点之间的关系是用线来表示的，因此 (V1,V2) 还可以用来表示无向图中连接 V1 和 V2 的线，又称为**边**；同样，<V1,V2> 也可用来表示有向图中从 V1 到 V2 带方向的线，又称为**弧**。

**集合 VR 的含义**

图中习惯用 VR 表示图中所有顶点之间关系的集合。例如，图2 中无向图的集合 VR={(v1,v2),(v1,v4),(v1,v3),(v3,v4)}，图3 中有向图的集合 VR={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>}。

  图 2： 无向图          |  图 3： 有向图
:-------------------------:|:-------------------------:
  |

## 路径、路径长度和回路

无论是无向图还是有向图，从一个顶点到另一顶点途径的所有顶点组成的序列（包含这两个顶点），称为一条**路径**。如果路径中第一个顶点和最后一个顶点相同，则此路径称为**"回路"（或"环"）**。

并且，若路径中各顶点都不重复，此路径又被称为 **"简单路径"**；同样，若回路中的顶点互不重复，此回路被称为 **"简单回路"（或简单环）**。

拿左上图来说，从 V1 存在一条路径还可以回到 V1，此路径为 {V1,V3,V4,V1}，这是一个回路（环），而且还是一个简单回路（简单环）

在有向图中，每条路径或回路都是有方向的。

## 权和网的含义, 子图

在某些实际场景中，图中的每条边（或弧）会赋予一个实数来表示一定的含义，这种与边（或弧）相匹配的实数被称为"权"，而带权的图通常称为网。如下图所示，就是一个网结构：

**子图**：指的是由图中一部分顶点和边构成的图，称为原图的子图。

## 连通图

**连通图**： 无向图中，如果任意两个顶点之间都能够连通，则称此无向图为连通图。例如，图 2 中的无向图就是一个连通图，因为此图中任意两顶点之间都是连通的。

**连通分量**： 若无向图不是连通图，但图中存储某个子图符合连通图的性质，则称该子图为连通分量。

**极大连通子图**： 由图中部分顶点和边构成的图为该图的一个子图，但这里的子图指的是图中"最大"的连通子图（也称"极大连通子图"）。

如上图所示，虽然图 a) 中的无向图不是连通图，但可以将其分解为 3 个"最大子图"（图 b)），它们都满足连通图的性质，因此都是连通分量。图 a) 中的无向图只能分解为 3 部分各自连通的"最大子图"。

**Note**: 需要注意的是，连通分量的提出是以"整个无向图不是连通图"为前提的，因为如果无向图是连通图，则其无法分解出多个最大连通子图，因为图中所有的顶点之间都是连通的。

## 强连通图

1. 有向图中，若任意两个顶点 $V_i$ 和 $V_j$，满足从 $V_i$  到 $V_j$ 以及从 $V_j$ 到 $V_i$  都连通，也就是都含有至少一条通路，则称此有向图为强连通图。如图 4 所示就是一个强连通图。$v_1\rightarrow v_3$ 和 $v_3 \rightarrow v_4 \rightarrow v1$

2. 与此同时，若有向图本身不是强连通图，但其包含的最大连通子图具有强连通图的性质，则称该子图为强连通分量. 如图 5 所示，整个有向图虽不是强连通图，但其含有两个强连通分量。

  图 4        |  图 5
:-------------------------:|:-------------------------:
  |

## 生成树（生成森林）

- **连通图**是在无向图的基础上对图中顶点之间的连通做了更高的要求
- **强连通图**是在有向图的基础上对图中顶点的连通做了更高的要求。
- **生成树**是对应连通图来说，而**生成森林**是对应非连通图来说的。

**生成树**: 对连通图（无向图）进行遍历，过程中所经过的边和顶点的组合可看做是一棵普通树。如下午图所示，图 a) 是一张连通图，图 b) 是其对应的 2 种生成树。连通图中，由于任意两顶点之间可能含有多条通路，遍历连通图的方式有多种，往往一张连通图可能有多种不同的生成树与之对应。连通图中的生成树必须满足以下 3 个条件：

1. 包含连通图中所有的顶点
2. 任意两顶点之间有且仅有一条通路
3. 连通图的生成树具有这样的特征，即生成树中 *边的数量 = 顶点数 - 1*

**生成森林**：我们知道，非连通图可分解为多个连通分量，而每个连通分量又各自对应多个生成树（至少是 1 棵），因此与整个非连通图相对应的，是由多棵生成树组成的生成森林。

如上图所示，这是一张非连通图，可分解为 3 个连通分量，其中各个连通分量对应的生成树如图所示（列出的仅是各个连通分量的其中一种生成树）：

因此，多个连通分量对应的多棵生成树就构成了整个非连通图的生成森林

# 图的表示形式

1. 邻接矩阵(Adjacency matrix)
2. 度矩阵( Degree matrix)
3. 邻域( Neighborhood) 或 邻接列表

所以使用哪一个呢？大多数时候，选择邻接列表是正确的。下面是两种实现方法更详细的比较。

假设 V 表示图中顶点的个数，E 表示边的个数。

| 操作 | 邻接列表 | 邻接矩阵 |
| ----| -------| --------|
|  存储空间   | O(V + E)       |  O($V^2$) |
|   添加顶点  |   O(1)     | O($V^2$)  |
|   添加边  |   O(1)     |  O(1) |
|   检查相邻性  |    O(V)    | O(1)  |

检查相邻性” 是指对于给定的顶点，尝试确定它是否是另一个顶点的邻居。在邻接列表中检查相邻性的时间复杂度是O(V)，因为最坏的情况是一个顶点与每一个顶点都相连。

在稀疏图的情况下，每一个顶点都只会和少数几个顶点相连，这种情况下相邻列表是最佳选择。如果这个图比较密集，每一个顶点都和大多数其他顶点相连，那么相邻矩阵更合适。

## 邻接矩阵(Adjacency matrix)

邻接矩阵是图的常用存储表示，它的底层依赖一个二维数组。它用两个数组分别存储数据元素（顶点）的信息和数据元素之间的关系（边或弧）的信息。

邻接矩阵是一个元素为Bool值或是权值的$N \times N$矩阵， 该矩阵的定义如下：

\begin{equation}
  A \in R^{N \times N}, A_{i,j} =
    \begin{cases}
      a_{ij} \neq 0 & e_{ij} \in E\\
      0 & \text{otherwise}
    \end{cases}
\end{equation}

若图中存在一条连接顶点$v_i$和$v_j$的边$e_ij$, 则$a_{ij} \neq 0$, 否则为0. 当图是稠密时，邻接矩阵是比较合适的表达方法。但是如果是一个sparse的图， 这样的表示方法是相当浪费空间

**邻接矩阵：** 在邻接矩阵实现中，由行和列都表示顶点，由两个顶点所决定的矩阵对应元素表示这里两个顶点是否相连、如果相连这个值表示的是相连边的权重。例如，如果从顶点A到顶点B有一条权重为 5.6 的边，那么矩阵中第A行第B列的位置的元素值应该是5.6:

**优点**
1. 邻接矩阵的存储方式简单、直接，可以高效的获取两个顶点的关系；
2. 计算方便。（求解最短路径 Floyd-Warshall 算法）

用邻接矩阵来表示一个图，虽然简单、直观，但是比较浪费存储空间。

1. 对于无向图，a[i][j] == a[j][i]，我们只需要存储一个就好，在二维数组中，通过对角线可以划分为两部分，我们只要利用其中一部分的空间就可以了，另外一部分则是多余的。
2. 存储的是稀疏图（Sparse Matrix）：顶点很多，但每个顶点的边并不多，邻接矩阵的存储方法就更加浪费空间了。

## 度矩阵( Degree matrix)

度矩阵( Degree matrix) 是一个 $D_{i,i}$为节点$v_i$的度的对角矩阵， 其定义如下所示：
$$ D \in R^{N \times N}, D_{i,i} = \sum_{j} A_{i,j}$$

## 邻域( Neighborhood)

领域（Neighborhood）表示与某个顶点有边连接的点集，其定义如下所示： $u(v_i) = \{ v_j | v_{i,j} \in E\}$, 例如， 节点1的领域为$\{2, 5\}$

**邻接列表（Adjacency List）：** 在邻接列表实现中，每一个顶点会存储一个从它这里开始的边的列表。比如，如果顶点A 有一条边到B、C和D，那么A的列表中会有3条边. 邻接列表只描述了指向外部的边。A 有一条边到B，但是B没有边到A，所以 A没有出现在B的邻接列表中。查找两个顶点之间的边或者权重会比较费时，因为遍历邻接列表直到找到为止。

图的顶点对应一条链表，链表中存储的是与这个顶点相连接的其他顶点。

**邻接表存储起来比较节省空间，但是使用起来就比较耗时间。**

当链表过长，为了提高查找效率，我们可以将链表换成其他更加高效的数据结构，如平衡二叉树（红黑树）、跳表、散列表等

# 图的遍历（Graph Traversal）

遍历图有两种常见的方式，一种是深度优先搜索（Depth-first Search），另一种是宽度优先搜索（Breadth-first search）。首先我们来学习深度优先搜索：

## 深度优先搜索（Depth-First Search）

The Depth First Search (DFS) is a graph traversal algorithm. In this algorithm one starting vertex is given, and when an adjacent vertex is found, it moves to that adjacent vertex first and try to traverse in the same manner.

## 广度优先搜索（Breadth-First Search)

# Implementation Example

For example, 如下图所示， $V = \{1, 2, 3, 4, 5, 6\}$, 边集 $E = \{\{1, 2\}, \{1, 5\}, \{2, 3\}, \{2, 5\}, \{3, 4\}, \{4, 5\}, \{4, 6\}\}$

```python
from collections import defaultdict
from typing import List, Tuple

class AdjListGraph():
    def __init__(self, edges=None):
        self.adjList = defaultdict(list)
        if edges is not None:
            self.build_graph(edges)

    def build_graph(self, edges: List[Tuple]):
        """
        edges: a list of edge where an edge is a tuple <src,dest>
        src is the source node, and dest is the destination node
        """
        for src, dst in edges:
            self.add_edge(src, dst)

    def add_edge(self, src, dst):
        self.adjList[src].append(dst)
        self.adjList[dst].append(src)

    def pretty_print_graph(self):
        for src, adjlist in self.adjList.items():
            res = f"{src}: "
            for dest in adjlist:
                res += f" -> {dest}"

            print(res)

    # Depth First Search or DFS for a Graph
    def dfs_traversal_recursive(self, src, visited=None, results=None):
        if visited is None:
            # A list of Boolean value to indicate if a node is accessed
            visited = [False]*(max(self.adjList) + 1)

        if results is None:
            results = []

        results.append(src)
        visited[src] = True
#         print(f"Node {src}: {visited}")

        for dst in self.adjList[src]:
            if not visited[dst]:
                self.dfs_traversal_recursive(dst, visited, results)

        return results

    def dfs_traversal_stack(self, src):
        # 如果图包含环，遍历框架就要一个visited数组进行辅助
        visited = [False]*(max(self.adjList) + 1)
        stack = []
        results = []

        stack.append(src)
        while stack:

            src = stack.pop()

            # if src is accessed, then skip
            if visited[src]:
                continue

            visited[src] = True
            results.append(src)
#             print(f"Node {src}: {visited}")

            for dst in self.adjList[src]:
                if not visited[dst]:
                    stack.append(dst)
        return results

    def dfs_traversal_stack_with_set(self, src):
        visited = set() # 如果图包含环，遍历框架就要一个visited数组进行辅助
        stack = []
        results = []

        stack.append(src)
        while stack:
            src = stack.pop()
            if src in visited:
                continue

            visited.add(src)
            results.append(src)
#             print(f"Node {src}: {visited}")

            for dst in self.adjList[src]:
                if dst not in visited:
                    stack.append(dst)
        return results

    def bfs_traversal(self, src):
        visited = [False]*(max(self.adjList) + 1)
        queue = []
        queue.append(src)
        results = []
        while queue:

            src = queue.pop(0)
            if visited[src]:
                continue

            results.append(src)
            visited[src] = True
#             print(f"Node {src}: {visited}")

            for dst in self.adjList[src]:
                if not visited[dst]:
                    queue.append(dst)
        return results

#     def bfs_recursive(self, src, queue=None, visited=None, results=None):
#         if visited is None:
#             # A list of Boolean value to indicate if a node is accessed
#             visited = [False]*(max(self.adjList) + 1)

#         if results is None:
#             results = []

#         if queue is None:
#             queue = []

    def traversal(self, src, dfs=False):
        visited = [False]*(max(self.adjList) + 1)
        queue = []
        queue.append(src)
        results = []
        index = 0 if dfs else -1
        while queue:

            src = queue.pop(index)
            if visited[src]:
                continue

            results.append(src)
            visited[src] = True
#             print(f"Node {src}: {visited}")

            for dst in self.adjList[src]:
                if not visited[dst]:
                    queue.append(dst)
        return results

    def find_path(self, src, dst, path=[], results=[]):

        path.append(src)

        if src == dst:
            results.append(list(path))
            return

        for n in self.adjList[src]:
            if n not in path:
                self.find_path(n, dst, path, results)
                path.pop()

        return results

    def find_all_paths(self, src, dst, path=[], results=[]):
        path.append(src)
        if src == dst:
            results.append(list(path))
            return

        for n in self.adjList[src]:
            if n not in path:
                self.find_all_paths(n, dst, path, results)
                path.pop()

        return results

    # Universal interface to get all paths given the number of paths
    def find_paths(self, src, dst, nums_path=1, path=[], results=[]):
        path.append(src)
        if src == dst:
            results.append(list(path))
            return

        for n in self.adjList[src]:
            if n not in path:
                self.find_paths(n, dst, nums_path, path, results)
                if nums_path > 1:
                    path.pop()

        return results[:nums_path]

    def find_shortest_path(self, src, dst):
        from collections import defaultdict
        dist = defaultdict(list)

        queue = [src]

        while queue:
            src = queue.pop(0)
            for n in self.adjList[src]:
                if n not in dist:
                    [dist[n].append(x) for x in dist[src]]
                    dist[n].append(n)
                    queue.append(n)
        return dist[dst]

edges = [(1,5),(1,2),(2,3),(2,5),(3,4),(4,5),(4,6)]
graph = AdjListGraph(edges)
graph.pretty_print_graph()
visited = graph.dfs_traversal_recursive(1)
print(f"DFS traverse recursively: {visited}")

visited = graph.dfs_traversal_stack(1)
print(f"DFS traverse using a stack: {visited}")

visited = graph.bfs_traversal(1)
print(f"BFS traverse using a stack: {visited}")

visited = graph.traversal(1, False)
print(f"DFS = False, traverse using a stack: {visited}")

visited = graph.traversal(1, True)
print(f"DFS = True, traverse using a stack: {visited}")

print(f"Path between 1 and 4: {graph.find_path(1, 4)}")
print(f"All Paths between 1 and 6: {graph.find_all_paths(1, 6)}")

print(f"Paths between 1 and 6: {graph.find_paths(1, 6, nums_path=100)}")

print(f"Shortest Path between 1 and 4: {graph.find_shortest_path(1, 4)}")
```

    1:  -> 5 -> 2
    5:  -> 1 -> 2 -> 4
    2:  -> 1 -> 3 -> 5
    3:  -> 2 -> 4
    4:  -> 3 -> 5 -> 6
    6:  -> 4
    DFS traverse recursively: [1, 5, 2, 3, 4, 6]
    DFS traverse using a stack: [1, 2, 5, 4, 6, 3]
    BFS traverse using a stack: [1, 5, 2, 4, 3, 6]
    DFS = False, traverse using a stack: [1, 2, 5, 4, 6, 3]
    DFS = True, traverse using a stack: [1, 5, 2, 4, 3, 6]
    Path between 1 and 4: [[1, 5, 2, 3, 4], [1, 5, 4], [1, 2, 3, 4], [1, 2, 5, 4]]
    All Paths between 1 and 6: [[1, 5, 2, 3, 4, 6], [1, 5, 4, 6], [1, 2, 3, 4, 6], [1, 2, 5, 4, 6]]
    Paths between 1 and 6: [[1, 5, 2, 3, 4, 6], [1, 5, 4, 6], [1, 2, 3, 4, 6], [1, 2, 5, 4, 6]]
    Shortest Path between 1 and 4: [5, 4]

# 图上的学习任务

介绍完图的基本术语之后，我们来看看有了图结构数据，我们可以进行哪些机器学习的任务
1. 图节点分类任务：图中每个节点都有对应的特征，当我们已知一些节点的类别的时候，可以设计分类任务针对未知节点进行分类。我们接下来要介绍的 GCN、GraphSAGE、GAT模型都是对图上的节点分类。
2. 图边结构预测任务：图中的节点和节点之间的边关系可能在输入数据中能够采集到，而有些隐藏的边需要我们挖掘出来，这类任务就是对边的预测任务，也就是对节点和节点之间关系的预测。
3. 图的分类：对于整个图来说，我们也可以对图分类，图分类又称为图的同构问题，基本思路是将图中节点的特征聚合起来作为图的特征，再进行分类。

# 图数据应用举例

对于一个简单的电商的图，其包含卖家，商品和用户三个关键节点，其中，商品节点关联商品类别节点，用户节点关联注册 IP 节点和 注册地址节点。当用户在购买商品时，用户节点和商品节点就会关联交易节点，同时，交易节点也会关联用户下单时所对应的 IP 节点以及收获地址节点，对应的图结构如下图所示。

从图数据中节点间的关系以及特征，我们可以进行反欺诈以及商品推荐的操作。

1. 节点分类—反欺诈：因为图中每个节点都拥有自己的特征信息。通过该特征信息，我们可以构建一个风控系统，如果交易节点所关联的用户 IP 和收货地址与用户注册 IP 和注册地址不匹配，那么系统将有可能认为该用户存在欺诈风险。
2. 边结构预测—商品推荐：图中每个节点都具有结构信息。如果用户频繁购买某种类别商品或对某种类别商品评分较高，那么系统就可以认定该用户对该类商品比较感兴趣，所以就可以向该用户推荐更多该类别的商品。

总而言之，图数据的丰富应用价值促使更多的研究者加入图数据的研究当中，但是对图数据进行数据分析时，我们需要同时考虑到节点的特征信息以及结构信息。如果靠手工规则来提取，必将失去很多隐蔽和复杂的模式，那么有没有一种方法能自动化地同时学到图的特征信息与结构信息呢？这就是近年来兴起的机器学习的一个热点方向—图神经网络（Graph Neural Networks）。接下来我们将以一个系列的文章介绍它们。

# Reference

1. [数据结构图(Graph)详解](http://data.biancheng.net/graph/)
2. [算法与数据结构基础 - 图(Graph)](https://www.cnblogs.com/bangerlee/p/11540393.html)