Exercise material of the MSc-level course **Numerical Methods in Geotechnical Engineering**.
Held at Technische Universität Bergakademie Freiberg.

Comments to:

*Prof. Dr. Thomas Nagel
Chair of Soil Mechanics and Foundation Engineering
Geotechnical Institute
Technische Universität Bergakademie Freiberg.*

https://tu-freiberg.de/en/soilmechanics

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

#Some plot settings
plt.style.use('seaborn-deep')
plt.rcParams['lines.linewidth']= 2.0
plt.rcParams['lines.color']= 'black'
plt.rcParams['legend.frameon']=True
plt.rcParams['font.family'] = 'serif'
plt.rcParams['legend.fontsize']=14
plt.rcParams['font.size'] = 14
plt.rcParams['axes.spines.right'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.left'] = True
plt.rcParams['axes.spines.bottom'] = True
plt.rcParams['axes.axisbelow'] = True
plt.rcParams['figure.figsize'] = (12, 6)
```

# Exercise 7 - 1D Consolidation, Terzaghi: Explicit version

## Governing differential equation

Neglecting lateral displacements and fluid flow

$$
	\epsilon_{xx} = \epsilon_{yy} = 0 \quad \text{and} \quad q_x = q_y = 0
$$

the volume balance simplifies to

$$
	\dot{\epsilon}_{zz} = \frac{\partial q_z}{\partial z}
$$

Now we substitute the constitutive law (linear elasticity) of the solid and Darcy's law for the fluid motion into the volume balance and get

$$
    \dot{\sigma}_{zz}^\text{eff} = E_\text{S} \dot{\epsilon}_{zz} \quad \text{and} \quad \frac{\partial q_z}{\partial z} = - \frac{k}{\mu_\text{F}} \frac{\partial^2 p}{\partial z^2} \quad \text{ergibt} \quad
		 \frac{\dot{\sigma}_{zz}^\text{eff}}{E_\text{S}} = - \frac{k}{\mu_\text{F}} \frac{\partial^2 p}{\partial z^2}
$$

Assuming constant total stress (constant external loads) we find with the use of the effective stress principle

$$
	\sigma_{zz} = \sigma_{zz}^\text{eff} + p = \text{const} \quad \rightarrow \quad \dot{\sigma}_{zz} = 0 = \dot{\sigma}_{zz}^\text{eff} + \dot{p}
$$

Substitution yields the partial, linear and homogeneous differential equation

$$
	\dot{p} = \frac{E_\text{S} k}{\mu_\text{F}} p_{,zz} = \frac{E_\text{S} K_\text{D}}{\gamma_\text{F}} p_{,zz} = c_\text{v} p_{,zz} \quad \text{with } K_\text{D} = \frac{k\rho_\text{F} g}{\mu_\text{F}}
$$

$[k] = \text{m}^2$: intrinsic permeability

$[K_\text{D}] = \text{m s}^{-1}$: hydraulic conductivity

$[c_\text{v}] = \text{m}^2\text{s}^{-1}$: coefficient of consolidation

## Weak form

The pore pressure can have (the essential/Dirichlet) boundary conditions in the form:

$$
    p = \bar{p}\ \forall z \in \partial \Omega_\mathrm{D}
$$

We now introduce a test function $\eta$ which vanishes where the pore pressure is given

$$
    \eta = 0\ \forall z \in \partial \Omega_\mathrm{D}
$$

and construct the weak form (using integration by parts):

\begin{align}
   0 &= \int \limits_0^H \eta \left[\dot{p} - c_\text{v} p_{,zz} \right] \text{d}z
   \\
   &= \int \limits_0^H \left[\eta \dot{p} - \left( \eta c_\text{v} p_{,z} \right)_{,z} + \eta_{,z} c_\text{v} p_{,z} \right] \, \text{d}z
   \\
   &= \int \limits_0^H \left[\eta \dot{p} + \eta_{,z} c_\text{v} p_{,z} \right] \, \text{d}z + \left[ \eta E_\text{S} q_z \right]^H_0
\end{align}

where the natural/Neumann boundary conditions have appeared.

## Finite elements in 1D

We have a soil column of height $H$ on top of the bed rock at $z=0$.

We first create an element class. An element knows the number of nodes it has, their IDs in the global node vector, and the coordinates of its nodes. Linear elements have 2 nodes and 2 quadrature points, quadratic elements 3 nodes and 3 quadrature points. The natural coordinates of the element run from -1 to 1, and the quadrature points and weights are directly taken from Numpy.

```python
#element class
class line_element():#local coordinates go from -1 to 1
    #takes number of nodes, global nodal coordinates, global node ids
    def __init__(self, nnodes=2, ncoords=[0.,1.], nids=[0,1]):
        self.__nnodes = nnodes
        if (len(ncoords) != self.__nnodes):
            raise Exception("Number of coordinates does not match number \
                            of nodes of element (%i vs of %i)" %(self.__nnodes,len(ncoords)))
        else:
            self.__coords = np.array(ncoords)

        self.__natural_coords = (self.__coords-self.__coords[0])/(self.__coords[-1]-self.__coords[0])*2. - 1.

        if (len(nids) != self.__nnodes):
            raise Exception("Number of node IDs does not match number \
                            of nodes of element (%i vs of %i)" %(self.__nnodes,len(nids)))
        else:
            self.__global_ids = np.array(nids)
        self.__quad_degree = self.__nnodes
        self.__quad_points, self.__quad_weights = np.polynomial.legendre.leggauss(self.__quad_degree)

```

Next, we wish to generate a one-dimensional mesh by specifying the length of a line, the number of elements into which the mesh is to be split, and the number of nodes per element.

```python
def number_of_nodes(nelems,nodes_per_elem):
    return nelems*nodes_per_elem - (nelems - 1)

def generate_mesh(domain_length,nelems,nodes_per_elem):
    nn = number_of_nodes(nelems,nodes_per_elem)
    #coordinate vector of global nodes
    global_nodal_coordinates = np.linspace(0.,domain_length,nn)
    global_solution = np.array([0.]*nn)

    #generate elements
    element_vector = []
    for i in range(nelems):
        node_start = (nodes_per_elem-1)*i
        element_vector.append(
            line_element(nodes_per_elem,
                         global_nodal_coordinates[node_start:node_start+nodes_per_elem],
                         list(range(node_start,node_start+nodes_per_elem))))

    return global_nodal_coordinates, element_vector, global_solution
```

## Shape functions in 1D

As in exercise 06, we allow linear and higher-order shape functions.

```python
#N
def shape_function(element_order,xi):
    if (element_order == 2): #-1,1
            return np.array([(1.-xi)/2., (1.+xi)/2.])
    elif (element_order == 3): #-1, 0, 1
            return np.array([(xi - 1.)*xi/2., (1-xi)*(1+xi), (1+xi)*xi/2.])
#dN_dxi
def dshape_function_dxi(element_order,xi):
    if (element_order == 2): #-1,1
        return np.array([-0.5*xi/xi, 0.5*xi/xi])  #xi only later for plotting dimensions
    elif (element_order == 3):#-1,0,1
        return np.array([xi - 0.5,-2.*xi,xi + 0.5])

#dz_dxi
def element_jacobian(element,xi):
    element_order = element._line_element__nnodes
    Jacobian = 0.
    Jacobian += dshape_function_dxi(element_order,xi).dot(element._line_element__coords)
    return Jacobian

#dN_dz
def grad_shape_function(element,xi):
    element_order = element._line_element__nnodes
    Jac = element_jacobian(element,xi)
    return dshape_function_dxi(element_order,xi)/Jac

```

## Time and space discretization, Picard iterations

Using a forward Euler approach for simplicity we find the time-discrete weak form as ($p_{n+1} \equiv p$)

$$
    0 = \int \limits_0^H \left[\eta \frac{p - p_n}{\Delta t} + \eta_{,z} c_\text{v} p_{n,z} \right] \, \text{d}z + \left[ \eta E_\text{S} q_z \right]^H_0
$$

Introducing standard FE approximations

$$
    p \approx N_i \hat{p}_i, \quad \eta \approx N_i \hat{\eta}_i, \quad \frac{\partial p}{\partial z} \approx \nabla  N_i \hat{p}_i, \quad \frac{\partial \eta}{\partial z} \approx \nabla N_i \hat{\eta}_i
$$

This yields

$$
\begin{align}
 0 &= \int \limits_0^H \left[ N_i \hat{\eta}_i \frac{N_k \hat{p}_k - N_k \hat{p}_{n,k}}{\Delta t} + \nabla N_i \hat{\eta}_i c_\text{v} \nabla N_k \hat{p}_{n,k} \right] \, \text{d}z + \left[ N_i \hat{\eta}_i E_\text{S} q_z \right]^H_0
 \end{align}
$$

Now we bring all quantities associated with the unknown pressure to the left-hand side (LHS) and all known quantities to the RHS:

$$
    \hat{\eta}_i \int \limits_0^H \left[ N_i \frac{1}{\Delta t} N_k + \nabla N_i c_\text{v} \nabla  N_k \right] \, \text{d}z\ \hat{p}_k =  \hat{\eta}_i \left[ N_{n_\text{n}} E_\text{S} \bar{q}_z|_{z=H} \delta_{n_\text{n}i} - N_{n_\text{n}} E_\text{S} \bar{q}_z|_{z=0} \delta_{i0} \right] +  \hat{\eta}_i \int \limits_0^H N_i \frac{1}{\Delta t} N_k \, \text{d}z\ \hat{p}_{n,k}
$$

can be simplified by realizing that the nodal test function values are arbitrary and thus

$$
    \int \limits_0^H \left[ N_i \frac{1}{\Delta t} N_k \right] \, \text{d}z\ \hat{p}_k = N_{n_\text{n}} E_\text{S} \bar{q}_z|_{z=H} \delta_{n_\text{n}i} - N_{n_\text{n}} E_\text{S} \bar{q}_z|_{z=0} \delta_{i0} +  \int \limits_0^H N_i \frac{1}{\Delta t} N_k \, \text{d}z\ \hat{p}_{n,k} - \int \limits_0^H \left[ \nabla N_i c_\text{v} \nabla N_k \right] \, \text{d}z\ \hat{p}_{n,k}
$$

which leaves us with $n_\text{n}$ equations for the $n_\text{n}$ unknown nodal pressures $\hat{p}_k$. If any coefficients in the above are taken as pressure-dependent, the system could be solved repeatedly usind Picard iterations. For strong non-linearities, a Newton linearization would typically be used.

### Question:

How would the equation differ for an explicit time integration scheme (forward Euler)?

What we require now is the local assembler to calculate the stiffness matrix and the local right-hand side. Local integration is performed by Gauss quadrature:

$$
    \int \limits_{-1}^1 f(\xi)\,\text{d}\xi \approx \sum \limits_{i=1}^{n_\text{gp}} f(\xi_i) w_i
$$

## Local assember

```python
def Stiffness(z):
    E0 = 5.e6 #Pa
    return E0

def Conductivity(z):
    kf = 1.e-9#m/s
    return kf

def SpecificWeight(z):
    g = 9.81 #m/s²
    rhow = 1000. #kg/m³
    return g*rhow

def ConsolidationCoeff(z):#m²/s
    return Stiffness(z)*Conductivity(z)/SpecificWeight(z)
```

```python
def local_assembler(elem,dt,prev_sol):
    element_order = elem._line_element__nnodes
    K_loc = np.zeros((element_order,element_order))
    b_loc = np.zeros(element_order)
    z_nodes = elem._line_element__coords
    for i in range(elem._line_element__quad_degree):
        #local integration point coordinate
        xi = elem._line_element__quad_points[i]
        #shape function
        N = shape_function(element_order,xi)
        #gradient of shape function
        dN_dX = grad_shape_function(elem,xi)
        #determinant of Jacobian
        detJ = np.abs(element_jacobian(elem,xi))
        #integration weight
        w = elem._line_element__quad_weights[i]

        #global integration point coordinate (for spatially varying properties)
        z_glob = np.dot(N,z_nodes)
        #evaluation of local material/structural properties
        E = Stiffness(z_glob)
        #evaluation of local body force
        CV = ConsolidationCoeff(z_glob)

        #assembly of local stiffness matrix
        K_loc += (np.outer(N,N) / dt )* w * detJ

        #assembly of local RHS
        p_prev = np.dot(N,prev_sol)#pressure in integration point
        grad_p_prev = np.dot(dN_dX,prev_sol)
        b_loc += (N * p_prev/dt - dN_dX * CV * grad_p_prev) * w * detJ
    return K_loc,b_loc
```

## Global assembly

Now we can construct the global matrix system $\mathbf{K}\mathbf{u} = \mathbf{f}$ or $\mathbf{A}\mathbf{x}=\mathbf{b}$ (see lecture script).

```python
def global_assembler(nodes,elements,solution,dt):
    K_glob = np.zeros((len(nodes),len(nodes)))
    b_glob = np.zeros(len(nodes))
    for i,elem in enumerate(elements):
        start_id = elem._line_element__global_ids[0]
        end_id = elem._line_element__global_ids[-1]
        K_i, b_i = local_assembler(elem,dt,solution[start_id:end_id+1])
        K_glob[start_id:end_id+1,start_id:end_id+1] += K_i
        b_glob[start_id:end_id+1] += b_i
    return K_glob, b_glob
```

## Application of boundary conditions

First we apply flux boundary conditions

```python
def apply_Neumann_bc(b_glob,node_id,value):
    b_glob[node_id] += value
    return b_glob
```

Then we apply Dirichlet bc

```python
def apply_Dirichlet_bc(K_glob,b_glob,node_id,value):
    K_glob[node_id,:] = 0.# = K_glob[:,node_id] = 0.
    K_glob[node_id,node_id] = 1.
    b_glob[node_id] = value
    return K_glob, b_glob
```

## Application of initial conditions

Since we're dealing with a time-dependent problem (rate problem) we require initial conditions for the pore pressure, i.e. $p_0 = p(t=0)\ \forall\ z$. Due to the very specific assumptions in deriving this consolidation equation, the initial pressure is given by the (suddenly) applied load:

$$
    p_0 = \sigma_{zz} = \text{const.}
$$

```python
def apply_initial_conditions(solution,sig_v):
    solution *= 0.
    solution += sig_v
    return
```

## Time loop and problem solution

We now establish the time loop and in each time step perform the global assembly, apply a vanishing traction on the top and constrain the displacement at the bottom to zero.

```python
def time_loop(dt,nodes,elements,solution):
    #Startwerte
    t_end = 100*24*60*60 #s
    absolute_tolerance = 1.e-6
    apply_initial_conditions(solution,200.e3)
    y = [solution] #create a list that will hold the solution vectors at all time points
    times = np.array([0.])
    #
    while times[-1]+dt < t_end: #repeat the loop as long as the final time step is below the end point
        times = np.append(times,times[-1]+dt) #here define the next time point as the previous time point plus the time increment dt
        K, f = global_assembler(nodes,elements,y[-1],dt)
        #f = apply_Neumann_bc(f,len(nodes)-1,0)
        K, f = apply_Dirichlet_bc(K, f, len(nodes)-1, 0.)#free draining top
        solution = np.linalg.solve(K,f)
        y.append(solution) #append the new found solution to the solution vector
    return times, y
```

```python
#spatial discretization
H = 10.
nel = 20
n_per_el = 3
nodes,elements,solution=generate_mesh(H,nel,n_per_el)
```

```python
times, sols = time_loop(1*60.*60.,nodes,elements,solution)
```

```python
plt.xlabel('$z$ / m')
plt.ylabel('$p$ / kPa')
plt.title('Finite element solution')
plt.plot(nodes, sols[0]/1.e3, marker='o', label='$t = %i$ h' %(times[0]/60/60))
plt.plot(nodes, sols[1]/1.e3, marker='o', label='$t = %i$ h' %(times[1]/60/60))
plt.plot(nodes, sols[10]/1.e3, marker='o', label='$t = %i$ h' %(times[10]/60/60))
plt.plot(nodes, sols[-1]/1.e3, marker='o', label='$t = %i$ d' %(times[-1]/60/60/24))
#plt.plot(nodes, sols[200]/1.e3, marker='o', label='$t = %i$ d' %(times[200]/60/60/24))
#plt.plot(nodes, sols[-1]/1.e3, marker='o', label='$t = %i$ d' %(times[-1]/60/60/24))
plt.legend();
```

### Convergence study

Let's do a simple convergence study.

```python
dts = [1*3600,10.*3600,20.*3600]
nels = [10,20,40]
H = 10.

fig, ax = plt.subplots(nrows=3,ncols=3,figsize=(18,18))

for i,dt in enumerate(dts):
    for j,nel in enumerate(nels):
        #print("Running (n,dt) combination ", dt, nel)
        number_of_elements = nel
        nodes_per_element = 2
        nodes,elements,solution=generate_mesh(H,number_of_elements,nodes_per_element)
        times, sols = time_loop(dt,nodes,elements,solution)
        ax[i][j].plot(nodes, sols[0]/1.e3, marker='o', label='$t = %i$ h' %(times[0]/60/60))
        ax[i][j].plot(nodes, sols[1]/1.e3, marker='o', label='$t = %i$ h' %(times[1]/60/60))
        ax[i][j].plot(nodes, sols[10]/1.e3, marker='o', label='$t = %i$ h' %(times[10]/60/60))
        ax[i][j].plot(nodes, sols[-1]/1.e3, marker='o', label='$t = %i$ d' %(times[10]/60/60/24))
        ax[i][j].set_xlabel('$z$ / m')
        ax[i][j].set_ylabel('$p$ / kPa')
        ax[i][j].set_title('dt = %i h, nel = %i' %(dt/3600,nel))
        ax[i][j].legend(fontsize=10)
fig.tight_layout()
```

We observe that time step size and discretization size act together, and that unsuitable choices can lead to unphysical oscillations.

## Tasks

* What happens if you switch the element order to 3 in the last example?
* Explicit solutions can be coded so as to avoid matrix inversion.

```python

```


# TPHLCT 3階の導関数を相殺する

```python
import numpy as np
import scipy.misc
from scipy.fftpack import dct, idct
import sys
from PIL import Image
import matplotlib
import matplotlib.pyplot as plt
import random
from tqdm._tqdm_notebook import tqdm_notebook
from scipy.fftpack import dct, idct
import seaborn as sns
from skimage.metrics import structural_similarity as ssim
import pandas as pd

%matplotlib inline

class ImageLoader:
    def __init__(self, FILE_PATH):
        self.img = np.array(Image.open(FILE_PATH))
        # 行数
        self.row_blocks_count = self.img.shape[0] // 8
        # 列数
        self.col_blocks_count = self.img.shape[1] // 8

    def get_points(self, POINT):
        Row = random.randint(0, len(self.img) - POINT - 1)
        Col = random.randint(0, len(self.img) - 1)
        return self.img[Row : Row + POINT, Col]

    def get_block(self, col, row):
        return self.img[col * 8 : (col + 1) * 8, row * 8 : (row + 1) * 8]

# plt.rcParams['font.family'] ='sans-serif'#使用するフォント
# plt.rcParams["font.sans-serif"] = "Source Han Sans"
plt.rcParams["font.family"] = "Source Han Sans JP"  # 使用するフォント
plt.rcParams["xtick.direction"] = "in"  # x軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["ytick.direction"] = "in"  # y軸の目盛線が内向き('in')か外向き('out')か双方向か('inout')
plt.rcParams["xtick.major.width"] = 1.0  # x軸主目盛り線の線幅
plt.rcParams["ytick.major.width"] = 1.0  # y軸主目盛り線の線幅
plt.rcParams["font.size"] = 12  # フォントの大きさ
plt.rcParams["axes.linewidth"] = 1.0  # 軸の線幅edge linewidth。囲みの太さ
matplotlib.font_manager._rebuild()

MONO_DIR_PATH = "../../Mono/"
AIRPLANE = ImageLoader(MONO_DIR_PATH + "airplane512.bmp")
BARBARA = ImageLoader(MONO_DIR_PATH + "barbara512.bmp")
BOAT = ImageLoader(MONO_DIR_PATH + "boat512.bmp")
GOLDHILL = ImageLoader(MONO_DIR_PATH + "goldhill512.bmp")
LENNA = ImageLoader(MONO_DIR_PATH + "lenna512.bmp")
MANDRILL = ImageLoader(MONO_DIR_PATH + "mandrill512.bmp")
MILKDROP = ImageLoader(MONO_DIR_PATH + "milkdrop512.bmp")
SAILBOAT = ImageLoader(MONO_DIR_PATH + "sailboat512.bmp")

N = 32
```

# DCT

## 基底関数

$$
\phi_k[i] = \begin{cases}
\cfrac{1}{\sqrt{N}}  \quad \quad \quad (k=0) \\
\sqrt{\cfrac{2}{N}} \cos \left({\cfrac{\pi}{2N}(2i+1)k}\right) \quad  (k=1,2,...,N-1)
\end{cases}
$$

```python
class DCT:
    def __init__(self, N):
        self.N = N  # データ数
        # 1次元DCTの基底ベクトルの生成
        self.phi_1d = np.array([self.phi(i) for i in range(self.N)])
        # 2次元DCTの基底ベクトルの格納
        self.phi_2d = np.zeros((N, N))

    def phi(self, k):
        """ 離散コサイン変換(DCT)の基底関数 """
        # DCT-II
        if k == 0:
            return np.ones(self.N) / np.sqrt(self.N)
        else:
            return np.sqrt(2.0 / self.N) * np.cos(
                (k * np.pi / (2 * self.N)) * (np.arange(self.N) * 2 + 1)
            )

    def dct(self, data):
        """ 1次元離散コサイン変換を行う """
        return self.phi_1d.dot(data)

    def idct(self, c):
        """ 1次元離散コサイン逆変換を行う """
        return np.sum(self.phi_1d.T * c, axis=1)

    def get_dct2_phi(self, y, x):
        """ 2次元離散コサイン変換の基底を返す """
        phi_x, phi_y = np.meshgrid(self.phi_1d[x], self.phi_1d[y])
        return phi_x * phi_y

    def get_dct2(self, y, x, data):
        """ i,jの2次元DCT係数を返す """
        phi_2d_phi = np.zeros((self.N, self.N))
        phi_2d_phi = self.get_dct2_phi(y, x)
        return np.sum(np.sum(phi_2d_phi * data))

    def dct2(self, data):
        """ 2次元離散コサイン変換を行う """
        for y in range(self.N):
            for x in range(self.N):
                self.phi_2d[y, x] = self.get_dct2(y, x, data)

        return self.phi_2d

    def idct2(self, c):
        """ 2次元離散コサイン逆変換を行う """
        idct2_data = np.zeros((self.N, self.N))
        phi_2d_phi = np.zeros((self.N, self.N))
        for y in range(self.N):
            for x in range(self.N):
                phi_2d_phi = self.get_dct2_phi(y, x)
                idct2_data += c[y,x] * phi_2d_phi

        return idct2_data
```

# MSDS

```python
def msds(N,arr):
    w_e = 0
    e_e = 0
    n_e = 0
    s_e = 0
    nw_e = 0
    ne_e = 0
    sw_e = 0
    se_e = 0
    for row in range(arr.shape[0] // N):
        for col in range(arr.shape[1] // N):
            f_block = arr[row * N : (row + 1) * N, col * N : (col + 1) * N]
            # w
            if col == 0:
                w_block = np.fliplr(f_block)
            else:
                w_block = arr[row * N : (row + 1) * N, (col - 1) * N : col * N]
            # e
            if col == arr.shape[1] // N - 1:
                e_block = np.fliplr(f_block)
            else:
                e_block = arr[row * N : (row + 1) * N, (col + 1) * N : (col + 2) * N]
            # n
            if row == 0:
                n_block = np.flipud(f_block)
            else:
                n_block = arr[(row - 1) * N : row * N, col * N : (col + 1) * N]
            # s
            if row == arr.shape[0] // N - 1:
                s_block = np.flipud(f_block)
            else:
                s_block = arr[(row + 1) * N : (row + 2) * N, col * N : (col + 1) * N]

            w_d1 = f_block[:, 0] - w_block[:, N-1]
            e_d1 = f_block[:, N-1] - e_block[:, 0]
            n_d1 = f_block[0, :] - n_block[N-1, :]
            s_d1 = f_block[N-1, :] - s_block[0, :]

            w_d2 = (w_block[:, N-1] - w_block[:, N-2] + f_block[:, 1] - f_block[:, 0]) / 2
            e_d2 = (e_block[:, 1] - e_block[:, 0] + f_block[:, N-1] - f_block[:, N-2]) / 2
            n_d2 = (n_block[N-1, :] - n_block[N-2, :] + f_block[1, :] - f_block[0, :]) / 2
            s_d2 = (s_block[1, :] - s_block[0, :] + f_block[N-1, :] - f_block[N-2, :]) / 2

            w_e += np.sum((w_d1 - w_d2) ** 2 )
            e_e += np.sum((e_d1 - e_d2) ** 2 )
            n_e += np.sum((n_d1 - n_d2) ** 2)
            s_e += np.sum((s_d1 - s_d2) ** 2)

            # nw
            if row == 0 or col == 0:
                nw_block = np.flipud(np.fliplr(f_block))
            else:
                nw_block = arr[(row - 1) * N : row * N, (col - 1) * N : col * N]
            # ne
            if row == 0 or col == arr.shape[1] // N - 1:
                ne_block = np.flipud(np.fliplr(f_block))
            else:
                ne_block = arr[(row-1) * N : row * N, (col + 1) * N : (col + 2) * N]
            # sw
            if row == arr.shape[0] // N -1 or col == 0:
                sw_block = np.flipud(np.fliplr(f_block))
            else:
                sw_block = arr[row * N : (row+1) * N, (col-1) * N : col * N]
            # se
            if  row == arr.shape[0]//N-1 or col == arr.shape[0] // N -1:
                se_block = np.flipud(np.fliplr(f_block))
            else:
                se_block = arr[(row + 1) * N : (row + 2) * N, (col+1) * N : (col + 2) * N]

            nw_g1 = f_block[0, 0] - nw_block[N-1, N-1]
            ne_g1 = f_block[0, N-1] - ne_block[N-1, 0]
            sw_g1 = f_block[N-1, 0] - sw_block[0, N-1]
            se_g1 = f_block[N-1, N-1] - se_block[0, 0]

            nw_g2 = (nw_block[N-1,N-1] - nw_block[N-2,N-2] + f_block[1,1] - f_block[0,0])/2
            ne_g2 = (ne_block[N-1,0] - ne_block[N-2,1] + f_block[1,N-2] - f_block[0,N-1])/2
            sw_g2 = (sw_block[0,N-1] - nw_block[1,N-2] + f_block[N-2,1] - f_block[N-1,0])/2
            se_g2 = (nw_block[0,0] - nw_block[1,1] + f_block[N-2,N-2] - f_block[N-1,N-1])/2

            nw_e += (nw_g1 - nw_g2) ** 2
            ne_e += (ne_g1 - ne_g2) ** 2
            sw_e += (sw_g1 - sw_g2) ** 2
            se_e += (se_g1 - se_g2) ** 2

    MSDSt = (w_e + e_e + n_e + s_e + nw_e + ne_e + sw_e + se_e)/ ((arr.shape[0]/N)**2)
    MSDS1 = (w_e + e_e + n_e + s_e)/ ((arr.shape[0]/N)**2)
    MSDS2 = (nw_e + ne_e + sw_e + se_e)/ ((arr.shape[0]/N)**2)
    return MSDSt, MSDS1, MSDS2
```

# 係数の計算

$\alpha_k, \beta_k$を計算する。
1階導関数を相殺するような予測関数は
$$u(x)=-\frac{f_x(0)}{2}(1-x^2)+\frac{f_x(1)}{2}x^2$$
で与えられる。

そのとき、
$$\alpha_k = -\sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{(1-x_\ell)^2}{2}\cos(\pi k x_\ell)$$
$$\beta_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{x_\ell^2}{2}\cos(\pi k x_\ell)$$

$a_k, b_k, c_k, d_k$を計算する。
3階導関数を相殺するような予測関数は
$$
\begin{align}
u(x) & =( -\frac{1}{2}f_x(0) + \frac{1}{12}f^{(3)}_x(0) )(1-x^2)\\
     & -\frac{1}{24}f^{(3)}_x(0)(1-x)^4\\
     & +( \frac{1}{2}f_x(1)-\frac{1}{12}f^{(3)}_x(1) )x^2\\
     & + \frac{1}{24}f^{(3)}_x(1)x^4
\end{align}
$$
で与えられる。

そのとき、
$$a_k = -\sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{(1-x_\ell)^2}{2}\cos(\pi k x_\ell)$$

$$b_k= \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}( \frac{1}{24}-\frac{x_\ell^2}{6}+\frac{x_\ell^3}{6}-\frac{x_\ell^4}{24} )\cos(\pi k x_\ell)$$

$$c_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}\frac{x_\ell^2}{2}\cos(\pi k x_\ell)$$

$$d_k = \sqrt{\frac{2}{N}}\sum_{\ell=0}^{N-1}( -\frac{x_\ell^2}{12}+\frac{x_\ell^4}{24} )\cos(\pi k x_\ell)$$

```python
sampling_x = (0.5 + np.arange(N)) / N
u_1 = (1 - sampling_x) ** 2 / 2
u_2 = 1 / 24 - sampling_x ** 2 / 6 + sampling_x ** 3 / 6 - sampling_x ** 4 / 24
u_3 = sampling_x ** 2 / 2
u_4 = -sampling_x ** 2 / 12 + sampling_x ** 4 / 24
plt.plot(u_1, label="u_1")
plt.plot(u_2, label="u_2")
plt.plot(u_3, label="u_3")
plt.plot(u_4, label="u_4")
plt.legend()
```

```python
ak = - scipy.fftpack.dct(u_1,norm="ortho")
ak
```

    array([-9.42578864e-01, -8.10243674e-01, -2.02315746e-01, -8.97352733e-02,
           -5.03306486e-02, -3.20903214e-02, -2.21802622e-02, -1.62029774e-02,
           -1.23215704e-02, -9.65848827e-03, -7.75151710e-03, -6.33840654e-03,
           -5.26136563e-03, -4.42082807e-03, -3.75144214e-03, -3.20886401e-03,
           -2.76213586e-03, -2.38910402e-03, -2.07356609e-03, -1.80343954e-03,
           -1.56955394e-03, -1.36483505e-03, -1.18374180e-03, -1.02186977e-03,
           -8.75667049e-04, -7.42226865e-04, -6.19133680e-04, -5.04346991e-04,
           -3.96111897e-04, -2.92888805e-04, -1.93296799e-04, -9.60665949e-05])

```python
bk = scipy.fftpack.dct(u_2,norm="ortho")
bk
```

    array([1.09994382e-01, 8.21278487e-02, 5.13298176e-03, 1.01391469e-03,
           3.20802291e-04, 1.31394699e-04, 6.33601140e-05, 3.41952121e-05,
           2.00398805e-05, 1.25062623e-05, 8.20094224e-06, 5.59700060e-06,
           3.94751701e-06, 2.86162774e-06, 2.12308561e-06, 1.60652401e-06,
           1.23630756e-06, 9.65206497e-07, 7.62828824e-07, 6.09091833e-07,
           4.90407443e-07, 3.97383602e-07, 3.23400654e-07, 2.63707517e-07,
           2.14834779e-07, 1.74205550e-07, 1.39872296e-07, 1.10335392e-07,
           8.44154936e-08, 6.11617680e-08, 3.97841193e-08, 1.96014032e-08])

```python
ck = scipy.fftpack.dct(u_3,norm="ortho")
ck
```

    array([ 9.42578864e-01, -8.10243674e-01,  2.02315746e-01, -8.97352733e-02,
            5.03306486e-02, -3.20903214e-02,  2.21802622e-02, -1.62029774e-02,
            1.23215704e-02, -9.65848827e-03,  7.75151710e-03, -6.33840654e-03,
            5.26136563e-03, -4.42082807e-03,  3.75144214e-03, -3.20886401e-03,
            2.76213586e-03, -2.38910402e-03,  2.07356609e-03, -1.80343954e-03,
            1.56955394e-03, -1.36483505e-03,  1.18374180e-03, -1.02186977e-03,
            8.75667049e-04, -7.42226865e-04,  6.19133680e-04, -5.04346991e-04,
            3.96111897e-04, -2.92888805e-04,  1.93296799e-04, -9.60665949e-05])

```python
dk = scipy.fftpack.dct(u_4,norm="ortho")
dk
```

    array([-1.09994382e-01,  8.21278487e-02, -5.13298176e-03,  1.01391469e-03,
           -3.20802291e-04,  1.31394699e-04, -6.33601140e-05,  3.41952121e-05,
           -2.00398805e-05,  1.25062623e-05, -8.20094224e-06,  5.59700060e-06,
           -3.94751701e-06,  2.86162774e-06, -2.12308561e-06,  1.60652401e-06,
           -1.23630756e-06,  9.65206497e-07, -7.62828824e-07,  6.09091833e-07,
           -4.90407443e-07,  3.97383602e-07, -3.23400654e-07,  2.63707517e-07,
           -2.14834779e-07,  1.74205550e-07, -1.39872296e-07,  1.10335392e-07,
           -8.44154936e-08,  6.11617680e-08, -3.97841193e-08,  1.96014032e-08])

```python
alpha = ak
alpha
```

    array([-9.42578864e-01, -8.10243674e-01, -2.02315746e-01, -8.97352733e-02,
           -5.03306486e-02, -3.20903214e-02, -2.21802622e-02, -1.62029774e-02,
           -1.23215704e-02, -9.65848827e-03, -7.75151710e-03, -6.33840654e-03,
           -5.26136563e-03, -4.42082807e-03, -3.75144214e-03, -3.20886401e-03,
           -2.76213586e-03, -2.38910402e-03, -2.07356609e-03, -1.80343954e-03,
           -1.56955394e-03, -1.36483505e-03, -1.18374180e-03, -1.02186977e-03,
           -8.75667049e-04, -7.42226865e-04, -6.19133680e-04, -5.04346991e-04,
           -3.96111897e-04, -2.92888805e-04, -1.93296799e-04, -9.60665949e-05])

```python
beta = ck
beta
```

    array([ 9.42578864e-01, -8.10243674e-01,  2.02315746e-01, -8.97352733e-02,
            5.03306486e-02, -3.20903214e-02,  2.21802622e-02, -1.62029774e-02,
            1.23215704e-02, -9.65848827e-03,  7.75151710e-03, -6.33840654e-03,
            5.26136563e-03, -4.42082807e-03,  3.75144214e-03, -3.20886401e-03,
            2.76213586e-03, -2.38910402e-03,  2.07356609e-03, -1.80343954e-03,
            1.56955394e-03, -1.36483505e-03,  1.18374180e-03, -1.02186977e-03,
            8.75667049e-04, -7.42226865e-04,  6.19133680e-04, -5.04346991e-04,
            3.96111897e-04, -2.92888805e-04,  1.93296799e-04, -9.60665949e-05])

```python
Ak = (2 * ak - 16 * bk) / np.sqrt(N)
Ak
```

    array([-6.44363046e-01, -5.18757033e-01, -8.60476830e-02, -3.45939939e-02,
           -1.87019374e-02, -1.17172823e-02, -8.02111637e-03, -5.82533628e-03,
           -4.41301434e-03, -3.45016433e-03, -2.76377092e-03, -2.25679583e-03,
           -1.87133892e-03, -1.57109266e-03, -1.33234008e-03, -1.13904869e-03,
           -9.80059306e-04, -8.47405844e-04, -7.35273927e-04, -6.39334937e-04,
           -5.56308197e-04, -4.83666031e-04, -4.19430641e-04, -3.62031398e-04,
           -3.10202699e-04, -2.62909552e-04, -2.19292430e-04, -1.78625664e-04,
           -1.40285467e-04, -1.03724822e-04, -6.84532652e-05, -3.40201115e-05])

```python
Bk = (2 * ck - 16 * dk) / np.sqrt(N)
Bk
```

    array([ 6.44363046e-01, -5.18757033e-01,  8.60476830e-02, -3.45939939e-02,
            1.87019374e-02, -1.17172823e-02,  8.02111637e-03, -5.82533628e-03,
            4.41301434e-03, -3.45016433e-03,  2.76377092e-03, -2.25679583e-03,
            1.87133892e-03, -1.57109266e-03,  1.33234008e-03, -1.13904869e-03,
            9.80059306e-04, -8.47405844e-04,  7.35273927e-04, -6.39334937e-04,
            5.56308197e-04, -4.83666031e-04,  4.19430641e-04, -3.62031398e-04,
            3.10202699e-04, -2.62909552e-04,  2.19292430e-04, -1.78625664e-04,
            1.40285467e-04, -1.03724822e-04,  6.84532652e-05, -3.40201115e-05])

```python
Ck = (2 * ak - 32 * bk) / np.sqrt(N)
Ck
```

    array([-9.55474138e-01, -7.51049668e-01, -1.00565948e-01, -3.74617777e-02,
           -1.96093033e-02, -1.20889226e-02, -8.20032583e-03, -5.92205494e-03,
           -4.46969569e-03, -3.48553738e-03, -2.78696669e-03, -2.27262654e-03,
           -1.88250419e-03, -1.57918656e-03, -1.33834507e-03, -1.14359262e-03,
           -9.83556112e-04, -8.50135860e-04, -7.37431533e-04, -6.41057709e-04,
           -5.57695279e-04, -4.84790002e-04, -4.20345356e-04, -3.62777276e-04,
           -3.10810343e-04, -2.63402280e-04, -2.19688049e-04, -1.78937740e-04,
           -1.40524230e-04, -1.03897813e-04, -6.85657917e-05, -3.40755526e-05])

```python
Dk = (2 * ck - 32 * dk) / np.sqrt(N)
Dk
```

    array([ 9.55474138e-01, -7.51049668e-01,  1.00565948e-01, -3.74617777e-02,
            1.96093033e-02, -1.20889226e-02,  8.20032583e-03, -5.92205494e-03,
            4.46969569e-03, -3.48553738e-03,  2.78696669e-03, -2.27262654e-03,
            1.88250419e-03, -1.57918656e-03,  1.33834507e-03, -1.14359262e-03,
            9.83556112e-04, -8.50135860e-04,  7.37431533e-04, -6.41057709e-04,
            5.57695279e-04, -4.84790002e-04,  4.20345356e-04, -3.62777276e-04,
            3.10810343e-04, -2.63402280e-04,  2.19688049e-04, -1.78937740e-04,
            1.40524230e-04, -1.03897813e-04,  6.85657917e-05, -3.40755526e-05])

# DCTして残差を計算

$
V_k = F_k - U_k\\
V_k = F_k - A_k(F_0-F_0^L) -B_k(F_0^R-F_0)-C_k(F_1+F_1^L)-D_k(F_1^R+F_1)
$

```python
IMG = LENNA
```

```python
Fk = np.zeros(IMG.img.shape)
```

## DCT 縦方向

2次元入力信号を垂直方向の一次元信号が水平方向に並列に並んでいるものとみなし、各列において8画素単位の1次元DCTを適用する

```python
for row in range(IMG.img.shape[0] // 32):
    for col in range(IMG.img.shape[1]):
        eight_points = IMG.img[32 * row : 32 * (row + 1), col]
        c = scipy.fftpack.dct(eight_points,norm="ortho")
        Fk[32 * row : 32 * (row + 1), col] = c

```

## 縦方向の残差

### 3階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]

        F_0_r = 0
        F_1_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]
            F_1_r = Fk[32 * (row + 1) + 1, col]

        F_0_l = 0
        F_1_l = 1
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]
            F_1_l = Fk[32 * (row - 1) + 1, col]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            - Ak * (F_0 - F_0_l)
            - Bk * (F_0_r - F_0)
            - Ck * (F_1 + F_1_l)
            - Dk * (F_1_r + F_1)
        )
        # F_0, F_1は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[32 * row : 32 * (row + 1), col] = F

```

### 1階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]

        F_0_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]

        F_0_l = 0
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]

        # 残差
        F_0 = F[0]
        F_temp = F - alpha * (F_0_r - F_0) / np.sqrt(N) - beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[32 * row : 32 * (row + 1), col] = F

```

## DCT 横方向

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        eight_points = Fk[row, 32 * col : 32 * (col + 1)]
        c = scipy.fftpack.dct(eight_points,norm="ortho")
        Fk[row, 32 * col : 32 * (col + 1)] = c

```

## 横方向の残差

### 3階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * (col + 1)]

        F_0_r = 0
        F_1_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]
            F_1_r = Fk[row, 32 * (col + 1) + 1]

        F_0_l = 0
        F_1_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]
            F_1_l = Fk[row, 32 * (col - 1) + 1]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            - Ak * (F_0 - F_0_l)
            - Bk * (F_0_r - F_0)
            - Ck * (F_1 + F_1_l)
            - Dk * (F_1_r + F_1)
        )
        # F_0は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * (col + 1)] = F

