# Hold out

```python
# %load imports.py
%load_ext autoreload
%autoreload 2
%reload_kedro
%config Completer.use_jedi = False  ## (To fix autocomplete)

import pandas as pd
pd.options.display.max_rows = 100
from src.models.vmm import ModelSimulator
import matplotlib.pyplot as plt
import matplotlib
#matplotlib.rcParams["figure.figsize"] = (10,10)
plt.style.use('presentation')
from src.visualization.plot import track_plots, plot, captive_plot
import kedro
import numpy as np
import os.path
import anyconfig

from myst_nb import glue
from src.symbols import *
import src.symbols as symbols
from src.system_equations import *

from IPython.display import display, Math, Latex, Markdown
from sympy.physics.vector.printing import vpprint, vlatex

from src.models.regression import MotionRegression

from src.parameters import df_parameters
p = df_parameters["symbol"]

# Read configs:
conf_path = os.path.join("../../conf/base/")
runs_globals_path = os.path.join(
    conf_path,
    "runs_globals.yml",
)

runs_globals = anyconfig.load(runs_globals_path)
model_test_ids = runs_globals["model_test_ids"]

join_globals_path = os.path.join(
    conf_path,
    "join_globals.yml",
)

joins = runs_globals["joins"]
join_runs_dict = anyconfig.load(join_globals_path)

globals_path = os.path.join(
    conf_path,
    "globals.yml",
)
global_variables = anyconfig.load(globals_path)

vmm_names = global_variables["vmms"]
vmm_names.remove('vmm_linear')

only_joined = global_variables[
    "only_joined"
]  # (regress/predict with only models from joined runs)S

vmms = {}
for vmm_name in vmm_names:

    vmms[vmm_name] = catalog.load(vmm_name)

```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload
    2022-03-29 09:29:37,410 - kedro.framework.session.store - INFO - `read()` not implemented for `SQLiteStore`. Assuming empty store.
    2022-03-29 09:29:39,003 - root - INFO - ** Kedro project wPCC_pipeline
    2022-03-29 09:29:39,004 - root - INFO - Defined global variable `context`, `session`, `catalog` and `pipelines`
    2022-03-29 09:29:39,014 - root - INFO - Registered line magic `run_viz`
    2022-03-29 09:29:39,029 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz` (PickleDataSet)...
    2022-03-29 09:29:39,035 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz_simple` (PickleDataSet)...
    2022-03-29 09:29:39,039 - kedro.io.data_catalog - INFO - Loading data from `vmm_martin` (PickleDataSet)...
    2022-03-29 09:29:39,044 - kedro.io.data_catalog - INFO - Loading data from `vmm_martins_simple` (PickleDataSet)...
    2022-03-29 09:29:39,048 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz_expanded` (PickleDataSet)...

```python
id = 22774

ship_data = catalog.load("ship_data")
data = catalog.load(f"{ id }.data_ek_smooth")

dataframes = {}
styles = {}
for vmm_name in vmm_names:

    data_resimulate = catalog.load(f"{ vmm_name }.motion_regression.no_circle.{ id }.data_resimulate")

    #index = np.argmax(data_resimulate.isnull().any(axis=1))
    if data_resimulate.notnull().all(axis=1).all():
        dataframes[vmm_name] = data_resimulate.iloc[0:-7500].copy()

    styles[vmm_name] = {'alpha':0.75}

dataframes['model test'] = data.iloc[0:-7500]
styles['model test'] = {'style':'k-', 'alpha':1, 'lw':1}
```

    2022-03-29 09:32:39,439 - kedro.io.data_catalog - INFO - Loading data from `ship_data` (YAMLDataSet)...
    2022-03-29 09:32:39,442 - kedro.io.data_catalog - INFO - Loading data from `22774.data_ek_smooth` (CSVDataSet)...
    2022-03-29 09:32:39,483 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz.motion_regression.no_circle.22774.data_resimulate` (CSVDataSet)...
    2022-03-29 09:32:39,503 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz_simple.motion_regression.no_circle.22774.data_resimulate` (CSVDataSet)...
    2022-03-29 09:32:39,523 - kedro.io.data_catalog - INFO - Loading data from `vmm_martin.motion_regression.no_circle.22774.data_resimulate` (CSVDataSet)...
    2022-03-29 09:32:39,541 - kedro.io.data_catalog - INFO - Loading data from `vmm_martins_simple.motion_regression.no_circle.22774.data_resimulate` (CSVDataSet)...
    2022-03-29 09:32:39,573 - kedro.io.data_catalog - INFO - Loading data from `vmm_abkowitz_expanded.motion_regression.no_circle.22774.data_resimulate` (CSVDataSet)...

```python
fig = track_plots(dataframes, lpp=ship_data['L'], beam=ship_data['B'], plot_boats=True, styles=styles, N=7);
fig.axes.set_xlim(-1,17.5)
fig.axes.set_ylim(5,25)

```

```python

```


# Start-to-Finish Example: Unit Testing `GiRaFFE_NRPy`: Interpolating Metric Face-Values

## Author: Patrick Nelson

## This module Validates the `FCVAL` routine for `GiRaFFE`.

**Notebook Status:** Validated

**Validation Notes:** This module will validate the routines in [Tutorial-GiRaFFE_NRPy-Metric_Face_Values](Tutorial-GiRaFFE_NRPy-Metric_Face_Values.ipynb).

### NRPy+ Source Code for this module:
* [GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-FCVAL.ipynb) Generates the C code to interpolate gridfunctions to cell faces.

## Introduction:

This notebook validates the code that will interpolate the metric gridfunctions on cell faces. These values, along with the reconstruction of primitive variables on the faces, are necessary for the Riemann solvers to compute the fluxes through the cell faces.

It is, in general, good coding practice to unit test functions individually to verify that they produce the expected and intended output. We will generate test data with arbitrarily-chosen analytic functions and calculate gridfunctions at the cell centers on a small numerical grid. We will then compute the values on the cell faces in two ways: first, with our interpolator, then second, we will shift the grid and compute them analytically. Then, we will rerun the function at a finer resolution. Finally, we will compare the results of the two runs to show fourth-order convergence.

When this notebook is run, the difference between the approximate and exact metric gridfunctions will be output to text files that can be found in the same directory as this notebook. These will be read in in [Step 3](#convergence), and used there to confirm convergence order of the algorithm.

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

This notebook is organized as follows

1. [Step 1](#setup): Set up core functions and parameters for unit testing the FCVAL algorithm
    1. [Step 1.a](#expressions) Write expressions for the metric gridfunctions
    1. [Step 1.b](#ccodekernels) Generate C functions to calculate the gridfunctions
    1. [Step 1.c](#free_parameters) Set free parameters in the code
1. [Step 2](#mainc): `FCVAL_unit_test.c`: The Main C Code
    1. [Step 2.a](#compile_run): Compile and run the code
1. [Step 3](#convergence): Code validation: Verify that relative error in numerical solution converges to zero at the expected order
1. [Step 4](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Set up core functions and parameters for unit testing the FCVAL algorithm \[Back to [top](#toc)\]
$$\label{setup}$$

We'll start by appending the relevant paths to `sys.path` so that we can access sympy modules in other places. Then, we'll import NRPy+ core functionality and set up a directory in which to carry out our test.

```python
import os, sys, shutil         # Standard Python modules for multiplatform OS-level functions
# First, we'll add the parent directory to the list of directories Python will check for modules.
nrpy_dir_path = os.path.join("..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)
nrpy_dir_path = os.path.join("..","..")
if nrpy_dir_path not in sys.path:
    sys.path.append(nrpy_dir_path)

from outputC import outCfunction, lhrh # NRPy+: Core C code output module
import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
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

Ccodesdir = "Start-to-Finish-UnitTests/Metric_Face_Values_UnitTest/"

# 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)

outdir = os.path.join(Ccodesdir,"output/")
cmd.mkdir(outdir)

subdir = "FCVAL"
cmd.mkdir(os.path.join(Ccodesdir,subdir))

thismodule = "Start_to_Finish_UnitTest-GiRaFFE_NRPy-Metric_Face_Values"

# Set the finite-differencing order to 2
par.set_parval_from_str("finite_difference::FD_CENTDERIVS_ORDER", 2)

```

## Step 1.a: Write expressions for the metric gridfunctions \[Back to [top](#toc)\]
$$\label{expressions}$$

First we register the metric gridfunctions as such.

```python
gammaDD = ixp.register_gridfunctions_for_single_rank2("AUXEVOL","gammaDD","sym01",DIM=3)
betaU = ixp.register_gridfunctions_for_single_rank1("AUXEVOL","betaU",DIM=3)
alpha = gri.register_gridfunctions("AUXEVOL","alpha")
```

Now, we'll fill these gridfunctions with data, choosing smooth arbitrary functions to generate unphysical test data *for the purposes of numerical testing*.

We'll need to set ten gridfunctions, so expressions are being pulled from several previously written unit tests.

First let's fill the 3-metric with data of the following functional form:

\begin{align}
\gamma_{xx} &= ax^3 + by^3 + cz^3 + dy^2 + ez^2 + f \\
\gamma_{yy} &= gx^3 + hy^3 + lz^3 + mx^2 + nz^2 + p \\
\gamma_{zz} &= px^3 + qy^3 + rz^3 + sx^2 + ty^2 + u. \\
\gamma_{xy} &= a \exp\left(-\left((x-b)^2+(y-c)^2+(z-d)^2\right)\right) \\
\gamma_{xz} &= f \exp\left(-\left((x-g)^2+(y-h)^2+(z-l)^2\right)\right) \\
\gamma_{yz} &= m \exp\left(-\left((x-n)^2+(y-o)^2+(z-p)^2\right)\right), \\
\end{align}

```python
a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t,u = par.Cparameters("REAL",thismodule,["a","b","c","d",
                         "e","f","g","h","l","m","n","o","p","q","r","s","t","u"],1e300)

M_PI  = par.Cparameters("#define",thismodule,["M_PI"], "")

par.set_parval_from_str("reference_metric::CoordSystem","Cartesian")
rfm.reference_metric()
x = rfm.xx_to_Cart[0]
y = rfm.xx_to_Cart[1]
z = rfm.xx_to_Cart[2]

gammaDD[0][0] = a*x**3 + b*y**3 + c*z**3 + d*y**2 + e*z**2 + f
gammaDD[1][1] = g*x**3 + h*y**3 + l*z**3 + m*x**2 + n*z**2 + o
gammaDD[2][2] = p*x**3 + q*y**3 + r*z**3 + s*x**2 + t*y**2 + u
gammaDD[0][1] = a * sp.exp(-((x-b)**2 + (y-c)**2 + (z-d)**2))
gammaDD[0][2] = f * sp.exp(-((x-g)**2 + (y-h)**2 + (z-l)**2))
gammaDD[1][2] = m * sp.exp(-((x-n)**2 + (y-o)**2 + (z-p)**2))
```

Next we'll fill the lapse and shift with data of the following form:

\begin{align}
\beta^x &= \frac{2}{\pi} \arctan(ax + by + cz) \\
\beta^y &= \frac{2}{\pi} \arctan(bx + cy + az) \\
\beta^z &= \frac{2}{\pi} \arctan(cx + ay + bz) \\
\alpha &= 1 - \frac{1}{2+x^2+y^2+z^2} \\
\end{align}

```python
betaU[0] = (sp.sympify(2)/M_PI) * sp.atan(a*x + b*y + c*z)
betaU[1] = (sp.sympify(2)/M_PI) * sp.atan(b*x + c*y + a*z)
betaU[2] = (sp.sympify(2)/M_PI) * sp.atan(c*x + a*y + b*z)

alpha = sp.sympify(1) - sp.sympify(1) / (sp.sympify(2) + x**2 + y**2 + z**2)
```

## Step 1.b: Generate C functions to calculate the gridfunctions \[Back to [top](#toc)\]
$$\label{ccodekernels}$$

Here, we will use the NRPy+ function `outCfunction()` to generate C code that will calculate our metric gridfunctions over an entire grid. We will also call the function to generate the function we are testing.

```python
metric_gfs_to_print = [\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD00"),rhs=gammaDD[0][0]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD01"),rhs=gammaDD[0][1]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD02"),rhs=gammaDD[0][2]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD11"),rhs=gammaDD[1][1]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD12"),rhs=gammaDD[1][2]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","gammaDD22"),rhs=gammaDD[2][2]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU0"),rhs=betaU[0]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU1"),rhs=betaU[1]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","betaU2"),rhs=betaU[2]),\
                       lhrh(lhs=gri.gfaccess("aux_gfs","alpha"),rhs=alpha),\
                      ]

