# Sommerfeld Boundary Conditions

 Currently only supports Cartesian coordinates

## Authors: Terrence Pierre Jacques & Zach Etienne

## Abstract

The aim of this notebook is to describe the mathematical motivation behind the Sommerfeld boundary condition, document how it is implemented within the NRPy+ infrastructure in Cartesian coordinates, and record ongoing validation tests against the [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk).

**Notebook Status:**  Validated against the Einstein Toolkit

**Validation Notes:** The Sommerfeld boundary condition as implemented in NRPy+ has been validated against [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk)(ETK) for the case of a scalar wave propagating across a 3D Cartesian grid. We have agreement to roundoff error with the Einstein Toolkit. Specifically, we have achieved:

1. Roundoff level agreement for the wave propagating toward each of the individual faces

1. Roundoff level agreement using any combination of input parameters available in the [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk) (variable value at infinity, radial power, and wave speed)

[comment]: <> (Introduction: TODO)

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

This notebook is organized as follows

1. [Step 1](#initializenrpy): Set core NRPy+ parameters for numerical grids
1. [Step 2](#sbc): Definition and mathematical motivation
    1. [Step 2.a](#intro): Introduction & background mathematics
    1. [Step 2.b](#sbc_prelims): Preliminaries - The scalar wave equation in curvilinear coordinates
    1. [Step 2.c](#sbc_ansatz): Sommerfeld boundary condition ansatz
    1. [Step 2.d](#sbc_ansatz_dtf): Applying the ansatz to $\partial_t f$
    1. [Step 2.e](#curvicoords): Implementation in generic curvilinear coordinates
        1. [Step 2.e.i](#cartcoords_byhand): Sommerfeld boundary conditions implementation in Cartesian coordinates, derived by hand
        1. [Step 2.e.ii](#cartcoords_bynrpysympy): Sommerfeld boundary conditions implementation in Cartesian coordinates, derived by NRPy+/SymPy
1. [Step 3](#numalg): Numerical algorithm overview
    1. [Step 3.a](#k): Solving for the subdominant radial falloff proportionality constant $k$
1. [Step 4](#interface): NRPy+ Interface for Applying Sommerfeld Boundary Conditions
1. [Step 5](#etk_validation): Validation against the [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk)
1. [Latex Output](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set core NRPy+ parameters for numerical grids \[Back to [top](#toc)\]
$$\label{initializenrpy}$$

Import needed NRPy+ core modules and set working directories.

```python
# Step P1: Import needed NRPy+ core modules:
import NRPy_param_funcs as par   # NRPy+: Parameter interface
import grid as gri               # NRPy+: Functions having to do with numerical grids
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("SommerfeldBC_Codes")
# 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)

par.set_parval_from_str("grid::DIM",3)
DIM = par.parval_from_str("grid::DIM")
```

# Step 2: Sommerfeld Boundary Conditions \[Back to [top](#toc)\]
$$\label{sbc}$$

## Step 2.a: Introduction & background mathematics \[Back to [top](#toc)\]
$$\label{intro}$$

When we numerically solve an initial value problem, appropriate initial data must be provided, coupled to a technique for evolving the initial data forward in time (to construct the solution at $t>0$), and we must impose boundary conditions.

The subject of this notebook is implementation of Sommerfeld boundary conditions, which are often used when solving hyperbolic systems of partial differential equations (PDEs). Sommerfeld boundary conditions are also referred to as a radiation or transparent boundary conditions.

The essential idea of a transparent boundary is creating a boundary in which wave fronts can pass through with minimal reflections. In other words, the boundary condition acts to map our numerical solution to outside our numerical domain in a smooth fashion. Because this mapping is assumed to be linear, this treatment occurs at the same time-level as calculations within our numerical domain. This point will be revisited later.

Suppose we have a dynamical variable $f$; i.e., a variable in our hyperbolic system of PDEs that satisfies the equation

$$
\partial_t f = \text{something},
$$
where generally we refer to "something" as the "right-hand side" or "RHS" of the hyperbolic PDE, where we formulate the PDE such that the RHS contains no explicit time derivatives, but generally does contain spatial derivatives (typically computed in NRPy+ using finite differencing, though may in general be computed using other, e.g., pseudospectral techniques).

To construct the solution at times after the initial data, we adopt the [Method of Lines (MoL)](Tutorial-ScalarWave.ipynb) approach, which integrates the equations forward *in time* using standard explicit techniques typically used when solving *ordinary* differential equations. In doing so, MoL evaluates the RHS of the PDE at all points in our numerical domain (typically using finite difference derivatives), except the ghost zones (i.e., the gridpoints neighboring the precise point needed to evaluate a derivative using e.g., a finite difference derivative).

After each RHS evaluation we must fill in the data on the boundaries (i.e., the ghost zones), so that data exist at all gridpoints (including the boundaries) at the next MoL substep. In doing so, we have two options:

1. Perform an MoL substep to push the *interior* solution $f$ forward in time one substep, and then update the boundary values of $f$.
1. During the MoL substep, immediately after evaluating the RHS of the $\partial t f$ equation, update the boundary values of the RHS of the $\partial t f$ equation. Then push the solution $f$ forward in time *at all gridpoints, including ghost zones* by one substep.

Our implementation of the Sommerfeld boundary condition implements the second option, filling in the data for $\partial_t f$ (cf., our [extrapolation boundary condition implementation](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb)) on the boundaries (i.e., ghost zones).

## Step 2.b: Preliminaries - The scalar wave equation in curvilinear coordinates \[Back to [top](#toc)\]
$$\label{sbc_prelims}$$

Our Sommerfeld boundary condition implementation assumes $f$ behaves as a spherically symmetric, outgoing wave at the boundaries. Under these assumptions, the waves satisfy the wave equation in spherical coordinates with angular parts set to zero. I.e., under these assumptions the wavefunction $u(r,t)$ will satisfy:

\begin{align}
0 = \Box\, u &= \hat{D}^{\nu} \hat{D}_{\nu} u \\
&= \hat{g}^{\mu\nu} \hat{D}_{\mu} \hat{D}_{\nu} u \\
&= \hat{g}^{\mu\nu} \hat{D}_{\mu} \partial_{\nu} u \\
&= \hat{g}^{\mu\nu} \left[\partial_{\mu} (\partial_{\nu} u) - \hat{\Gamma}^\alpha_{\mu\nu} (\partial_{\alpha} u) \right],
\end{align}
here the hatted metric is defined as $\hat{g}^{tt} = -1/v^2$ (where $v$ is the wavespeed), $\hat{g}^{rr}=1$, $\hat{g}^{\theta\theta}=1/r^2$, and $\hat{g}^{\phi\phi}=1/(r^2\sin^2\theta)$ are the only nonzero metric terms for the spherical (contravariant) metric. However, the fact that $u=u(r,t)$ does not depend on the angular pieces greatly simplifies the expression:

\begin{align}
\Box\, u
&= \hat{g}^{\mu\nu} \left[\partial_{\mu} (\partial_{\nu} u) - \hat{\Gamma}^\alpha_{\mu\nu} (\partial_{\alpha} u) \right] \\
&= \left(-\frac{1}{v^2}\partial_t^2 + \partial_r^2\right)u - \hat{\Gamma}^\alpha_{\mu\nu} (\partial_{\alpha} u) \\
&= \left(-\frac{1}{v^2}\partial_t^2 + \partial_r^2\right)u - \hat{g}^{\mu\nu} \left[\hat{\Gamma}^t_{\mu\nu} \partial_{t} + \hat{\Gamma}^r_{\mu\nu} \partial_{r}\right]u.
\end{align}

We will now refer the reader to the [scalar wave in curvilinear coordinates notebook](Tutorial-ScalarWaveCurvilinear.ipynb) for the remainder of the derivation. The bottom line is, after removing terms implying angular variation in $u$, one obtains the wave equation for spherically symmetric waves:

$$
\frac{1}{v^2} \partial_t^2 u = \partial_r^2 u + \frac{2}{r} \partial_r u,
$$
which has the general solution

$$
u(r,t) = A \frac{u(r + vt)}{r} + B \frac{u(r - vt)}{r},
$$
where the (left) right term represents an (ingoing) outgoing wave.

## Step 2.c: Sommerfeld boundary condition ansatz \[Back to [top](#toc)\]
$$\label{sbc_ansatz}$$

Inspired by the solution to the scalar wave equation, our Sommerfeld boundary condition will assume the solution $f(r,t)$ acts as an *outgoing* spherical wave ($A=0$), with an asymptotic value $f_0$ at $r\to\infty$ and a correction term for incoming waves or transient non-wavelike behavior at the boundaries, with $r^n$ falloff (ignoring higher-order radial falloffs):

$$
f = f_0 + \frac{u(r-vt)}{r} + \frac{c}{r^n},
$$
where $c$ is a constant.

## Step 2.d: Applying the ansatz to $\partial_t f$ \[Back to [top](#toc)\]
$$\label{sbc_ansatz_dtf}$$

As described in the above section, we will not apply Sommerfeld boundary conditions to $f$ but $\partial_t f$ instead:

$$
\partial_t f = -v \frac{u'(r-vt)}{r}.
$$

To get a better understanding of the $u'(r-vt)$ term, let's compute the radial partial derivative as well:

\begin{align}
\partial_r f &= \frac{u'(r-vt)}{r} - \frac{u(r-vt)}{r^2} - n \frac{c}{r^{n+1}} \\
\implies \frac{u'(r-vt)}{r} &= \partial_r f + \frac{u(r-vt)}{r^2} + n \frac{c}{r^{n+1}}
\end{align}

Thus we get

\begin{align}
\partial_t f &= -v \frac{u'(r-vt)}{r} \\
&= -v \left[\partial_r f + \frac{u(r-vt)}{r^2} + n \frac{c}{r^{n+1}} \right]
\end{align}

To take care of the (as-yet) unknown $\frac{u(r-vt)}{r^2}$ term, notice our ansatz

$$
f = f_0 + \frac{u(r-vt)}{r} + \frac{c}{r^n}
$$
implies that

\begin{align}
\frac{f - f_0}{r} &= \frac{u(r-vt)}{r^2} + \frac{c}{r^{n+1}} \\
\implies \frac{u(r-vt)}{r^2} &= \frac{f - f_0}{r} - \frac{c}{r^{n+1}}
\end{align}

so we have

\begin{align}
\partial_t f &= -v \left[\partial_r f + \frac{u(r-vt)}{r^2} + n \frac{c}{r^{n+1}} \right]\\
&= -v \left[\partial_r f + \frac{f - f_0}{r} - \frac{c}{r^{n+1}} + n \frac{c}{r^{n+1}} \right] \\
&= -v \left[\partial_r f + \frac{f - f_0}{r}\right] + \frac{k}{r^{n+1}},
\end{align}
where $k=-v c(n-1)$ is just another constant.

Thus we have derived our boundary condition:

$$
\boxed{
\partial_t f = -\frac{v}{r} \left[r \partial_r f + (f - f_0)\right] + \frac{k}{r^{n+1}}.
}
$$

## Step 2.e: Implementation in generic curvilinear coordinates \[Back to [top](#toc)\]
$$\label{curvicoords}$$

The core equation
$$
\boxed{
\partial_t f = -\frac{v}{r} \left[r \partial_r f + (f - f_0)\right] + \frac{k}{r^{n+1}}.
}
$$

is implemented in NRPy+ using its `reference_metric.py` module ([Tutorial notebook](Tutorial-Reference_Metric.ipynb)), as this module requires *all* coordinate systems to define the spherical coordinate $r$ in terms of input quantities `(xx0,xx1,xx2)`. Thus we need only rewrite the above equation in terms of `(xx0,xx1,xx2)`. Defining $x^i$=`(xx0,xx1,xx2)`, we have, using the chain rule:

\begin{align}
\partial_t f &= -\frac{v}{r} \left[r \partial_r f + (f - f_0)\right] + \frac{k}{r^{n+1}} \\
&= -\frac{v}{r(x^i)} \left[r \frac{\partial x^i}{\partial r} \partial_i f + (f - f_0)\right] + \frac{k}{r^{n+1}}.
\end{align}

$\frac{\partial x^i}{\partial r}$ can be impossible to compute directly, as we are given $r(x^i)$ but not necessarily $x^i(r)$. The key here is to note that we are actually given $x^j_{\rm Sph} = (r(x^i),\theta(x^i),\phi(x^i))$ for all coordinate systems, so we can define the Jacobian

$$\frac{\partial x^j_{\rm Sph}(x^i)}{x^i},$$

and NRPy+ can invert this matrix to give us

$$\frac{\partial x^i}{x^j_{\rm Sph}},$$

In summary, the implementation of Sommerfeld boundary conditions in arbitrary curvilinear coordinates $x^i=$`(xx0,xx1,xx2)` is given by
$$
\boxed{
\partial_t f = -\frac{v}{r(x^i)} \left[r(x^i) \frac{\partial x^i}{\partial r} \partial_i f + (f - f_0)\right] + \frac{k}{r^{n+1}}.
}
$$

In the next subsections, we'll work through implementation of this general equation in the special case of Cartesian coordinates, first by hand, and then *automatically*, for Cartesian *or other curvilinear coordinate systems supported by NRPy+* using `reference_metric.py`.

### Step 2.e.i: Sommerfeld boundary conditions implementation in Cartesian coordinates, derived by hand \[Back to [top](#toc)\]
$$\label{cartcoords_byhand}$$

Let's now work this out for Cartesian coordinates in NRPy+, first by hand, and then using `reference_metric.py`:

In Cartesian coordinates $\frac{\partial f}{\partial r}$ may be expanded as

$$
\frac{\partial f}{\partial r} = \frac{\partial x}{\partial r} \partial_x f + \frac{\partial y}{\partial r}\partial_y f + \frac{\partial z}{\partial r}\partial_z f.
$$

Defining $x^i$ to the the $i$th component of the Cartesian reference metric, we have

\begin{align}
x^0 = x &= r\sin\theta \cos\phi \implies \frac{\partial x}{\partial r}=\frac{x}{r}, \\
x^1 = y &= r\sin\theta \sin\phi \implies \frac{\partial y}{\partial r}=\frac{y}{r}, \\
x^2 = z &= r\cos\theta \implies \frac{\partial z}{\partial r}=\frac{z}{r}.
\end{align}

Based on this, we can rewrite the above as

\begin{align}
\frac{\partial f}{\partial r}
&= \frac{x}{r} \partial_x f +\frac{y}{r} \partial_y f +\frac{z}{r} \partial_z f \\
&= \frac{x^0}{r} \partial_0 f +\frac{x^1}{r} \partial_1 f +\frac{x^2}{r} \partial_2 f \\
&= \frac{x^i}{r} \partial_i f,
\end{align}

yielding the Sommerfeld boundary condition in *Cartesian coordinates*

$$
\partial_t f = -\frac{v}{r} \left[x^i \partial_i f + \left( f - f_0 \right) \right] + \frac{k}{r^{n+1}}.
$$

### Step 2.e.ii: Sommerfeld boundary conditions implementation in Cartesian coordinates, derived automatically by NRPy+/SymPy \[Back to [top](#toc)\]
$$\label{cartcoords_bynrpysympy}$$

Now let's use NRPy+'s `reference_metric.py` to obtain the same expression for $\frac{\partial f}{\partial r}$ in Cartesian coordinates; i.e.,

$$
\frac{\partial f}{\partial r} = \frac{x^i}{r} \partial_i f,
$$
but using the generic coordinate system interface (`CoordSystem=Cartesian`; feel free to change it to another coordinate system of your choice). Note in the above that me we must also know the functional form of $r(x^i)$, so we use NRPy's `outputC` module to output the C code to calculate $r(x^i)$ and $\frac{\partial x^i}{\partial r} \partial_i f$, as shown below.

```python
from outputC import outputC     # NRPy+: Core C code output module
import NRPy_param_funcs as par  # NRPy+: Parameter interface
import indexedexp as ixp        # NRPy+: Symbolic indexed expression (e.g., tensors, vectors, etc.) support
import sympy as sp              # SymPy: The Python computer algebra package upon which NRPy+ depends
import reference_metric as rfm  # NRPy+: Reference metric support
import shutil, os, sys           # Standard Python modules for multiplatform OS-level functions

# Choices include: Spherical, SinhSpherical, SinhSphericalv2,
#                  Cylindrical, SinhCylindrical, SymTP, SinhSymTP
CoordSystem = "Cartesian"
par.set_parval_from_str("reference_metric::CoordSystem",CoordSystem)

# This code can be found in CurviBoundaryConditions/CurviBoundaryConditions.py

# Read what # of dimensions being usded
DIM = par.parval_from_str("grid::DIM")

# Set up the chosen reference metric from chosen coordinate system, set within NRPy+
# CoordSystem = par.parval_from_str("reference_metric::CoordSystem") - set above
rfm.reference_metric()

# Simplifying the results make them easier to interpret.
do_simplify = True
if "Sinh" in CoordSystem:
    # Simplification takes too long on Sinh* coordinate systems
    do_simplify = False

# Construct Jacobian matrix, output Jac_dUSph_dDrfmUD[i][j] = \partial x_{Sph}^i / \partial x^j:
Jac_dUSph_dDrfmUD = ixp.zerorank2()
for i in range(3):
    for j in range(3):
        Jac_dUSph_dDrfmUD[i][j] = sp.diff(rfm.xxSph[i],rfm.xx[j])

# Invert Jacobian matrix, output to Jac_dUrfm_dDSphUD.
Jac_dUrfm_dDSphUD, dummyDET = ixp.generic_matrix_inverter3x3(Jac_dUSph_dDrfmUD)

# Jac_dUrfm_dDSphUD[i][0] stores \partial x^i / \partial r
if do_simplify:
    for i in range(3):
        Jac_dUrfm_dDSphUD[i][0] = sp.simplify(Jac_dUrfm_dDSphUD[i][0])

# Declare \partial_i f, which is actually computed later on
fdD = ixp.declarerank1("fdD") # = [fdD0, fdD1, fdD2]
contraction = sp.sympify(0)
for i in range(3):
    contraction += fdD[i]*Jac_dUrfm_dDSphUD[i][0]

r_str_and_contraction_str = outputC([rfm.xxSph[0],contraction],
         ["*_r","*_dxU_fdD"],filename="returnstring",params="includebraces=False")

print(r_str_and_contraction_str)
```

    /*
     *  Original SymPy expressions:
     *  "[*_r = sqrt(xx0**2 + xx1**2 + xx2**2),
     *    *_dxU_fdD = fdD0*xx0/sqrt(xx0**2 + xx1**2 + xx2**2) + fdD1*xx1/sqrt(xx0**2 + xx1**2 + xx2**2) + fdD2*xx2/sqrt(xx0**2 + xx1**2 + xx2**2)]"
     */
    const double tmp_0 = sqrt(((xx0)*(xx0)) + ((xx1)*(xx1)) + ((xx2)*(xx2)));
    const double tmp_1 = (1.0/(tmp_0));
    *_r = tmp_0;
    *_dxU_fdD = fdD0*tmp_1*xx0 + fdD1*tmp_1*xx1 + fdD2*tmp_1*xx2;

# Step 3: Numerical algorithm overview \[Back to [top](#toc)\]
$$\label{numalg}$$

To implement the above Sommerfeld boundary condition, we must specify for each dynamical variable:

* its wave speed at the boundary,
* its asymptotic value at infinity, and
* its $r^{n+1}$ power that must be applied for the non-wavelike behavior at the boundary.

Note in our ansatz

$$
f = f_0 + \frac{u(r-vt)}{r} + \frac{c}{r^n},
$$

in this expansion it would be natural to have $n = 2$, but in the boundary condition we have

$$
\partial_t f = -\frac{v}{r(x^i)} \left[r(x^i) \frac{\partial x^i}{\partial r} \partial_i f + (f - f_0)\right] + \frac{k}{r^{n+1}}.
$$

Thus, the exponent in the $k$ term should be 3. And indeed, in our own implementations we have found that $n=2$ exhibits the best results for minimal reflections. The [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk) documentation page also states similar results, writing "empirically, we have found that taking (exponent)$ = 3$ almost completely eliminates the bad transient caused by the radiative boundary condition on its own". The following set of code cells implement the above equation, excluding the $\frac{k}{r^{n+1}}$ term, for a Cartesian grid.

Our procedure in implementing this boundary condition is as follows:

0) Define data for $f$ at **all** points in our numerical domain

1) Evaluate $\frac{df}{dt}$ at points in the **interior** only, using the       prescribed equations to evaluate the RHS and ghost zones for the finite         differences at the outer boundaries

2) For points in the ghost zones, apply the Sommerfeld condition to obtain      $\frac{df}{dt}$ in the ghost zones, assuming advection and radial fall off behavior. When evaluating the spatial derivatives, use forward or backward finite differences for directions perpendicular to the outer boundary face being considered, and centered derivatives for the directions which lie in the plane of the outer boundary face. Furthermore, to minimize error we loop through inner boundary points and work outwards, so that the forward or backward stencils never include untreated points.

3) Account for non-wavelike evolution of our numerical solution at the boundary

4) Perform RK-update for all points (interior + ghost zones). Since the linear mapping to outside our numerical domain is linear, treatment of points in the interior and points on the boundary must be at the same time level.

In this code cell we define the apply_bcs_sommerfeld() function, read in the boundary condition parameters for each grid function from the `sommerfeld_params.h` file, as well as the function used to calculate $r(x^i)$ and $\frac{\partial x^i}{\partial r} \partial_i f$, stored within `radial_derivative.h` and loop over each grid function, each ghost zone, and each point within the ghost zones, while also using the point's boundary face integer values using NRPy's `bcstruct`, documented [here](Tutorial-Start_to_Finish-Curvilinear_BCs.ipynb).

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

//          Boundary condtion driver routine: Apply BCs to all
//          boundary faces of the 3D numerical domain, filling in the
//          outer boundary ghost zone layers, starting with the innermost
//          layer and working outward.

#include "sommerfeld_params.h"
#include "radial_derivative.h"
#include <string.h>

void apply_bcs_sommerfeld(const paramstruct *restrict params, REAL *restrict xx[3],
                          const bc_struct *restrict bcstruct, const int NUM_GFS,
                          const int8_t *restrict gfs_parity, REAL *restrict gfs,
                          REAL *restrict rhs_gfs) {

    #pragma omp parallel for
        for(int which_gf=0;which_gf<NUM_GFS;which_gf++) {
          REAL var_at_infinity = evolgf_at_inf[which_gf];
          REAL radpower        = evolgf_radpower[which_gf];
          REAL char_speed      = evolgf_speed[which_gf];

          #include "RELATIVE_PATH__set_Cparameters.h" /* Header file containing correct #include for set_Cparameters.h;
                                                       * accounting for the relative path */

            for(int which_gz = 0; which_gz < NGHOSTS; which_gz++) {
                for(int pt=0;pt<bcstruct->num_ob_gz_pts[which_gz];pt++) {
                    int i0 = bcstruct->outer[which_gz][pt].outer_bc_dest_pt.i0;
                    int i1 = bcstruct->outer[which_gz][pt].outer_bc_dest_pt.i1;
                    int i2 = bcstruct->outer[which_gz][pt].outer_bc_dest_pt.i2;
                    int8_t FACEX0 = bcstruct->outer[which_gz][pt].FACEi0;
                    int8_t FACEX1 = bcstruct->outer[which_gz][pt].FACEi1;
                    int8_t FACEX2 = bcstruct->outer[which_gz][pt].FACEi2;

                    int8_t FACEXi[3] = {FACEX0, FACEX1, FACEX2};

                    // Initialize derivatives to crazy values, to ensure that
                    //   we will notice in case they aren't set properly.
                    REAL r = 1e100;
                    REAL dxU_fdD = 1e100;

```

    Writing SommerfeldBC_Codes//apply_bcs_sommerfeld.c

Here we generate the C code used to calculate all relevant spatial derivatives $\partial_i f$, using second order accurate finite differences. Specifically, if our ghost zone point lies on one of the faces, on an edge or corner, we use forward or backward differences depending on the specific direction, and centered differences otherwise. Note that all derivatives along the normal of the boundary faces are forward or backward, to minimize using non-updated points in the derivative calculations.

For example, consider some point with Cartesian coordinates $(i,j,k)$ on our grid, the derivative of $f$ along the $x$ direction will be the forward (backward with change of signs on coefficients)

$$
\frac{\partial f_{ijk}}{\partial x} \approx \frac{1}{2\Delta x} \left( -3f_{i,j,k} + 4f_{i+1,j,k} - f_{i+2,j,k} \right),
$$

or the ceneterd difference approximation

$$
\frac{\partial f_{ijk}}{\partial x} \approx \frac{1}{2\Delta x} \left( f_{i+1,j,k} - f_{i-1,j,k} \right).
$$

We determine the signs of the coefficents (corresponding to using either a foward or backward difference) by determining what face the point lies within. The above is applied for all three Cartesian directions. Note the use if the `SHIFTSTENCIL` variable, which helps determine when to use forward/backward difference to take derivatives along normals to boundary faces, or when to use central differences to either take derivatives parallel to the faces or at points on edges and corners.

```python
# https://stackoverflow.com/questions/8951020/pythonic-circular-list
cyc = ['0', '1', '2']
def output_dfdx(dirn, order):
    if order==2:
        def gen_order2_central_fd_stencil_str(dirn):
            if dirn == 0:
                return "(gfs[IDX4S(which_gf,i0+1,i1,i2)]-gfs[IDX4S(which_gf,i0-1,i1,i2)])"
            elif dirn == 1:
                return "(gfs[IDX4S(which_gf,i0,i1+1,i2)]-gfs[IDX4S(which_gf,i0,i1-1,i2)])"
            elif dirn == 2:
                return "(gfs[IDX4S(which_gf,i0,i1,i2+1)]-gfs[IDX4S(which_gf,i0,i1,i2-1)])"

        return """
// On a +x"""+str(dirn)+""" or -x"""+str(dirn)+""" face, do up/down winding as appropriate:
if(abs(FACEXi["""+str(dirn)+"""])==1 || i"""+str(dirn)+"""+NGHOSTS >= Nxx_plus_2NGHOSTS"""+str(dirn)+""" || i"""+str(dirn)+"""-NGHOSTS <= 0) {
    int8_t SHIFTSTENCIL"""+str(dirn)+""" = FACEXi["""+str(dirn)+"""];
    if(i"""+str(dirn)+"""+NGHOSTS >= Nxx_plus_2NGHOSTS"""+str(dirn)+""") SHIFTSTENCIL"""+str(dirn)+""" = -1;
    if(i"""+str(dirn)+"""-NGHOSTS <= 0)                  SHIFTSTENCIL"""+str(dirn)+""" = +1;
    SHIFTSTENCIL"""+str(cyc[(dirn + 1) % len(cyc)])+""" = 0;
    SHIFTSTENCIL"""+str(cyc[(dirn + 2) % len(cyc)])+""" = 0;

    fdD"""+str(dirn)+"""
        = SHIFTSTENCIL"""+str(dirn)+"""*(-3*gfs[IDX4S(which_gf,i0+0*SHIFTSTENCIL0,i1+0*SHIFTSTENCIL1,i2+0*SHIFTSTENCIL2)]
                         +4*gfs[IDX4S(which_gf,i0+1*SHIFTSTENCIL0,i1+1*SHIFTSTENCIL1,i2+1*SHIFTSTENCIL2)]
                         -1*gfs[IDX4S(which_gf,i0+2*SHIFTSTENCIL0,i1+2*SHIFTSTENCIL1,i2+2*SHIFTSTENCIL2)]
                        )*invdx"""+str(dirn)+"""*0.5;

// Not on a +x"""+str(dirn)+""" or -x"""+str(dirn)+""" face, using centered difference:
} else {
    fdD"""+str(dirn)+""" = """+gen_order2_central_fd_stencil_str(dirn)+"""*invdx"""+str(dirn)+"""*0.5;
}
"""
    else:
        print("Error: order = "+str(order)+" currently unsupported.")
        sys.exit(1)

contraction_term_func = """

void contraction_term(const paramstruct *restrict params, const int which_gf, REAL *restrict gfs, REAL *restrict xx[3],
           int8_t FACEXi[3], int i0, int i1, int i2, REAL *restrict _r, REAL *restrict _dxU_fdD) {

#include "RELATIVE_PATH__set_Cparameters.h" /* Header file containing correct #include for set_Cparameters.h;
                                             * accounting for the relative path */

// Initialize derivatives to crazy values, to ensure that
//   we will notice in case they aren't set properly.
REAL fdD0=1e100;
REAL fdD1=1e100;
REAL fdD2=1e100;

REAL xx0 = xx[0][i0];
REAL xx1 = xx[1][i1];
REAL xx2 = xx[2][i2];

int8_t SHIFTSTENCIL0;
int8_t SHIFTSTENCIL1;
int8_t SHIFTSTENCIL2;

"""
for i in range(DIM):
    if "fdD"+str(i) in r_str_and_contraction_str:
        contraction_term_func += output_dfdx(i, order=2)

contraction_term_func += "\n" + r_str_and_contraction_str

contraction_term_func +="""

} // END contraction_term function
"""
with open(os.path.join(Ccodesdir,"radial_derivative.h"),"w") as file:
    file.write(contraction_term_func)

print(contraction_term_func)
```

    void contraction_term(const paramstruct *restrict params, const int which_gf, REAL *restrict gfs, REAL *restrict xx[3],
               int8_t FACEXi[3], int i0, int i1, int i2, REAL *restrict _r, REAL *restrict _dxU_fdD) {

    #include "RELATIVE_PATH__set_Cparameters.h" /* Header file containing correct #include for set_Cparameters.h;
                                                 * accounting for the relative path */

    // Initialize derivatives to crazy values, to ensure that
    //   we will notice in case they aren't set properly.
    REAL fdD0=1e100;
    REAL fdD1=1e100;
    REAL fdD2=1e100;

    REAL xx0 = xx[0][i0];
    REAL xx1 = xx[1][i1];
    REAL xx2 = xx[2][i2];

    int8_t SHIFTSTENCIL0;
    int8_t SHIFTSTENCIL1;
    int8_t SHIFTSTENCIL2;

    // On a +x0 or -x0 face, do up/down winding as appropriate:
    if(abs(FACEXi[0])==1 || i0+NGHOSTS >= Nxx_plus_2NGHOSTS0 || i0-NGHOSTS <= 0) {
        int8_t SHIFTSTENCIL0 = FACEXi[0];
        if(i0+NGHOSTS >= Nxx_plus_2NGHOSTS0) SHIFTSTENCIL0 = -1;
        if(i0-NGHOSTS <= 0)                  SHIFTSTENCIL0 = +1;
        SHIFTSTENCIL1 = 0;
        SHIFTSTENCIL2 = 0;

        fdD0
            = SHIFTSTENCIL0*(-3*gfs[IDX4S(which_gf,i0+0*SHIFTSTENCIL0,i1+0*SHIFTSTENCIL1,i2+0*SHIFTSTENCIL2)]
                             +4*gfs[IDX4S(which_gf,i0+1*SHIFTSTENCIL0,i1+1*SHIFTSTENCIL1,i2+1*SHIFTSTENCIL2)]
                             -1*gfs[IDX4S(which_gf,i0+2*SHIFTSTENCIL0,i1+2*SHIFTSTENCIL1,i2+2*SHIFTSTENCIL2)]
                            )*invdx0*0.5;

    // Not on a +x0 or -x0 face, using centered difference:
    } else {
        fdD0 = (gfs[IDX4S(which_gf,i0+1,i1,i2)]-gfs[IDX4S(which_gf,i0-1,i1,i2)])*invdx0*0.5;
    }

    // On a +x1 or -x1 face, do up/down winding as appropriate:
    if(abs(FACEXi[1])==1 || i1+NGHOSTS >= Nxx_plus_2NGHOSTS1 || i1-NGHOSTS <= 0) {
        int8_t SHIFTSTENCIL1 = FACEXi[1];
        if(i1+NGHOSTS >= Nxx_plus_2NGHOSTS1) SHIFTSTENCIL1 = -1;
        if(i1-NGHOSTS <= 0)                  SHIFTSTENCIL1 = +1;
        SHIFTSTENCIL2 = 0;
        SHIFTSTENCIL0 = 0;

        fdD1
            = SHIFTSTENCIL1*(-3*gfs[IDX4S(which_gf,i0+0*SHIFTSTENCIL0,i1+0*SHIFTSTENCIL1,i2+0*SHIFTSTENCIL2)]
                             +4*gfs[IDX4S(which_gf,i0+1*SHIFTSTENCIL0,i1+1*SHIFTSTENCIL1,i2+1*SHIFTSTENCIL2)]
                             -1*gfs[IDX4S(which_gf,i0+2*SHIFTSTENCIL0,i1+2*SHIFTSTENCIL1,i2+2*SHIFTSTENCIL2)]
                            )*invdx1*0.5;

    // Not on a +x1 or -x1 face, using centered difference:
    } else {
        fdD1 = (gfs[IDX4S(which_gf,i0,i1+1,i2)]-gfs[IDX4S(which_gf,i0,i1-1,i2)])*invdx1*0.5;
    }

    // On a +x2 or -x2 face, do up/down winding as appropriate:
    if(abs(FACEXi[2])==1 || i2+NGHOSTS >= Nxx_plus_2NGHOSTS2 || i2-NGHOSTS <= 0) {
        int8_t SHIFTSTENCIL2 = FACEXi[2];
        if(i2+NGHOSTS >= Nxx_plus_2NGHOSTS2) SHIFTSTENCIL2 = -1;
        if(i2-NGHOSTS <= 0)                  SHIFTSTENCIL2 = +1;
        SHIFTSTENCIL0 = 0;
        SHIFTSTENCIL1 = 0;

        fdD2
            = SHIFTSTENCIL2*(-3*gfs[IDX4S(which_gf,i0+0*SHIFTSTENCIL0,i1+0*SHIFTSTENCIL1,i2+0*SHIFTSTENCIL2)]
                             +4*gfs[IDX4S(which_gf,i0+1*SHIFTSTENCIL0,i1+1*SHIFTSTENCIL1,i2+1*SHIFTSTENCIL2)]
                             -1*gfs[IDX4S(which_gf,i0+2*SHIFTSTENCIL0,i1+2*SHIFTSTENCIL1,i2+2*SHIFTSTENCIL2)]
                            )*invdx2*0.5;

    // Not on a +x2 or -x2 face, using centered difference:
    } else {
        fdD2 = (gfs[IDX4S(which_gf,i0,i1,i2+1)]-gfs[IDX4S(which_gf,i0,i1,i2-1)])*invdx2*0.5;
    }

    /*
     *  Original SymPy expressions:
     *  "[*_r = sqrt(xx0**2 + xx1**2 + xx2**2),
     *    *_dxU_fdD = fdD0*xx0/sqrt(xx0**2 + xx1**2 + xx2**2) + fdD1*xx1/sqrt(xx0**2 + xx1**2 + xx2**2) + fdD2*xx2/sqrt(xx0**2 + xx1**2 + xx2**2)]"
     */
    const double tmp_0 = sqrt(((xx0)*(xx0)) + ((xx1)*(xx1)) + ((xx2)*(xx2)));
    const double tmp_1 = (1.0/(tmp_0));
    *_r = tmp_0;
    *_dxU_fdD = fdD0*tmp_1*xx0 + fdD1*tmp_1*xx1 + fdD2*tmp_1*xx2;

    } // END contraction_term function

Finally, we calculate $\frac{df}{dt}$ without the $\frac{k}{r^{n+1}}$ term;

$$
\frac{\partial f}{\partial t} = -\frac{v}{r} \left( x^i \partial_i f + \left( f - f_0 \right) \right).
$$

```python
%%writefile -a $Ccodesdir/apply_bcs_sommerfeld.c

                contraction_term(params, which_gf, gfs, xx, FACEXi, i0, i1, i2, &r, &dxU_fdD);

                REAL invr = 1./r;

                REAL source_rhs = -char_speed*(dxU_fdD + invr*(gfs[IDX4S(which_gf,i0,i1,i2)] - var_at_infinity));
                rhs_gfs[IDX4S(which_gf,i0,i1,i2)] = source_rhs;

```

    Appending to SommerfeldBC_Codes//apply_bcs_sommerfeld.c

## Step 3.a: Solving for $k$ \[Back to [top](#toc)\]
$$\label{k}$$

 Here we formulate a way to approximate $k$. If our solution satisfies the advection equation both at the ghost zones and at interior points close to the boundaries, then we may find $k$ by determining the portion of $f$ that is not accounted for by the equation

$$
\frac{\partial f}{\partial t} = \left[\frac{\partial f}{\partial t} \right]_{adv}= -\frac{v}{r} \left( x^i \partial_i f + \left( f - f_0 \right) \right).
$$

The above is the advection equation we arrive at assuming $f$ behaves purely as an outgoing spherical wave. For an interior point directly adjacent to a ghost zone point, $f$ must satisfy **both** the time evolution equation for prescribed points within the interior, $\left[\frac{\partial f}{\partial t} \right]_{evol}$, and the advection equation $\left[\frac{\partial f}{\partial t} \right]_{adv}$. We then find the difference as

$$
\delta = \left[\frac{\partial f}{\partial t} \right]_{evol} - \left[\frac{\partial f}{\partial t} \right]_{adv} = \frac{k}{r^{n+1}},
$$

i.e. $\delta$ represents the numerical departure from the expected purely wave-like behavior at that point. We solve for $\delta$ at this interior point and express $k$ as

$$
k = \delta r^{n+1}_{int}.
$$

Thus, the $\frac{k}{r^{n+1}}$ term for the associated ghost zone point may be expressed as

$$
\frac{k}{r^{n+1}_{gz}} = \delta \left( \frac{r_{int}}{r_{gz}} \right) ^{n+1}.
$$

We approximate $k$ in this fashion using the code below. Note that we activate this term only when $n>0$.

```python
%%writefile -a $Ccodesdir/apply_bcs_sommerfeld.c

/************* For radial falloff and the extrapolated k term *************/
                    if (radpower > 0) {

                      // Move one point away from gz point to compare pure advection to df/dt|interior

                      int i0_offset = i0+FACEX0;
                      int i1_offset = i1+FACEX1;
                      int i2_offset = i2+FACEX2;

                      // Initialize derivatives to crazy values, to ensure that
                      //   we will notice in case they aren't set properly.
                      REAL r_offset = 1e100;
                      REAL dxU_fdD_offset = 1e100;

                      contraction_term(params, which_gf, gfs, xx, FACEXi, i0_offset, i1_offset, i2_offset, &r_offset, &dxU_fdD_offset);

                      REAL invr_offset = 1./r_offset;

                      // Pure advection
                      REAL extrap_rhs = char_speed*(dxU_fdD_offset + invr_offset*(gfs[IDX4S(which_gf,i0_offset,i1_offset,i2_offset)] - var_at_infinity));

                      // Take difference between pure advection and df/dt|interior
                      REAL aux = rhs_gfs[IDX4S(which_gf,i0_offset,i1_offset,i2_offset)] + extrap_rhs;

                      // Solve for k/(r_gz)^n+1 term
                      rhs_gfs[IDX4S(which_gf,i0,i1,i2)] += aux*pow(r_offset*invr,radpower);
                    }
                }// END for(int pt=0;pt<num_ob_gz_pts[which_gz];pt++)
```

    Appending to SommerfeldBC_Codes//apply_bcs_sommerfeld.c

Finally, we apply parity conditions for inner boundary conditions.

```python
%%writefile -a $Ccodesdir/apply_bcs_sommerfeld.c

// Then apply INNER (parity) boundary conditions:
            for(int pt=0;pt<bcstruct->num_ib_gz_pts[which_gz];pt++) {
                const int i0dest = bcstruct->inner[which_gz][pt].inner_bc_dest_pt.i0;
                const int i1dest = bcstruct->inner[which_gz][pt].inner_bc_dest_pt.i1;
                const int i2dest = bcstruct->inner[which_gz][pt].inner_bc_dest_pt.i2;
                const int i0src  = bcstruct->inner[which_gz][pt].inner_bc_src_pt.i0;
                const int i1src  = bcstruct->inner[which_gz][pt].inner_bc_src_pt.i1;
                const int i2src  = bcstruct->inner[which_gz][pt].inner_bc_src_pt.i2;
                const int8_t *prty= bcstruct->inner[which_gz][pt].parity;
//                printf("%d\n",bcstruct->inner_bc_parity[which_gz][pt].parity[gfs_parity[which_gf]]);
                gfs[IDX4S(which_gf,i0dest,i1dest,i2dest)] =
                        bcstruct->inner[which_gz][pt].parity[gfs_parity[which_gf]] * gfs[IDX4S(which_gf, i0src,i1src,i2src)];
            }// END for(int pt=0;pt<num_ib_gz_pts[which_gz];pt++)
        } // END for(int which_gz = 0; which_gz < NGHOSTS; which_gz++)
    } // END for(int which_gf=0;which_gf<NUM_GFS;which_gf++)
```

    Appending to SommerfeldBC_Codes//apply_bcs_sommerfeld.c

Finally, notice that in our C code generation, coordinate system is only incorporated once, i.e. we can easily generate the C code to be used for any coordinate system. This is due to the fact that as long as we have a generic code to calculate $\partial_i f$, we can always use NRPy+ to calculate $\partial_r f$.

# Step 4: NRPy+ Interface for Applying Sommerfeld Boundary Conditions \[Back to [top](#toc)\]
$$\label{interface}$$

To apply the Sommerfled boundary condition to any given grid function, its wave speed at the boundaries, asymptotic value at infinity, and radial falloff power must be specified. In general, a radial power of 3 has been found to yield the best results, i.e. minimal initial transients and reflections.

Here we showcase the features of the NRPy+ interface for implementing this boundary condition and defining these values. The interface is a python class structure that allows the user to specify default values for each grid function, which may then be changed. This may be useful when the user wants to define the default values for several grid functions but wishes to specifically alter few others.

First let's define a few grid functions. Note that the boundary condition should be used only for variables defined in three spatial dimensions.

```python
# Step P3: Defining a couple of grid functions
uu, vv, ww, xx, yy, zz, = gri.register_gridfunctions("EVOL",["uu","vv","ww","xx","yy","zz"])

```

NRPy+ can now access these grid function names, and will store default values for the boundary condition. First we import `CurviBoundaryConditions.CurviBoundaryConditions`, and define a class instance _bcs_ using `sommerfeld_bc`.

```python
import CurviBoundaryConditions.CurviBoundaryConditions as cbcs
cbcs.Set_up_CurviBoundaryConditions(os.path.join(Ccodesdir,"boundary_conditions/"),Cparamspath=os.path.join("../"))

bcs = cbcs.sommerfeld_bc(vars_radpower_default=3., vars_speed_default=1., \
                         vars_at_inf_default=0.)
```

    Wrote to file "SommerfeldBC_Codes/boundary_conditions/parity_conditions_symbolic_dot_products.h"
    Evolved parity: ( uu:0, vv:0, ww:0, xx:0, yy:0, zz:0 )

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

Using the instance _bcs_ we may change these default values using the grid function names, since NRPy+ stores these values in python dictionaries. We then print out the contents of the dictionaries.

```python
# Changing values for uu
bcs.vars_at_infinity['UUGF'] = 5.
bcs.vars_speed['UUGF']       = 0.5

# Changing values for zz
bcs.vars_at_infinity['ZZGF'] = 4.
bcs.vars_speed['ZZGF']       = 2.**0.5

print('GF values at infinity =', bcs.vars_at_infinity.items())
print('GF speeds = ', bcs.vars_speed.items())
print('GF radial powers = ' ,bcs.vars_radpower.items())
```

    GF values at infinity = dict_items([('UUGF', 5.0), ('VVGF', 0.0), ('WWGF', 0.0), ('XXGF', 0.0), ('YYGF', 0.0), ('ZZGF', 4.0)])
    GF speeds =  dict_items([('UUGF', 0.5), ('VVGF', 1.0), ('WWGF', 1.0), ('XXGF', 1.0), ('YYGF', 1.0), ('ZZGF', 1.4142135623730951)])
    GF radial powers =  dict_items([('UUGF', 3.0), ('VVGF', 3.0), ('WWGF', 3.0), ('XXGF', 3.0), ('YYGF', 3.0), ('ZZGF', 3.0)])

Finally, we write these values into the `sommerfeld_params.h` file, and generate the `radial_derivative.h` file, which defines the function used to calculate the $\partial_r f term$, which our C code may read from later, using the function write_sommerfeld_files(), which takes the C codes directory path and finite differencing order as inputs. __Only second order finite differences are supported at this time.__

```python
bcs.write_sommerfeld_files(Ccodesdir, fd_order=2)
```

# Validation against the [Einstein Toolkit's NewRad boundary condition driver](https://www.einsteintoolkit.org/EinsteinEvolve/thornguide/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk) \[Back to [top](#toc)\]
$$\label{etk_validation}$$

Here we showcase some validation results of our Sommerfeld boundary condition as implemented in NRPy+ against [ETK's NewRad boundary condition driver](https://www.einsteintoolkit.org/thornguide/EinsteinEvolve/NewRad/documentation.html#XEinsteinEvolve_NewRad_Alcubierre:2002kk), storing the ETK data and subsequent plots in the [SommerfeldBoundaryCondition folder](SommerfeldBoundaryCondition). Specifically, we do so by

1. Generating the scalar wave initial data for ETK using the [ETK_thorn-IDScalarWaveNRPy notebook](Tutorial-ETK_thorn-IDScalarWaveNRPy.ipynb), with the NRPy+ code generation documented [here](Tutorial-ScalarWave.ipynb)

2. Generating the ETK evolution C codes using the [ETK_thorn-WaveToyNRPy notebook](Tutorial-ETK_thorn-WaveToyNRPy.ipynb), with the NRPy+ code generation generation also documented [here](Tutorial-ScalarWave.ipynb)

3. Compare results to [Tutorial-Start_to_Finish-ScalarWaveCurvilinear](Tutorial-Start_to_Finish-ScalarWaveCurvilinear.ipynb), adding 1e-16 to the log$_{10}$(relative error) plots to avoid taking the log$_{10}$ of zero where we have perfect agreement. $t$=0.3 represents 6 time steps into the systems

For both the evolution thorn and NRPy+ code we define the gridfunctions __UUGF__ and __VVGF__, use RK4 time stepping, and fourth order finite differencing. For the boundary condition parameters, we set uu_at_infinity = 2.0, vv_at_infinity = 0.0, and char_speed = 1.0, and var_radpower = 3.0 for both evolution variables.

First we show validation results for the case of a scalar wave propagating in the +x direction (initial data, documented [here](Tutorial-ScalarWave.ipynb), with kk0 = 1, kk1 = kk2 = 0), overlaying $u\left(x, y=0,z=0,t=0.3\right)$ from the ETK thorn and from NRPy+, and plotting the relative difference between the two.

```python
from IPython.display import Image
from IPython.display import display

x_axis_plot = Image("SommerfeldBoundaryCondition/NRPy_vs_ETK_x-axis.png", width=400, height=400)
E_relx_axis_plot = Image("SommerfeldBoundaryCondition/E_rel_x-axis.png", width=400, height=400)
display(x_axis_plot,E_relx_axis_plot)
```

Lastly we show validation results for the case of a scalar wave propagating along the -x, -y, +z diagonal, and taking a slice along the y-axis at x=z=0 (initial data, documented [here](Tutorial-ScalarWave.ipynb), with kk0 = -1, kk1 = -1, kk2 = 1), overlaying $u\left(x=0, y,z=0,t=0.3\right)$ from the ETK thorn and from NRPy+, and plotting the relative difference between the two.

```python
diagonal = Image("SommerfeldBoundaryCondition/NRPy_vs_ETK_diagonal.png", width=400, height=400)
E_rel_diagonal = Image("SommerfeldBoundaryCondition/E_rel_diagonal.png", width=400, height=400)
display(diagonal, E_rel_diagonal)
```

# 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-SommerfeldBoundaryCondition.pdf](Tutorial-SommerfeldBoundaryCondition.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-SommerfeldBoundaryCondition")
```

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


```python
%matplotlib widget
```

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

```python
from rl_2sources import rl_2sources_class
```

## Instantiate system

```python
syst = rl_2sources_class()
```

## Solve steady state

```python
syst.initialize()
```

    True

```python
syst.report_x()
syst.report_y()
syst.report_u()
```

    i     = 10.00
    p     = 1000.00
    v_1   = 101.00

## Run simulation

```python
events = [{'p':1000.0},
          {'t_end':0.1,'v_1':102},
          {'t_end':1.0,'v_1':102},
          {'t_end':2.0,'v_1':100}]
syst.simulate(events);
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('i'))
axes[1].plot(syst.T,syst.get_values('p'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')

```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

## Run interactive simulation

```python
Δt = 0.01
times = np.arange(0.0,2,Δt)
```

```python
syst.initialize()

it = 0
for t in times:

    v_1 = 101 + np.sin(2*np.pi*1.0*t)

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('i'))
axes[1].plot(syst.T,syst.get_values('v_1'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python
syst.Dt
```

    0.001

## Run simulation with feedback control (proportional control)

```python
Δt = 0.1
times = np.arange(0.0,2,Δt)
```

```python
syst.initialize()
K_p = 2e-3
K_i = 0.01
syst.xi = 0.0
it = 0
for t in times:

    p = syst.get_value('p')

    p_ref = 1000

    if t>0.1:
        p_ref = 1200

    error = (p_ref - p)

    v_1 = 101 + K_p*error + K_i*syst.xi

    syst.xi += Δt*error

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('p'))
axes[1].plot(syst.T,syst.get_values('v_1'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python
T_ctrl = np.zeros((len(times),1))
X_ctrl = np.zeros((len(times),N_u_d*2))
U_ctrl = np.zeros((len(times),2))
X_obs = np.zeros((len(times),N_x_d))
Z_obs = np.zeros((len(times),N_z_o))

    z_ref = np.copy(np.array([[p_t_ref],
                              [v_1_ref]]))

    u_d = control(syst,z_ref)

    p_m_ref = u_d[0,0]
    v_f =     u_d[1,0]
```

## Run simulation with feedback control (PI control)

```python
Δt = 0.1
times = np.arange(0.0,2,Δt)
```

```python
syst.initialize()
K_p = 2e-3
K_i = 0.01
syst.xi = 0.0
it = 0
for t in times:

    p = syst.get_value('p')

    p_ref = 1000

    if t>0.1:
        p_ref = 1200

    error = (p_ref - p)

    v_1 = 101 + K_p*error + K_i*syst.xi

    syst.xi += Δt*error

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('p'))
axes[1].plot(syst.T,syst.get_values('v_1'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

```python

```

## PI control design

```python
import pydae.ssa as ssa
import scipy.signal as sctrl

```

### Sistema DAE linealizado
\begin{eqnarray}
\Delta \dot x_p &=& A_p \Delta x_p + B_p \Delta u_p \\
\Delta z_p &=& C_p \Delta x_p + D_p \Delta u_p \\
\end{eqnarray}

### Discretized system
\begin{eqnarray}
\Delta x_d^{k+1} &=& A_d \Delta x_d^{k}  + B_d \Delta u_d^{k}  \\
\Delta z_d^{k} &=& C_d^{k} \Delta x_d^{k} + D_d^{k} \Delta u_d^{k} \\
\end{eqnarray}

### Dynamic extension
\begin{eqnarray}
\Delta x_i^{k+1} &=& \Delta x_i^{k} + \Delta t \left(\Delta z_c- \Delta z_c^\star \right)  \\
\end{eqnarray}

with:
\begin{eqnarray}
\Delta z_c  &=& z_c - z_c^0  \\
\Delta z_c^\star &=& z_c^\star -z_c^0
\end{eqnarray}

### Extended system
\begin{equation}
\Delta x_e^{k+1}  =
\begin{bmatrix}
A_d & 0\\
\Delta t C_c & I
\end{bmatrix}
\begin{bmatrix}
\Delta x_d^{k}\\
\Delta x_i^{k}
\end{bmatrix}
+
\begin{bmatrix}
B_d \\
0
\end{bmatrix}
\Delta u_d^{k}
\end{equation}

\begin{equation}
A_e  =
\begin{bmatrix}
A_d & 0\\
\Delta t C_c & I
\end{bmatrix}
\;\;\;\;\;
B_e =
\begin{bmatrix}
B_d \\
0
\end{bmatrix}
\end{equation}

### State feedback control
\begin{equation}
\Delta u_d^{k} = -K_e \Delta x_e^{k}
\end{equation}

$K_e$ can be obtained with LQR:

K_e = lqr(A_e,B_e,Q,R)

```python
Δt = 0.1
x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = ['p']     # outputs to consider in the controller

z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'p':1e3}])
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:]

N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d, O_xu],    # Δx_d
                [ Δt*C_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   B_d],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])
R_c = np.diag([0.1])

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)
E_c = np.log(E_c)/Δt
```

```python
E_c
```

    array([-68.85462939+0.j, -47.29891607+0.j])

## PI control validation

```python
times = np.arange(0.0,2,Δt)
```

```python
syst.initialize()
i_0 = syst.get_value('i')
K_p = K_c[0,0]
K_i = K_c[0,1]
syst.Δxi = 0.0
u_d_0 = syst.get_value('v_1')
p_0 =  syst.get_value('p')
it = 0
for t in times:

    p = syst.get_value('p')
    i = syst.get_value('i')

    Δx_d = i - i_0
    Δx_i = syst.Δxi

    p_ref = p_0

    if t>0.1:
        p_ref = 1200

    error = (p - p_ref)

    Δx_e = np.block([Δx_d, Δx_i]).T

    Δu_d = -K_c @ Δx_e

    u_d = Δu_d + u_d_0

    syst.Δxi += Δt*error

    v_1 = u_d

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('p'))
axes[1].plot(syst.T,syst.get_values('v_1'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python
syst.initialize()
K_p = 2e-3
it = 0
for t in times:

    p = syst.get_value('p')

    p_ref = 1000

    if t>0.1:
        p_ref = 1200

    v_1 = 101 + K_p*(p_ref - p)

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

## PI control with delay design

```python
import pydae.ssa as ssa
import scipy.signal as sctrl

```

### Linealized DAE system
\begin{eqnarray}
\Delta \dot x_p &=& A_p \Delta x_p + B_p \Delta u_p \\
\Delta z_p &=& C_p \Delta x_p + D_p \Delta u_p \\
\end{eqnarray}

### Discretized system
\begin{eqnarray}
\Delta x_d^{k+1} &=& A_d \Delta x_d^{k}  + B_d \Delta u_d^{k}  \\
\Delta z_d^{k} &=& C_d  \Delta x_d^{k} + D_d  \Delta u_d^{k} \\
\end{eqnarray}

### Discretized system from control point of view
\begin{eqnarray}
\Delta x_d^{k+1} &=& A_d \Delta x_d^{k} + B_d \Delta x_r^{k}  \\
\Delta z_c^{k}   &=& C_c \Delta x_d^{k} + D_c \Delta x_r^{k} \\
\end{eqnarray}

### Delay extension
\begin{eqnarray}
\Delta x_r^{k+1} &=& \Delta u_r^{k} \\
\end{eqnarray}

### Dynamic extension
\begin{eqnarray}
\Delta x_i^{k+1} &=& \Delta x_i^{k} + \Delta t \left(\Delta z_c- \Delta z_c^\star \right)  \\
\end{eqnarray}

with:
\begin{eqnarray}
\Delta z_c  &=& z_c - z_c^0  \\
\Delta z_c^\star &=& z_c^\star -z_c^0 \\
\Delta z_c  &=& C_c \Delta x_d^{k} + D_c \Delta u_d^{k}
\end{eqnarray}

### Extended system

\begin{eqnarray}
\Delta x_d^{k+1} &=& A_d \Delta x_d^{k}  + B_d \Delta x_r^{k}  \\
\Delta x_r^{k+1} &=& \Delta u_r^{k} \\
\Delta x_i^{k+1} &=& \Delta x_i^{k} + \Delta t C_c \Delta x_d^k - \Delta t \Delta z_c^\star   \\
\end{eqnarray}

\begin{equation}
\Delta x_e^{k+1}  =
\begin{bmatrix}
A_d & B_d & 0 \\
0 & 0 & 0 \\
\Delta t C_c& \Delta t D_c & I
\end{bmatrix}
\begin{bmatrix}
\Delta x_d^{k}\\
\Delta x_r^{k}\\
\Delta x_i^{k}
\end{bmatrix}
+
\begin{bmatrix}
0\\
I \\
0
\end{bmatrix}
\Delta u_r^{k}
\end{equation}

\begin{equation}
A_e  =
\begin{bmatrix}
A_d & B_d & 0\\
0 & 0 & 0\\
\Delta t C_c & 0 & I
\end{bmatrix}
\;\;\;\;\;
B_e =
\begin{bmatrix}
0 \\
I \\
0
\end{bmatrix}
\end{equation}

### State feedback control
\begin{equation}
\Delta u_r^{k} = -K_e \Delta x_e^{k}
\end{equation}

$K_e$ can be obtained with LQR:

K_e = lqr(A_e,B_e,Q,R)

### Control dynamics
\begin{equation}
\Delta x_{ctrl}^{k+1}  =
\begin{bmatrix}
0 & 0 \\
0 & I
\end{bmatrix}
\begin{bmatrix}
\Delta x_r^{k}\\
\Delta x_i^{k}
\end{bmatrix}
+
\begin{bmatrix}
I \\
0
\end{bmatrix}
\Delta u_d^{k}
\end{equation}

\begin{equation}
A_{ctrl}  =
\begin{bmatrix}
 0 & 0\\
 0 & I
\end{bmatrix}
\;\;\;\;\;
B_{ctrl} =
\begin{bmatrix}
I \\
0
\end{bmatrix}
\end{equation}

```python
Δt = 0.01
x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = ['p']     # outputs to consider in the controller

z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]

syst.Δt = Δt

## Calculate equilibirum point
syst.initialize([{'p':1e3}])
ssa.eval_ss(syst)

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

# plant discretization
A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Δt,method='zoh')

N_z_d,N_x_d = C_d.shape  # discreticed plant dimensions
N_x_d,N_u_d = B_d.shape

# convenient matrices
O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:]

N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d,    B_d, O_xu],    # Δx_d
                [   O_ux,   O_uu, O_uu],    # Δx_r
                [ Δt*C_c, Δt*D_c, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

A_ctrl = A_e[N_x_d:,N_x_d:]
B_ctrl = B_e[N_x_d:]

# weighting matrices
Q_c = np.eye(A_e.shape[0])
R_c = np.diag([1])

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)
E_c = np.log(E_c)/Δt

syst.A_ctrl = A_ctrl
syst.B_ctrl = B_ctrl
syst.K_c = K_c
syst.N_x_d = N_x_d  # number of plant states
syst.N_u_d = N_u_d  # number of plant inputs
syst.N_z_c = N_z_c  # number of plant outputs considered for the controller
```

```python
K_c = np.array([[0.23551695, 1.35717384, 0.01565326]])
```

## PI control validation

```python
times = np.arange(0.0,10,Δt)
```

```python
syst.initialize()
x_0 = syst.get_x()
syst.Δx_ctrl = np.zeros((N_u_d+N_z_c,1))
syst.Δu_d = np.zeros((N_u_d,1))
u_d_0 = syst.get_value('v_1')
x_r_0 = u_d_0
p_0 =  syst.get_value('p')
it = 0
for t in times:

    p = syst.get_value('p')
    Δx_d = syst.get_x() - x_0

    Δx_ctrl = np.copy(syst.Δx_ctrl)
    Δx_r = Δx_ctrl[:N_u_d]
    Δx_i = Δx_ctrl[N_u_d:]
    Δu_d = np.copy(syst.Δu_d)
    p_ref = p_0

    if t>=0.1:
        p_ref = 1200

    Δz_c = np.block([[p - p_ref]])

    Δx_e = np.block([[Δx_d], [Δx_ctrl]])

    #x_r += Δu_d
    #Δx_i += Δt*Δz
    #Δx_ctrl = np.block([Δx_r, Δx_i]).T

    # control dynamics
    Δu_d = -K_c @ Δx_e
    Δx_r_k1 = Δu_d
    Δx_i_k1 = Δx_i +  Δt*Δz_c

    #u_d = Δu_d + u_d_0

    syst.Δx_ctrl[:syst.N_u,:] = np.copy(Δx_r_k1)
    syst.Δx_ctrl[syst.N_u:,:] = np.copy(Δx_i_k1)

    syst.Δu_d  = np.copy(Δu_d)
    #yst.Δx_ctrl = np.copy(Δx_ctrl)

    v_1 =  Δx_r + x_r_0
   # v_1 = u_d
    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    it += 1

syst.post();
```

```python
B_d
```

    array([[0.95162582]])

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(syst.T,syst.get_values('p'))
axes[1].plot(syst.T,syst.get_values('v_1'))

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python
Δt
```

    0.01

```python
Δx_e = np.zeros((N_x_d+N_u_d+N_z_c,1))
syst.Δx_ctrl = np.zeros((N_u_d+N_z_c,1))
syst.Δu_d = np.zeros((N_u_d,1))
u_d_0 = syst.get_value('v_1')
x_r_0 = u_d_0
p_0 =  1
X_e = np.zeros((len(times),Δx_e.shape[0]))
it = 0
for t in times:
    X_e[it,:] = Δx_e.T

    p_ref = p_0
    if t>=0.1:
        p_ref = 1.2

    C_e = np.block([[C_c , 0 , 0]])
    Δz_c = np.block([[C_e@Δx_e - (p_ref-p_0)]])
    B_z = np.block([[0],[0],[-Δt]])

    Δu_r = -K_c @Δx_e
    Δx_e = A_e@Δx_e + B_e@Δu_r + B_z@Δz_c

    it += 1

syst.post();
```

```python
C_e
```

    array([[100.,   0.,   0.]])

```python
fig, axes = plt.subplots(nrows=2, ncols=1, figsize=(7, 7))

axes[0].plot(times,X_e)

for ax in axes:
    ax.grid()
    #ax.legend()
ax.set_xlabel('Time (s)')
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    Text(0.5, 0, 'Time (s)')

```python

```

```python

```

```python
Ts_control = 0.1
Δt = Ts_control
x_d_ctrl_list = ['i']   # states to consider in the reduction
z_ctrl_list = ['p']                           # outputs to consider in the controller
z_obs_list =  ['p']       # outputs to consider in the observer

syst.Δt = Δt

## Calculate equilibirum point
events=[{'p':1000}]
syst.initialize(events,xy0=1)
print(syst.get_value('p'))
ssa.eval_ss(syst)

z_ctrl_idxs = [syst.outputs_list.index(item) for item in z_ctrl_list]
z_obs_idxs  = [syst.outputs_list.index(item) for item in z_obs_list]

# linear continous plant
A_p = syst.A
B_p = syst.B
C_p = syst.C
D_p = syst.D

N_z_p,N_x_p = C_p.shape
N_x_p,N_u_p = B_p.shape

x_ctrl_keep_idx = [syst.x_list.index(item)  for item in x_d_ctrl_list]
x_ctrl_elim_idx = list(set(range(N_x_p)) - set(x_ctrl_keep_idx))

# Reduction and discretization ##################################################################
if len(x_ctrl_elim_idx) == 0:
    # without reduction:
    A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_p,B_p,C_p,D_p),Ts_control,method='zoh')
    T_r = np.eye(N_x_p)

else:
    # with reduction:
    sys = ctrl.ss(A_p, B_p, C_p, D_p)
    T_r =np.eye(N_x_p)

    rsys = ctrl.modred(sys, x_ctrl_elim_idx, method='truncate')
    A_pr,B_pr,C_pr,D_pr = rsys.A,rsys.B,rsys.C,rsys.D
    A_d,B_d,C_d,D_d,Dt = sctrl.cont2discrete((A_pr,B_pr,C_pr,D_pr),Ts_control,method='zoh')
    T_r = np.delete(np.eye(N_x_p),x_ctrl_elim_idx,axis=0)

N_z_d,N_x_d = C_d.shape
N_x_d,N_u_d = B_d.shape

syst.T_r = T_r  # Full linear discrete states to reduced (truncated) states

syst.A_d = A_d
syst.B_d = B_d

# Controller ##################################################################################
C_c = C_d[z_ctrl_idxs,:]
D_c = D_d[z_ctrl_idxs,:]

N_z_c,N_x_c = C_c.shape

O_ux = np.zeros((N_u_d,N_x_d))
O_xu = np.zeros((N_x_d,N_u_d))
O_uu = np.zeros((N_u_d,N_u_d))
I_uu = np.eye(N_u_d)

# discretized plant:
# Δx_d = A_d*Δx_d + B_d*Δu_d
# Δz_c = C_c*Δx_d + D_c*Δu_d

# delay in the input:
# Δx_d = A_d*Δx_d + B_d*Δx_r
# Δz_c = C_c*Δx_c
# Δx_r = Δu_d

# dinamic extension:
# Δx_d = A_d*Δx_d + B_d*Δx_r
# Δx_r = Δu_d
# Δx_i = Δx_i + Δt*(Δz_c-Δz_c_ref) = Δx_i + Δt*C_c*Δx_d - Dt*Δz_c_ref
# Δz_c = z_c - z_c_0
# Δz_c_ref = z_c_ref - z_c_0
# (Δz_c-Δz_c_ref) = z_c - z_c_ref

A_e = np.block([
                [    A_d,  B_d, O_xu],    # Δx_d
                [   O_ux, O_uu, O_uu],    # Δx_r
                [ Δt*C_c, O_uu, I_uu],    # Δx_i
               ])

B_e = np.block([
                [   O_xu],
                [   I_uu],
                [   O_uu],
               ])

# weighting matrices
Q_c = np.eye(A_e.shape[0])

R_c = np.diag([1])

K_c,S_c,E_c = ssa.dlqr(A_e,B_e,Q_c,R_c)
E_c = np.log(E_c)/Δt

syst.x_ctrl_keep_idx = x_ctrl_keep_idx
syst.x_ctrl_elim_idx = x_ctrl_elim_idx
syst.z_ctrl_list = z_ctrl_list

syst.N_z_c = N_z_c
syst.N_u_d = N_u_d

syst.x_ctrl = np.zeros((N_u_d+N_z_d,1))
syst.K_c = K_c
syst.C_c = C_c
syst.D_c = D_c

# Observer ###########################################################

# discretized plant:
# Dx_d = A_d*Dx_d + B_d*Du_d
# z_o  = C_o*Dx_d + D_o*Du_d

# x_o = A_d*x_o + B_d*u_d + L_o*(z_o - C_o*x_o - D_o*Du_d)

C_o = C_d[z_obs_idxs,:]
D_o = D_d[z_obs_idxs,:]

N_z_o = C_o.shape[0]

Q_o = np.eye(A_d.shape[0])

R_o = np.diag([1]*N_z_o)
K_o_T,S_o,E_o = ssa.dlqr(A_d.T,C_o.T,Q_o,R_o)
K_o = K_o_T.T

syst.K_o = K_o
syst.C_o = C_o
syst.D_o = D_o

syst.z_obs_list = z_obs_list
syst.N_z_o = N_z_o
syst.x_obs = np.zeros((N_x_d,1))

print('damp_ctrl',-E_c.real/np.abs(E_c))
print('damp_obs',-E_o.real/np.abs(E_o))
```

    1000.0000000000063
    damp_ctrl [0.96560365 0.96560365 0.99270532]
    damp_obs [-1.]

```python
E_c
```

    array([ -43.10502553+11.6073527j ,  -43.10502553-11.6073527j ,
           -258.66973143+31.41592654j])

```python
def control(syst,z_ref):

    Δt = syst.Δt                    # disctretization time
    T_r = syst.T_r                  # Full linear discrete states to reduced (truncated) states

    x_d = T_r @ syst.struct[0].x    # plant dynamic states (discrete)
    x_d_0 = syst.x_d_0              # plant initial steady state (discrete)
    Δx_d = x_d - x_d_0              # plant dynamic states increment (discrete)

    x_r_0  = syst.x_r_0              # initial delays values

    Δx_e = np.copy(syst.Δx_e)     # extended plant dynamic states increment (discrete)

    u_d = np.copy(syst.u_d)       # plant inputs
    u_d_0 = syst.u_d_0              # plant initial steady inputs (discrete)
    Δu_d = syst.Δu_d              # plant inputs increment (discrete)

    z_c_0 = np.copy(syst.z_c_0)     # controller considered initial plant ouputs
    z_o_0 = np.copy(syst.z_o_0)     # observer considered initial plant ouputs

    A_d = syst.A_d            # plant A matrix without delay and integrators extension (discrete)
    B_d = syst.B_d            # plant B matrix without delay and integrators extension (discrete)
    C_c = syst.C_c            # controller considered plant C matrix (discrete)
    D_c = syst.D_c            # controller considered plant D matrix (discrete)
    C_o = syst.C_o            # observer considered plant C matrix (discrete)
    D_o = syst.D_o            # observer considered plant D matrix (discrete)

    N_x_d,N_u_d = B_d.shape  # dimensions
    N_z_c,N_x_c = C_c.shape
    N_z_o,N_x_o = C_o.shape

    K_c = syst.K_c            # controller gains matrix
    K_o = syst.K_o            # observer gains matrix

    Δx_c = np.copy(syst.x_ctrl)  # controler states
    Δx_r = Δx_c[:syst.N_u_d,:]   # delay discrete states
    Δx_i = Δx_c[syst.N_u_d:,:]   # integrators discrete states

    Δx_o = syst.x_obs       # observer states

    # outputs to control
    z_ctrl_values_list = []
    for item in syst.z_ctrl_list:
         z_ctrl_values_list += [syst.get_value(item)]
    z_c = np.array(z_ctrl_values_list).reshape(N_z_c,1)

    Δz_c =   (z_c-z_c_0) - (z_ref-z_c_0)

    # outputs for observer
    z_obs_values_list = []
    for item in syst.z_obs_list:
         z_obs_values_list += [syst.get_value(item)]
    z_o = np.array(z_obs_values_list).reshape(N_z_o,1)

    Δz_o = z_o - z_o_0

    # control dynamics
    Δx_r_k1 = Δu_d
    Δx_i_k1 = Δx_i + Δt*Δz_c

    # control law
    Δu_d = -K_c @ Δx_e

    # observer dynamics
    Δx_o_m1 = A_d @ Δx_o + B_d@Δu_d  +  K_o @ (Δz_o - C_o @ Δx_o - D_o @ Δu_d)

    # save statates for next step
    syst.x_ctrl[:syst.N_u,:] = np.copy(Δx_r_k1)
    syst.x_ctrl[syst.N_u:,:] = np.copy(Δx_i_k1)
    syst.x_obs = np.copy(Δx_o_m1)

    if syst.observer == False:
        syst.Δx_e  = np.copy(np.block([[Δx_d],[Δx_r_k1],[Δx_i_k1]]))

    if syst.observer == True:
        syst.Δx_e  = np.copy(np.block([[Δx_o_m1],[Δx_r_k1],[Δx_i_k1]]))

    syst.Δu_d = Δu_d

    # outputs for post-processing
    syst.z_obs = C_o @ Δx_o + D_o @ Δu_d

    return Δx_r_k1 + x_r_0
```

```python
B_d
```

    array([[6.32120559]])

## Initialization

```python
times = np.arange(0.0,20,Δt)

## Calculate initial references
events=[{'p':1000}]
syst.initialize(events,xy0=1.0)

# initial inputs
u_values_list = []
for item in syst.u_run_list:
     u_values_list += [syst.get_value(item)]
u_d_0 = np.array(u_values_list).reshape(syst.N_u_d,1)

# initial states
x_d_0 = T_r @ syst.struct[0].x    # initial plant states
x_r_0 = u_d_0                     # initial delay states
x_i_0 = np.zeros((syst.N_u_d,1))  # initial integrator states

Δx_e = np.block([[x_d_0*0],[x_r_0*0],[x_i_0*0]])

# initial outputs
z_ctrl_values_list = []
for item in syst.z_ctrl_list:
     z_ctrl_values_list += [syst.get_value(item)]
z_c_0 = np.array(z_ctrl_values_list).reshape(N_z_c,1)

# outputs for observer
z_obs_values_list = []
for item in syst.z_obs_list:
     z_obs_values_list += [syst.get_value(item)]
z_o_0 = np.array(z_obs_values_list).reshape(N_z_o,1)

syst.Δx_e = Δx_e
syst.Δu_d = np.zeros((syst.N_u_d,1))
syst.x_d_0 = x_d_0
syst.x_r_0 = x_r_0
syst.u_d_0 = u_d_0
syst.z_c_0 = z_c_0
syst.z_o_0 = z_o_0

syst.u_d =  np.copy(u_d_0)
syst.x_ctrl = np.block([[x_r_0],[x_i_0]])*0
syst.x_obs = np.copy(x_d_0)*0

syst.observer = False
```

## Simulation

```python
T_ctrl = np.zeros((len(times),1))
X_ctrl = np.zeros((len(times),N_u_d*2))
U_ctrl = np.zeros((len(times),2))
X_obs = np.zeros((len(times),N_x_d))
Z_obs = np.zeros((len(times),N_z_o))
it = 0
for t in times:

    # references
    p_ref = 1000

    if t>=1:
        p_ref = 1200

    z_ref = np.copy(np.array([[p_ref],
                              ]))

    u_d = control(syst,z_ref)

    v_1 = u_d[0,0]

    events=[{'t_end':t,'v_1':v_1}]
    syst.run(events)

    #X_ctrl[it,:] = np.hstack((x_i.T))
    U_ctrl[it,:] = u_d.T
    X_ctrl[it,:] = syst.x_ctrl.T
    X_obs[it,:]  = syst.x_obs.T
    Z_obs[it,:]  = syst.z_obs.T
    it += 1
syst.post();
```

## Post processing

```python
plt.close('all')
fig, axes = plt.subplots(nrows=2,ncols=2, figsize=(10, 5), frameon=False, dpi=80)

#axes[0,0].plot(syst.T, syst.get_values('omega'), label=f'$\omega$')
#axes[0,0].step(times,X_obs[:,1] + x_d_0[1],   label=f'$\hat \omega$')

axes[0,0].plot(syst.T, syst.get_values('p'), label=f"$p$")

axes[1,0].plot(syst.T, syst.get_values('v_1'), label=f"$v_1$")
for ax in axes.flatten():
    ax.legend(loc='best')
    ax.grid(True)
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    No handles with labels found to put in legend.
    No handles with labels found to put in legend.

```python

```

```python

```