```

### 1階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * (col + 1)]

        F_0_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]

        F_0_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]

        # 残差
        F_0 = F[0]
        F_temp = F - alpha * (F_0_r - F_0) / np.sqrt(N) - beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * (col + 1)] = F

```

# 係数の確保

```python
Fk_Ori = np.copy(Fk)
```

# 低域3成分 (0,1)(1,0)(1,1)の絶対値の和

```python
low_3_value = 0
others_value = 0
```

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1] // 32):
        block = Fk[row * 32 : (row + 1) * 32, col * 32 : (col + 1) * 32]
        low_3_value += np.abs(block[0, 1]) + np.abs(block[1, 0]) + np.abs(block[1, 1])
        others_value += (
            np.sum(np.sum(np.abs(block)))
            - np.abs(block[0, 0])
            - np.abs(block[0, 1])
            - np.abs(block[1, 0])
            - np.abs(block[1, 1])
        )

```

```python
low_3_value
```

    172751.75137192418

```python
others_value
```

    1596228.5233772963

# 逆変換

$
F_k = F_k + U_k\\
F_k = V_k + A_k(F_0-F_0^L) +B_k(F_0^R-F_0)+C_k(F_1+F_1^L)+D_k(F_1^R+F_1)
$

```python
# recover = np.zeros(IMG.img.shape).astype("uint8")
recover = np.zeros(IMG.img.shape)
```

## 横方向の残差

### 1階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * col + 32]

        F_0_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]

        F_0_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]

        # 残差
        F_0 = F[0]
        F_temp = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * col + 32] = F

```