desc = "Calculate the metric gridfunctions"
name = "calculate_metric_gfs"
outCfunction(
    outfile  = os.path.join(Ccodesdir,name+".h"), desc=desc, name=name,
    params   ="const paramstruct *restrict params,REAL *restrict xx[3],REAL *restrict auxevol_gfs",
    body     = fin.FD_outputC("returnstring",metric_gfs_to_print,params="outCverbose=False"),
    loopopts="AllPoints,Read_xxs")

import GiRaFFE_NRPy.GiRaFFE_NRPy_Metric_Face_Values as FCVAL
FCVAL.GiRaFFE_NRPy_FCVAL(os.path.join(Ccodesdir,subdir))
```

    Output C function calculate_metric_gfs() to file Start-to-Finish-UnitTests/Metric_Face_Values_UnitTest/calculate_metric_gfs.h

## Step 1.c: Set free parameters in the code \[Back to [top](#toc)\]
$$\label{free_parameters}$$

We also need to create the files that interact with NRPy's C parameter interface.

```python
# Step 3.d.i: Generate declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
# par.generate_Cparameters_Ccodes(os.path.join(out_dir))

# Step 3.d.ii: Set free_parameters.h
with open(os.path.join(Ccodesdir,"free_parameters.h"),"w") as file:
    file.write("""
// Override parameter defaults with values based on command line arguments and NGHOSTS.
params.Nxx0 = atoi(argv[1]);
params.Nxx1 = atoi(argv[2]);
params.Nxx2 = atoi(argv[3]);
params.Nxx_plus_2NGHOSTS0 = params.Nxx0 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS1 = params.Nxx1 + 2*NGHOSTS;
params.Nxx_plus_2NGHOSTS2 = params.Nxx2 + 2*NGHOSTS;
// Step 0d: Set up space and time coordinates
// Step 0d.i: Declare \Delta x^i=dxx{0,1,2} and invdxx{0,1,2}, as well as xxmin[3] and xxmax[3]:
const REAL xxmin[3] = {-1.0,-1.0,-1.0};
const REAL xxmax[3] = { 1.0, 1.0, 1.0};

params.dxx0 = (xxmax[0] - xxmin[0]) / ((REAL)params.Nxx0);
params.dxx1 = (xxmax[1] - xxmin[1]) / ((REAL)params.Nxx1);
params.dxx2 = (xxmax[2] - xxmin[2]) / ((REAL)params.Nxx2);
printf("dxx0,dxx1,dxx2 = %.5e,%.5e,%.5e\\n",params.dxx0,params.dxx1,params.dxx2);
params.invdx0 = 1.0 / params.dxx0;
params.invdx1 = 1.0 / params.dxx1;
params.invdx2 = 1.0 / params.dxx2;
\n""")

# Generates declare_Cparameters_struct.h, set_Cparameters_default.h, and set_Cparameters[-SIMD].h
par.generate_Cparameters_Ccodes(os.path.join(Ccodesdir))
```

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

`interpolate_metric_gfs_to_cell_faces()` may be found inside [GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py](../../edit/in_progress/GiRaFFE_NRPy/GiRaFFE_NRPy_Metric_Face_Values.py) [\[**tutorial**\]](Tutorial-GiRaFFE_NRPy-FCVAL.ipynb).

```python
%%writefile $Ccodesdir/FCVAL_unit_test.c
// These are common packages that we are likely to need.
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "string.h" // Needed for strncmp, etc.
#include "stdint.h" // Needed for Windows GCC 6.x compatibility
#include <time.h>   // Needed to set a random seed.

#define REAL double
#include "declare_Cparameters_struct.h"

const int NGHOSTS = 3;

REAL a,b,c,d,e,f,g,h,l,m,n,o,p,q,r,s,t,u;

// Standard NRPy+ memory access:
#define IDX4S(g,i,j,k) \
( (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (g) ) ) )

const int kronecker_delta[4][3] = { { 0,0,0 },
                                    { 1,0,0 },
                                    { 0,1,0 },
                                    { 0,0,1 } };

// Give gridfunctions their names:
#define GAMMADD00GF 0
#define GAMMADD01GF 1
#define GAMMADD02GF 2
#define GAMMADD11GF 3
#define GAMMADD12GF 4
#define GAMMADD22GF 5
#define BETAU0GF 6
#define BETAU1GF 7
#define BETAU2GF 8
#define ALPHAGF 9
#define GAMMA_FACEDD00GF 10
#define GAMMA_FACEDD01GF 11
#define GAMMA_FACEDD02GF 12
#define GAMMA_FACEDD11GF 13
#define GAMMA_FACEDD12GF 14
#define GAMMA_FACEDD22GF 15
#define BETA_FACEU0GF 16
#define BETA_FACEU1GF 17
#define BETA_FACEU2GF 18
#define ALPHA_FACEGF 19
#define GAMMAUU00GF 20
#define GAMMAUU01GF 21
#define GAMMAUU02GF 22
#define GAMMAUU11GF 23
#define GAMMAUU12GF 24
#define GAMMAUU22GF 25
#define GAMMA_FACEUU00GF 26
#define GAMMA_FACEUU01GF 27
#define GAMMA_FACEUU02GF 28
#define GAMMA_FACEUU11GF 29
#define GAMMA_FACEUU12GF 30
#define GAMMA_FACEUU22GF 31
#define NUM_AUXEVOL_GFS 32

#include "calculate_metric_gfs.h"
#include "FCVAL/interpolate_metric_gfs_to_cell_faces.h"

int main(int argc, const char *argv[]) {
    paramstruct params;
#include "set_Cparameters_default.h"

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

    // Step 0e: Set up cell-centered Cartesian coordinate grids
    REAL *xx[3];
    xx[0] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS0);
    xx[1] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS1);
    xx[2] = (REAL *)malloc(sizeof(REAL)*Nxx_plus_2NGHOSTS2);
    for(int j=0;j<Nxx_plus_2NGHOSTS0;j++) xx[0][j] = xxmin[0] + (j-NGHOSTS)*dxx0;
    for(int j=0;j<Nxx_plus_2NGHOSTS1;j++) xx[1][j] = xxmin[1] + (j-NGHOSTS)*dxx1;
    for(int j=0;j<Nxx_plus_2NGHOSTS2;j++) xx[2][j] = xxmin[2] + (j-NGHOSTS)*dxx2;

    //for(int i=0;i<Nxx_plus_2NGHOSTS0;i++) printf("xx[0][%d] = %.15e\n",i,xx[0][i]);

    // This is the array to which we'll write the NRPy+ variables.
    REAL *auxevol_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);
    // And another for exact data:
    REAL *auxevol_exact_gfs  = (REAL *)malloc(sizeof(REAL) * NUM_AUXEVOL_GFS * Nxx_plus_2NGHOSTS2 * Nxx_plus_2NGHOSTS1 * Nxx_plus_2NGHOSTS0);

    // Generate some random coefficients. Leave the random seed on its default for consistency between trials.
    a = (double)(rand()%20)/5.0;
    f = (double)(rand()%20)/5.0;
    m = (double)(rand()%20)/5.0;
    b = (double)(rand()%10-5)/100.0;
    c = (double)(rand()%10-5)/100.0;
    d = (double)(rand()%10-5)/100.0;
    g = (double)(rand()%10-5)/100.0;
    h = (double)(rand()%10-5)/100.0;
    l = (double)(rand()%10-5)/100.0;
    n = (double)(rand()%10-5)/100.0;
    o = (double)(rand()%10-5)/100.0;
    p = (double)(rand()%10-5)/100.0;

    // First, calculate the test data on our grid:
    calculate_metric_gfs(&params,xx,auxevol_gfs);
    // Now, run our function:
    int flux_dirn = 0;
    interpolate_metric_gfs_to_cell_faces(&params,auxevol_gfs,flux_dirn+1);
    // For comparison, shift the grid half a gridpoint, then calculate the test data again.
    REAL dxx[3] = {dxx0,dxx1,dxx2};
    REAL Nxxp2NG[3] = {Nxx_plus_2NGHOSTS0,Nxx_plus_2NGHOSTS1,Nxx_plus_2NGHOSTS2};
    for(int i=0;i<Nxxp2NG[flux_dirn];i++) {
        xx[flux_dirn][i] -= dxx[flux_dirn]/2.0;
    }
    calculate_metric_gfs(&params,xx,auxevol_exact_gfs);

    char filename[100];
    sprintf(filename,"out%d-numer.txt",Nxx0);
    FILE *out2D = fopen(filename, "w");
    for(int i2=0;i2<Nxx_plus_2NGHOSTS2;i2++) for(int i1=0;i1<Nxx_plus_2NGHOSTS1;i1++) for(int i0=0;i0<Nxx_plus_2NGHOSTS0;i0++) {
        // We print the difference between approximate and exact numbers.
        fprintf(out2D,"%.16e\t %e %e %e\n",
                auxevol_gfs[IDX4S(BETA_FACEU0GF,i0,i1,i2)]-auxevol_exact_gfs[IDX4S(BETAU0GF,i0,i1,i2)],
                xx[0][i0],xx[1][i1],xx[2][i2]
                );
    }
    fclose(out2D);
}
```

    Writing Start-to-Finish-UnitTests/Metric_Face_Values_UnitTest//FCVAL_unit_test.c

## Step 2.a: Compile and run the code \[Back to [top](#toc)\]
$$\label{compile_run}$$

Now that we have our file, we can compile it and run the executable.

```python
import time

print("Now compiling, should take ~2 seconds...\n")
start = time.time()
cmd.C_compile(os.path.join(Ccodesdir,"FCVAL_unit_test.c"), os.path.join(outdir,"FCVAL_unit_test"))
end = time.time()
print("Finished in "+str(end-start)+" seconds.\n\n")

# Change to output directory
os.chdir(outdir)

print("Now running...\n")
start = time.time()
cmd.Execute(os.path.join("FCVAL_unit_test"), "10 10 10","out.txt")
# To do a convergence test, we'll also need a second grid with twice the resolution.
cmd.Execute(os.path.join("FCVAL_unit_test"), "20 20 20","out.txt")
end = time.time()
print("Finished in "+str(end-start)+" seconds.\n\n")