### 3階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * (col + 1)]

        F_0_r = 0
        F_1_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]
            F_1_r = Fk[row, 32 * (col + 1) + 1]

        F_0_l = 0
        F_1_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]
            F_1_l = Fk[row, 32 * (col - 1) + 1]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * (col + 1)] = F

```

### IDCT 横方向

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * col + 32]
        data = scipy.fftpack.idct(F,norm="ortho")
        # Fkに代入した後、縦方向に対して処理
        Fk[row, 32 * col : 32 * col + 32] = data
        # 復元画像
#         recover[row, 32 * col : 32 * col + 32] = data

```

## 縦方向

### 1階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * row + 32, col]

        F_0_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]

        F_0_l = 0
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]

        # 残差
        F_0 = F[0]
        F_temp = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 F_1 F_2 F_3 F_4 F_5 F_6 F_7
        Fk[32 * row : 32 * row + 32, col] = F

```

### 3階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]

        F_0_r = 0
        F_1_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]
            F_1_r = Fk[32 * (row + 1) + 1, col]

        F_0_l = 0
        F_1_l = 1
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]
            F_1_l = Fk[32 * (row - 1) + 1, col]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0, F_1は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[32 * row : 32 * (row + 1), col] = F

```

### 縦方向IDCT

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]
        data = scipy.fftpack.idct(F,norm="ortho")
        # 復元画像
        recover[32 * row : 32 * (row + 1), col] = data
        # FKに代入した後、横方向に対して処理
#         Fk[32 * row : 32 * (row + 1), col] = data

```

```python
plt.imshow(np.round(recover), cmap="gray")
```

```python
plt.imshow(IMG.img, cmap="gray")
```

```python
recover[0, 0:10]
```

    array([162., 162., 162., 161., 162., 157., 163., 161., 165., 161.])

```python
IMG.img[0, 0:10]
```

    array([162, 162, 162, 161, 162, 157, 163, 161, 165, 161], dtype=uint8)

ちゃんと復元できた

# 量子化テーブル

```python
Q = 38.5
Q_Luminance = np.ones((32,32)) * Q
```

# 量子化

```python
Fk = np.copy(Fk_Ori)
```

```python
plt.imshow(Fk)
```

```python
Q_Fk = np.zeros(Fk.shape)
```

```python
for row in range(IMG.img.shape[0] // 32):
    for col in range(IMG.img.shape[1] // 32):
        block = Fk[row * 32 : (row + 1) * 32, col * 32 : (col + 1) * 32]
        # 量子化
        block = np.round(block / Q_Luminance)
        # 逆量子化
        block = block * Q_Luminance
        Q_Fk[row * 32 : (row+1)*32, col * 32 : (col+1)*32] = block

```

# 逆変換

```python
Fk = np.copy(Q_Fk)
# Fk = np.copy(Fk_Ori)
```

```python
Q_recover = np.zeros(Q_Fk.shape)
```

## 横方向の残差

### 1階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * col + 32]

        F_0_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]

        F_0_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]

        # 残差
        F_0 = F[0]
        F_temp = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * col + 32] = F

```

### 3階の導関数

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * (col + 1)]

        F_0_r = 0
        F_1_r = 0
        if col is not Fk.shape[1] // 32 - 1:
            F_0_r = Fk[row, 32 * (col + 1)]
            F_1_r = Fk[row, 32 * (col + 1) + 1]

        F_0_l = 0
        F_1_l = 0
        if col is not 0:
            F_0_l = Fk[row, 32 * (col - 1)]
            F_1_l = Fk[row, 32 * (col - 1) + 1]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[row, 32 * col : 32 * (col + 1)] = F

```

### IDCT 横方向

```python
for row in range(Fk.shape[0]):
    for col in range(Fk.shape[1] // 32):
        F = Fk[row, 32 * col : 32 * col + 32]
        data = scipy.fftpack.idct(F,norm="ortho")
        # Fkに代入した後、縦方向に対して処理
        Fk[row, 32 * col : 32 * col + 32] = data
        # 復元画像
#         recover[row, 32 * col : 32 * col + 32] = data

```

## 縦方向

### 1階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * row + 32, col]

        F_0_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]

        F_0_l = 0
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]

        # 残差
        F_0 = F[0]
        F_temp = F + alpha * (F_0_r - F_0) / np.sqrt(N) + beta * (F_0 - F_0_l) / np.sqrt(N)
        # F_0は残す
        F[1] = F_temp[1]
        # F_0 F_1 F_2 F_3 F_4 F_5 F_6 F_7
        Fk[32 * row : 32 * row + 32, col] = F

```

### 3階の導関数

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]

        F_0_r = 0
        F_1_r = 0
        if row is not Fk.shape[0] // 32 - 1:
            F_0_r = Fk[32 * (row + 1), col]
            F_1_r = Fk[32 * (row + 1) + 1, col]

        F_0_l = 0
        F_1_l = 1
        if row is not 0:
            F_0_l = Fk[32 * (row - 1), col]
            F_1_l = Fk[32 * (row - 1) + 1, col]

        # 残差
        F_0 = F[0]
        F_1 = F[1]

        F = (
            F
            + Ak * (F_0 - F_0_l)
            + Bk * (F_0_r - F_0)
            + Ck * (F_1 + F_1_l)
            + Dk * (F_1_r + F_1)
        )
        # F_0, F_1は残す
        F[0] = F_0
        F[1] = F_1
        # F_0 V_1 V_2 V_3 V_4 V_5 V_6 V_7
        Fk[32 * row : 32 * (row + 1), col] = F

```

### IDCT 縦方向

```python
for row in range(Fk.shape[0] // 32):
    for col in range(Fk.shape[1]):
        F = Fk[32 * row : 32 * (row + 1), col]
        data = scipy.fftpack.idct(F,norm="ortho")
        # 復元画像
        Q_recover[32 * row : 32 * (row + 1), col] = data
        # FKに代入した後、横方向に対して処理
#         Fk[32 * row : 32 * (row + 1), col] = data

```

```python
Q_recover = np.round(Q_recover)
```

```python
plt.imshow(Q_recover, cmap="gray")
plt.imsave("TPHLCT3_32x32_LENNA.png",Q_recover,cmap="gray")
```

# 情報量

$$
S = - \sum ^{255}_{i=0} p_i log_2 p_i
$$

```python
qfk = pd.Series(Q_Fk.flatten())
pro = qfk.value_counts() / qfk.value_counts().sum()
pro.head()
```

     0.0     0.942108
     38.5    0.020073
    -38.5    0.019886
    -77.0    0.004166
     77.0    0.004082
    dtype: float64

```python
S = 0
for pi in pro:
    S -= pi * np.log2(pi)
S
```

    0.4820449820442598

# PSNR

$$
PSNR = 10 log_{10} \frac{MAX^2}{MSE}
$$

$$
{MSE = \frac{1}{m \, n} \sum^{m-1}_{i=0} \sum^{n-1}_{j=0} [ I(i,j) - K(i,j) ]^2}
$$
$I(i,j)$は原画像, $K(i,j)$は圧縮画像

```python
MSE = np.sum(np.sum(np.power((IMG.img - Q_recover),2)))/(Q_recover.shape[0] * Q_recover.shape[1])
```

```python
PSNR = 10 * np.log10(255 * 255 / MSE)
PSNR
```

    33.56155255401865

# MSSIM

```python
MSSIM = ssim(IMG.img,Q_recover.astype(IMG.img.dtype),gaussian_weights=True,sigma=1.5,K1=0.01,K2=0.03)
MSSIM
```

    0.8744387095101352

# MSDS

```python
MSDSt, MSDS1, MSDS2 = msds(32,Q_recover)
```

```python
MSDS1
```

    10828.880859375

```python
MSDS2
```

    2996.297119140625

```python

```


# Start-to-Finish Example: Numerical Solution of the Scalar Wave Equation, in Curvilinear Coordinates

## Author: Zach Etienne
### Formatting improvements courtesy Brandon Clark

## This module solves the scalar wave equation in *spherical coordinates* (though other coordinates, including Cartesian, may be chosen).

**Notebook Status:**  Validated

**Validation Notes:** This module has been validated to converge at the expected order to the exact solution (see [plot](#convergence) at bottom).

### NRPy+ Source Code for this module:
* [ScalarWave/ScalarWaveCurvilinear_RHSs.py](../edit/ScalarWave/ScalarWaveCurvilinear_RHSs.py) [\[**tutorial**\]](Tutorial-ScalarWaveCurvilinear.ipynb) Generates the right-hand side for the Scalar Wave Equation in curvilinear coordinates
* [ScalarWave/InitialData.py](../edit/ScalarWave/InitialData.py) [\[**tutorial**\]](Tutorial-ScalarWave.ipynb) Generating C code for either plane wave or spherical Gaussian initial data for the scalar wave equation

## Introduction:
As outlined in the [previous NRPy+ tutorial notebook](Tutorial-ScalarWaveCurvilinear.ipynb), we first use NRPy+ to generate initial data for the scalar wave equation, and then we use it to generate the RHS expressions for [Method of Lines](https://reference.wolfram.com/language/tutorial/NDSolveMethodOfLines.html) time integration based on the [explicit Runge-Kutta fourth-order scheme](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods) (RK4).

The entire algorithm is outlined below, with NRPy+-based components highlighted in green.

1. Allocate memory for gridfunctions, including temporary storage for the RK4 time integration.
1. Set gridfunction values to initial data.
1. Evolve the system forward in time using RK4 time integration. At each RK4 substep, do the following:
    1. Evaluate scalar wave RHS expressions.
    1. Apply boundary conditions.
1. At the end of each iteration in time, output the relative error between numerical and exact solutions.

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#writec): Generate C code to solve the scalar wave equation in curvilinear coordinates
    1. [Step 1.a](#id_rhss): C code generation: Initial data and scalar wave right-hand-sides
    1. [Step 1.b](#boundaryconditions): C code generation: Boundary condition driver
    1. [Step 1.c](#cparams_rfm_and_domainsize): Generate Cparameters files; set reference metric parameters, including `domain_size`
    1. [Step 1.d](#cfl): C code generation: Finding the minimum proper distance between grid points, needed for [CFL](https://en.wikipedia.org/w/index.php?title=Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition&oldid=806430673)-limited timestep
1. [Step 2](#mainc): `ScalarWaveCurvilinear_Playground.c`: The Main C Code
1. [Step 3](#compileexec): Compile generated C codes & solve the scalar wave equation
1. [Step 4](#convergence): Code validation: Plot the numerical error, and confirm that it converges to zero at expected rate with increasing numerical resolution (sampling)
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Using NRPy+ to generate necessary C code to solve the scalar wave equation in curvilinear, singular coordinates \[Back to [top](#toc)\]
$$\label{writec}$$

## Step 1.a: C code generation: Initial data and scalar wave RHSs \[Back to [top](#toc)\]
$$\label{id_rhss}$$

We choose simple plane wave initial data, which is documented in the [Cartesian scalar wave module](Tutorial-ScalarWave.ipynb). Specifically, we implement monochromatic (single-wavelength) wave traveling in the $\hat{k}$ direction with speed $c$
$$u(\vec{x},t) = f(\hat{k}\cdot\vec{x} - c t),$$
where $\hat{k}$ is a unit vector.

The scalar wave RHSs in curvilinear coordinates (documented [in the previous module](Tutorial-ScalarWaveCurvilinear.ipynb)) are simply the right-hand sides of the scalar wave equation written in curvilinear coordinates
\begin{align}
\partial_t u &= v \\
\partial_t v &= c^2 \left(\hat{g}^{ij} \partial_{i} \partial_{j} u - \hat{\Gamma}^i \partial_i u\right),
\end{align}
where $\hat{g}^{ij}$ is the inverse reference 3-metric (i.e., the metric corresponding to the underlying coordinate system we choose$-$spherical coordinates in our example below), and $\hat{\Gamma}^i$ is the contracted Christoffel symbol $\hat{\Gamma}^\tau = \hat{g}^{\mu\nu} \hat{\Gamma}^\tau_{\mu\nu}$.

Below we generate
+ the initial data by calling `InitialData(Type="PlaneWave")` inside the NRPy+ [ScalarWave/InitialData.py](../edit/ScalarWave/InitialData.py) module (documented in [this NRPy+ Jupyter notebook](Tutorial-ScalarWave.ipynb)), and
+ the RHS expressions by calling `ScalarWaveCurvilinear_RHSs()` inside the NRPy+ [ScalarWave/ScalarWaveCurvilinear_RHSs.py](../edit/ScalarWave/ScalarWaveCurvilinear_RHSs.py) module (documented in [this NRPy+ Jupyter notebook](Tutorial-ScalarWaveCurvilinear.ipynb)).

```python
# Step P1: Import needed NRPy+ core modules:
from outputC import lhrh,outCfunction # NRPy+: Core C code output module
import finite_difference as fin       # NRPy+: Finite difference C code generation module
import NRPy_param_funcs as par        # NRPy+: Parameter interface
import grid as gri                    # NRPy+: Functions having to do with numerical grids
import indexedexp as ixp              # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import reference_metric as rfm        # NRPy+: Reference metric support
import cmdline_helper as cmd          # NRPy+: Multi-platform Python command-line interface
import shutil, os, sys                # Standard Python modules for multiplatform OS-level functions

# Step P2: Create C code output directory:
Ccodesdir = os.path.join("ScalarWaveCurvilinear_Playground_Ccodes/")
# First remove C code output directory if it exists
# Courtesy https://stackoverflow.com/questions/303200/how-do-i-remove-delete-a-folder-that-is-not-empty
shutil.rmtree(Ccodesdir, ignore_errors=True)
# Then create a fresh directory
cmd.mkdir(Ccodesdir)

# Step P3: Create executable output directory:
outdir = os.path.join(Ccodesdir,"output/")
cmd.mkdir(outdir)

# Step 1: Set the spatial dimension parameter
#         to three this time, and then read
#         the parameter as DIM.
par.set_parval_from_str("grid::DIM",3)
DIM = par.parval_from_str("grid::DIM")

# Step 2: Set some core parameters, including CoordSystem, boundary condition,
#                                             MoL, timestepping algorithm, FD order,
#                                             floating point precision, and CFL factor:

# Step 2.a: Set the coordinate system for the numerical grid
# Choices are: Spherical, SinhSpherical, SinhSphericalv2, Cylindrical, SinhCylindrical,
#              SymTP, SinhSymTP
CoordSystem     = "SinhSpherical"
par.set_parval_from_str("reference_metric::CoordSystem",CoordSystem)
rfm.reference_metric()

# Step  2.b: Set boundary conditions
# Current choices are QuadraticExtrapolation (quadratic polynomial extrapolation) or Sommerfeld
BoundaryCondition = "QuadraticExtrapolation"

# Step 2.c: Set defaults for Coordinate system parameters.
#           These are perhaps the most commonly adjusted parameters,
#           so we enable modifications at this high level.

# domain_size sets the default value for:
#   * Spherical's params.RMAX
#   * SinhSpherical*'s params.AMAX
#   * Cartesians*'s -params.{x,y,z}min & .{x,y,z}max
#   * Cylindrical's -params.ZMIN & .{Z,RHO}MAX
#   * SinhCylindrical's params.AMPL{RHO,Z}
#   * *SymTP's params.AMAX
domain_size     = 10.0 # Needed for all coordinate systems.

# sinh_width sets the default value for:
#   * SinhSpherical's params.SINHW
#   * SinhCylindrical's params.SINHW{RHO,Z}
#   * SinhSymTP's params.SINHWAA
sinh_width      = 0.4 # If Sinh* coordinates chosen

# sinhv2_const_dr sets the default value for:
#   * SinhSphericalv2's params.const_dr
#   * SinhCylindricalv2's params.const_d{rho,z}
sinhv2_const_dr = 0.05# If Sinh*v2 coordinates chosen

# SymTP_bScale sets the default value for:
#   * SinhSymTP's params.bScale
SymTP_bScale    = 1.0 # If SymTP chosen

# Step 2.d: Set the order of spatial and temporal derivatives;
#           the core data type, and the CFL factor.
# RK_method choices include: Euler, "RK2 Heun", "RK2 MP", "RK2 Ralston", RK3, "RK3 Heun", "RK3 Ralston",
#              SSPRK3, RK4, DP5, DP5alt, CK5, DP6, L6, DP8
RK_method = "RK4"
FD_order  = 4        # Finite difference order: even numbers only, starting with 2. 12 is generally unstable
REAL      = "double" # Best to use double here.
CFL_FACTOR= 1.0

# Step 3: Generate Runge-Kutta-based (RK-based) timestepping code.
#         Each RK substep involves two function calls:
#      3.A: Evaluate RHSs (RHS_string)
#      3.B: Apply boundary conditions (post_RHS_string)
import MoLtimestepping.C_Code_Generation as MoL
from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict
RK_order  = Butcher_dict[RK_method][1]
cmd.mkdir(os.path.join(Ccodesdir,"MoLtimestepping/"))

RHS_string = "rhs_eval(&rfmstruct, &params, RK_INPUT_GFS, RK_OUTPUT_GFS);"

if BoundaryCondition == "QuadraticExtrapolation":
    # Extrapolation BCs are applied to the evolved gridfunctions themselves after the MoL update
    post_RHS_string = "apply_bcs_curvilinear(&params, &bcstruct, NUM_EVOL_GFS, evol_gf_parity, RK_OUTPUT_GFS);"
elif BoundaryCondition == "Sommerfeld":
    # Sommerfeld BCs are applied to the gridfunction RHSs directly
    RHS_string += "apply_bcs_sommerfeld(&params, xx, &bcstruct, NUM_EVOL_GFS, evol_gf_parity, RK_INPUT_GFS, RK_OUTPUT_GFS);"
    post_RHS_string = ""
else:
    print("Invalid choice of boundary condition")
    sys.exit(1)

MoL.MoL_C_Code_Generation(RK_method, RHS_string = RHS_string, post_RHS_string = post_RHS_string,
                          outdir = os.path.join(Ccodesdir,"MoLtimestepping/"))

# Step 4: Import the ScalarWave.InitialData module.
#         This command only declares ScalarWave initial data
#         parameters and the InitialData() function.
import ScalarWave.InitialData as swid

# Step 5: Import ScalarWave_RHSs module.
#         This command only declares ScalarWave RHS parameters
#         and the ScalarWave_RHSs function (called later)
import ScalarWave.ScalarWaveCurvilinear_RHSs as swrhs

# Step 6: Set the finite differencing order to FD_order (set above).
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER",FD_order)

# Step 7: Call the InitialData() function to set up initial data.
#         Options include:
#    "PlaneWave": monochromatic (single frequency/wavelength) plane wave
#    "SphericalGaussian": spherically symmetric Gaussian, with default stdev=3
swid.InitialData(CoordSystem=CoordSystem,Type="PlaneWave")

# Step 8: Generate SymPy symbolic expressions for
#         uu_rhs and vv_rhs; the ScalarWave RHSs.
#         This function also declares the uu and vv
#         gridfunctions, which need to be declared
#         to output even the initial data to C file.
cmd.mkdir(os.path.join(Ccodesdir,"rfm_files/"))
par.set_parval_from_str("reference_metric::enable_rfm_precompute","True")
par.set_parval_from_str("reference_metric::rfm_precompute_Ccode_outdir",os.path.join(Ccodesdir,"rfm_files/"))
swrhs.ScalarWaveCurvilinear_RHSs()
# Step 8.a: Now that we are finished with all the rfm hatted
#           quantities, let's restore them to their closed-
#           form expressions.
par.set_parval_from_str("reference_metric::enable_rfm_precompute","False") # Reset to False to disable rfm_precompute.
rfm.ref_metric__hatted_quantities()

# Step 9: Copy SIMD/SIMD_intrinsics.h to $Ccodesdir/SIMD/SIMD_intrinsics.h
cmd.mkdir(os.path.join(Ccodesdir,"SIMD"))
shutil.copy(os.path.join("SIMD/")+"SIMD_intrinsics.h",os.path.join(Ccodesdir,"SIMD/"))

# Step 10: Generate all needed C functions
FD_functions_enable = False
par.set_parval_from_str("finite_difference::FD_functions_enable",FD_functions_enable)

desc="Part P3: Declare the function for the exact solution at a single point. time==0 corresponds to the initial data."
name="exact_solution_single_point"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const REAL xx0,const REAL xx1,const REAL xx2,const paramstruct *restrict params,REAL *uu_exact,REAL *vv_exact",
    body     = fin.FD_outputC("returnstring",[lhrh(lhs="*uu_exact",rhs=swid.uu_ID),
                                              lhrh(lhs="*vv_exact",rhs=swid.vv_ID)]),
    loopopts = "")

desc="Part P4: Declare the function for the exact solution at all points. time==0 corresponds to the initial data."
name="exact_solution_all_points"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *restrict params,REAL *restrict xx[3], REAL *restrict in_gfs",
    body     ="""exact_solution_single_point(xx[0][i0],xx[1][i1],xx[2][i2],params,
                            &in_gfs[IDX4S(UUGF,i0,i1,i2)],&in_gfs[IDX4S(VVGF,i0,i1,i2)]);""",
    loopopts = "AllPoints")