```

    Now compiling, should take ~2 seconds...

    Compiling executable...
    (EXEC): Executing `gcc -Ofast -fopenmp -march=native -funroll-loops Start-to-Finish-UnitTests/Metric_Face_Values_UnitTest/FCVAL_unit_test.c -o Start-to-Finish-UnitTests/Metric_Face_Values_UnitTest/output/FCVAL_unit_test -lm`...
    (BENCH): Finished executing in 0.40785694122314453 seconds.
    Finished compilation.
    Finished in 0.41390180587768555 seconds.

    Now running...

    (EXEC): Executing `taskset -c 0,1,2,3,4,5 ./FCVAL_unit_test 10 10 10`...
    (BENCH): Finished executing in 0.2058565616607666 seconds.
    (EXEC): Executing `taskset -c 0,1,2,3,4,5 ./FCVAL_unit_test 20 20 20`...
    (BENCH): Finished executing in 0.20676016807556152 seconds.
    Finished in 0.42228078842163086 seconds.

# Step 3: Code validation: Verify that relative error in numerical solution converges to zero at the expected order \[Back to [top](#toc)\]
$$\label{convergence}$$

Here, we import the data at two resolutions and wrote to text files. This data consists of the absolute error of a metric gridfunction at each in the grid. We'll plot a portion of this data along the axis at the lower resolution along with that same data at the higher resolution scaled to demonstrate that this error converges to 0 at the expected rate. Since our algorithm uses a third-order polynomial, we expect fourth-order convergence here.

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

plt.rcParams.update({'font.size': 12})

Data1 = np.loadtxt("out10-numer.txt")
Data2 = np.loadtxt("out20-numer.txt")

def IDX4(i,j,k,Nxx_plus_2NGHOSTS0,Nxx_plus_2NGHOSTS1,Nxx_plus_2NGHOSTS2):
    return (i) + Nxx_plus_2NGHOSTS0 * ( (j) + Nxx_plus_2NGHOSTS1 * ( (k) + Nxx_plus_2NGHOSTS2 * (0) ) )

x1 = np.zeros(10)
a1 = np.zeros(10)
for i in range(10):
    x1[i] = Data1[IDX4(i+3,8,8,16,16,16),1]
    a1[i] = Data1[IDX4(i+3,8,8,16,16,16),0]
x2 = np.zeros(20)
a2 = np.zeros(20)
for i in range(20):
    x2[i] = Data2[IDX4(i+3,13,13,26,26,26),1]
    a2[i] = Data2[IDX4(i+3,13,13,26,26,26),0]

plt.figure()
a = plt.plot(x1,a1,'.',label="dx")
b = plt.plot(x2,a2*(2**4),label="dx/2, times (20/10)^4")
plt.legend()
plt.xlabel("x")
plt.ylabel("E_rel")
plt.title('Convergence test: betaU0 Rel. Error')
plt.show()

convergence_satisfactory = np.log2(a1/a2[0:-1:2])>3
if not convergence_satisfactory.all():
    sys.exit(1)
```

# Step 4: 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_UnitTest-GiRaFFE_NRPy-Metric_Face_Values.pdf](Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-Metric_Face_Values.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

# Change to NRPy directory
os.chdir("../../../")

cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-Metric_Face_Values",location_of_template_file=os.path.join(".."))
```

    Created Tutorial-Start_to_Finish_UnitTest-GiRaFFE_NRPy-
        Metric_Face_Values.tex, and compiled LaTeX file to PDF file Tutorial-
        Start_to_Finish_UnitTest-GiRaFFE_NRPy-Metric_Face_Values.pdf


```python
import discretize
from discretize import utils
import numpy as np
import scipy.sparse as sp
import sympy
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from scipy.constants import mu_0, inch, foot

from SimPEG.EM import TDEM
from SimPEG.EM.Static import DC
from SimPEG import Utils, Maps

from geoana.em import tdem

import casingSimulations

from pymatsolver import Pardiso

%matplotlib inline
```

```python
sigma_back = 1e-1
```

```python
csx, ncx, npadx = 1.5, 50, 30
ncy = 10
csz, ncz, npadz = 1.5, 1, 30

hx = Utils.meshTensor([(csx, ncx), (csx, npadx, 1.3)])
hy = 2*np.pi / ncy * np.ones(ncy)
hz = Utils.meshTensor([(csz, npadz, -1.3), (csz, ncz), (csz, npadz, 1.3)])
```

```python
mesh2D = discretize.CylMesh([hx, 1., hz], x0='00C')
mesh2D.plotGrid()
```

```python
src_a = np.r_[csx/2., 0., -csz*7/2.]
src_b = np.r_[csx/2., 0., csz*7/2.]
```

```python
fig, ax = plt.subplots(1, 1)
mesh2D.plotGrid(ax=ax)

ax.plot(src_a[0], src_a[2], 'ro')
ax.plot(src_b[0], src_b[2], 'rs')

ax.set_xlim([-1., 50.])
ax.set_ylim([-10., 10.])
```

## DC problem

```python
srcDC = DC.Src.Dipole([], src_a, src_b)
prob2DDC = DC.Problem3D_CC(mesh2D, sigmaMap=Maps.SurjectFull(mesh2D), bc_type='Dirichlet', Solver=Pardiso)
survey2DDC = DC.Survey([srcDC])
prob2DDC.pair(survey2DDC)
```

    Homogeneous Dirichlet is the natural BC for this CC discretization.

```python
%time
fields2DDC = prob2DDC.fields(np.r_[sigma_back])
```

    CPU times: user 4 µs, sys: 2 µs, total: 6 µs
    Wall time: 8.11 µs

## TDEM problem

```python
dg_x = np.zeros(mesh2D.vnF[0])
dg_y = np.zeros(mesh2D.vnF[1])
dg_z = np.zeros(mesh2D.vnF[2])

wire_in_boreholex = (
    (mesh2D.gridFz[:, 0] < src_a[0] + mesh2D.hx.min()/2.) &
    (mesh2D.gridFz[:, 0] > src_a[0] - mesh2D.hx.min()/2.)
)
wire_in_boreholez = (
    (mesh2D.gridFz[:, 2] >= src_a[2] - 0.5*mesh2D.hz.min()) &
    (mesh2D.gridFz[:, 2] < src_b[2] + 0.5*mesh2D.hz.min())
)

wire_in_borehole = wire_in_boreholex & wire_in_boreholez

dg_z[wire_in_borehole] = 1.

# assemble the source (downhole grounded primary)
dg = np.hstack([dg_x, dg_y, dg_z])
src = TDEM.Src.RawVec_Grounded([], s_e=dg/mesh2D.area)
# src = TDEM.Src.RawVec_Grounded([], s_e=mesh2D.getFaceInnerProduct(invMat=True)*dg)
```

```python
se0 = dg/mesh2D.area
se1 = mesh2D.getFaceInnerProduct(invMat=True)*dg
print(se0[se0.nonzero()])
print(se1[se1.nonzero()])
```

    [ 0.14147106  0.14147106  0.14147106  0.14147106  0.14147106  0.14147106]
    [ 0.04852793  0.06308631  0.08201221  0.08201221  0.06308631  0.04852793]

```python
# solve the problem at these times
timeSteps = [(1e-6, 10), (1e-5, 20), (1e-4, 20), (1e-3, 20), (1e-2, 20), (1e-1, 30)]

# define a problem - the statement of which discrete pde system we want to solve
prob = TDEM.Problem3D_j(mesh2D, timeSteps = timeSteps, sigmaMap=Maps.SurjectFull(mesh2D))
prob.solver = Pardiso

survey = TDEM.Survey([src])

# tell the problem and survey about each other - so the RHS can be constructed for
# the problem and the resulting fields and fluxes can be sampled by the receiver.
prob.pair(survey)

print('The maximum time is {:1.1e}'.format(prob.times[-1]))
```

    The maximum time is 3.2e+00

```python
%%time
fields = prob.fields(np.r_[sigma_back])
```

    CPU times: user 17.2 s, sys: 608 ms, total: 17.8 s
    Wall time: 10.1 s

```python
# log-scale the colorbar
from matplotlib.colors import LogNorm
import ipywidgets
```

```python
def plot_j(
    time_ind=0 # which frequency would you like to look at?
):
    fig, ax = plt.subplots(1,1, figsize=(6,5))

    # Plot the magnetic flux
    field_to_plot = fields[src, 'j', time_ind]
    max_field = np.abs(field_to_plot).max() #use to set colorbar limits
    cb_range = 5e2  # dynamic range of colorbar

    cb = plt.colorbar(mesh2D.plotImage(
        field_to_plot,
        vType='F', view='vec',
        range_x=[-100., 100.], range_y=[-100., 100.],
        pcolorOpts={
                'norm': LogNorm(),
                'cmap': plt.get_cmap('viridis')
            },
        streamOpts={'color': 'w'}, mirror=True, ax=ax,
        clim=[max_field/cb_range, max_field]
    )[0], ax=ax)

    cb.set_label('j')

    # plot the surface of the earth
    ax.plot(np.r_[-200, 200], np.r_[0., 0.], 'w:')

    # give it a title
    ax.set_title(
        'j, {time:10.2e} s'.format(
            time=prob.times[time_ind]
        )
    )
    plt.show()
    return ax

ipywidgets.interact(
    plot_j,
    time_ind=ipywidgets.IntSlider(min=2, max=len(prob.timeSteps)-1, value=1)
)
```

```python
wirez = mesh2D.gridFz[wire_in_borehole, 2]
loc = wirez
```

```python
ana_dipole = tdem.ElectricDipole(sigma=sigma_back, time=prob.times[1], orientation='Z', length=wirez.max()-wirez.min()+np.diff(wirez[-2:]))
```

```python
ana_dipole.sigma
```

    0.1

```python
def plot_j(
    time_ind=0 # which frequency would you like to look at?
):
    fig, ax = plt.subplots(1,3, figsize=(15,5))

    # Numeric solution
    numeric = fields[src, 'j', time_ind]

    # analytic solution
    ana_dipole.time = prob.times[time_ind]

    j_x_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFx)[:,0], 2)
    j_z_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFz)[:,2], 2)
    analytic = np.vstack([j_x_ana, j_z_ana])

    diff = numeric - analytic

    # plotting params
    max_field = np.abs(numeric).max() #use to set colorbar limits
    cb_range = 1e5  # dynamic range of colorbar
    cb_max = max_field

    titles = ['num', 'ana', 'diff']
    field_to_plot = [numeric, analytic, diff]
    for i, a in enumerate(ax):
        cb = plt.colorbar(mesh2D.plotImage(
            field_to_plot[i],
            vType='F', view='vec',
            range_x=[-100., 100.], range_y=[-100., 100.],
            pcolorOpts={
                    'norm': LogNorm(),
                    'cmap': plt.get_cmap('viridis')
                },
            streamOpts={'color': 'w'}, mirror=True, ax=a,
            clim=[cb_max/cb_range, cb_max]
        )[0], ax=a)

        cb.set_label('j')

        # plot the surface of the earth
        a.plot(np.r_[-200, 200], np.r_[0., 0.], 'w:')

        # give it a title
        a.set_title(
            '{title} j, {time:10.2e} s'.format(
                title=titles[i],
                time=prob.times[time_ind]
            )
        )

    plt.tight_layout()
    plt.show()
    return ax

ipywidgets.interact(
    plot_j,
    time_ind=ipywidgets.IntSlider(min=2, max=len(prob.timeSteps)-1, value=1)
)
```

```python
def plot_j(
    time=1e-1 # which frequency would you like to look at?
):
    fig, ax = plt.subplots(1,1, figsize=(5,5))

    # analytic solution
    ana_dipole.time = time

    j_x_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFx)[:,0], 2)
    j_z_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFz)[:,2], 2)
    analytic = np.vstack([j_x_ana, j_z_ana])

    diff = numeric - analytic

    # plotting params
    max_field = np.abs(numeric).max() #use to set colorbar limits
    cb_range = 1e5  # dynamic range of colorbar
    cb_max = 0.1*max_field

    titles = ['ana']
    field_to_plot = [analytic]
    a = ax
    cb = plt.colorbar(mesh2D.plotImage(
        field_to_plot[0],
        vType='F', view='vec',
        range_x=[-100., 100.], range_y=[-100., 100.],
        pcolorOpts={
                'norm': LogNorm(),
                'cmap': plt.get_cmap('viridis')
            },
        streamOpts={'color': 'w'}, mirror=True, ax=a,
        clim=[cb_max/cb_range, cb_max]
    )[0], ax=a)

    cb.set_label('j')

    # plot the surface of the earth
    a.plot(np.r_[-200, 200], np.r_[0., 0.], 'w:')

    # give it a title
    a.set_title(
        '{title} j, {time:10.2e} s'.format(
            title=titles,
            time=time
        )
    )

    plt.tight_layout()
    plt.show()
    return ax

ipywidgets.interact(
    plot_j,
    time=ipywidgets.IntSlider(min=1e-1, max=500, value=1)
)
```

```python
time_ind=36
numeric = fields[src, 'j', time_ind]

# analytic solution
ana_dipole.time = prob.times[time_ind]

j_x_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFx)[:,0], 2)
j_z_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFz)[:,2], 2)
analytic = np.vstack([j_x_ana, j_z_ana])

diff = numeric - analytic

```

```python
plt.plot(np.abs(numeric))
plt.plot(np.abs(analytic))
plt.plot(np.abs(diff))

plt.legend(['num', 'ana', 'diff'])
```

```python
src = survey.srcList[0]
```

```python
(prob.mesh.faceDiv * src._s_e).nonzero()
```

    (array([2160, 2240, 2320, 2480, 2560, 2640]),)

```python
src.getRHSdc(prob).nonzero()
```

    (array([4080, 4720]),)

# 3D solution

```python
ny = 2
hy = np.ones(ny)*2*np.pi / ny
mesh3D = discretize.CylMesh([mesh2D.hx, hy, mesh2D.hz], mesh2D.x0)
```

```python
mesh3D.plotGrid()
```

```python
dg_x = np.zeros(mesh3D.vnF[0])
dg_y = np.zeros(mesh3D.vnF[1])
dg_z = np.zeros(mesh3D.vnF[2])

wire_in_boreholex = (
    (mesh3D.gridFz[:, 0] < src_a[0] + mesh3D.hx.min()/2.) &
    (mesh3D.gridFz[:, 0] > src_a[0] - mesh3D.hx.min()/2.)
)
wire_in_boreholez = (
    (mesh3D.gridFz[:, 2] >= src_a[2] - 0.5*mesh3D.hz.min()) &
    (mesh3D.gridFz[:, 2] < src_b[2] + 1.5*mesh3D.hz.min())
)

wire_in_borehole = wire_in_boreholex & wire_in_boreholez

dg_z[wire_in_borehole] = 1.

# assemble the source (downhole grounded primary)
dg = np.hstack([dg_x, dg_y, dg_z])
src3D = TDEM.Src.RawVec_Grounded([], s_e=dg/mesh3D.area)
```

```python
# define a problem - the statement of which discrete pde system we want to solve
prob3D = TDEM.Problem3D_j(mesh3D, timeSteps = timeSteps, sigmaMap=Maps.SurjectFull(mesh3D))
prob3D.solver = Pardiso

survey3D = TDEM.Survey([src3D])

# tell the problem and survey about each other - so the RHS can be constructed for
# the problem and the resulting fields and fluxes can be sampled by the receiver.
prob3D.pair(survey3D)

print('The maximum time is {:1.1e}'.format(prob.times[-1]))
```

    The maximum time is 1.2e-03

```python
%%time
fields3D = prob3D.fields(np.r_[sigma_back])
```

```python
def plot_j(
    time_ind=0 # which frequency would you like to look at?
):
    fig, ax = plt.subplots(1,3, figsize=(15,5))

    # Numeric solution
    numeric = casingSimulations.face3DthetaSlice(
        mesh3D, fields3D[:, 'j', time_ind], theta_ind=0
    )

#     numeric = fields[src, 'j', time_ind]

    # analytic solution
    ana_dipole.time = prob.times[time_ind]

    j_x_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFx)[:,0], 2)
    j_z_ana = utils.mkvc(ana_dipole.current_density(mesh2D.gridFz)[:,2], 2)
    analytic = np.vstack([j_x_ana, j_z_ana])*mesh3D.vnC[1]  # since we normalize the current going through the faces to be 1

    diff = numeric - analytic

    # plotting params
    max_field = np.abs(numeric).max() #use to set colorbar limits
    cb_range = 1e4  # dynamic range of colorbar

    titles = ['num', 'ana', 'diff']
    field_to_plot = [numeric, analytic, diff]
    for i, a in enumerate(ax):
        cb = plt.colorbar(mesh2D.plotImage(
            field_to_plot[i],
            vType='F', view='vec',
            range_x=[-100., 100.], range_y=[-100., 100.],
            pcolorOpts={
                    'norm': LogNorm(),
                    'cmap': plt.get_cmap('viridis')
                },
            streamOpts={'color': 'w'}, mirror=True, ax=a,
            clim=[max_field/cb_range, max_field]
        )[0], ax=a)

        cb.set_label('j')

        # plot the surface of the earth
        a.plot(np.r_[-200, 200], np.r_[0., 0.], 'w:')

        # give it a title
        a.set_title(
            '{title} j, {time:10.2e} s'.format(
                title=titles[i],
                time=prob.times[time_ind]
            )
        )

    plt.tight_layout()
    plt.show()
    return ax

ipywidgets.interact(
    plot_j,
    time_ind=ipywidgets.IntSlider(min=2, max=len(prob.timeSteps)-1, value=1)
)
```

```python

```


```python
import igraph as ig
import numpy as np
from sympy.solvers import nsolve
from sympy import *
```

```python
from scipy.stats import norm
```

```python
from __future__ import division
```

```python
import powerlaw as pl
%matplotlib inline
import matplotlib.pyplot as plt
```

```python
from sympy.solvers import nsolve
from sympy import *
```

```python
from scipy import special
from scipy import stats
import pandas as pd
```

```python
import scipy as sp
from scipy import stats
```

```python
from scipy.spatial import distance
def dist(v1,v2):
    dist = distance.euclidean((v1[0],v1[1]),(v2[0],v2[1]))
    return dist
```

```python
aggragate=pd.read_csv('final_table_galicia.csv')
```

```python
x_coords = (aggragate['X'].values).reshape(550,1)
```

```python
y_coords = (aggragate['Y'].values).reshape(550,1)
```

```python
population = ((aggragate['population_y'].values).reshape(550,1)).tolist()
```

```python
coords = np.concatenate([x_coords,y_coords],axis=1)
```

```python
n = len(x_coords)
alldist = []
A = np.zeros((n,n))
A_binom = np.zeros((n,n))
count=0
for i in range(0,n):
    for j in range(i+1,n):
        m = (population[i][0]*population[j][0])/(dist(coords[i],coords[j]))**2
        A[i][j] = m
        A[j][i] = m

```

```python
Dt = np.sum(A,axis=1).tolist()
DD = np.diag(Dt)
L = A - DD
```

```python

```

```python
q = np.linalg.eigvals(L) ### eigenvalue ditribution
qsorted = sorted(q,reverse=True)
q = qsorted
q = np.real(q)
qq = q
qq[0] = 0.
```

```python
#coefficients in lotka volterra model

c = 0.5
ax = 1
ay = 1
sx = 1
sy = 1
```

```python
d1 = 0.01
d2 = 0.01
```

```python
x0 = ay*sx*(c*sy +ax)/(ax*ay + sx*sy*c**2 )
```

```python
y0 = ax*sy*(-c*sx +ay)/(ax*ay + sx*sy*c**2 )
```

```python
x, y = symbols('x y ')
```

```python
f_x = diff(x*(1-x) + c*x*y ,x)
f_y = diff( x*(1-x) + c*x*y,y)
g_x = diff(y*(1-y) - c*x*y,x)
g_y = diff(y*(1-y) - c*x*y,y)
```

```python
fx = np.array([(f_x.subs(x,x0)).subs(y,y0)],dtype='float')[0]
fy = np.array([(f_y.subs(x,x0)).subs(y,y0)],dtype='float')[0]
gx = np.array([(g_x.subs(x,x0)).subs(y,y0)],dtype='float')[0]
gy = np.array([(g_y.subs(x,x0)).subs(y,y0)],dtype='float')[0]
```

```python
J = [[fx,fy],[gx,gy]]### Jacobian matrix
```

```python
d1 = 0.01
d2 = 0.01
a12 = 2.1
a21 = 0.0
```

```python
D = [[d1 + a12*y0,a12*x0],[a21*y0, d2 + a21*x0]]
J = [[fx,fy],[gx,gy]]
trj = np.trace(J)
trd = np.trace(D)
detj = np.linalg.det(J)
detd = np.linalg.det(D)
```

```python
bb=(fx*D[1][1] + gy*D[0][0] - D[1][0]*fy - gx*D[0][1])
aa=detd
cc = detj
print((-bb/(2*aa)))
lambda1 = ((-bb - np.sqrt(bb**2 - 4*aa*cc))/(2*aa))
lambda2 = ((-bb + np.sqrt(bb**2 - 4*aa*cc))/(2*aa))
```

```python
lambda1
```

    -11.999999999999975

```python
lambda2
```

    -5.882352941176481

```python
bth = []
cth=[]
xq = np.linspace(-240,0,700)
for i in range(len(xq)):
    bth.append(-(trj + trd*xq[i]))
    cth.append(detj + (fx*D[1][1] + gy*D[0][0] - D[1][0]*fy - gx*D[0][1])*xq[i] + detd*xq[i]**2)
```

```python
solth = []
for j in range(len(xq)):
    if (bth[j]*bth[j] - 4*cth[j]) > 0:
        solth.append((-bth[j] + np.sqrt(bth[j]*bth[j] - 4*cth[j]))/2)
    if (bth[j]*bth[j] - 4*cth[j]) < 0:
        solth.append(-bth[j]/2)
```

```python
b1 = []
c1=[]
for i in range(len(qq)):
    b1.append(-(trj + trd*qq[i]))
    c1.append(detj + (fx*D[1][1] + gy*D[0][0] - D[1][0]*fy - gx*D[0][1])*q[i] + detd*q[i]**2)
```

```python
sol = []
for j in range(len(q)):
    if (b1[j]*b1[j] - 4*c1[j]) > 0:
        sol.append((-b1[j] + np.sqrt(b1[j]*b1[j] - 4*c1[j]))/2)
    if (b1[j]*b1[j] - 4*c1[j]) < 0:
        sol.append(-b1[j]/2)
```

```python
f = plt.figure(figsize=[2*6.4, 2*4.8])
font0 = FontProperties()
font1 = font0.copy()
font1.set_weight('bold')
font1.set_size('large')
font1.set_family('sans-serif')

ax1  = plt.subplot(2,2,1)
plt.text(0.05, 0.93, '(a)',
        verticalalignment='center', horizontalalignment='center',color='black',fontproperties=font1,fontsize=20,transform=ax1.transAxes)

plt.plot(xq,solth,'--',color='brown')
#plt.plot(qq,sol,'.',color = '#6495ED',label = 'Complete Graph')
plt.plot(qq,sol,'.',color = 'black')
plt.plot(lambda1,0,'o',color = 'red')
plt.plot(lambda2,0,'o',color = 'red')
plt.annotate(r'$\Lambda_{\alpha_1}^G$', xy=(lambda1, 0), xytext=(-17, -0.25),
            arrowprops=dict(arrowstyle="->"),fontsize=15)
plt.annotate(r'$\Lambda_{\alpha_2}^G$', xy=(lambda2, 0), xytext=(-6, -0.25),
            arrowprops=dict(arrowstyle="->"),fontsize=15)