desc="Part P5: Declare the function to evaluate the scalar wave RHSs"
includes = None
if FD_functions_enable:
    includes = ["finite_difference_functions.h"]
name="rhs_eval"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), includes=includes, desc=desc, name=name,
    params   ="""rfm_struct *restrict rfmstruct,const paramstruct *restrict params,
                 const REAL *restrict in_gfs, REAL *restrict rhs_gfs""",
    body     =fin.FD_outputC("returnstring",[lhrh(lhs=gri.gfaccess("rhs_gfs","uu"),rhs=swrhs.uu_rhs),
                                             lhrh(lhs=gri.gfaccess("rhs_gfs","vv"),rhs=swrhs.vv_rhs)],
                             params="SIMD_enable=True").replace("IDX4","IDX4S"),
    loopopts = "InteriorPoints,EnableSIMD,Enable_rfm_precompute")

# Step 10.b Output functions for computing all finite-difference stencils
if FD_functions_enable:
    fin.output_finite_difference_functions_h(path=Ccodesdir)
```

    Output C function exact_solution_single_point() to file ScalarWaveCurvilinear_Playground_Ccodes/exact_solution_single_point.h
    Output C function exact_solution_all_points() to file ScalarWaveCurvilinear_Playground_Ccodes/exact_solution_all_points.h
    Output C function rhs_eval() to file ScalarWaveCurvilinear_Playground_Ccodes/rhs_eval.h

## Step 1.b: Output needed C code for boundary condition driver \[Back to [top](#toc)\]
$$\label{boundaryconditions}$$

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions(os.path.join(Ccodesdir,"boundary_conditions/"),
                                    Cparamspath=os.path.join("../"),
                                    BoundaryCondition=BoundaryCondition)
if BoundaryCondition == "Sommerfeld":
    bcs = cbcs.sommerfeld_boundary_condition_class(fd_order=2,
                                                 vars_radial_falloff_power_default=3,
                                                 vars_speed_default=1.,
                                                 vars_at_inf_default=0.)
    # bcs.vars_radpower.items()
    bcs.write_sommerfeld_file(Ccodesdir)
```

    Wrote to file "ScalarWaveCurvilinear_Playground_Ccodes/boundary_conditions/parity_conditions_symbolic_dot_products.h"
    Evolved parity: ( uu:0, vv:0 )

    Wrote to file "ScalarWaveCurvilinear_Playground_Ccodes/boundary_conditions/EigenCoord_Cart_to_xx.h"