plt.xlabel(r'$\Lambda_{\alpha}$',fontsize=15)
plt.ylabel(r'$Re (\lambda_{\alpha})$',fontsize=15)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.xlim(-40,5)
plt.ylim(-0.8,0.2)

ax2  = plt.subplot(2,2,2)
plt.text(0.05, 0.93, '(b)',
        verticalalignment='center', horizontalalignment='center',color='black',fontproperties=font1,fontsize=20,transform=ax2.transAxes)

plt.plot(xq_c,solth_c,'--',color='brown')
#plt.plot(qq,sol,'.',color = '#6495ED',label = 'Complete Graph')
plt.plot(qq_c,sol_c,'.',color = 'black')
plt.plot(lambda1_c,0,'o',color = 'red')
plt.plot(lambda2_c,0,'o',color = 'red')
plt.annotate(r'$\Lambda_{\alpha_1}^C$', xy=(lambda1_c, 0), xytext=(-2, -0.25),
            arrowprops=dict(arrowstyle="->"),fontsize=15)
plt.annotate(r'$\Lambda_{\alpha_2}^C$', xy=(lambda2_c, 0), xytext=(-1, -0.25),
            arrowprops=dict(arrowstyle="->"),fontsize=15)
plt.xlabel(r'$\Lambda_{\alpha}$',fontsize=15)
plt.ylabel(r'$Re (\lambda_{\alpha})$',fontsize=15)
plt.tick_params(axis='both', which='major', labelsize=15)
plt.xlim(-6,1)
plt.ylim(-0.8,0.2)

plt.tight_layout()

#plt.savefig('esi_eig_dist.pdf',bbox_inches='tight')
```

```python

```

```python
unew2y = []
stdnew2y = []
for i in range(len(degree)):
    unew2y.append(mean2y[degree[i]][0])
    stdnew2y.append(std2y[degree[i]][0])
```

```python

```

```python
plt.errorbar(np.linspace(0,549,550),np.array(unew2y),yerr=np.array(stdnew2y), fmt='.',elinewidth=0.4,ecolor='grey',color='black')
plt.xlabel(r'node index $i$',fontsize = 12)
plt.ylabel(r'$\langle v_i \rangle$',fontsize = 12)
#plt.ylim(0,1.4)
```


# Optimization of a State-to-State Transfer in a Lambda System in the RWA

```python
# NBVAL_IGNORE_OUTPUT
%load_ext watermark
import os
import numpy as np
import scipy
import matplotlib
import matplotlib.pylab as plt
import krotov
import qutip
from qutip import Qobj
%watermark -v --iversions
```

    Python implementation: CPython
    Python version       : 3.7.6
    IPython version      : 7.19.0

    krotov    : 1.2.1
    matplotlib: 3.3.3
    qutip     : 4.5.0
    numpy     : 1.17.2
    scipy     : 1.3.1

$\newcommand{tr}[0]{\operatorname{tr}}
\newcommand{diag}[0]{\operatorname{diag}}
\newcommand{abs}[0]{\operatorname{abs}}
\newcommand{pop}[0]{\operatorname{pop}}
\newcommand{aux}[0]{\text{aux}}
\newcommand{opt}[0]{\text{opt}}
\newcommand{tgt}[0]{\text{tgt}}
\newcommand{init}[0]{\text{init}}
\newcommand{lab}[0]{\text{lab}}
\newcommand{rwa}[0]{\text{rwa}}
\newcommand{bra}[1]{\langle#1\vert}
\newcommand{ket}[1]{\vert#1\rangle}
\newcommand{Bra}[1]{\left\langle#1\right\vert}
\newcommand{Ket}[1]{\left\vert#1\right\rangle}
\newcommand{Braket}[2]{\left\langle #1\vphantom{#2}\mid{#2}\vphantom{#1}\right\rangle}
\newcommand{ketbra}[2]{\vert#1\rangle\!\langle#2\vert}
\newcommand{op}[1]{\hat{#1}}
\newcommand{Op}[1]{\hat{#1}}
\newcommand{dd}[0]{\,\text{d}}
\newcommand{Liouville}[0]{\mathcal{L}}
\newcommand{DynMap}[0]{\mathcal{E}}
\newcommand{identity}[0]{\mathbf{1}}
\newcommand{Norm}[1]{\lVert#1\rVert}
\newcommand{Abs}[1]{\left\vert#1\right\vert}
\newcommand{avg}[1]{\langle#1\rangle}
\newcommand{Avg}[1]{\left\langle#1\right\rangle}
\newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
\newcommand{Re}[0]{\operatorname{Re}}
\newcommand{Im}[0]{\operatorname{Im}}$

This example is illustrates the use of complex-valued control fields. This is
accomplished by rewriting the Hamiltonian as the sum of two independent
controls (real and imaginary parts). We consider a 3-level system in a
$\Lambda$ configuration, and seek control pulses that implement a
(phase-sensitive) state-to-state transition $\ket{1} \rightarrow \ket{3}$.

## The rotating wave Hamiltonian

The system consists of three levels $\ket{1}$, $\ket{2}$ and $\ket{3}$ with
energy levels $E_{1}, E_{2}$ and $E_{3}$ which interact with a pair of laser
pulses $\epsilon_{P}(t)$ ("pump laser") and $\epsilon_{S}(t)$ ("Stokes laser"),
respectively, see Chapter 15.4.2 in ["Introduction to Quantum Mechanics: A
Time-Dependent Perspective" by David Tannor][Tannor] for details.

[Tannor]: http://www.weizmann.ac.il/chemphys/tannor/Book/

In the lab frame, the Hamiltonian reads

$$
\Op{H}_{\text{lab}} = \begin{pmatrix}
    E_1                     & -\mu_{12} \epsilon_P(t) & 0                          \\
    -\mu_{12} \epsilon_P(t) & E_2                     & - \mu_{23} \epsilon_S(t) \\
    0                       & -\mu_{23} \epsilon_S(t) &  E_2
\end{pmatrix}\,.
$$

with the dipole values $\mu_{12}$, $\mu_{23}$ describing the coupling to the
(real-valued) control fields $\epsilon_P(t)$, $\epsilon_S(t)$. The "rotating
frame" is defined as

$$\ket{\Psi_{\text{rot}}} = \Op{U}_0^\dagger \ket{\Psi_{\text{lab}}}$$

with the transformation

$$\op{U}_{0} = \ketbra{1}{1}
e^{-i\left(E_{2} - \omega_{P} \right)t} + \ketbra{2}{2} e^{-iE_{2}t} +
\ketbra{3}{3} e^{-i\left(E_{2}-\omega_{S}\right)t}\,,$$

where $\omega_{P}$ and $\omega_{S}$ are the two central frequencies defining
the rotating frame.

The condition of having to fulfill the Schrödinger equation in the rotating
frame implies a rotating frame Hamiltonian defined as

$$\op{H}_{\text{rot}}  = \op{U}_{0}^{\dagger} \op{H}_{\text{lab}} \op{U}_{0}  - i \op{U}_{0}^{\dagger} \dot{\op{U}}_{0}\,.$$

Note that most textbooks use $\Op{U}$ instead of $\Op{U}^\dagger$, and thus the
adjoint of the above equation to define the rotating frame transformation, but
we follow the example of Tannor's book here.

The rotating frame Hamiltonian reads
$$
\Op{H}_\text{rot} = \begin{pmatrix}
    E_1 + \omega_P - E_2                      & -\mu_{12} \epsilon_P(t) e^{-i \omega_P t} & 0                          \\
    -\mu_{12} \epsilon_P(t) e^{+i \omega_P t} &  0                                        & - \mu_{23} \epsilon_S(t) e^{-i \omega_S t}\\
    0                                         & -\mu_{23} \epsilon_S(t) e^{+i \omega_S t} &  E3 + \omega_S -E_2
\end{pmatrix}\,.
$$

We can now write the fields as

$$
\begin{split}
\mu_{12} \epsilon_{P}(t)
    &= \Omega_{P}^{(1)}(t) \cos{(\omega_P t)} - \Omega_{P}^{(2)}(t) \sin{(\omega_P t)} \\
    &= \Omega_{P}^{(1)}(t) \left( e^{i \omega_P t} + e^{-i \omega_P t}\right)
        + i \Omega_{P}^{(2)}(t) \left( e^{i \omega_P t} - e^{-i \omega_P t} \right) \,,
\end{split}
$$

and similarly for $\epsilon_{S}(t)$, where we have split each field into two
arbitrary (real-valued) auxiliary fields $\Omega_{P}^{(1)}(t),
\Omega_{P}^{(2)}(t)$, and $\Omega_{S}^{(1)}(t), \Omega_{S}^{(2)}(t)$. This
rewriting is suggestive of controls being spectrally centered around $\omega_P$
and $\omega_S$, respectively, in which case any oscillations in
$\Omega_{P,S}^{(1,2)}(t)$ are on a much slower time scale than $\omega_{P, S}$.
Mathematically, however, *any* control fields can written in the above form.
Thus, we have not placed any restriction on the controls at this time.

Plugging this into $\Op{H}_\text{rot}$ and invoking the rotating wave
approximation that neglects all fast oscillating terms $\propto e^{\pm i 2
\omega_{P,S} t}$, we find

$$
\Op{H}_\text{RWA} = \begin{pmatrix}
    \Delta_P                    & -\frac{1}{2} \Omega_P(t)   & 0                          \\
    -\frac{1}{2} \Omega_P^*(t)  &  0                         &  -\frac{1}{2} \Omega_S(t)  \\
    0                           & -\frac{1}{2} \Omega_S^*(t) & \Delta_S
\end{pmatrix}\,,
$$

with the detunings $\Delta_P \equiv E_1 + \omega_P - E_2$, $\Delta_S  \equiv E3
+ \omega_S -E_2$ and the complex-valued control fields $\Omega_P(t) \equiv
\Omega_{P}^{(1)}(t) + i \Omega_{P}^{(2)}(t)$ and $\Omega_S(t) \equiv
\Omega_{S}^{(1)}(t) + i \Omega_{S}^{(2)}(t)$, illustrated in the following
diagram:

Most textbooks (including Tannor's) only allow control fields of the form
$\epsilon_{P,S}(t) \propto \Omega_{P,S}(t) \cos{(\omega_{P,S} t)}$ with the
pulse envelopes $\Omega_{P,S}(t) \in \mathbb{R}^+$. This will result in the
same $\Op{H}_\text{RWA}$ as above, but with the positive real-valued envelopes
instead of the complex-valued $\Omega_{P,S}(t)$. However, this restriction is
unnecessary: complex-valued control fields in the RWA are more general and
entirely physical, with the relation to the real-valued field in the lab
frame as defined above. The spectra of the optimized pulses are free to deviate
from the frequencies of the rotating frame, limited only by the numerical
resolution of the time grid and the RWA.

The `krotov` package requires that all control pulses are real-valued.
Therefore, the real and imaginary parts of $\Omega_{P}$ and $\Omega_{S}$ are
treated as independent Hamiltonians, and we write

$$
\Op{H}_\text{RWA}
    = \Op{H_0}
    + \Omega_{P}^{(1)}(t) \Op{H}_{P,\text{re}}
    + \Omega_{P}^{(2)}(t) \Op{H}_{P,\text{im}}
    + \Omega_{S}^{(1)}(t) \Op{H}_{S,\text{re}}
    + \Omega_{S}^{(2)}(t) \Op{H}_{S,\text{im}}
$$

for the purpose of the optimization, with

$$
\begin{align}
\Op{H_0} &= \Delta_P \ketbra{1}{1} + \Delta_S \ketbra{3}{3}\,, \\
\Op{H}_{P,\text{re}} &= -\frac{1}{2} \left(\ketbra{1}{2} + \ketbra{2}{1}\right)\,, \\
\Op{H}_{P,\text{im}} &= -\frac{i}{2} \left(\ketbra{1}{2} - \ketbra{2}{1}\right)\,, \\
\Op{H}_{S,\text{re}} &= -\frac{1}{2} \left(\ketbra{2}{3} + \ketbra{3}{2}\right)\,, \\
\Op{H}_{S,\text{im}} &= -\frac{i}{2} \left(\ketbra{2}{3} - \ketbra{3}{2}\right)\,.
\end{align}
$$

## Guess controls

We choose the initial guess for the four control fields based on the intuition
of the "stimulated Raman adiabatic passage" (STIRAP) scheme. STIRAP allows to
transfer the population in $\ket{1}$ $\ket{3}$ without having to pass through
$\ket{2}$; it requires the Stokes-pulse to precede but overlap the pump-pulse.

Here, we leave it up to Krotov's method to find appropriate pulses for a
STIRAP-like transfer (without requiring that the $\ket{2}$ level remains
unpopulated).  We start from a low intensity real-valued $\Omega_S(t)$ pulse
with a Blackman shape, followed by an overlapping real-valued $\Omega_P(t)$ of
the same shape. The entire scheme is in the time interval [0, 5].

```python
def Omega_P1(t, args):
    """Guess for the real part of the pump pulse"""
    Ω0 = 5.0
    return Ω0 * krotov.shapes.blackman(t, t_start=2.0, t_stop=5.0)