## Step 1.c: Output C codes needed for declaring and setting Cparameters; also set `free_parameters.h` \[Back to [top](#toc)\]
$$\label{cparams_rfm_and_domainsize}$$

Based on declared NRPy+ Cparameters, first we generate `declare_Cparameters_struct.h`, `set_Cparameters_default.h`, and `set_Cparameters[-SIMD].h`.

Then we output `free_parameters.h`, which sets initial data parameters, as well as grid domain & reference metric parameters, applying `domain_size` and `sinh_width`/`SymTP_bScale` (if applicable) as set above

```python
# Step 1.c.i: Set free_parameters.h
with open(os.path.join(Ccodesdir,"free_parameters.h"),"w") as file:
    file.write("""
// Set free-parameter values.
params.time = 0.0; // Initial simulation time time corresponds to exact solution at time=0.
params.wavespeed = 1.0;\n""")

# Append to $Ccodesdir/free_parameters.h reference metric parameters based on generic
#    domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale,
#    parameters set above.
rfm.out_default_free_parameters_for_rfm(os.path.join(Ccodesdir,"free_parameters.h"),
                                        domain_size,sinh_width,sinhv2_const_dr,SymTP_bScale)

# Step 1.c.ii: Generate set_Nxx_dxx_invdx_params__and__xx.h:
rfm.set_Nxx_dxx_invdx_params__and__xx_h(os.path.join(Ccodesdir))

# Step 1.c.iii: Generate xx_to_Cart.h, which contains xx_to_Cart() for
#               (the mapping from xx->Cartesian) for the chosen
#               CoordSystem:
rfm.xx_to_Cart_h("xx_to_Cart","./set_Cparameters.h",os.path.join(Ccodesdir,"xx_to_Cart.h"))

# Step 1.c.iv: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))
```

## Step 1.d: Output needed C code for finding the minimum proper distance between grid points, needed for [CFL](https://en.wikipedia.org/w/index.php?title=Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition&oldid=806430673)-limited timestep \[Back to [top](#toc)\]
$$\label{cfl}$$

In order for our explicit-timestepping numerical solution to the scalar wave equation to be stable, it must satisfy the [CFL](https://en.wikipedia.org/w/index.php?title=Courant%E2%80%93Friedrichs%E2%80%93Lewy_condition&oldid=806430673) condition:
$$
\Delta t \le \frac{\min(ds_i)}{c},
$$
where $c$ is the wavespeed, and
$$ds_i = h_i \Delta x^i$$
is the proper distance between neighboring gridpoints in the $i$th direction (in 3D, there are 3 directions), $h_i$ is the $i$th reference metric scale factor, and $\Delta x^i$ is the uniform grid spacing in the $i$th direction:

```python
# Output the find_timestep() function to a C file.
rfm.out_timestep_func_to_file(os.path.join(Ccodesdir,"find_timestep.h"))
```

# Step 2: `ScalarWaveCurvilinear_Playground.c`: The Main C Code \[Back to [top](#toc)\]
$$\label{mainc}$$

Just as in [the start-to-finish, solving the scalar wave equation in Cartesian coordinates module](Tutorial-Start_to_Finish-ScalarWave.ipynb), we will implement the scalar wave equation via the Method of Lines. As discussed above, the critical differences between this code and the Cartesian version are as follows:
1. The CFL-constrained timestep depends on the proper distance between neighboring gridpoints
1. The boundary conditions must account for the fact that ghost zone points lying in the domain exterior can map either to the interior of the domain, or lie on the outer boundary. In the former case, we simply copy the data from the interior. In the latter case, we apply the usual outer boundary conditions.
1. The numerical grids must be staggered to avoid direct evaluation of the equations on coordinate singularities.

```python
# Part P0: Define REAL, set the number of ghost cells NGHOSTS (from NRPy+'s FD_CENTDERIVS_ORDER),
#          and set the CFL_FACTOR (which can be overwritten at the command line)

with open(os.path.join(Ccodesdir,"ScalarWaveCurvilinear_Playground_REAL__NGHOSTS__CFL_FACTOR.h"), "w") as file:
    file.write("""
// Part P0.a: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
#define NGHOSTS """+str(int(FD_order/2))+"""
// Part P0.b: Set the numerical precision (REAL) to double, ensuring all floating point
//            numbers are stored to at least ~16 significant digits
#define REAL """+REAL+"""
// Part P0.c: Set the number of ghost cells, from NRPy+'s FD_CENTDERIVS_ORDER
REAL CFL_FACTOR = """+str(CFL_FACTOR)+";\n")
```

```python
%%writefile $Ccodesdir/ScalarWaveCurvilinear_Playground.c

// Step P0: Define REAL and NGHOSTS; and declare CFL_FACTOR. This header is generated in NRPy+.
#include "ScalarWaveCurvilinear_Playground_REAL__NGHOSTS__CFL_FACTOR.h"

#include "rfm_files/rfm_struct__declare.h"

#include "declare_Cparameters_struct.h"

// All SIMD intrinsics used in SIMD-enabled C code loops are defined here:
#include "SIMD/SIMD_intrinsics.h"

// Step P1: Import needed header files
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "stdint.h" // Needed for Windows GCC 6.x compatibility
#ifndef M_PI
#define M_PI 3.141592653589793238462643383279502884L
#endif
#ifndef M_SQRT1_2
#define M_SQRT1_2 0.707106781186547524400844362104849039L
#endif

// Step P2: Declare the IDX4S(gf,i,j,k) macro, which enables us to store 4-dimensions of
//           data in a 1D array. In this case, consecutive values of "i"
//           (all other indices held to a fixed value) are consecutive in memory, where
//           consecutive values of "j" (fixing all other indices) are separated by
//           Nxx_plus_2NGHOSTS0 elements in memory. Similarly, consecutive values of
//           "k" are separated by Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1 in memory, etc.
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )
#define IDX3S(i,j,k) ( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) ) ) )
#define LOOP_REGION(i0min,i0max, i1min,i1max, i2min,i2max) \
  for(int i2=i2min;i2<i2max;i2++) for(int i1=i1min;i1<i1max;i1++) for(int i0=i0min;i0<i0max;i0++)
#define LOOP_ALL_GFS_GPS(ii) _Pragma("omp parallel for") \
  for(int (ii)=0;(ii)<Nxx_plus_2NGHOSTS_tot*NUM_EVOL_GFS;(ii)++)

// Step P3: Set UUGF and VVGF macros, as well as xx_to_Cart()
#include "boundary_conditions/gridfunction_defines.h"

// Step P4: Set xx_to_Cart(const paramstruct *restrict params,
//                     REAL *restrict xx[3],
//                     const int i0,const int i1,const int i2,
//                     REAL xCart[3]),
//           which maps xx->Cartesian via
//    {xx[0][i0],xx[1][i1],xx[2][i2]}->{xCart[0],xCart[1],xCart[2]}
#include "xx_to_Cart.h"

// Step P5: Defines set_Nxx_dxx_invdx_params__and__xx(const int EigenCoord, const int Nxx[3],
//                                       paramstruct *restrict params, REAL *restrict xx[3]),
//          which sets params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for
//          the chosen Eigen-CoordSystem if EigenCoord==1, or
//          CoordSystem if EigenCoord==0.
#include "set_Nxx_dxx_invdx_params__and__xx.h"

// Step P6: Include basic functions needed to impose curvilinear
//          parity and boundary conditions.
#include "boundary_conditions/CurviBC_include_Cfunctions.h"

// Step P7: Find the CFL-constrained timestep
#include "find_timestep.h"

// Part P8: Declare the function for the exact solution at a single point. time==0 corresponds to the initial data.
#include "exact_solution_single_point.h"

// Part P9: Declare the function for the exact solution at all points. time==0 corresponds to the initial data.
#include "exact_solution_all_points.h"

// Part P10: Declare the function to evaluate the scalar wave RHSs
#include "rhs_eval.h"

// main() function:
// Step 0: Read command-line input, set up grid structure, allocate memory for gridfunctions, set up coordinates
// Step 1: Set up scalar wave initial data
// Step 2: Output relative error between numerical and exact solution.
// Step 3: Evolve scalar wave initial data forward in time using Method of Lines with chosen RK-like algorithm,
//         applying quadratic extrapolation outer boundary conditions.
// Step 4: Free all allocated memory
int main(int argc, const char *argv[]) {
    paramstruct params;
#include "set_Cparameters_default.h"

    // Step 0a: Read command-line input, error out if nonconformant
    if(argc != 4 || atoi(argv[1]) < NGHOSTS || atoi(argv[2]) < NGHOSTS || atoi(argv[3]) < NGHOSTS) {
        printf("Error: Expected one command-line argument: ./ScalarWaveCurvilinear_Playground Nx0 Nx1 Nx2,\n");
        printf("where Nx[0,1,2] is the number of grid points in the 0, 1, and 2 directions.\n");
        printf("Nx[] MUST BE larger than NGHOSTS (= %d)\n",NGHOSTS);
        exit(1);
    }
    // Step 0b: Set up numerical grid structure, first in space...
    const int Nxx[3] = { atoi(argv[1]), atoi(argv[2]), atoi(argv[3]) };
    if(Nxx[0]%2 != 0 || Nxx[1]%2 != 0 || Nxx[2]%2 != 0) {
        printf("Error: Cannot guarantee a proper cell-centered grid if number of grid cells not set to even number.\n");
        printf("       For example, in case of angular directions, proper symmetry zones will not exist.\n");
        exit(1);
    }

    // Step 0c: Set free parameters, overwriting Cparameters defaults
    //          by hand or with command-line input, as desired.
#include "free_parameters.h"

   // Step 0d: Uniform coordinate grids are stored to *xx[3]
    REAL *xx[3];
    // Step 0d.i: Set bcstruct
    bc_struct bcstruct;
    {
        int EigenCoord = 1;
        // Step 0d.ii: Call set_Nxx_dxx_invdx_params__and__xx(), which sets
        //             params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the
        //             chosen Eigen-CoordSystem.
        set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &params, xx);
        // Step 0d.iii: Set Nxx_plus_2NGHOSTS_tot
#include "set_Cparameters-nopointer.h"
        const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;
        // Step 0e: Find ghostzone mappings; set up bcstruct
#include "boundary_conditions/driver_bcstruct.h"
        // Step 0e.i: Free allocated space for xx[][] array
        for(int i=0;i<3;i++) free(xx[i]);
    }

    // Step 0f: Call set_Nxx_dxx_invdx_params__and__xx(), which sets
    //          params Nxx,Nxx_plus_2NGHOSTS,dxx,invdx, and xx[] for the
    //          chosen (non-Eigen) CoordSystem.
    int EigenCoord = 0;
    set_Nxx_dxx_invdx_params__and__xx(EigenCoord, Nxx, &params, xx);

    // Step 0g: Set all C parameters "blah" for params.blah, including
    //          Nxx_plus_2NGHOSTS0 = params.Nxx_plus_2NGHOSTS0, etc.
#include "set_Cparameters-nopointer.h"
    const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;

    // Step 0h: Time coordinate parameters
    const REAL t_final =  0.7*domain_size; /* Final time is set so that at t=t_final,
                                            * data at the origin have not been corrupted
                                            * by the approximate outer boundary condition */

    // Step 0i: Set timestep based on smallest proper distance between gridpoints and CFL factor
    REAL dt = find_timestep(&params, xx);
    //printf("# Timestep set to = %e\n",(double)dt);
    int N_final = (int)(t_final / dt + 0.5); // The number of points in time.
                                             // Add 0.5 to account for C rounding down
                                             // typecasts to integers.
    int output_every_N = (int)((REAL)N_final/800.0);
    if(output_every_N == 0) output_every_N = 1;

    // Step 0j: Error out if the number of auxiliary gridfunctions outnumber evolved gridfunctions.
    //              This is a limitation of the RK method. You are always welcome to declare & allocate
    //              additional gridfunctions by hand.
    if(NUM_AUX_GFS > NUM_EVOL_GFS) {
        printf("Error: NUM_AUX_GFS > NUM_EVOL_GFS. Either reduce the number of auxiliary gridfunctions,\n");
        printf("       or allocate (malloc) by hand storage for *diagnostic_output_gfs. \n");
        exit(1);
    }

    // Step 0k: Allocate memory for gridfunctions
#include "MoLtimestepping/RK_Allocate_Memory.h"

    // Step 0l: Set up precomputed reference metric arrays
    // Step 0l.i: Allocate space for precomputed reference metric arrays.
#include "rfm_files/rfm_struct__malloc.h"

    // Step 0l.ii: Define precomputed reference metric arrays.
    {
#include "set_Cparameters-nopointer.h"
#include "rfm_files/rfm_struct__define.h"
    }

    // Step 1: Set up initial data to be exact solution at time=0:
    params.time = 0.0; exact_solution_all_points(&params, xx, y_n_gfs);

    for(int n=0;n<=N_final;n++)
       { // Main loop to progress forward in time.

        // Step 1a: Set current time to correct value & compute exact solution
        params.time = ((REAL)n)*dt;

        // Step 2: Code validation: Compute log of L2 norm of difference
        //         between numerical and exact solutions:
        //   log_L2_Norm = log10( sqrt[Integral( [numerical - exact]^2 * dV)] ),
        //         where integral is within 30% of the grid outer boundary (domain_size)
        if(n%output_every_N == 0) {
            REAL integral = 0.0;
            REAL numpts   = 0.0;
#pragma omp parallel for reduction(+:integral,numpts)
            LOOP_REGION(NGHOSTS,Nxx_plus_2NGHOSTS0-NGHOSTS,
                        NGHOSTS,Nxx_plus_2NGHOSTS1-NGHOSTS,
                        NGHOSTS,Nxx_plus_2NGHOSTS2-NGHOSTS) {
                REAL xCart[3]; xx_to_Cart(&params,xx,i0,i1,i2, xCart);
                if(sqrt(xCart[0]*xCart[0] + xCart[1]*xCart[1] + xCart[2]*xCart[2]) < domain_size*0.3) {
                    REAL uu_exact,vv_exact; exact_solution_single_point(xx[0][i0],xx[1][i1],xx[2][i2],&params,
                                                                        &uu_exact,&vv_exact);
                    double num   = (double)y_n_gfs[IDX4S(UUGF,i0,i1,i2)];
                    double exact = (double)uu_exact;
                    integral += (num - exact)*(num - exact);
                    numpts   += 1.0;
                }
            }
            // Compute and output the log of the L2 norm.
            REAL log_L2_Norm = log10(sqrt(integral/numpts));
            printf("%e %e\n",(double)params.time,log_L2_Norm);
        }

        // Step 3: Step forward one timestep (t -> t+dt) in time using
        //           chosen RK-like MoL timestepping algorithm
#include "MoLtimestepping/RK_MoL.h"

    } // End main loop to progress forward in time.

    // Step 4: Free all allocated memory
#include "rfm_files/rfm_struct__freemem.h"
#include "boundary_conditions/bcstruct_freemem.h"
#include "MoLtimestepping/RK_Free_Memory.h"
    for(int i=0;i<3;i++) free(xx[i]);
    return 0;
}
```

    Writing ScalarWaveCurvilinear_Playground_Ccodes//ScalarWaveCurvilinear_Playground.c

# Step 3: Compile generated C codes & solve the scalar wave equation \[Back to [top](#toc)\]
$$\label{compileexec}$$

To aid in the cross-platform-compatible (with Windows, MacOS, & Linux) compilation and execution, we make use of `cmdline_helper` [(**Tutorial**)](Tutorial-cmdline_helper.ipynb).

```python
import cmdline_helper as cmd

cmd.C_compile(os.path.join(Ccodesdir,"ScalarWaveCurvilinear_Playground.c"),
              os.path.join(outdir,"ScalarWaveCurvilinear_Playground"),compile_mode="optimized")
# !clang -Ofast -fopenmp -mavx2 -mfma ScalarWave/ScalarWaveCurvilinear_Playground.c -o ScalarWaveCurvilinear_Playground -lm
# !icc -align -qopenmp -xHost -O2 -qopt-report=5 -qopt-report-phase ipo -qopt-report-phase vec -vec-threshold1 -qopt-prefetch=4 ScalarWave/ScalarWaveCurvilinear_Playground.c -o ScalarWaveCurvilinear_Playground
# !gcc-7 -Ofast -fopenmp -march=native ScalarWave/ScalarWaveCurvilinear_Playground.c -o ScalarWaveCurvilinear_Playground -lm

# Change to output directory
os.chdir(outdir)
# Clean up existing output files
cmd.delete_existing_files("out-*resolution.txt")
# Run executable
if par.parval_from_str("reference_metric::CoordSystem") == "Cartesian":
    cmd.Execute("ScalarWaveCurvilinear_Playground", "16 16 16", "out-lowresolution.txt")
    cmd.Execute("ScalarWaveCurvilinear_Playground", "24 24 24", "out-medresolution.txt")
else:
    cmd.Execute("ScalarWaveCurvilinear_Playground", "16  8 16", "out-lowresolution.txt")
    # 4.28s with icc and FD order = 10.
    cmd.Execute("ScalarWaveCurvilinear_Playground", "24 12 24", "out-medresolution.txt")
    ########################################
    # BENCHMARK 48x24x48 RUN, FD order = 4. desktop: 17.33s
    # laptop: 51.82s on icc. 45.02s on GCC 9, 45.03s on GCC 7, 51.67s on clang
    #     cmd.Execute("ScalarWaveCurvilinear_Playground", "48 24 48", "out-hghresolution.txt")

# %timeit cmd.Execute("ScalarWaveCurvilinear_Playground", "48 24 48", "out-hghresolution.txt", verbose=False)
# FD functions disabled:
# 16 s ± 702 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# FD functions enabled:
# 16.1 s ± 384 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# Return to root directory
os.chdir(os.path.join("../../"))
```

    Compiling executable...
    (EXEC): Executing `gcc -Ofast -fopenmp -march=native -funroll-loops ScalarWaveCurvilinear_Playground_Ccodes/ScalarWaveCurvilinear_Playground.c -o ScalarWaveCurvilinear_Playground_Ccodes/output/ScalarWaveCurvilinear_Playground -lm`...
    (BENCH): Finished executing in 1.8240478038787842 seconds.
    Finished compilation.
    (EXEC): Executing `taskset -c 0,1 ./ScalarWaveCurvilinear_Playground 16  8 16`...
    (BENCH): Finished executing in 0.4196474552154541 seconds.
    (EXEC): Executing `taskset -c 0,1 ./ScalarWaveCurvilinear_Playground 24 12 24`...
    (BENCH): Finished executing in 1.821354627609253 seconds.

# Step 4: Code validation: Plot the numerical error, and confirm that it converges to zero at expected rate with increasing numerical resolution (sampling) \[Back to [top](#toc)\]
$$\label{convergence}$$
The numerical solution $u_{\rm num}(x0,x1,x2,t)$ should converge to the exact solution $u_{\rm exact}(x0,x1,x2,t)$ at fourth order, which means that
$$
u_{\rm num}(x0,x1,x2,t) = u_{\rm exact}(x0,x1,x2,t) + \mathcal{O}\left((\Delta x0)^4\right)+ \mathcal{O}\left((\Delta x1)^4\right)+ \mathcal{O}\left((\Delta x2)^4\right)+ \mathcal{O}\left((\Delta t)^4\right).
$$

Thus the relative error $E_{\rm rel}$ should satisfy:
$$
\left|\frac{u_{\rm num}(x0,x1,x2,t) - u_{\rm exact}(x0,x1,x2,t)}{u_{\rm exact}(x0,x1,x2,t)}\right| + \mathcal{O}\left((\Delta x0)^4\right)+ \mathcal{O}\left((\Delta x1)^4\right)+ \mathcal{O}\left((\Delta x2)^4\right)+ \mathcal{O}\left((\Delta t)^4\right).
$$

We confirm this convergence behavior by first solving the scalar wave equation at two resolutions: $16\times 8\times 16$ (or $16^3$ if `reference_metric::CoordSystem` is set to `Cartesian`), and $24\times 12\times 24$ (or $24^3$ if `reference_metric::CoordSystem` is set to `Cartesian`) and evaluating the maximum logarithmic relative error $\log_{10} E_{\rm rel,max}$ between numerical and exact solutions within a region $R < 0.1 {\rm RMAX}$ at all iterations.

Since we increase the resolution uniformly over all four coordinates $(x0,x1,x2,t)$, $E_{\rm rel}$ should drop uniformly as $(\Delta x0)^4$:
$$
E_{\rm rel} \propto (\Delta x0)^4.
$$

So at the two resolutions, we should find that
$$
\frac{E_{\rm rel}(16\times 8\times 16)}{E_{\rm rel}(24\times 12\times 24)} = \frac{E_{\rm rel}(16^3)}{E_{\rm rel}(24^3)} \approx \left(\frac{(\Delta x0)_{16}}{(\Delta x0)_{24}}\right)^{4} = \left(\frac{24}{16}\right)^4 \approx 5.
$$

Since we're measuring logarithmic relative error, this should be
$$
\log_{10}\left(\frac{E_{\rm rel}(16\times 8\times 16)}{E_{\rm rel}(24\times 12\times 24)}\right) = \log_{10}\left(\frac{E_{\rm rel}(16^3)}{E_{\rm rel}(24^3)}\right) \approx \log_{10}(5).
$$

```python
%matplotlib inline
import matplotlib.pyplot as plt
import mpmath as mp
import csv

def file_reader(filename):
    with open(filename) as file:
        reader = csv.reader(file, delimiter=" ")
        data  = list(zip(*reader))
        # data is a tuple of strings. Tuples are immutable, and we need to perform math on
        #   the data, so here we convert tuple to lists of floats:
        data0 = []
        data1 = []
        for i in range(len(data[0])):
            data0.append(float(data[0][i]))
            data1.append(float(data[1][i]))
        return data0,data1

first_col16,second_col16 = file_reader(os.path.join(outdir,'out-lowresolution.txt'))
first_col24,second_col24 = file_reader(os.path.join(outdir,'out-medresolution.txt'))

second_col16_rescaled4o = []
second_col16_rescaled5o = []
for i in range(len(second_col16)):
    # data16 = data24*(16/24)**4
    # -> log10(data24) = log10(data24) + 4*log10(16/24)
    second_col16_rescaled4o.append(second_col16[i] + 4*mp.log10(16./24.))
    second_col16_rescaled5o.append(second_col16[i] + 5*mp.log10(16./24.))

# https://matplotlib.org/gallery/text_labels_and_annotations/legend.html#sphx-glr-gallery-text-labels-and-annotations-legend-py
fig, ax = plt.subplots()

plt.title("Demonstrating 4th-order Convergence: "+par.parval_from_str("reference_metric::CoordSystem")+" Coordinates")
plt.xlabel("time")
plt.ylabel("log10(Max relative error)")

ax.plot(first_col24, second_col24, 'k-', label='logErel(N0=24)')
ax.plot(first_col16, second_col16_rescaled4o, 'k--', label='logErel(N0=16) + log((16/24)^4)')
ax.set_ylim([-8.05,-1.7]) # Manually set the y-axis range case, since the log10
                          # relative error at t=0 could be -inf or about -16,
                          # resulting in very different-looking plots
                          # despite the data being the same to roundoff.
if par.parval_from_str("reference_metric::CoordSystem") == "Cartesian":
    ax.set_ylim([-2.68,-1.62])
if par.parval_from_str("reference_metric::CoordSystem") == "Cylindrical":
    ax.plot(first_col16, second_col16_rescaled5o, 'k.',  label='(Assuming 5th-order convergence)')
legend = ax.legend(loc='lower right', shadow=True, fontsize='large')
legend.get_frame().set_facecolor('C1')
plt.show()
```

# Step 5: Output this notebook to $\LaTeX$-formatted PDF file \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-Start_to_Finish-ScalarWaveCurvilinear.pdf](Tutorial-Start_to_Finish-ScalarWaveCurvilinear.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Start_to_Finish-ScalarWaveCurvilinear")
```

    Created Tutorial-Start_to_Finish-ScalarWaveCurvilinear.tex, and compiled
        LaTeX file to PDF file Tutorial-Start_to_Finish-
        ScalarWaveCurvilinear.pdf