def Omega_P2(t, args):
    """Guess for the imaginary part of the pump pulse"""
    return 0.0

def Omega_S1(t, args):
    """Guess for the real part of the Stokes pulse"""
    Ω0 = 5.0
    return Ω0 * krotov.shapes.blackman(t, t_start=0.0, t_stop=3.0)

def Omega_S2(t, args):
    """Guess for the imaginary part of the Stokes pulse"""
    return 0.0
```

We can now instantiate the Hamiltonian including these guess controls:

```python
def hamiltonian(E1=0.0, E2=10.0, E3=5.0, omega_P=9.5, omega_S=4.5):
    """Lambda-system Hamiltonian in the RWA"""

    # detunings
    ΔP = E1 + omega_P - E2
    ΔS = E3 + omega_S - E2

    H0 = Qobj([[ΔP, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, ΔS]])

    HP_re = -0.5 * Qobj([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
    HP_im = -0.5 * Qobj([[0.0, 1.0j, 0.0], [-1.0j, 0.0, 0.0], [0.0, 0.0, 0.0]])

    HS_re = -0.5 * Qobj([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
    HS_im = -0.5 * Qobj([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0j], [0.0, -1.0j, 0.0]])

    return [
        H0,
        [HP_re, Omega_P1],
        [HP_im, Omega_P2],
        [HS_re, Omega_S1],
        [HS_im, Omega_S2],
    ]

```

```python
H = hamiltonian()
```

## Target state in the rotating frame

The basis states of the $\Lambda$-system are defined as

```python
ket1 = qutip.Qobj(np.array([1.0, 0.0, 0.0]))
ket2 = qutip.Qobj(np.array([0.0, 1.0, 0.0]))
ket3 = qutip.Qobj(np.array([0.0, 0.0, 1.0]))
```

We would like to implement a phase-sensitive transition $\ket{1} \rightarrow
\ket{3}$ *in the lab frame*. Since we are defining the dynamics in the RWA,
this means we have to adjust the target state to be in the rotating frame as
well (the initial state at $t=0$ is not affected by the RWA).

As defined earlier, the states in the rotating frame are obtained from the
states in the lab frame by the transformation $\ket{\Psi_{\text{rot}}} =
\Op{U}_0^\dagger \ket{\Psi_{\text{lab}}}$. In our case, this means that we get
$\ket{3}$ with and additional phase factor:

```python
def rwa_target_state(ket3, E2=10.0, omega_S=4.5, T=5):
    return np.exp(1j * (E2 - omega_S) * T) * ket3
```

```python
psi_target = rwa_target_state(ket3)
```

We can now instantiate the control objective:

```python
objective = krotov.Objective(initial_state=ket1, target=psi_target, H=H)
objective
```

    Objective[|Ψ₀(3)⟩ to |Ψ₁(3)⟩ via [H₀[3,3], [H₁[3,3], u₁(t)], [H₂[3,3], u₂(t)], [H₃[3,3], u₃(t)], [H₄[3,3], u₄(t)]]]

## Simulate dynamics under the guess field

We use a time grid with 500 steps between $t=0$ and $T=5$:

```python
tlist = np.linspace(0, 5, 500)
```

Before propagating, we visually verify the guess pulses we defined earlier:

```python
def plot_pulse(pulse, tlist, label):
    fig, ax = plt.subplots()
    if callable(pulse):
        pulse = np.array([pulse(t, args=None) for t in tlist])
    ax.plot(tlist, pulse)
    ax.set_xlabel('time')
    ax.set_ylabel('%s pulse amplitude' % label)
    plt.show(fig)
```

```python
plot_pulse(H[1][1], tlist, 'Ωₚ')
plot_pulse(H[3][1], tlist, 'Ωₛ')
```

The imaginary parts are zero:

```python
assert np.all([H[2][1](t, None) == 0 for t in tlist])
assert np.all([H[4][1](t, None) == 0 for t in tlist])
```

We introduce projectors $\op{P}_{i} =
\ketbra{i}{i}$ for each of the three energy levels, allowing use to plot the population dynamics:

```python
proj1 = qutip.ket2dm(ket1)
proj2 = qutip.ket2dm(ket2)
proj3 = qutip.ket2dm(ket3)
```

```python
guess_dynamics = objective.mesolve(tlist, e_ops=[proj1,proj2,proj3])
```

```python
def plot_population(result):
    fig, ax = plt.subplots()
    ax.plot(result.times, result.expect[0], label='1')
    ax.plot(result.times, result.expect[1], label='2')
    ax.plot(result.times, result.expect[2], label='3')
    ax.legend()
    ax.set_xlabel('time')
    ax.set_ylabel('population')
    plt.show(fig)
```

```python
plot_population(guess_dynamics)
```

We find that our guess pulses are too disjoint to implement the STIRAP scheme.
Thus, the Stokes pulse has no effect, whilst the pump pulse merely transfers
population out of $\ket{1}$ into $\ket{2}$ and back again.

## Optimize

In order to invoke `optimize_pulses`, we must define the required parameters
for each control, a pulse shape (used to ensure that the controls remain 0 at
$t=0$ and $t=T$), and the parameter $\lambda_a$ that determines the overall
magnitude of the pulse updates in each iteration.

```python
def S(t):
    """Scales the Krotov methods update of the pulse value at the time t"""
    return krotov.shapes.flattop(
        t, t_start=0.0, t_stop=5.0, t_rise=0.3, func='sinsq'
    )
```

```python
pulse_options = {
    H[1][1]: dict(lambda_a=0.5, update_shape=S),
    H[2][1]: dict(lambda_a=0.5, update_shape=S),
    H[3][1]: dict(lambda_a=0.5, update_shape=S),
    H[4][1]: dict(lambda_a=0.5, update_shape=S)
}
```

We now run the optimization, using the phase-sensitive functional $J_{T,
\text{re}} = 1 - \Re\Braket{\Psi(t)}{\Psi_{\tgt}}$, printing the integrated
pulse update for each control in each iteration. The optimization stops when
$J_T$ falls below $10^{-3}$, changes by less than $10^{-5}$, or after at most
15 iterations. We also check for monotonic convergence.

```python
opt_result = krotov.optimize_pulses(
    [objective],
    pulse_options,
    tlist,
    propagator=krotov.propagators.expm,
    chi_constructor=krotov.functionals.chis_re,
    info_hook=krotov.info_hooks.print_table(
        J_T=krotov.functionals.J_T_re,
        show_g_a_int_per_pulse=True,
        unicode=False,
    ),
    check_convergence=krotov.convergence.Or(
        krotov.convergence.value_below(1e-3, name='J_T'),
        krotov.convergence.delta_below(1e-5),
        krotov.convergence.check_monotonic_error,
    ),
    iter_stop=15,
)
```

    iter.      J_T   g_a_int_1   g_a_int_2   g_a_int_3   g_a_int_4    g_a_int          J  Delta J_T    Delta J  secs
    0     1.01e+00    0.00e+00    0.00e+00    0.00e+00    0.00e+00   0.00e+00   1.01e+00        n/a        n/a     1
    1     6.72e-01    1.72e-01    5.72e-04    1.63e-01    7.44e-04   3.37e-01   1.01e+00  -3.37e-01  -2.15e-06     2
    2     4.02e-01    1.44e-01    8.42e-04    1.24e-01    8.40e-04   2.70e-01   6.72e-01  -2.70e-01  -1.23e-05     3
    3     2.22e-01    9.81e-02    9.26e-04    7.98e-02    7.75e-04   1.80e-01   4.02e-01  -1.80e-01  -2.16e-05     2
    4     1.17e-01    5.78e-02    7.70e-04    4.58e-02    6.02e-04   1.05e-01   2.22e-01  -1.05e-01  -2.30e-05     2
    5     6.00e-02    3.13e-02    5.35e-04    2.46e-02    4.20e-04   5.68e-02   1.17e-01  -5.69e-02  -1.86e-05     2
    6     3.05e-02    1.62e-02    3.40e-04    1.27e-02    2.78e-04   2.95e-02   6.00e-02  -2.95e-02  -1.28e-05     2
    7     1.54e-02    8.16e-03    2.11e-04    6.47e-03    1.82e-04   1.50e-02   3.05e-02  -1.50e-02  -8.10e-06     2
    8     7.85e-03    4.08e-03    1.33e-04    3.25e-03    1.20e-04   7.58e-03   1.54e-02  -7.59e-03  -4.87e-06     2
    9     4.02e-03    2.03e-03    8.59e-05    1.63e-03    8.01e-05   3.82e-03   7.85e-03  -3.83e-03  -2.86e-06     2
    10    2.09e-03    1.01e-03    5.76e-05    8.13e-04    5.45e-05   1.93e-03   4.02e-03  -1.94e-03  -1.66e-06     2
    11    1.10e-03    5.03e-04    3.97e-05    4.06e-04    3.76e-05   9.86e-04   2.09e-03  -9.87e-04  -9.67e-07     3
    12    5.90e-04    2.51e-04    2.79e-05    2.03e-04    2.62e-05   5.08e-04   1.10e-03  -5.09e-04  -5.66e-07     3

```python
opt_result
```

    Krotov Optimization Result
    --------------------------
    - Started at 2021-01-13 14:17:05
    - Number of objectives: 1
    - Number of iterations: 12
    - Reason for termination: Reached convergence: J_T < 0.001
    - Ended at 2021-01-13 14:17:40 (0:00:35)

We dump the result of the optimization to disk for later use in the [Ensemble
Optimization for Robust Pulses](08_example_ensemble.ipynb).

```python
if not os.path.isfile('lambda_rwa_opt_result.dump'):
    opt_result.dump('lambda_rwa_opt_result.dump')
```

The optimized complex pulses look as follows:

```python
def plot_pulse_amplitude_and_phase(pulse_real, pulse_imaginary,tlist):
    ax1 = plt.subplot(211)
    ax2 = plt.subplot(212)
    amplitudes = [np.sqrt(x*x + y*y) for x,y in zip(pulse_real,pulse_imaginary)]
    phases = [np.arctan2(y,x)/np.pi for x,y in zip(pulse_real,pulse_imaginary)]
    ax1.plot(tlist,amplitudes)
    ax1.set_xlabel('time')
    ax1.set_ylabel('pulse amplitude')
    ax2.plot(tlist,phases)
    ax2.set_xlabel('time')
    ax2.set_ylabel('pulse phase (π)')
    plt.show()

print("pump pulse amplitude and phase:")
plot_pulse_amplitude_and_phase(
    opt_result.optimized_controls[0], opt_result.optimized_controls[1], tlist)
print("Stokes pulse amplitude and phase:")
plot_pulse_amplitude_and_phase(
    opt_result.optimized_controls[2], opt_result.optimized_controls[3], tlist)
```

We can convert the complex controls in the rotating frame back into the
real-valued pulses in the lab frame:

```python
def plot_physical_field(pulse_re, pulse_im, tlist, case=None):

    if case == 'pump':
        w = 9.5
    elif case == 'stokes':
        w = 4.5
    else:
        print('Error: selected case is not a valid option')
        return

    ax = plt.subplot(111)
    ax.plot(tlist,pulse_re*np.cos(w*tlist)-pulse_im*np.sin(w*tlist), 'r')
    ax.set_xlabel('time', fontsize = 16)
    if case == 'pump':
        ax.set_ylabel(r'$\mu_{12}\,\epsilon_{P}$')
    elif case == 'stokes':
        ax.set_ylabel(r'$ \mu_{23}\,\epsilon_{S}$')
    plt.show()

print('Physical electric pump pulse in the lab frame:')
plot_physical_field(
    opt_result.optimized_controls[0], opt_result.optimized_controls[1], tlist, case = 'pump')

print('Physical electric Stokes pulse in the lab frame:')
plot_physical_field(
    opt_result.optimized_controls[2], opt_result.optimized_controls[3], tlist, case = 'stokes')
```

Lastly, we check the population dynamics to verify that we indeed implement the
desired state-to-state transfer:

```python
opt_dynamics = opt_result.optimized_objectives[0].mesolve(
    tlist, e_ops=[proj1, proj2, proj3])
```

```python
plot_population(opt_dynamics)
```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c) 2019 Daniel Koehn, based on (c)2018 L.A. Barba, G.F. Forsyth [CFD Python](https://github.com/barbagroup/CFDPython#cfd-python), (c)2014 L.A. Barba, I. Hawke, B. Knaepen [Practical Numerical Methods with Python](https://github.com/numerical-mooc/numerical-mooc#practical-numerical-methods-with-python), also under CC-BY.

```python
from IPython.core.display import HTML
css_file = '../style/custom.css'
HTML(open(css_file, 'r').read())
```

<link href="https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i,900,900i" rel='stylesheet' >
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,300i,400,400i,700,700i" rel='stylesheet' >
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' >
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 800px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.5em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Source Sans Pro', sans-serif;
    line-height: 140%;
    font-size: 110%;
    width:680px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Merriweather', serif;
    font-style:regular;
    font-weight: bold;
    font-size: 250%;
    line-height: 100%;
    color: #004065;
    margin-bottom: 1em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Merriweather', serif;
    font-weight: bold;
    font-size: 180%;
    line-height: 100%;
    color: #0096d6;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Merriweather', serif;
	font-size: 150%;
    margin-top:12px;
    margin-bottom: 3px;
    font-style: regular;
    color: #008367;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Merriweather', serif;
    font-weight: 300;
    font-size: 100%;
    line-height: 120%;
    text-align: left;
    width:500px;
    margin-top: 1em;
    margin-bottom: 2em;
    margin-left: 80pt;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Source Sans Pro', sans-serif;
    font-weight: regular;
    font-size: 130%;
    color: #e31937;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 1em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>

# 1D Heat Conduction

Heat conduction is a diffusive process. Let's remind ourselves of the heat equation in one spatial dimension:

$$
\begin{equation}
\frac{\partial T}{\partial t} = \alpha \frac{\partial^2 T}{\partial x^2} \tag{1}
\end{equation}
$$

Here, $\alpha$ is the thermal diffusivity, a property of the material, and $T$ is the temperature.

In the [first lesson of module 5](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_03_1DDiffusion.ipynb), we discretized the diffusion equation  with a forward-time, centered-space scheme, subject to the following stability constraint:

$$
\begin{equation}
\alpha \frac{\Delta t}{(\Delta x)^2} \leq \frac{1}{2} \tag{2}
\end{equation}
$$

Let's look into it more deeply now, using a 1D temperature-evolution problem.

### Problem set up

Say we have a graphite rod, with [thermal diffusivity](http://en.wikipedia.org/wiki/Thermal_diffusivity) of $\alpha=1.22\times10^{-3} {\rm m}^2/{\rm s}$, length $L=1{\rm m}$, and temperature $T=0{\rm ^oC}$ everywhere. At time $t=0$, we raise the temperature on the left-side end, $x=0$, to $T=100{\rm ^oC}$, and hold it there. *How will the temperature evolve in the rod?*

As usual, start by importing some libraries and setting up the discretization. We'll begin by using a spatial grid with 51 points and advance for 100 time steps, using a forward-time/centered scheme.

.
#### Figure 1. Graphite rod, temperature fixed on ends.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

```python
# Set parameters.
L = 1.0  # length of the rod
nx = 51  # number of locations on the rod
dx = L / (nx - 1)  # distance between two consecutive locations
alpha = 1.22e-3  #  thermal diffusivity of the rod

# Define the locations along the rod.
x = numpy.linspace(0.0, L, num=nx)

# Set the initial temperature along the rod.
T0 = numpy.zeros(nx)
T0[0] = 100.0
```

Remember the forward-time, centered-space discretization? You should work it out on a piece of paper yourself (if you can't do it without looking it up, it means you need to do this more!).

$$
\begin{equation}
\frac{T_{i}^{n+1}-T_{i}^{n}}{\Delta t}=\alpha\frac{T_{i+1}^{n}-2T_{i}^{n}+T_{i-1}^{n}}{\Delta x^2} \tag{3}
\end{equation}
$$

To obtain the temperature at the next time step, $T^{n+1}_i$, from the known information at the current time step, we compute

$$
\begin{equation}
T_{i}^{n+1}=T_{i}^{n}+\frac{\alpha\Delta t}{\Delta x^2}(T_{i+1}^{n}-2T_{i}^{n}+T_{i-1}^{n}) \tag{4}
\end{equation}
$$

Check the [first notebook of module 5](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_03_1DDiffusion.ipynb), if you need to refresh your memory!

The following function implements this numerical scheme:

```python
def ftcs(T0, nt, dt, dx, alpha):
    """
    Computes and returns the temperature along the rod
    after a provided number of time steps,
    given the initial temperature and thermal diffusivity.
    The diffusion equation is integrated using forward
    differencing in time and central differencing in space.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature along the rod as a 1D array of floats.
    nt : integer
        The number of time steps to compute.
    dt : float
        The time-step size to integrate.
    dx : float
        The distance between two consecutive locations.
    alpha : float
        The thermal diffusivity of the rod.

    Returns
    -------
    T : numpy.ndarray
        The temperature along the rod as a 1D array of floats.
    """

    T = T0.copy()
    sigma = alpha * dt / dx**2

    # loop over time steps
    for n in range(nt):

        Tn = T.copy() # store old temperature field

        # loop over spatial grid points
        for i in range(1,nx-1):
            T[i] = (Tn[i] + sigma * (Tn[i+1] - 2.0 * Tn[i] + Tn[i-1])) # FTCS scheme

    return T
```

We are all set to run! First, let's use a time step `dt` that satisfies the stability constraint.

```python
# Set the time-step size based on CFL limit.
nt = 100  # number of time steps to compute
sigma = 0.5
dt = sigma * dx**2 / alpha  # time-step size

# Compute the temperature along the rod.
T = ftcs(T0, nt, dt, dx, alpha)
```

Now plot the solution!

```python
# Plot the temperature along the rod.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('Distance [m]')
pyplot.ylabel('Temperature [C]')
pyplot.grid()
pyplot.plot(x, T, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 100.0);
```

## Boundary Conditions

In the rod problem above, we stated that the left-hand side of the rod is held to a temperature $T = 100{\rm ^oC}$.  This is an example of a *boundary condition* (BC): a rule that defines how the problem interacts with the borders of the domain.  In this case, the domain spans the length of the rod, from $0 \leq x \leq 1$.

There are many types of boundary conditions, and they have an important effect on the solution of the problem. We need to discuss boundary conditions in a little more detail, and this is good place to do it. Read on!

### Dirichlet boundary

The rod example above uses a *Dirichlet* BC on the left-hand side.  A Dirichlet boundary is one in which the border is held to a specific value of the solution variable.

What about the right-hand end of the rod $(x=1)$?  In the discretization we set up above, the problem hasn't evolved for long enough time for the heat to travel the full length of the rod.  Let's increase the number of time steps, `nt`, and see what happens.

```python
# Increase the number of time steps.
nt = 2000

# Compute the temperature along the rod.
T = ftcs(T0, nt, dt, dx, alpha)
```

```python
# Plot the temperature along the rod.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('Distance [m]')
pyplot.ylabel('Temperature [C]')
pyplot.grid()
pyplot.plot(x, T, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 100.0);
```

Hmmm ... it looks like we're pinning the right BC to the value $T=0{\rm ^oC}$, do you agree?
Now study the code for the `ftcs` function, above, and try to figure out what happens at the right-most point of the spatial grid.

Did you figure it out?

It never updates the value at `T[-1]`!  That value is set to zero in the initial condition and stays that way throughout the entire simulation.  We effectively have a Dirichlet boundary at *both* ends.  The left-hand side of the rod is held to a temperature of $T = 100{\rm ^oC}$ and the right-hand side is held to $T = 0{\rm ^oC}$  Because both end temperatures are "pinned" to set values, the temperature distribution within the rod will relax (after enough time) to a linear temperature gradient across the length of the rod.

Once the solution is relaxed, no number of extra time steps will make it change.  This is an example of a *steady-state (fixpoint) solution* and we'll learn more about those later in the course.

Dirichlet BCs show up in many engineering applications, among them thermo- dynamics (e.g., setting a surface temperature) and fluid dynamics (e.g., the no-slip conditions at walls in viscous fluids).

### Neumann boundary

Another commonly used BC is the *Neumann* boundary: rather than specifying the value of the solution at the border, they specify the value of the *derivative* of the solution at the border.

In our example, if we apply a Neumann boundary to the right-hand end of the rod, we can represent that mathematically as

$$
\begin{equation}
\left. \frac{\partial T}{\partial x} \right|_{x = 1} = q(t) \notag
\end{equation}
$$

How can we enforce that in code?  One easy way is using a finite-difference discretization of the derivative at the boundary.  At time step $n$, for $N$ points, that would be

$$
\begin{equation}
\left. \frac{\partial T}{\partial x} \right|_{x=1} \approx \frac{T^n_N - T^n_{N-1}}{\Delta x} = q(t) \notag
\end{equation}
$$

In the context of heat conduction, the space derivative of temperature is the heat flux density $q$, the amount of heat per unit time, per unit area.

For example, if the rod at $x=1$ has some insulating material, no heat is going to be able to get out through that end, and the Neumann boundary condition is:

$$
\begin{equation}
\left. \frac{\partial T}{\partial x} \right|_{x=1} = 0 \notag
\end{equation}
$$

Think about what the equation is saying: the change in temperature when moving in the $x$ direction is zero at the right-most edge of the rod. That means that the temperature should be equal on the last two spatial grid points.

To enforce this Neumann boundary condition at the right-hand end of the rod, we add the following line of code:

```Python
T[-1] = T[-2]
```

That is, the temperature at the boundary (`T[-1]`) is equal to the temperature directly to its left (`T[-2]`).  The spatial gradient is zero and a Neumann condition is satisfied there.

The Dirichlet conditions in the rod example were enforced without any extra input from us, but the Neumann condition will require an update at each iteration of the loop.

Finally, Dirichlet and Neumann BCs can sometimes be mixed to better represent the physics of a problem.

Let's revisit the heated-rod problem.  This time, the temperature at $x = 0$ will remain fixed at $T = 100{\rm ^oC}$—that's the Dirichlet condition— and a Neumann BC applies at $x = 1$.

```python
def ftcs_mixed_bcs(T0, nt, dt, dx, alpha):
    """
    Computes and returns the temperature along the rod
    after a provided number of time steps,
    given the initial temperature and the thermal diffusivity.
    The diffusion equation is integrated using forward
    differencing in time and central differencing in space.
    The function uses a Dirichlet condition on the left side
    of the rod and a Neumann condition (zero-gradient) on the
    right side.

    Parameters
    ----------
    T0 : numpy.ndarray
        The initial temperature along the rod as a 1D array of floats.
    nt : integer
        The number of time steps to compute.
    dt : float
        The time-step size to integrate.
    dx : float
        The distance between two consecutive locations.
    alpha : float
        The thermal diffusivity of the rod.

    Returns
    -------
    T : numpy.ndarray
        The temperature along the rod as a 1D array of floats.
    """

    T = T0.copy()
    sigma = alpha * dt / dx**2

    # loop over time steps
    for n in range(nt):

        Tn = T.copy() # store old temperature field

        # loop over spatial grid
        for i in range(1,nx-1):
            T[i] = (Tn[i] + sigma * (Tn[i+1] - 2.0 * Tn[i] + Tn[i-1])) # FTCS scheme

        # Apply Neumann condition at the last location
        T[-1] = T[-2]

    return T
```

```python
nt = 1000  # number of time steps to compute

# Compute the temperature along the rod.
T = ftcs_mixed_bcs(T0, nt, dt, dx, alpha)
```

```python
# Plot the temperature along the rod
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('Distance [m]')
pyplot.ylabel('Temperature [C]')
pyplot.grid()
pyplot.plot(x, T, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 100.0);
```

Now heat is accumulating in the domain. Our insulator works!  If you increase the number of time steps `nt` further, you will see that the outflow temperature at $x = 1$ continues to increase.

### Explicit schemes and BCs

The numerical schemes we've learned so far are called _explicit_, because we compute the updated solution at $t^{n+1}$ using only known information at time $t^n$. This is simple but has limitations, particularly with the small sizes of time step necessary to obtain stability.

Here's another issue with explicit schemes. Figure 2 shows the superposed stencil of several grid points over three time steps computed using the forward-time, centered scheme. You know that to calculate $T_i^{n+1}$ you use the information from the grid points $i-1, i, i+1$ at the previous time step. Think about what happens at the boundary: any change in the boundary condition will feed into the solution only at the next time step, not immediately. But this contradicts the physics of the problem, as any change on the boundary should be felt right away under the diffusion equation. To include boundary effects on the same time level, we can use an _implicit_ update.

.
#### Figure 2. Stencil with explicit scheme over 3 time steps.

### Explicit schemes and time step

Let's replace the parameter $\sigma=\alpha \frac{\Delta t}{(\Delta x)^2}$ into equation (4) and rearrange it a little bit, grouping together the terms at grid point $i$ on the right-hand side:

$$
\begin{equation}
T_{i}^{n+1} = \sigma T_{i-1}^{n}+(1- 2 \sigma) T_{i}^{n} + \sigma T_{i+1}^{n}
\end{equation}
$$

It's helpful to look at a sketch of the stencil for the heat equation with the weights added in for the contribution of each grid point. See Figure 3, which shows the weights for two values of $\sigma$: $\frac{1}{2}$ and $1$. Notice that with $\sigma=\frac{1}{2}$, the solution variable at the next time step is _independent_ from its own value at the present step, which seems a bit weird. And with $\sigma=1$, the current value affects negatively the future value—now _that's_ a red flag!

In fact, the solution will develop growing errors with $\sigma>\frac{1}{2}$, i.e., become unstable. This limits the time step that can be used quite significantly, because $\Delta t \propto (\Delta x)^2$. This is a serious cost of _explicit_ methods and an incentive to consider _implicit_ alternatives.

.
#### Figure 3. Stencils for the heat equation with $\sigma= \frac{1}{2}, 1$.

## What we learned:

- How to solve the 1D heat conduction equation for a graphite rod heated on the left-hand side
- The influence of Dirichlet and Neumann boundary conditions on the solution of the 1D heat conduction equation


# Annotate

_this_ is __bold__

```python
# A quick sample construction to showcase how to use koebe.py

from koebe.algorithms.incrementalConvexHull import randomConvexHullE3
from koebe.graphics.spherical2viewer import *

import random

viewer = S2Viewer(600,600)

viewer.toggleSphere() # Hide the sphere
polyhedron = randomConvexHullE3(100)

viewer.addAll(polyhedron.edges)
viewer.addAll(polyhedron.faces)

#viewer.show()
```

    <IPython.core.display.Javascript object>

```python
from koebe.algorithms.incrementalConvexHull import randomConvexHullE3
from koebe.graphics.spherical2viewer import *

from random import randint

viewer = S2Viewer(600,600)

viewer.toggleSphere()

poly = randomConvexHullE3(500)

viewer.addAll(poly.edges)
viewer.addAll(poly.faces)

for e in poly.edges:
    viewer.setStyle(e, makeStyle(stroke="#000", strokeWeight = 0.5))

# Set each face to a random color:
for i in range(len(poly.faces)):
    h, l = str(randint(0, 360)), str(randint(50, 75))
    viewer.setStyle(poly.faces[i], makeStyle(fill = f"hsl({h}, 100%, {l}%)"))

viewer.show()
```

    <IPython.core.display.Javascript object>

    S2Sketch(height=600, objects='[[{"type": "SegmentE3", "endpoints": [[-0.10515393940955585, -0.0137387530149034…

```python
for i in range(10):
    print(randint(90,100))
```

    99
    98
    100
    93
    92
    98
    93
    100
    96
    100

```python
poly = polyhedron.duplicate()

for i in range(25):
    poly.verts[10].remove()
```

    WARNING: degree is $2 <= 2. This is almost certainly going to result in errors.

```python
viewer = S2Viewer(600,600)

viewer.toggleSphere() # Hide the sphere

viewer.addAll(poly.verts)
viewer.addAll(poly.edges)
viewer.addAll(poly.faces)

#viewer.show()
```

    <IPython.core.display.Javascript object>

```python
from koebe.geometries.spherical2 import *
from koebe.graphics.spherical2viewer import *

import random

viewer = S2Viewer(600,600)

disk1 = DiskS2(1000*3,1000*4,1000*1,1000*4.3)
disk2 = DiskS2(-1.05,1,-1,1)
disk3 = disk1.invertThrough(disk2)

viewer.setStyle(disk1, makeStyle(stroke="#ff0000"))
viewer.setStyle(disk3, makeStyle(stroke="#ff0000"))

viewer.add(disk1)
viewer.add(disk2)
viewer.add(disk3)

viewer.show()
```

    <IPython.core.display.Javascript object>

    S2Sketch(height=600, objects='[{"type": "DiskS2", "disk": [3000, 4000, 1000, 4300.0], "b1": [-0.8, 0.6, 0.0], …

```python
from koebe.geometries.spherical2 import *
from koebe.geometries.commonOps import *
from koebe.graphics.spherical2viewer import *

import random

viewer = S2Viewer(600,600)

a = DiskS2(1000*3,1000*4,1000*1,1000*4.3)
b = DiskS2(-1.05,1,-1,1)

viewer.setStyle(a, makeStyle(stroke="#f00"))
viewer.setStyle(b, makeStyle(stroke="#00f"))

viewer.add(a)
viewer.add(b)

for _ in [-tick / 10 for tick in range(10)]:
    s = inner_product31(*tuple(a), *tuple(b)) / inner_product31(*tuple(b), *tuple(b))
    c = (a.a - s*b.a, a.b - s*b.b, a.c - s*b.c, a.d - s*b.d)
    cinv = 1.0/math.sqrt(c[0]**2 + c[1]**2 + c[2]**2)
    c = (c[0] * cinv, c[1] * cinv, c[2] * cinv, c[3] * cinv)

    t = 0.005
    b2 = DiskS2(b.a + c[0] * t, b.b + c[1] * t, b.c + c[2] * t, b.d - c[3] * t)
    viewer.setStyle(b2, makeStyle(stroke = "#eee", strokeWeight = 1))
    viewer.add(b2)
    b = b2

viewer.show()
```

    <IPython.core.display.Javascript object>

    S2Sketch(height=600, objects='[{"type": "DiskS2", "disk": [3000, 4000, 1000, 4300.0], "b1": [-0.8, 0.6, 0.0], …

```python
from koebe.geometries.spherical2 import *
from koebe.geometries.commonOps import *
from koebe.graphics.spherical2viewer import *

import random

viewer = S2Viewer(1000,1000)

a = DiskS2(1000*3,1000*4,1000*1,1000*4.3)
b = DiskS2(-1.05,1,-1,1)

viewer.setStyle(a, makeStyle(stroke="#f00"))
viewer.setStyle(b, makeStyle(stroke="#00f"))

viewer.add(a)
viewer.add(b)

for _ in [-tick / 10 for tick in range(30)]:
    s = inner_product31(*tuple(a), *tuple(b)) / inner_product31(*tuple(b), *tuple(b))
    c = (a.a - s*b.a, a.b - s*b.b, a.c - s*b.c, a.d - s*b.d)
    cinv = 1.0/math.sqrt(c[0]**2 + c[1]**2 + c[2]**2)
    c = (c[0] * cinv, c[1] * cinv, c[2] * cinv, c[3] * cinv)

    t = -0.1
    b2 = DiskS2(b.a + c[0] * t, b.b + c[1] * t, b.c + c[2] * t, b.d + c[3] * t)
    viewer.setStyle(b2, makeStyle(stroke = "#eee", strokeWeight = 1))
    viewer.add(b2)
    b = b2

viewer.show()
```

    <IPython.core.display.Javascript object>

    S2Sketch(height=1000, objects='[{"type": "DiskS2", "disk": [3000, 4000, 1000, 4300.0], "b1": [-0.8, 0.6, 0.0],…

```python
from koebe.geometries.spherical2 import *
from koebe.graphics.spherical2viewer import *

import random

viewer = S2Viewer(600,600)

d1 = 0.75
d2 = -0.5

disk1 = DiskS2(0.0, 0.0, 1.0, d1)

z2 = -math.sqrt(1 - d1 * d1) * math.sqrt(1 - d2 * d2) + d1 * d2
x2 = math.sqrt(1 - z2 * z2)
disk2 = DiskS2(x2, 0.0, z2, d2)

viewer.add(disk1)
viewer.add(disk2)

viewer.setStyle(disk1, makeStyle(stroke="#f00"))
viewer.setStyle(disk2, makeStyle(stroke="#00f"))

viewer.show()
```

    <IPython.core.display.Javascript object>

    S2Sketch(height=600, objects='[{"type": "DiskS2", "disk": [0.0, 0.0, 1.0, 0.75], "b1": [0.0, -1.0, 0.0], "b2":…

```python
from sympy import symbols, sqrt, simplify

d1, d2 = symbols('d1 d2')

simplify(sqrt(1-(sqrt(1 - d1*d1) * sqrt(1 - d2 * d2) + d1 * d2)
              *(sqrt(1 - d1*d1) * sqrt(1 - d2 * d2) + d1 * d2))
        )
```

```python

```

```python

```