# Code generation for barycentric coordinates

This notebook consists of three main parts, all used in `ocellaris/utils/taylor_basis.py` for Lagrange to Taylor and Taylor to Lagrange transformations for discontinuous Galerkin shape functions on simplices.

**1: Code generation for quadratic barycentric shape functions**

Given a triangle/tetrahedron with barycentric coordinates, find the linear and quadratic shape functions. We here work only in barycentric coordinates. The linear basis functions should be the coordinates themselves and the interesting part is the quadratic shape functions

**2: Code generation for the cartesian to barycentric transformation**

Given cartesian coordinates of all cell vertices, compute the barycentric coordinate transformation for an arbitrary point inside the cell. This is done for 2D (fast) and 3D (dog slow)

**3: Code generation for the transformation matrices**

Produce the actual transformation matrices for the Lagrange to Taylor transform and some required parts of the matrices for the Taylor to Lagrange transform

```python
import time
from IPython.display import display, HTML
import numpy
from matplotlib import pyplot
import sympy
from sympy import *
sympy.init_printing(use_latex=True)
%matplotlib inline
```

## Barycentric basis functions in 2D

```python
barycentric_basis = {}

for P in (1, 2):
    display(HTML('<b>Barycentric basis in 2D of order %d</b>' % P))
    D = 2
    N = D + 1
    M = N*P

    # Define the barycentric coordinates L
    Ls = list(symbols('L_(1:%d)' % N))
    Ls.append(1 - sum(Ls))

    # Define the coeffcients of the basis functions, C
    Cs = symbols('C_(1:%d)' % (1 + M*M))

    # DOF locations
    w = 1/S(2)
    locs = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (0, w, w), (w, 0, w), (w, w, 0)][:M]

    # Create the linear basis
    Bs = []
    for i in range(M):
        cs = Cs[i*M:(i + 1)*M]
        if P == 1:
            Bs.append(cs[0] + cs[1]*Ls[0] + cs[2]*Ls[1])
        else:
            Bs.append(cs[0] + cs[1]*Ls[0] + cs[2]*Ls[1] + cs[3]*Ls[0]**2 + cs[4]*Ls[1]**2 + cs[5]*Ls[0]*Ls[1])

    # Conditions on the basis, Bi(xj) = delta_j(xj)
    eqs = []
    for iloc, loc in enumerate(locs):
        subs = {Ls[i]: loc[i] for i in range(N)}
        for jb in range(M):
            rhs = 1 if iloc == jb else 0
            eqs.append((Bs[jb] - rhs).subs(subs))

    # Find the coefficients of the basis
    sol = solve(eqs, Cs)
    Ln = Symbol('L_%d' % N)
    Lnsubs = {Ls[-1]: Ln}
    Bsol = []
    for i in range(M):
        Bi = Bs[i]
        Bi = factor(Bi).subs(Lnsubs).simplify().subs(Lnsubs)
        Bi = Bi.subs(sol)
        Bi = factor(Bi).subs(Lnsubs).simplify().subs(Lnsubs)
        Bi = Bi.subs(2*Ls[-1] - 1, 2*Ln -1)
        Bsol.append(Bi.simplify())
        display(Eq(Symbol('B_%d' % i), Bsol[-1]))

    barycentric_basis[(D, P)] = Bsol, Ls[:-1] + [Ln]

```

## Barycentric basis functions in 3D

```python
for P in (1, 2):
    display(HTML('<b>Barycentric basis in 3D of order %d</b>' % P))
    D = 3
    N = D + 1
    M = 4 if P == 1 else 10

    # Define the barycentric coordinates L
    Ls = list(symbols('L_(1:%d)' % N))
    Ls.append(1 - sum(Ls))

    # Define the coeffcients of the basis functions, C
    Cs = symbols('C_(1:%d)' % (1 + M*M))

    # DOF locations
    w = 1/S(2)
    locs = [(1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1), (0, 0, w, w),
            (0, w, 0, w), (0, w, w, 0), (w, 0, 0, w), (w, 0, w, 0), (w, w, 0, 0)][:M]

    # Define the basis in terms of the unknown coefficients, C_i
    Bs = []
    for i in range(M):
        cs = Cs[i*M:(i + 1)*M]
        if P == 1:
            Bs.append(cs[0] + cs[1]*Ls[0] + cs[2]*Ls[1] + cs[3]*Ls[2])
        else:
            Bs.append(cs[0] + cs[1]*Ls[0] + cs[2]*Ls[1] + cs[3]*Ls[2] +
                      cs[4]*Ls[0]**2 + cs[5]*Ls[1]**2 + cs[6]*Ls[2]**2 +
                      cs[7]*Ls[0]*Ls[1] + cs[8]*Ls[0]*Ls[2] + cs[9]*Ls[1]*Ls[2])

    # Conditions on the basis, Bi(xj) = delta_j(xj)
    eqs = []
    for iloc, loc in enumerate(locs):
        subs = {Ls[i]: loc[i] for i in range(N)}
        for jb in range(M):
            rhs = 1 if iloc == jb else 0
            eqs.append((Bs[jb] - rhs).subs(subs))

    # Find the coefficients of the basis
    sol = solve(eqs, Cs)
    Ln = Symbol('L_%d' % N)
    Lnsubs = {Ls[-1]: Ln}
    Bsol = []
    for i in range(M):
        Bi = Bs[i]
        Bi = factor(Bi).subs(Lnsubs).simplify().subs(Lnsubs)
        Bi = Bi.subs(sol)
        Bi = factor(Bi).subs(Lnsubs).simplify().subs(Lnsubs)
        Bi = Bi.subs(2*Ls[-1] - 1, 2*Ln -1)
        Bsol.append(Bi.simplify())
        display(Eq(Symbol('B_%d' % i), Bsol[-1]))

    barycentric_basis[(D, P)] = Bsol, Ls[:-1] + [Ln]

```

## Code generation

```python
def tocode(Bi):
    s = str(Bi)
    s = s.replace('L_1', 'L1')
    s = s.replace('L_2', 'L2')
    s = s.replace('L_3', 'L3')
    s = s.replace('L_4', 'L4')
    return s

for k, (Bs, Ls) in barycentric_basis.items():
    display(HTML('<b>Code for %dD, order %d' % k))
    s = '\n'.join('phi[%d] = %s' % (i, tocode(Bi)) for i, Bi in enumerate(Bs))
    display(HTML('<pre>' + s + '</pre>'))
```

Code for 2D, order 1

phi[0] = L1
phi[1] = L2
phi[2] = L3

Code for 2D, order 2

phi[0] = L1*(2*L1 - 1)
phi[1] = L2*(2*L2 - 1)
phi[2] = L3*(2*L3 - 1)
phi[3] = 4*L2*L3
phi[4] = 4*L1*L3
phi[5] = 4*L1*L2

Code for 3D, order 1

phi[0] = L1
phi[1] = L2
phi[2] = L3
phi[3] = L4

Code for 3D, order 2

phi[0] = L1*(2*L1 - 1)
phi[1] = L2*(2*L2 - 1)
phi[2] = L3*(2*L3 - 1)
phi[3] = L4*(2*L4 - 1)
phi[4] = 4*L3*L4
phi[5] = 4*L2*L4
phi[6] = 4*L2*L3
phi[7] = 4*L1*L4
phi[8] = 4*L1*L3
phi[9] = 4*L1*L2

## Compute barycentric coordinates from cartesian vertex coordinates

```python
barycentric_transform = {}

xs = x1, x2, x3, x4 = symbols('x(1:5)', real=True)
ys = y1, y2, y3, y4 = symbols('y(1:5)', real=True)
zs = z1, z2, z3, z4 = symbols('z(1:5)', real=True)
x, y, z = symbols('x y z', real=True)

for D in (2, 3):
    display(HTML('<h3>Cartesian to barycentric %dD</h3>' % D))
    t1 = time.time()
    N = D + 1

    # Coefficients
    Cs = symbols('C_(:%d)' % (N*D))

    # Barycentric coordinates
    if D == 2:
        Ls = [Cs[0] + Cs[1]*x + Cs[2]*y,
              Cs[3] + Cs[4]*x + Cs[5]*y]
        Ls.append(1 - Ls[0] - Ls[1])
    else:
        Ls = [Cs[0] + Cs[1]*x + Cs[2]*y + Cs[3]*z,
              Cs[4] + Cs[5]*x + Cs[6]*y + Cs[7]*z,
              Cs[8] + Cs[9]*x + Cs[10]*y + Cs[11]*z,]
        Ls.append(1 - Ls[0] - Ls[1] - Ls[2])

    # Build equation system
    eqs = []
    # Loop over corners, i
    for i in range(N):
        vertex_pos = {x: xs[i], y: ys[i], z: zs[i]}
        # Loop over barycentric coordinates, j
        for j in range(N-1):
            rhs = 1 if i == j else 0
            eqs.append(Ls[j].subs(vertex_pos) - rhs)

    display(Eq(Symbol('N_\\text{eq}'), len(eqs)))
    display(Eq(Symbol('N_\\text{Cs}'), len(Cs)))

    # Solve the equation system
    display(HTML('<b style="color:#999900">This is going to take some time, especially in 3D ...</b>'))
    sol = solve(eqs, Cs)

    F = Symbol('F')
    if D == 2:
        fac = x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2
        #diff = fac - (y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)
        #display(expand(diff).simplify())
    else:
        fac = x1*y2*z3 - x1*y2*z4 - x1*y3*z2 + x1*y3*z4 + x1*y4*z2 - x1*y4*z3 - x2*y1*z3 + x2*y1*z4 + x2*y3*z1 - x2*y3*z4 - x2*y4*z1 + x2*y4*z3 + x3*y1*z2 - x3*y1*z4 - x3*y2*z1 + x3*y2*z4 + x3*y4*z1 - x3*y4*z2 - x4*y1*z2 + x4*y1*z3 + x4*y2*z1 - x4*y2*z3 - x4*y3*z1 + x4*y3*z2
    display(Eq(F, factor(fac)))

    # Show the solution
    Lsol = []
    for i in range(N):
        Li = (Ls[i] * fac).subs(sol).simplify()
        Lsol.append(Li/F)
        display(Eq(Symbol('L_%d' % i), factor(Lsol[-1])))

    barycentric_transform[D] = Lsol
    display(HTML('<b style="color:#009900">Done in %.2f seconds</b>' % (time.time() - t1)))

```

## Code generation

```python
def tocode(Li):
    return str(Li)

for D, Ls in barycentric_transform.items():
    display(HTML('<b>Code for %dD' % D))
    s = '\n'.join('L%df = %s' % (i + 1, tocode(Li)) for i, Li in enumerate(Ls))
    display(HTML('<pre>' + s + '</pre>'))

```

Code for 2D

L1f = (x*(y2 - y3) + x2*y3 - x3*y2 - y*(x2 - x3))/F
L2f = (-x*(y1 - y3) - x1*y3 + x3*y1 + y*(x1 - x3))/F
L3f = (x*y1 - x*y2 - x1*y + x1*y2 + x2*y - x2*y1)/F

Code for 3D

L1f = (x*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) - x2*y3*z4 + x2*y4*z3 + x3*y2*z4 - x3*y4*z2 - x4*y2*z3 + x4*y3*z2 - y*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + z*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F
L2f = (-x*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + x1*y3*z4 - x1*y4*z3 - x3*y1*z4 + x3*y4*z1 + x4*y1*z3 - x4*y3*z1 + y*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3) - z*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3))/F
L3f = (x*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2) - x1*y2*z4 + x1*y4*z2 + x2*y1*z4 - x2*y4*z1 - x4*y1*z2 + x4*y2*z1 - y*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2) + z*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2))/F
L4f = (-x*y1*z2 + x*y1*z3 + x*y2*z1 - x*y2*z3 - x*y3*z1 + x*y3*z2 + x1*y*z2 - x1*y*z3 - x1*y2*z + x1*y2*z3 + x1*y3*z - x1*y3*z2 - x2*y*z1 + x2*y*z3 + x2*y1*z - x2*y1*z3 - x2*y3*z + x2*y3*z1 + x3*y*z1 - x3*y*z2 - x3*y1*z + x3*y1*z2 + x3*y2*z - x3*y2*z1)/F

### Test implementation against known answers

```python
# First we check for linearity
cell = {x1: 1, y1: 1, z1: 1,
        x2: 2, y2: 1, z2: 2,
        x3: 4, y3: 2, z3: 3,
        x4: 7, y4: 2, z4: 4}
Fs = {2: x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2,
      3: x1*y2*z3 - x1*y2*z4 - x1*y3*z2 + x1*y3*z4 + x1*y4*z2 - x1*y4*z3 - x2*y1*z3 + x2*y1*z4 + x2*y3*z1 - x2*y3*z4 - x2*y4*z1 + x2*y4*z3 + x3*y1*z2 - x3*y1*z4 - x3*y2*z1 + x3*y2*z4 + x3*y4*z1 - x3*y4*z2 - x4*y1*z2 + x4*y1*z3 + x4*y2*z1 - x4*y2*z3 - x4*y3*z1 + x4*y3*z2}
for D, Ls in barycentric_transform.items():
    display(HTML('<b>Code for %dD' % D))
    display(Eq(Symbol('F'), Fs[D].subs(cell)))
    for i, L in enumerate(Ls):
        LF = (L*F).subs(cell).subs(F, Fs[D].subs(cell)).simplify()
        display(Eq(Symbol('L_{%d}\\,F' % i), LF))
```

```python
def test_cell_2D(vertices):
    print('Checking cell', vertices, '...', end=' ')
    x1, y1 = vertices[0]
    x2, y2 = vertices[1]
    x3, y3 = vertices[2]

    def our(x, y):
        "Our solution"
        F = x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2
        L1f = (x*(y2 - y3) + x2*y3 - x3*y2 - y*(x2 - x3))/F
        L2f = (-x*(y1 - y3) - x1*y3 + x3*y1 + y*(x1 - x3))/F
        L3f = (x*y1 - x*y2 - x1*y + x1*y2 + x2*y - x2*y1)/F
        return L1f, L2f, L3f

    def ref(x, y):
        D = (y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3)
        L1f = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3))/D
        L2f = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3))/D
        L3f = 1 - L1f - L2f
        return L1f, L2f, L3f

    # Generate some positions to check
    other_positions = []
    for A, B in [(1/3, 1/3), (1/2, 1/4), (1/4, 1/2),
                 (3/5, 1/5), (1/5, 3/5), (1/5, 1/5)]:
        C = 1 - A - B
        other_positions.append((A*x1 + B*x2 + C*x3,
                                A*y1 + B*y2 + C*y3))

    OK = True
    for v in vertices + other_positions:
        #print('Checking', v)
        o1, o2, o3 = our(v[0], v[1])
        r1, r2, r3 = ref(v[0], v[1])
        for o, r in zip((o1, o2, o3), (r1, r2, r3)):
            if not abs(o - r) < 1e-15:
                print(o, '-', r, '=', o-r, '   <-- ERROR!!!')
                OK = False

    print('OK' if OK else 'ERROR')

print('Testing 2D cells')
test_cell_2D([(0, 0), (1, 0), (0, 1)])
test_cell_2D([(1, 1), (1, 0), (0, 1)])
test_cell_2D([(cell[x1], cell[y1]),
              (cell[x2], cell[y2]),
              (cell[x3], cell[y3])])
test_cell_2D([(-3, 4), (8, -1), (-15, -1)])

def test_cell_3D(vertices):
    print('Checking cell', vertices, '...', end=' ')
    x1, y1, z1 = vertices[0]
    x2, y2, z2 = vertices[1]
    x3, y3, z3 = vertices[2]
    x4, y4, z4 = vertices[3]

    def our(x, y, z):
        "Our solution"
        F = x1*y2*z3 - x1*y2*z4 - x1*y3*z2 + x1*y3*z4 + x1*y4*z2 - x1*y4*z3 - x2*y1*z3 + x2*y1*z4 + x2*y3*z1 - x2*y3*z4 - x2*y4*z1 + x2*y4*z3 + x3*y1*z2 - x3*y1*z4 - x3*y2*z1 + x3*y2*z4 + x3*y4*z1 - x3*y4*z2 - x4*y1*z2 + x4*y1*z3 + x4*y2*z1 - x4*y2*z3 - x4*y3*z1 + x4*y3*z2
        L1f = (x*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) - x2*y3*z4 + x2*y4*z3 + x3*y2*z4 - x3*y4*z2 - x4*y2*z3 + x4*y3*z2 - y*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + z*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F
        L2f = (-x*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + x1*y3*z4 - x1*y4*z3 - x3*y1*z4 + x3*y4*z1 + x4*y1*z3 - x4*y3*z1 + y*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3) - z*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3))/F
        L3f = (x*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2) - x1*y2*z4 + x1*y4*z2 + x2*y1*z4 - x2*y4*z1 - x4*y1*z2 + x4*y2*z1 - y*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2) + z*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2))/F
        L4f = (-x*y1*z2 + x*y1*z3 + x*y2*z1 - x*y2*z3 - x*y3*z1 + x*y3*z2 + x1*y*z2 - x1*y*z3 - x1*y2*z + x1*y2*z3 + x1*y3*z - x1*y3*z2 - x2*y*z1 + x2*y*z3 + x2*y1*z - x2*y1*z3 - x2*y3*z + x2*y3*z1 + x3*y*z1 - x3*y*z2 - x3*y1*z + x3*y1*z2 + x3*y2*z - x3*y2*z1)/F
        return L1f, L2f, L3f, L4f

    def ref(x, y, z):
        if (x, y, z) == vertices[0]:
            return 1, 0, 0, 0
        elif (x, y, z) == vertices[1]:
            return 0, 1, 0, 0
        elif (x, y, z) == vertices[2]:
            return 0, 0, 1, 0
        elif (x, y, z) == vertices[3]:
            return 0, 0, 0, 1
        elif (x, y, z) == other_positions[0]: # centroid
            return 1/4, 1/4, 1/4, 1/4

    ## Generate some positions to check
    other_positions = []
    for A, B, C in [(1/4, 1/4, 1/4)]:
        D = 1 - A - B - C
        other_positions.append((A*x1 + B*x2 + C*x3 + D*x4,
                                A*y1 + B*y2 + C*y3 + D*y4,
                                A*z1 + B*z2 + C*z3 + D*z4))

    OK = True
    for v in vertices + other_positions:
        #print('Checking', v)
        os = our(v[0], v[1], v[2])
        rs = ref(v[0], v[1], v[2])
        for i in range(4):
            o, r = os[i], rs[i]
            #print('    %d: % .2f - % .2f = % .2f' % (i, o, r, o-r),
            #      '   <-- ERROR!!!' if not abs(o - r) < 1e-15 else '')
            if not abs(o - r) < 1e-15:
                OK = False

    print('OK' if OK else 'ERROR')

print('Testing 3D cells')
test_cell_3D([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)])
test_cell_3D([(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 1)])
test_cell_3D([(cell[x1], cell[y1], cell[z1]),
              (cell[x2], cell[y2], cell[z2]),
              (cell[x3], cell[y3], cell[z3]),
              (cell[x4], cell[y4], cell[z4])])
test_cell_3D([(-4, 2, 33), (-2, -1, -50), (-10, 1, -30), (55, 22, -30)])
```

    Testing 2D cells
    Checking cell [(0, 0), (1, 0), (0, 1)] ... OK
    Checking cell [(1, 1), (1, 0), (0, 1)] ... OK
    Checking cell [(1, 1), (2, 1), (4, 2)] ... OK
    Checking cell [(-3, 4), (8, -1), (-15, -1)] ... OK
    Testing 3D cells
    Checking cell [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] ... OK
    Checking cell [(0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 1)] ... OK
    Checking cell [(1, 1, 1), (2, 1, 2), (4, 2, 3), (7, 2, 4)] ... OK
    Checking cell [(-4, 2, 33), (-2, -1, -50), (-10, 1, -30), (55, 22, -30)] ... OK

## Code generation for transformation matrices, 2D

Use sympy to derive the coefficients needed to convert from DG polynomials
on a triangle to a Taylor basis by expressing the derivatives at the centre
point of the triangle

```python
%reset -f

from sympy import Symbol, symbols, S
x1, x2, x3, y1, y2, y3 = symbols('x1 x2 x3 y1 y2 y3')
x, y, D = symbols('x y D')

# Barycentric coordinates from cartesian corner coordinates
L1f = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3))/D
L2f = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3))/D
L3f = 1 - L1f - L2f
L1, L2, L3 = Symbol('L1')(x, y), Symbol('L2')(x, y), Symbol('L3')(x, y)

# Construct replacements
Ls = [L1, L2, L3]
Lfs = [L1f, L2f, L3f]
replacements = []
for LX, LXf in zip(Ls, Lfs):
    replacements.append((LX.diff(x, x), LXf.diff(x, x)))
    replacements.append((LX.diff(y, y), LXf.diff(y, y)))
    replacements.append((LX.diff(x, y), LXf.diff(x, y)))
    replacements.append((LX.diff(x), LXf.diff(x)))
    replacements.append((LX.diff(y), LXf.diff(y)))
    replacements.append((LX, 1/S(3)))
replacements.append((x, (x1 + x2 + x3)/3))
replacements.append((y, (y1 + y2 + y3)/3))

def print_code(name, func, index):
    code = ['# %s' % name]
    for i, phii in enumerate(phi):
        v = func(phii)
        v = v.subs(replacements)
        v = v.simplify()
        code.append('A[icell, %d, %d] = %s' % (index, i, v))
    code.append('')
    print('\n'.join(code))

print('########################################')
print('Degree 1 in 2D:\n')

# Barycentric linear shape functions on a triangle
phi = [L1, L2, L3]

print_code('Value at xc (also the cell average value)', lambda q: q, 0)
print_code('d/dx', lambda q: q.diff(x), 1)
print_code('d/dy', lambda q: q.diff(y), 2)

print('########################################')
print('Degree 2 in 2D:\n')

# Barycentric quadratic shape functions on a triangle
# see documentation/notebooks/barycentric.ipynb
phi = [None]*6
phi[0] = L1*(2*L1 - 1)
phi[1] = L2*(2*L2 - 1)
phi[2] = L3*(2*L3 - 1)
phi[3] = 4*L2*L3
phi[4] = 4*L1*L3
phi[5] = 4*L1*L2

#print_code('Value at xc (MODIFY ME TO GET THE CELL AVERAGE)', lambda q: q, 0)
print('# Cell average value')
print('A[icell, 0, 3] = 1/3')
print('A[icell, 0, 4] = 1/3')
print('A[icell, 0, 5] = 1/3\n')
print_code('d/dx', lambda q: q.diff(x), 1)
print_code('d/dy', lambda q: q.diff(y), 2)
print_code('d/dx^2', lambda q: q.diff(x, x), 3)
print_code('d/dy^2', lambda q: q.diff(y, y), 4)
print_code('d/dx*dy', lambda q: q.diff(x, y), 5)

# Cell average terms
BT_QUADRATURE_TABLE = {}
BT_QUADRATURE_TABLE[1] = ([[1/S(3), 1/S(3)]],
                          [1.0])
BT_QUADRATURE_TABLE[2] = ([[2/S(3), 1/S(6)], [1/S(6), 2/S(3)], [1/S(6), 1/S(6)]],
                          [1/S(3), 1/S(3), 1/S(3)])
BT_QUADRATURE_TABLE[3] = ([[1/S(3), 1/S(3)], [S(3)/5, S(1)/5], [S(1)/5, S(3)/5], [S(1)/5, S(1)/5]],
                          [-S(9)/16, S(25)/48, S(25)/48, S(25)/48])

xv = x1*L1 + x2*L2 + x3*L3
yv = y1*L1 + y2*L2 + y3*L3
xc = xv.subs({L1: 1/S(3), L2: 1/S(3), L3: 1/S(3)})
yc = yv.subs({L1: 1/S(3), L2: 1/S(3), L3: 1/S(3)})

exprs = [('bar_xx', (xv-xc)**2/2),
         ('bar_yy', (yv-yc)**2/2),
         ('bar_xy', (xv-xc)*(yv-yc))]
for i in range(6):
    exprs.append(('A[icell, 0, %d]' % i, phi[i]))

order = 3
points, weights = BT_QUADRATURE_TABLE[order]
for name, expr in exprs:
    v = 0
    for point, weight in zip(points, weights):
        loc = {L1: point[0],
               L2: point[1],
               L3: 1 - point[0] - point[1]}
        v += weight*expr.subs(loc)
    v = v.simplify().simplify()
    v = v.subs({x1: Symbol('x1'), x2: Symbol('x2'), x3: Symbol('x3'),
                y1: Symbol('y1'), y2: Symbol('y2'), y3: Symbol('y3')})
    print('%s = %s' % (name, v))

```

    ########################################
    Degree 1 in 2D:

    # Value at xc (also the cell average value)
    A[icell, 0, 0] = 1/3
    A[icell, 0, 1] = 1/3
    A[icell, 0, 2] = 1/3

    # d/dx
    A[icell, 1, 0] = (y2 - y3)/D
    A[icell, 1, 1] = (-y1 + y3)/D
    A[icell, 1, 2] = (y1 - y2)/D

    # d/dy
    A[icell, 2, 0] = (-x2 + x3)/D
    A[icell, 2, 1] = (x1 - x3)/D
    A[icell, 2, 2] = (-x1 + x2)/D

    ########################################
    Degree 2 in 2D:

    # Cell average value
    A[icell, 0, 3] = 1/3
    A[icell, 0, 4] = 1/3
    A[icell, 0, 5] = 1/3

    # d/dx
    A[icell, 1, 0] = (y2 - y3)/(3*D)
    A[icell, 1, 1] = (-y1 + y3)/(3*D)
    A[icell, 1, 2] = (y1 - y2)/(3*D)
    A[icell, 1, 3] = 4*(-y2 + y3)/(3*D)
    A[icell, 1, 4] = 4*(y1 - y3)/(3*D)
    A[icell, 1, 5] = 4*(-y1 + y2)/(3*D)

    # d/dy
    A[icell, 2, 0] = (-x2 + x3)/(3*D)
    A[icell, 2, 1] = (x1 - x3)/(3*D)
    A[icell, 2, 2] = (-x1 + x2)/(3*D)
    A[icell, 2, 3] = 4*(x2 - x3)/(3*D)
    A[icell, 2, 4] = 4*(-x1 + x3)/(3*D)
    A[icell, 2, 5] = 4*(x1 - x2)/(3*D)

    # d/dx^2
    A[icell, 3, 0] = 4*(y2 - y3)**2/D**2
    A[icell, 3, 1] = 4*(y1 - y3)**2/D**2
    A[icell, 3, 2] = 4*(-y1 + y2)**2/D**2
    A[icell, 3, 3] = 8*(-y1 + y2)*(y1 - y3)/D**2
    A[icell, 3, 4] = 8*(y1 - y2)*(y2 - y3)/D**2
    A[icell, 3, 5] = -8*(y1 - y3)*(y2 - y3)/D**2

    # d/dy^2
    A[icell, 4, 0] = 4*(x2 - x3)**2/D**2
    A[icell, 4, 1] = 4*(x1 - x3)**2/D**2
    A[icell, 4, 2] = 4*(x1 - x2)**2/D**2
    A[icell, 4, 3] = -8*(x1 - x2)*(x1 - x3)/D**2
    A[icell, 4, 4] = 8*(x1 - x2)*(x2 - x3)/D**2
    A[icell, 4, 5] = -8*(x1 - x3)*(x2 - x3)/D**2

    # d/dx*dy
    A[icell, 5, 0] = -4*(x2 - x3)*(y2 - y3)/D**2
    A[icell, 5, 1] = -4*(x1 - x3)*(y1 - y3)/D**2
    A[icell, 5, 2] = 4*(x1 - x2)*(-y1 + y2)/D**2
    A[icell, 5, 3] = 4*((x1 - x2)*(y1 - y3) + (x1 - x3)*(y1 - y2))/D**2
    A[icell, 5, 4] = -(4*(x1 - x2)*(y2 - y3) + 4*(x2 - x3)*(y1 - y2))/D**2
    A[icell, 5, 5] = 4*((x1 - x3)*(y2 - y3) + (x2 - x3)*(y1 - y3))/D**2

    bar_xx = x1**2/36 - x1*x2/36 - x1*x3/36 + x2**2/36 - x2*x3/36 + x3**2/36
    bar_yy = y1**2/36 - y1*y2/36 - y1*y3/36 + y2**2/36 - y2*y3/36 + y3**2/36
    bar_xy = x1*y1/18 - x1*y2/36 - x1*y3/36 - x2*y1/36 + x2*y2/18 - x2*y3/36 - x3*y1/36 - x3*y2/36 + x3*y3/18
    A[icell, 0, 0] = 0
    A[icell, 0, 1] = 0
    A[icell, 0, 2] = 0
    A[icell, 0, 3] = 1/3
    A[icell, 0, 4] = 1/3
    A[icell, 0, 5] = 1/3

## Code generation for transformation matrices, 3D

Use sympy to derive the coefficients needed to convert from DG polynomials
on a tetrahedron to a Taylor basis by expressing the derivatives at the centre
point of the triangle

```python
%reset -f

from sympy import Symbol, symbols, S, sqrt
x1, x2, x3, x4 = symbols('x1 x2 x3 x4', real=True)
y1, y2, y3, y4 = symbols('y1 y2 y3 y4', real=True)
z1, z2, z3, z4 = symbols('z1 z2 z3 z4', real=True)
x, y, z, F = symbols('x y z F', real=True)

#F = x1*y2*z3 - x1*y2*z4 - x1*y3*z2 + x1*y3*z4 + x1*y4*z2 - x1*y4*z3 - x2*y1*z3 + x2*y1*z4 + x2*y3*z1 - x2*y3*z4 - x2*y4*z1 + x2*y4*z3 + x3*y1*z2 - x3*y1*z4 - x3*y2*z1 + x3*y2*z4 + x3*y4*z1 - x3*y4*z2 - x4*y1*z2 + x4*y1*z3 + x4*y2*z1 - x4*y2*z3 - x4*y3*z1 + x4*y3*z2
L1f = (x*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) - x2*y3*z4 + x2*y4*z3 + x3*y2*z4 - x3*y4*z2 - x4*y2*z3 + x4*y3*z2 - y*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + z*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F
L2f = (-x*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + x1*y3*z4 - x1*y4*z3 - x3*y1*z4 + x3*y4*z1 + x4*y1*z3 - x4*y3*z1 + y*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3) - z*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3))/F
L3f = (x*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2) - x1*y2*z4 + x1*y4*z2 + x2*y1*z4 - x2*y4*z1 - x4*y1*z2 + x4*y2*z1 - y*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2) + z*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2))/F
L4f = (-x*y1*z2 + x*y1*z3 + x*y2*z1 - x*y2*z3 - x*y3*z1 + x*y3*z2 + x1*y*z2 - x1*y*z3 - x1*y2*z + x1*y2*z3 + x1*y3*z - x1*y3*z2 - x2*y*z1 + x2*y*z3 + x2*y1*z - x2*y1*z3 - x2*y3*z + x2*y3*z1 + x3*y*z1 - x3*y*z2 - x3*y1*z + x3*y1*z2 + x3*y2*z - x3*y2*z1)/F
L1, L2, L3, L4 = Symbol('L1')(x, y, z), Symbol('L2')(x, y, z), Symbol('L3')(x, y, z), Symbol('L4')(x, y, z)

# Construct replacements
Ls = [L1, L2, L3, L4]
Lfs = [L1f, L2f, L3f, L4f]
replacements = []
for LX, LXf in zip(Ls, Lfs):
    replacements.append((LX.diff(x, x), LXf.diff(x, x)))
    replacements.append((LX.diff(y, y), LXf.diff(y, y)))
    replacements.append((LX.diff(z, z), LXf.diff(z, z)))
    replacements.append((LX.diff(x, y), LXf.diff(x, y)))
    replacements.append((LX.diff(x, z), LXf.diff(x, z)))
    replacements.append((LX.diff(y, z), LXf.diff(y, z)))
    replacements.append((LX.diff(x), LXf.diff(x)))
    replacements.append((LX.diff(y), LXf.diff(y)))
    replacements.append((LX.diff(z), LXf.diff(z)))
    replacements.append((LX, 1/S(4)))
replacements.append((x, (x1 + x2 + x3 + x4)/4))
replacements.append((y, (y1 + y2 + y3 + y4)/4))
replacements.append((z, (z1 + z2 + z3 + z4)/4))

def print_code(name, func, index):
    code = ['# %s' % name]
    for i, phii in enumerate(phi):
        v = func(phii)
        v = v.subs(replacements)
        v = v.simplify()
        vstr = str(v).replace('Abs', 'abs')
        code.append('A[icell, %d, %d] = %s' % (index, i, vstr))
    code.append('')
    print('\n'.join(code))

print('########################################')
print('Degree 1 in 3D:\n')

# Barycentric linear shape functions on a tetrahedron
phi = [L1, L2, L3, L4]

print_code('Value at xc (also the cell average value)', lambda q: q, 0)
print_code('d/dx', lambda q: q.diff(x), 1)
print_code('d/dy', lambda q: q.diff(y), 2)
print_code('d/dz', lambda q: q.diff(z), 3)

print('########################################')
print('Degree 2 in 3D:\n')

# Barycentric quadratic shape functions on a tetrahedron
# see documentation/notebooks/barycentric.ipynb
phi = [None]*10
phi[0] = L1*(2*L1 - 1)
phi[1] = L2*(2*L2 - 1)
phi[2] = L3*(2*L3 - 1)
phi[3] = L4*(2*L4 - 1)
phi[4] = 4*L3*L4
phi[5] = 4*L2*L4
phi[6] = 4*L2*L3
phi[7] = 4*L1*L4
phi[8] = 4*L1*L3
phi[9] = 4*L1*L2

#print_code('Value at xc', lambda q: q, 0)
print('# Cell average value')
print('A[icell, 0, 0] = -1/20')
print('A[icell, 0, 1] = -1/20')
print('A[icell, 0, 2] = -1/20')
print('A[icell, 0, 3] = -1/20')
print('A[icell, 0, 4] = 1/5')
print('A[icell, 0, 5] = 1/5')
print('A[icell, 0, 6] = 1/5')
print('A[icell, 0, 7] = 1/5')
print('A[icell, 0, 8] = 1/5')
print('A[icell, 0, 9] = 1/5\n')

print_code('d/dx', lambda q: q.diff(x), 1)
print_code('d/dy', lambda q: q.diff(y), 2)
print_code('d/dz', lambda q: q.diff(z), 3)
print_code('d/dx^2', lambda q: q.diff(x, x), 4)
print_code('d/dy^2', lambda q: q.diff(y, y), 5)
print_code('d/dz^2', lambda q: q.diff(z, z), 6)
print_code('d/dx*dy', lambda q: q.diff(x, y), 7)
print_code('d/dx*dz', lambda q: q.diff(x, z), 8)
print_code('d/dy*dz', lambda q: q.diff(y, z), 9)

xv = x1*L1 + x2*L2 + x3*L3 + x4*L4
yv = y1*L1 + y2*L2 + y3*L3 + y4*L4
zv = z1*L1 + z2*L2 + z3*L3 + z4*L4
xc = xv.subs({L1: 1/S(4), L2: 1/S(4), L3: 1/S(4), L4: 1/S(4)})
yc = yv.subs({L1: 1/S(4), L2: 1/S(4), L3: 1/S(4), L4: 1/S(4)})
zc = zv.subs({L1: 1/S(4), L2: 1/S(4), L3: 1/S(4), L4: 1/S(4)})

exprs = [('bar_xx', (xv-xc)**2/2),
         ('bar_yy', (yv-yc)**2/2),
         ('bar_zz', (zv-zc)**2/2),
         ('bar_xy', (xv-xc)*(yv-yc)),
         ('bar_xz', (xv-xc)*(zv-zc)),
         ('bar_yz', (yv-yc)*(zv-zc))]
for i in range(10):
    exprs.append(('A[icell, 0, %d]' % i, phi[i]))

# see, e.g., https://github.com/libMesh/libmesh/blob/master/src/quadrature/quadrature_gauss_3D.C
BT_QUADRATURE_TABLE = {}
I = S(1)
a = 1/(3*sqrt(5) - 5)
b = 1/(1*sqrt(5) + 5)
BT_QUADRATURE_TABLE[1] = ([[I/4, I/4, I/4]], [I/6])
BT_QUADRATURE_TABLE[2] = ([[a, b, b], [b, a, b], [b, b, a], [b, b, b]], [I/24, I/24, I/24, I/24])
BT_QUADRATURE_TABLE[3] = ([[I/4, I/4, I/4],
                           [I/2, I/6, I/6],
                           [I/6, I/2, I/6],
                           [I/6, I/6, I/2],
                           [I/6, I/6, I/6]],
                          [S(-2)/15, S(3)/40, S(3)/40, S(3)/40, S(3)/40])

order = 3
points, weights = BT_QUADRATURE_TABLE[order]
for name, expr in exprs:
    v = 0
    for point, weight in zip(points, weights):
        loc = {L1: point[0],
               L2: point[1],
               L3: point[2],
               L4: 1 - point[0] - point[1] - point[2]}
        v += 6*weight*expr.subs(loc).simplify()
    v = v.simplify().simplify()
    v = v.subs({x1: Symbol('x1'), x2: Symbol('x2'), x3: Symbol('x3'), x4: Symbol('x4'),
                y1: Symbol('y1'), y2: Symbol('y2'), y3: Symbol('y3'), y4: Symbol('y4'),
                z1: Symbol('z1'), z2: Symbol('z2'), z3: Symbol('z3'), z4: Symbol('z4')})
    print('%s = %s' % (name, v))
```

    ########################################
    Degree 1 in 3D:

    # Value at xc (also the cell average value)
    A[icell, 0, 0] = 1/4
    A[icell, 0, 1] = 1/4
    A[icell, 0, 2] = 1/4
    A[icell, 0, 3] = 1/4

    # d/dx
    A[icell, 1, 0] = (y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F
    A[icell, 1, 1] = (-y1*z3 + y1*z4 + y3*z1 - y3*z4 - y4*z1 + y4*z3)/F
    A[icell, 1, 2] = (y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)/F
    A[icell, 1, 3] = (-y1*z2 + y1*z3 + y2*z1 - y2*z3 - y3*z1 + y3*z2)/F

    # d/dy
    A[icell, 2, 0] = (-x2*z3 + x2*z4 + x3*z2 - x3*z4 - x4*z2 + x4*z3)/F
    A[icell, 2, 1] = (x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)/F
    A[icell, 2, 2] = (-x1*z2 + x1*z4 + x2*z1 - x2*z4 - x4*z1 + x4*z2)/F
    A[icell, 2, 3] = (x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)/F

    # d/dz
    A[icell, 3, 0] = (x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)/F
    A[icell, 3, 1] = (-x1*y3 + x1*y4 + x3*y1 - x3*y4 - x4*y1 + x4*y3)/F
    A[icell, 3, 2] = (x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)/F
    A[icell, 3, 3] = (-x1*y2 + x1*y3 + x2*y1 - x2*y3 - x3*y1 + x3*y2)/F

    ########################################
    Degree 2 in 3D:

    # Cell average value
    A[icell, 0, 0] = -1/20
    A[icell, 0, 1] = -1/20
    A[icell, 0, 2] = -1/20
    A[icell, 0, 3] = -1/20
    A[icell, 0, 4] = 1/5
    A[icell, 0, 5] = 1/5
    A[icell, 0, 6] = 1/5
    A[icell, 0, 7] = 1/5
    A[icell, 0, 8] = 1/5
    A[icell, 0, 9] = 1/5

    # d/dx
    A[icell, 1, 0] = 0
    A[icell, 1, 1] = 0
    A[icell, 1, 2] = 0
    A[icell, 1, 3] = 0
    A[icell, 1, 4] = (y1*z3 - y1*z4 - y2*z3 + y2*z4 - y3*z1 + y3*z2 + y4*z1 - y4*z2)/F
    A[icell, 1, 5] = (-y1*z2 + y1*z4 + y2*z1 - y2*z3 + y3*z2 - y3*z4 - y4*z1 + y4*z3)/F
    A[icell, 1, 6] = (y1*z2 - y1*z3 - y2*z1 + y2*z4 + y3*z1 - y3*z4 - y4*z2 + y4*z3)/F
    A[icell, 1, 7] = (-y1*z2 + y1*z3 + y2*z1 - y2*z4 - y3*z1 + y3*z4 + y4*z2 - y4*z3)/F
    A[icell, 1, 8] = (y1*z2 - y1*z4 - y2*z1 + y2*z3 - y3*z2 + y3*z4 + y4*z1 - y4*z3)/F
    A[icell, 1, 9] = (-y1*z3 + y1*z4 + y2*z3 - y2*z4 + y3*z1 - y3*z2 - y4*z1 + y4*z2)/F

    # d/dy
    A[icell, 2, 0] = 0
    A[icell, 2, 1] = 0
    A[icell, 2, 2] = 0
    A[icell, 2, 3] = 0
    A[icell, 2, 4] = (-x1*z3 + x1*z4 + x2*z3 - x2*z4 + x3*z1 - x3*z2 - x4*z1 + x4*z2)/F
    A[icell, 2, 5] = (x1*z2 - x1*z4 - x2*z1 + x2*z3 - x3*z2 + x3*z4 + x4*z1 - x4*z3)/F
    A[icell, 2, 6] = (-x1*z2 + x1*z3 + x2*z1 - x2*z4 - x3*z1 + x3*z4 + x4*z2 - x4*z3)/F
    A[icell, 2, 7] = (x1*z2 - x1*z3 - x2*z1 + x2*z4 + x3*z1 - x3*z4 - x4*z2 + x4*z3)/F
    A[icell, 2, 8] = (-x1*z2 + x1*z4 + x2*z1 - x2*z3 + x3*z2 - x3*z4 - x4*z1 + x4*z3)/F
    A[icell, 2, 9] = (x1*z3 - x1*z4 - x2*z3 + x2*z4 - x3*z1 + x3*z2 + x4*z1 - x4*z2)/F

    # d/dz
    A[icell, 3, 0] = 0
    A[icell, 3, 1] = 0
    A[icell, 3, 2] = 0
    A[icell, 3, 3] = 0
    A[icell, 3, 4] = (x1*y3 - x1*y4 - x2*y3 + x2*y4 - x3*y1 + x3*y2 + x4*y1 - x4*y2)/F
    A[icell, 3, 5] = (-x1*y2 + x1*y4 + x2*y1 - x2*y3 + x3*y2 - x3*y4 - x4*y1 + x4*y3)/F
    A[icell, 3, 6] = (x1*y2 - x1*y3 - x2*y1 + x2*y4 + x3*y1 - x3*y4 - x4*y2 + x4*y3)/F
    A[icell, 3, 7] = (-x1*y2 + x1*y3 + x2*y1 - x2*y4 - x3*y1 + x3*y4 + x4*y2 - x4*y3)/F
    A[icell, 3, 8] = (x1*y2 - x1*y4 - x2*y1 + x2*y3 - x3*y2 + x3*y4 + x4*y1 - x4*y3)/F
    A[icell, 3, 9] = (-x1*y3 + x1*y4 + x2*y3 - x2*y4 + x3*y1 - x3*y2 - x4*y1 + x4*y2)/F

    # d/dx^2
    A[icell, 4, 0] = 4*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)**2/F**2
    A[icell, 4, 1] = 4*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)**2/F**2
    A[icell, 4, 2] = 4*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)**2/F**2
    A[icell, 4, 3] = 4*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)**2/F**2
    A[icell, 4, 4] = -8*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)/F**2
    A[icell, 4, 5] = 8*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)/F**2
    A[icell, 4, 6] = -8*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)/F**2
    A[icell, 4, 7] = -8*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F**2
    A[icell, 4, 8] = 8*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F**2
    A[icell, 4, 9] = -8*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F**2

    # d/dy^2
    A[icell, 5, 0] = 4*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)**2/F**2
    A[icell, 5, 1] = 4*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)**2/F**2
    A[icell, 5, 2] = 4*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)**2/F**2
    A[icell, 5, 3] = 4*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)**2/F**2
    A[icell, 5, 4] = -8*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)/F**2
    A[icell, 5, 5] = 8*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)/F**2
    A[icell, 5, 6] = -8*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)/F**2
    A[icell, 5, 7] = -8*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)/F**2
    A[icell, 5, 8] = 8*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)/F**2
    A[icell, 5, 9] = -8*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)/F**2

    # d/dz^2
    A[icell, 6, 0] = 4*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)**2/F**2
    A[icell, 6, 1] = 4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)**2/F**2
    A[icell, 6, 2] = 4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)**2/F**2
    A[icell, 6, 3] = 4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)**2/F**2
    A[icell, 6, 4] = -8*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)/F**2
    A[icell, 6, 5] = 8*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)/F**2
    A[icell, 6, 6] = -8*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)/F**2
    A[icell, 6, 7] = -8*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)/F**2
    A[icell, 6, 8] = 8*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)/F**2
    A[icell, 6, 9] = -8*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)/F**2

    # d/dx*dy
    A[icell, 7, 0] = -4*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F**2
    A[icell, 7, 1] = -4*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)/F**2
    A[icell, 7, 2] = -4*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)/F**2
    A[icell, 7, 3] = -4*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)/F**2
    A[icell, 7, 4] = 4*((x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2) + (x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 7, 5] = -(4*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + 4*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 7, 6] = 4*((x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + (x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2))/F**2
    A[icell, 7, 7] = 4*((x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + (x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 7, 8] = -(4*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + 4*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2))/F**2
    A[icell, 7, 9] = 4*((x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + (x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3))/F**2

    # d/dx*dz
    A[icell, 8, 0] = 4*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3)/F**2
    A[icell, 8, 1] = 4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3)/F**2
    A[icell, 8, 2] = 4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2)/F**2
    A[icell, 8, 3] = 4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2)/F**2
    A[icell, 8, 4] = -(4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2) + 4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 8, 5] = 4*((x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + (x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 8, 6] = -(4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3) + 4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2))/F**2
    A[icell, 8, 7] = -(4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + 4*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)*(y1*z2 - y1*z3 - y2*z1 + y2*z3 + y3*z1 - y3*z2))/F**2
    A[icell, 8, 8] = 4*((x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + (x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)*(y1*z2 - y1*z4 - y2*z1 + y2*z4 + y4*z1 - y4*z2))/F**2
    A[icell, 8, 9] = -(4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(y2*z3 - y2*z4 - y3*z2 + y3*z4 + y4*z2 - y4*z3) + 4*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)*(y1*z3 - y1*z4 - y3*z1 + y3*z4 + y4*z1 - y4*z3))/F**2

    # d/dy*dz
    A[icell, 9, 0] = -4*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3)/F**2
    A[icell, 9, 1] = -4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)/F**2
    A[icell, 9, 2] = -4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)/F**2
    A[icell, 9, 3] = -4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)/F**2
    A[icell, 9, 4] = 4*((x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2) + (x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2))/F**2
    A[icell, 9, 5] = -(4*(x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3) + 4*(x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2))/F**2
    A[icell, 9, 6] = 4*((x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3) + (x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2))/F**2
    A[icell, 9, 7] = 4*((x1*y2 - x1*y3 - x2*y1 + x2*y3 + x3*y1 - x3*y2)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + (x1*z2 - x1*z3 - x2*z1 + x2*z3 + x3*z1 - x3*z2)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F**2
    A[icell, 9, 8] = -(4*(x1*y2 - x1*y4 - x2*y1 + x2*y4 + x4*y1 - x4*y2)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + 4*(x1*z2 - x1*z4 - x2*z1 + x2*z4 + x4*z1 - x4*z2)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F**2
    A[icell, 9, 9] = 4*((x1*y3 - x1*y4 - x3*y1 + x3*y4 + x4*y1 - x4*y3)*(x2*z3 - x2*z4 - x3*z2 + x3*z4 + x4*z2 - x4*z3) + (x1*z3 - x1*z4 - x3*z1 + x3*z4 + x4*z1 - x4*z3)*(x2*y3 - x2*y4 - x3*y2 + x3*y4 + x4*y2 - x4*y3))/F**2

    bar_xx = (-3*x1 + x2 + x3 + x4)**2/640 + (x1 - 3*x2 + x3 + x4)**2/640 + (x1 + x2 - 3*x3 + x4)**2/640 + (x1 + x2 + x3 - 3*x4)**2/640
    bar_yy = (-3*y1 + y2 + y3 + y4)**2/640 + (y1 - 3*y2 + y3 + y4)**2/640 + (y1 + y2 - 3*y3 + y4)**2/640 + (y1 + y2 + y3 - 3*y4)**2/640
    bar_zz = (-3*z1 + z2 + z3 + z4)**2/640 + (z1 - 3*z2 + z3 + z4)**2/640 + (z1 + z2 - 3*z3 + z4)**2/640 + (z1 + z2 + z3 - 3*z4)**2/640
    bar_xy = 3*x1*y1/80 - x1*y2/80 - x1*y3/80 - x1*y4/80 - x2*y1/80 + 3*x2*y2/80 - x2*y3/80 - x2*y4/80 - x3*y1/80 - x3*y2/80 + 3*x3*y3/80 - x3*y4/80 - x4*y1/80 - x4*y2/80 - x4*y3/80 + 3*x4*y4/80
    bar_xz = 3*x1*z1/80 - x1*z2/80 - x1*z3/80 - x1*z4/80 - x2*z1/80 + 3*x2*z2/80 - x2*z3/80 - x2*z4/80 - x3*z1/80 - x3*z2/80 + 3*x3*z3/80 - x3*z4/80 - x4*z1/80 - x4*z2/80 - x4*z3/80 + 3*x4*z4/80
    bar_yz = 3*y1*z1/80 - y1*z2/80 - y1*z3/80 - y1*z4/80 - y2*z1/80 + 3*y2*z2/80 - y2*z3/80 - y2*z4/80 - y3*z1/80 - y3*z2/80 + 3*y3*z3/80 - y3*z4/80 - y4*z1/80 - y4*z2/80 - y4*z3/80 + 3*y4*z4/80
    A[icell, 0, 0] = -1/20
    A[icell, 0, 1] = -1/20
    A[icell, 0, 2] = -1/20
    A[icell, 0, 3] = -1/20
    A[icell, 0, 4] = 1/5
    A[icell, 0, 5] = 1/5
    A[icell, 0, 6] = 1/5
    A[icell, 0, 7] = 1/5
    A[icell, 0, 8] = 1/5
    A[icell, 0, 9] = 1/5

```python

```


# The $n$-Queens puzzle: Introduction and overview

The classic [eight queens puzzle](https://en.wikipedia.org/wiki/Eight_queens_puzzle) refers to the challenge of placing eight [chess](https://en.wikipedia.org/wiki/Chess) [queens](https://en.wikipedia.org/wiki/Queen_(chess)) on an $8 \times 8$ board such that no queen is attacking any other.  Specifically, there can only be (at most) a single queen on any row, column, or diagonal of the board.  Here is one solution to the eight queens puzzle (there are 92 distinct solutions in total, or 12 if rotations and reflections are not counted as separate solutions):

In principle, the eight queens puzzle may be generalized to imaginary chessboards of any size.  For $n \geq 1$, placing $n$ queens on an $n \times n$ chess board is referred to as the $n$-queens puzzle.

For this assignment, you'll be building a solver for the $n$-queens puzzle.  There are two general functions you'll need to write:
1. A function for printing out a board position (e.g., depicting the configuration of the pieces visually)
2. A function for checking whether a given position corresponds to a solution (i.e., whether any queens are attacking each other)

Using these two functions, you'll be provided with functions for returning all solutions (given $n \geq 1$) and for returning the *number* of solutions (given $n \geq 1$).

## Some history of the $n$-queens puzzle
In [this 2017 paper](https://www.jair.org/index.php/jair/article/view/11079), the $n$-queens puzzle (also called the $n$-queens problem) was shown to be a member of a set of problems in computer science that are [NP-complete](https://en.wikipedia.org/wiki/NP-completeness).  Exploring or proving NP-completeness is beyond the scope of our course, but the super high-level intuition for NP-completeness may be summarized as follows:
  - For all known ways of solving them, NP-complete problems take a very long time to solve (their associated computations don't scale well).  NP stands for "nondeterministic polynomial".  The "polynomial" part refers to how many computations it takes to solve the problem and verify its solution as a function of the size of the problem (e.g., the '$n$' in the $n$-queens puzzle).  The "nondeterministic" part means that there's no known direct (deterministic) strategy or set of rules that can be followed to generate a solution.
  - Because all NP-complete problems can be efficiently(ish) converted to other NP-complete problems, solving any one NP-complete problem efficiently would also provide an efficient solution for *all* NP-complete problems.

Solving NP-complete problems efficiently (or even proving whether or not NP-complete problems *can* be solved efficiently) is an [open question in computer science](https://en.wikipedia.org/wiki/P_versus_NP_problem).  Even early on in your journey towards learning to program, it can still be fun and instructive to explore problems like the $n$-queens puzzle.  Before reading the next section, where I'll present the way of solving the puzzle that we'll be using in this assignment, it's worth taking a few moments to think about how *you* might go about solving it.  For example:
  - How might you represent the chess board using some of the Python datatypes that you've already learned about?
  - How might you represent the positions of the pieces (queens) on the board?
  - How might you go about systematically generating "guesses" about possible solutions?
  - How might you go about *checking* whether a particular guess is a valid solution?

I encourage you to sketch out some thoughts and/or [discuss](https://gitter.im/cs-for-psych/PSYC-132-Winter-2021) with classmates.

# General (naïve-ish) solution

**(Spoiler alert!)**

First, we'll represent the board in an efficient way: an $n$-element list, where the position reflects the column number and the value reflects the row of the queen at that column.

For example, the above solution to the 8-queens puzzle could be represented by the list [1, 3, 5, 7, 2, 0, 6, 4].  Make sure you understand why that particular list corresponds to that particular position before moving on.  (Hint: remember that positions are [zero-indexed](https://en.wikipedia.org/wiki/Zero-based_numbering) in Python.)  Try writing out the board positions for some other lists of different lengths-- e.g. [0, 3, 1, 2] or [4, 3, 2, 1, 0], etc.

Notice that, with this notation, every possible solution to the $n$-queens puzzle must be a permutation of the integers from 0 to $n-1$. This ensures that at most a single queen is placed in each row and column.  As long as there are no repeated numbers in the list, we know that no queens are attacking each other vertically or horizontally (since each list element only has one number).

Next we need to check for diagonal attacks. Using the same notation, we can check to see if queens are attacking each other along a "forward" (top-left to bottom-right) diagonal by asking whether any queens share the same difference between their row and column. In other words: if $r_0...(n-1)$ represents the queens' rows and $c_0...(n-1)$ represents the queens' columns, we must ensure that $r_i - c_i$ never equals $r_j - c_j$ for any value of $i \neq j$. This can be checked efficiently by ensuring that `len(np.unique(np.arange(n) - pos)) == n`. Finally, to check the "reverse" diagonals (i.e. top-right to bottom-left), we can use analogous logic and verify that `len(np.unique(np.arange(n-1, -1, -1) - pos)) == n` as well.

If no queens share a row, column, forward diagonal, or reverse diagonal, and if $n$ queens have been placed on the $n \times n$ board, then we have found a solution!

Naïvely, to find every solution of the $n$-queens puzzle for a given $n$, we could simply iterate through all possible permutations of the integers from $0...(n-1)$, check whether each is a valid solution, and collect up all of the permutations that are valid solutions. The challenge is that the number of permutations of $n$ numbers is $n$ factorial (i.e., $n!$), which becomes intractable as $n$ gets large. For example, storing a single integer in short (16 [bit](https://en.wikipedia.org/wiki/Bit)) format requires 2 [bytes](https://en.wikipedia.org/wiki/Byte). Therefore storing a single position (of length $n$) requires $2n$ bytes. Storing all possible $n!$ permutations for a $13 \times 13$ board would require over 12[GB](https://en.wikipedia.org/wiki/Gigabyte) of memory, and for a $14 \times 14$ board would require over 174GB!

Since our main objective for this assignment is to practice and improve our programming skills, we won't worry about scalability here-- you can safely assume that you'll never need to solve (or count solutions for) $n > 12$.

# Grading

This assignment is worth a total of 5 points.  You may view the tests in the public rubric [here](https://github.com/ContextLab/cs-for-psych/blob/master/assignments/n-queens/public_rubric.xls).  To get full credit, you must (correctly) fill in all of the code between the commented `### BEGIN YOUR CODE` and `### END YOUR CODE` blocks throughout this notebook.  (No other code should be modified.)

```
import numpy as np #the numpy library is used to represent and manipulate ordered sets (think of them like fancy lists)
from sympy.utilities.iterables import multiset_permutations #this is used to compute every permutation of a list
```

# Represent a board position as a string: `board2str`

We'll write a function to convert a board position to a string that may be printed out:
```python
>> print(board2str([0, 4, 7, 5, 2, 6, 1, 3]))

-|0|1|2|3|4|5|6|7|
0|*|-|-|-|-|-|-|-|
1|-|-|-|-|-|-|*|-|
2|-|-|-|-|*|-|-|-|
3|-|-|-|-|-|-|-|*|
4|-|*|-|-|-|-|-|-|
5|-|-|-|*|-|-|-|-|
6|-|-|-|-|-|*|-|-|
7|-|-|*|-|-|-|-|-|
```

Some things to note:
  - The `board2str` function takes as input a single argument, `pos`, which is a list representation of the board position
  - From this list representation, the function automatically determines how large the board is (`n = len(pos)`)
  - The rows and columns of the board are numbered (starting with 0)
  - Empty squares are denoted by `-` and queens are denoted by `*`
  - Rows are separated using the [newline character](https://www.freecodecamp.org/news/python-new-line-and-how-to-python-print-without-a-newline/), `\n`.  This tells Python that a single string occupies several lines, e.g. when it is printed out.

Given the input `[0, 4, 7, 5, 2, 6, 1, 3]`, `board2str` should return the following string:
```python
>> board2str([0, 4, 7, 5, 2, 6, 1, 3])

'-|0|1|2|3|4|5|6|7|\n0|*|-|-|-|-|-|-|-|\n1|-|-|-|-|-|-|*|-|\n2|-|-|-|-|*|-|-|-|\n3|-|-|-|-|-|-|-|*|\n4|-|*|-|-|-|-|-|-|\n5|-|-|-|*|-|-|-|-|\n6|-|-|-|-|-|*|-|-|\n7|-|-|*|-|-|-|-|-|'
```
Compare this to the printout above to get a feel for how it works.

```
def get_n(pos): #given a board position, compute the board size
  ### BEGIN YOUR CODE
  return 0
  ### END YOUR CODE

def board2str(pos):
  def row2str(c):
    #print out a single row with a queen in column c
    #e.g. row2str(0) should return the string '|*|-|-|-|-|-|-|-|'

    ### BEGIN YOUR CODE
    return ''
    ### END YOUR CODE

  def top_row(n):
    #print out the top row of labels, given the board size
    #e.g. top_row(3) should return '-|0|1|2|'

    ### BEGIN YOUR CODE
    return ''
    ### END YOUR CODE

  n = get_n(pos)
  board = [top_row(n)]
  for r in range(n):
    ### BEGIN YOUR CODE
    #compute the column containing the queen in row r.
    #hint: check out the np.where function
    c = 0
    ### END YOUR CODE
    board.append(str(r) + row2str(c))

  return '\n'.join(board)
```

# Check whether a position is a solution: `is_solved`

```
def is_solved(pos):
  n = get_n(pos)

  #check rows
  #hint: the columns are *always* unique, because of how the positions are
  #represented. how can you check whether the queens from different columns
  #occupy the same row?  you may find it useful to check out the np.unique
  #function!
  ### BEGIN YOUR CODE
  unique_rows = False
  ### END YOUR CODE

  #check forward and backward diagonals
  #hint: see the suggestions written out above in the "General (naïve-ish)
  #solution" section
  ### BEGIN YOUR CODE
  unique_forward_diagonals = False
  unique_backward_diagonals = False
  ### END YOUR CODE

  #return True if pos is a solution and False otherwise
  return unique_rows and unique_forward_diagonals and unique_backward_diagonals
```

# Solving the puzzle!

Now the fun part-- we can use your work in the cells above to solve the $n$-queens puzzle.  We'll churn through every possible permutation of the numbers $0...(n-1)$, check whether each of these "positions" is a valid solution to the $n$-queens puzzle, and keep track of all of the corresponding permutations (i.e., that checked out as valid solutions).

```
def get_solutions(n):
    solutions = []
    for p in multiset_permutations(np.arange(n).astype('int16')):
        if is_solved(p):
            solutions.append(p)
    return solutions

def count_solutions(n):
    return len(get_solutions(n))
```

# Sanity checks

Let's check that this works for the 8-queens puzzle (there should be 92 solutions)

```
eights = get_solutions(8)
print(f'Found {count_solutions(8)} solutions!')
```

And let's also print out one of the solutions; you should get the printout shown below:
```
-|0|1|2|3|4|5|6|7|
0|*|-|-|-|-|-|-|-|
1|-|-|-|-|-|-|*|-|
2|-|-|-|-|*|-|-|-|
3|-|-|-|-|-|-|-|*|
4|-|*|-|-|-|-|-|-|
5|-|-|-|*|-|-|-|-|
6|-|-|-|-|-|*|-|-|
7|-|-|*|-|-|-|-|-|
```

```
try:
    print(board2str(eights[0]))
except:
    pass
```


# 2D Shallow Water Equations

This notebook is a devito reimplementation from this [notebook](https://github.com/daniel-koehn/Differential-equations-earth-system/blob/master/10_Shallow_Water_Equation_2D/01_2D_Shallow_Water_Equations.ipynb). We will use an approximation to the Navier-Stokes equations - the 2D Shallow Water equations - in order to model the propagation of Tsunamis.

I've only made minor modifications to the text, in order to make it more compatible to the new coding format.

Author:

## Governing Equations

Starting from the continuity and momentum conservation equations, we want to solve the following problem:

For a given bathymetry model, which can include a complex seafloor topography, we want to model the amplitudes, speed and interaction of waves at the seasurface. At a given point $(x,\; y)$, the thickness of the water column between the seafloor and undisturbed water surface is defined by the variable $h$, while the wave amplitude is $\eta$ and therefore the whole thickness of the water column $D = h + \eta$.

Using appropriate boundary conditions at the water surface/seafloor, assuming that the horizontal wavelength of the modelled waves are much larger than the water depth and integrating the conservation of mass and momentum equations over the water column, we can derive the following equations to decribe wave propagation

\begin{equation}
\begin{split}
\frac{\partial \eta}{\partial t} &+ \frac{\partial M}{\partial x} + \frac{\partial N}{\partial y} = 0\\
\frac{\partial M}{\partial t} &+ \frac{\partial}{\partial x} \biggl(\frac{M^2}{D}\biggr) + \frac{\partial}{\partial y} \biggl(\frac{MN}{D}\biggr) + g D \frac{\partial \eta}{\partial x} + \frac{g \alpha^2}{D^{7/3}} M \sqrt{M^2+N^2} = 0\\
\frac{\partial N}{\partial t} &+ \frac{\partial}{\partial x} \biggl(\frac{MN}{D}\biggr) + \frac{\partial}{\partial y} \biggl(\frac{N^2}{D}\biggr) + g D \frac{\partial \eta}{\partial y} + \frac{g \alpha^2}{D^{7/3}} N \sqrt{M^2+N^2} = 0
\end{split}
\tag{1}
\end{equation}

known as **2D Shallow Water Equations (SWE)**. The derivation of these equations is beyond the scope of this notebook. Therefore, I refer to the [Tsunami Modelling Handbook](http://www.tsunami.civil.tohoku.ac.jp/hokusai3/J/projects/manual-ver-3.1.pdf) and the lecture [Shallow Water Derivation and Applications by Christian Kühbacher](http://www.mathematik.tu-dortmund.de/lsiii/cms/papers/Kuehbacher2009.pdf) for further details.

In eqs. (1) the discharge fluxes $M,\; N$ in x- and y-direction, respectively are given by

\begin{equation}
\begin{split}
M & = \int_{-h}^\eta u dz = u(h+\eta) = uD\\
N & = \int_{-h}^\eta v dz = v(h+\eta) = vD\\
\end{split}
\tag{2}
\end{equation}

with the horizontal velocity components $u,\;v$ in x- and y-direction, while $g$ denotes the gravity acceleration. The terms $\frac{g \alpha^2}{D^{7/3}} M \sqrt{M^2+N^2}$ and $\frac{g \alpha^2}{D^{7/3}} N \sqrt{M^2+N^2}$ describe the influence of seafloor friction on the wave amplitude. $\alpha$ denotes the Manning's roughness which can be as small as 0.01 for neat cement or smooth metal up to 0.06 for very poor natural channels (see [Tsunami modelling handbook](http://www.tsunami.civil.tohoku.ac.jp/hokusai3/J/projects/manual-ver-3.1.pdf)).

The Shallow Water Equations can be applied to

- Tsunami prediction
- Atmospheric flow
- Storm surges
- Flows around structures
- Planetary flows

and easily extended to incorporate the effects of Coriolis forces, tides or wind stress.

## Operator implementation

This is a simple function which returns the operator that solves this equation. The important part is the operator, which contains all the equations expressed above.

```python
from devito import Eq, TimeFunction, sqrt, Function, Operator, Grid, solve, ConditionalDimension
from matplotlib import pyplot as plt
import numpy as np

def op_shallowWaterWave(etasave, eta, M, N, h, D, g, alpha):
    """
    Operator that solves the equations expressed above.
    It computes and returns the discharge fluxes M, N and wave height eta from
    the 2D Shallow water equation using the FTCS finite difference method.

    Parameters
    ----------
    eta : numpy.ndarray
        The initial wave height field as a 2D array of floats.
    M : numpy.ndarray
        The initial discharge flux field in x-direction as a 2D array of floats.
    N : numpy.ndarray
        The initial discharge flux field in y-direction as a 2D array of floats.
    h : numpy.ndarray
        Bathymetry model as a 2D array of floats.
    D : numpy.ndarray
        Total thickness of the water column.
    g : float
        gravity acceleration.
    alpha : float
        Manning's roughness coefficient.
    etasave : TimeFunction
        Function that is sampled in a different interval than the normal propagation
        and is responsible for saving the snapshots required for the following
        animations.
    """
    # Friction term expresses the loss of amplitude from the friction with the seafloor
    frictionTerm = g * alpha**2 * sqrt(M**2 + N**2 ) / D**(7./3.)

    # System of equations
    pde_eta = Eq(eta.dt + M.dxc + N.dyc)
    pde_M   = Eq(M.dt + (M**2/D).dxc + (M*N/D).dyc + g*D*eta.forward.dxc + frictionTerm*M)
    pde_N   = Eq(N.dt + (M*N/D).dxc + (N**2/D).dyc + g*D*eta.forward.dyc + frictionTerm*N)

    # Defining boundary conditions
    x, y = grid.dimensions
    t    = grid.stepping_dim
    bc_left   = Eq(eta[t+1, 0, y], eta[t+1, 1, y])
    bc_right  = Eq(eta[t+1, nx-1, y], eta[t+1, nx-2, y])
    bc_top    = Eq(eta[t+1, x, 0], eta[t+1, x, 1])
    bc_bottom = Eq(eta[t+1, x, ny-1], eta[t+1, x, ny-2])

    stencil_eta = solve(pde_eta, eta.forward)
    stencil_M   = solve(pde_M, M.forward)
    stencil_N   = solve(pde_N, N.forward)

    # Equations with the forward in time term isolated
    update_eta  = Eq(eta.forward, stencil_eta, subdomain=grid.interior)
    update_M    = Eq(M.forward, stencil_M, subdomain=grid.interior)
    update_N    = Eq(N.forward, stencil_N, subdomain=grid.interior)
    eq_D        = Eq(D, eta.forward + h)

    return Operator([update_eta, bc_left, bc_right, bc_top, bc_bottom,
                     update_M, update_N, eq_D] + [Eq(etasave, eta)])
```

    oi

The following function transforms the saved wavefield snapshots and transform them into a video compatible with jupyter notebook.

```python
#NBVAL_IGNORE_OUTPUT
#NBVAL_SKIP
from IPython.display import HTML, display
import matplotlib.pyplot as plt
import matplotlib.animation as animation

def snaps2video (eta, title):
    fig, ax = plt.subplots()
    matrice = ax.imshow(eta.data[0, :, :].T, vmin=-1, vmax=1, cmap="seismic")
    plt.colorbar(matrice)

    plt.xlabel('x')
    plt.ylabel('z')
    plt.title(title)

    def update(i):
        matrice.set_array(eta.data[i, :, :].T)
        return matrice,

    # Animation
    ani = animation.FuncAnimation(fig, update, frames=nsnaps, interval=50, blit=True)

    #plt.show()
    plt.close(ani._fig)
    display(HTML(ani.to_html5_video()))
```

## Example I: Tsunami in an ocean with constant depth

After writing the `Shallow_water_2D` code and all the required functions attached to it, we can define and run our first 2D Tsunami modelling run.

Let's assume that the ocean model is $ L_x = 100\; m$ in x-direction and $L_y = 100\; m$ in y-direction. The model is discretized with $nx=401$ gridpoints in x-direction and $ny=401$ gridpoints in y-direction, respectively.

In this first modelling run, we assume a constant bathymetry $h=50\;m$. The initial wave height field $\eta_0$ is defined as a Gaussian at the center of the model, with a half-width of 10 m and an amplitude of 0.5 m. Regarding the initial discharge fluxes, we assume that

\begin{equation}
\begin{split}
M_0(x,y) &= 100 \eta_0(x,y)\\
N_0(x,y) &= 0\\
\end{split}\notag
\end{equation}

Furthermore, Dirichlet boundary conditions for the discharge fluxes $M,\;N$ are assumed at all boundaries:

\begin{equation}
\begin{split}
M(0,y) &= M(L_x,y) = M(x,0) = M(x,L_y) = 0\\
N(0,y) &= N(L_x,y) = N(x,0) = N(x,L_y) = 0\\
\end{split} \notag
\end{equation}

For the wave height field $\eta$, it is essential that Neumann boundary conditions are set at all boundaries:

\begin{equation}
\begin{split}
&\frac{\partial \eta}{\partial x}(0,y) = 0\\
&\frac{\partial \eta}{\partial x}(L_x,y) = 0\\
&\frac{\partial \eta}{\partial y}(x,0) = 0\\
&\frac{\partial \eta}{\partial y}(x,L_y) = 0\\
\end{split}\notag
\end{equation}

in order to avoid the occurence of high frequency artifacts, when waves are interacting with the boundaries. Notice, that the assumed boundary conditions lead to significant boundary reflections which might be not realistic for a given problem.

Let's assume the gravity $g = 9.81$ and the Manning's roughness coefficient $\alpha = 0.025$ for the all the remaining examples.

```python
Lx    = 100.0   # width of the mantle in the x direction []
Ly    = 100.0   # thickness of the mantle in the y direction []
nx    = 401     # number of points in the x direction
ny    = 401     # number of points in the y direction
dx    = Lx / (nx - 1)  # grid spacing in the x direction []
dy    = Ly / (ny - 1)  # grid spacing in the y direction []
g     = 9.81  # gravity acceleration [m/s^2]
alpha = 0.025 # friction coefficient for natural channels in good condition
# Maximum wave propagation time [s]
Tmax  = 6.
dt    = 1/4500.
nt    = (int)(Tmax/dt)
print(dt, nt)

x = np.linspace(0.0, Lx, num=nx)
y = np.linspace(0.0, Ly, num=ny)

# Define initial eta, M, N
X, Y = np.meshgrid(x,y) # coordinates X,Y required to define eta, h, M, N

# Define constant ocean depth profile h = 50 m
h0 = 50. * np.ones_like(X)

# Define initial eta Gaussian distribution [m]
eta0 = 0.5 * np.exp(-((X-50)**2/10)-((Y-50)**2/10))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + 50.

grid  = Grid(shape=(ny, nx), extent=(Ly, Lx))
```

    0.00022222222222222223 27000

```python
nsnaps = 100

# Defining devito symbolic functions
eta   = TimeFunction(name='eta', grid=grid, space_order=2)
M     = TimeFunction(name='M', grid=grid, space_order=2)
N     = TimeFunction(name='N', grid=grid, space_order=2)
h     = Function(name='h', grid=grid)
D     = Function(name='D', grid=grid)

# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Compile the operator
optime = op_shallowWaterWave(etasave, eta, M, N, h, D, g, alpha)

# Use the operator
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling a tsunami in an ocean with constant depth")
```

    The nt/nsnaps factor is 270
    (100, 401, 401)

    Operator `Kernel` ran in 17.25 s

Let's take a look on what the code looks like

```python
print(optime.ccode)
```

    #define _POSIX_C_SOURCE 200809L
    #define START_TIMER(S) struct timeval start_ ## S , end_ ## S ; gettimeofday(&start_ ## S , NULL);
    #define STOP_TIMER(S,T) gettimeofday(&end_ ## S, NULL); T->S += (double)(end_ ## S .tv_sec-start_ ## S.tv_sec)+(double)(end_ ## S .tv_usec-start_ ## S .tv_usec)/1000000;

    #include "stdlib.h"
    #include "math.h"
    #include "sys/time.h"
    #include "xmmintrin.h"
    #include "pmmintrin.h"
    #include "omp.h"

    struct dataobj
    {
      void *restrict data;
      int * size;
      int * npsize;
      int * dsize;
      int * hsize;
      int * hofs;
      int * oofs;
    } ;

    struct profiler
    {
      double section0;
      double section1;
      double section2;
      double section3;
      double section4;
    } ;

    int Kernel(struct dataobj *restrict D_vec, struct dataobj *restrict M_vec, struct dataobj *restrict N_vec, const float dt, struct dataobj *restrict eta_vec, struct dataobj *restrict etasave_vec, struct dataobj *restrict h_vec, const float h_x, const float h_y, const int i0x_ltkn, const int i0x_rtkn, const int i0y_ltkn, const int i0y_rtkn, const int time_M, const int time_m, const int x_M, const int x_m, const int y_M, const int y_m, const int nthreads, struct profiler * timers)
    {
      float (*restrict D)[D_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[D_vec->size[1]]) D_vec->data;
      float (*restrict M)[M_vec->size[1]][M_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[M_vec->size[1]][M_vec->size[2]]) M_vec->data;
      float (*restrict N)[N_vec->size[1]][N_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[N_vec->size[1]][N_vec->size[2]]) N_vec->data;
      float (*restrict eta)[eta_vec->size[1]][eta_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[eta_vec->size[1]][eta_vec->size[2]]) eta_vec->data;
      float (*restrict etasave)[etasave_vec->size[1]][etasave_vec->size[2]] __attribute__ ((aligned (64))) = (float (*)[etasave_vec->size[1]][etasave_vec->size[2]]) etasave_vec->data;
      float (*restrict h)[h_vec->size[1]] __attribute__ ((aligned (64))) = (float (*)[h_vec->size[1]]) h_vec->data;

      /* Flush denormal numbers to zero in hardware */
      _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
      _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);

      float r0 = 1.0F/h_x;
      float r1 = 1.0F/h_y;
      float r2 = 1.0F/dt;

      for (int time = time_m, t0 = (time)%(2), t1 = (time + 1)%(2); time <= time_M; time += 1, t0 = (time)%(2), t1 = (time + 1)%(2))
      {
        /* Begin section0 */
        START_TIMER(section0)
        #pragma omp parallel num_threads(nthreads)
        {
          #pragma omp for collapse(1) schedule(static,1)
          for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
          {
            #pragma omp simd aligned(M,N,eta:32)
            for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
            {
              eta[t1][i0x + 2][i0y + 2] = dt*(r2*eta[t0][i0x + 2][i0y + 2] + 5.0e-1F*(-r0*(-M[t0][i0x + 1][i0y + 2] + M[t0][i0x + 3][i0y + 2]) - r1*(-N[t0][i0x + 2][i0y + 1] + N[t0][i0x + 2][i0y + 3])));
            }
          }
        }
        STOP_TIMER(section0,timers)
        /* End section0 */

        /* Begin section1 */
        START_TIMER(section1)
        #pragma omp parallel num_threads(nthreads)
        {
          #pragma omp for collapse(1) schedule(static,1)
          for (int y = y_m; y <= y_M; y += 1)
          {
            eta[t1][2][y + 2] = eta[t1][3][y + 2];
            eta[t1][402][y + 2] = eta[t1][401][y + 2];
          }
        }
        STOP_TIMER(section1,timers)
        /* End section1 */

        /* Begin section2 */
        START_TIMER(section2)
        #pragma omp parallel num_threads(nthreads)
        {
          #pragma omp for collapse(1) schedule(static,1)
          for (int x = x_m; x <= x_M; x += 1)
          {
            eta[t1][x + 2][2] = eta[t1][x + 2][3];
            eta[t1][x + 2][402] = eta[t1][x + 2][401];
          }
        }
        STOP_TIMER(section2,timers)
        /* End section2 */
        if ((time)%(270) == 0)
        {
          /* Begin section3 */
          START_TIMER(section3)
          #pragma omp parallel num_threads(nthreads)
          {
            #pragma omp for collapse(1) schedule(static,1)
            for (int x = x_m; x <= x_M; x += 1)
            {
              #pragma omp simd aligned(eta,etasave:32)
              for (int y = y_m; y <= y_M; y += 1)
              {
                etasave[time / 270][x + 2][y + 2] = eta[t0][x + 2][y + 2];
              }
            }
          }
          STOP_TIMER(section3,timers)
          /* End section3 */
        }
        /* Begin section4 */
        START_TIMER(section4)
        #pragma omp parallel num_threads(nthreads)
        {
          #pragma omp for collapse(1) schedule(dynamic,1)
          for (int i0x = i0x_ltkn + x_m; i0x <= -i0x_rtkn + x_M; i0x += 1)
          {
            #pragma omp simd aligned(D,M,N,eta:32)
            for (int i0y = i0y_ltkn + y_m; i0y <= -i0y_rtkn + y_M; i0y += 1)
            {
              float r7 = 1.0F/D[i0x + 2][i0y + 1];
              float r6 = 1.0F/D[i0x + 1][i0y + 2];
              float r5 = 1.0F/D[i0x + 1][i0y + 1];
              float r4 = pow(D[i0x + 1][i0y + 1], -2.33333333333333F);
              float r3 = sqrt(pow(M[t0][i0x + 2][i0y + 2], 2) + pow(N[t0][i0x + 2][i0y + 2], 2));
              M[t1][i0x + 2][i0y + 2] = dt*(-4.905F*r0*(-eta[t1][i0x + 1][i0y + 2] + eta[t1][i0x + 3][i0y + 2])*D[i0x + 1][i0y + 1] + r2*M[t0][i0x + 2][i0y + 2] + r3*(-6.13125e-3F*r4*M[t0][i0x + 2][i0y + 2]) - r5*(-r0*M[t0][i0x + 2][i0y + 2]*M[t0][i0x + 2][i0y + 2]) - r5*(-r1*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2]) - r6*r1*M[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3] - r7*r0*(M[t0][i0x + 3][i0y + 2]*M[t0][i0x + 3][i0y + 2]));
              N[t1][i0x + 2][i0y + 2] = dt*(-4.905F*r1*(-eta[t1][i0x + 2][i0y + 1] + eta[t1][i0x + 2][i0y + 3])*D[i0x + 1][i0y + 1] + r2*N[t0][i0x + 2][i0y + 2] + r3*(-6.13125e-3F*r4*N[t0][i0x + 2][i0y + 2]) - r5*(-r1*N[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2]) - r5*(-r0*M[t0][i0x + 2][i0y + 2]*N[t0][i0x + 2][i0y + 2]) - r6*r1*(N[t0][i0x + 2][i0y + 3]*N[t0][i0x + 2][i0y + 3]) - r7*r0*M[t0][i0x + 3][i0y + 2]*N[t0][i0x + 3][i0y + 2]);
            }
          }
        }
        #pragma omp parallel num_threads(nthreads)
        {
          #pragma omp for collapse(1) schedule(static,1)
          for (int x = x_m; x <= x_M; x += 1)
          {
            #pragma omp simd aligned(D,eta,h:32)
            for (int y = y_m; y <= y_M; y += 1)
            {
              D[x + 1][y + 1] = h[x + 1][y + 1] + eta[t1][x + 2][y + 2];
            }
          }
        }
        STOP_TIMER(section4,timers)
        /* End section4 */
      }

      return 0;
    }

## Example II: Two Tsunamis in an ocean with constant depth

In example II, we will model the propagation and interaction of two Tsunamis, where the initial conditions for the wave height field $\eta_0$ consists of Gaussian functions with opposite sign located at $(x_1,\;y_1) = (35\; m,\; 35\; m)$ and $(x_2,\;y_2) = (65\; m,\; 65\; m)$. All other modelling parameters remain the same ...

```python
# Define constant ocean depth profile h = 50 m
h0 = 50 * np.ones_like(X)

# Define initial Gaussian eta distribution [m]
eta0 = 0.5 * np.exp(-((X-35)**2/10)-((Y-35)**2/10)) # first Tsunami source
eta0 -= 0.5 * np.exp(-((X-65)**2/10)-((Y-65)**2/10)) # add second Tsunami source

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + h0

# Maximum wave propagation time [s]
Tmax = 3.
dt = 1/8000.
nt = (int)(Tmax/dt)
```

```python
# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Use the operator. No need to recompile since the etasave factor is smaller
# than before
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling two tsunamis in an ocean with constant depth")
```

    The nt/nsnaps factor is 240

    Operator `Kernel` ran in 18.77 s

## Example III: Tsunami in an ocean with 1D Tanh depth variation

So far, so good, we can achieve stable and accurate modelling results. However, a constant bathymetry model is a little bit boring. In this example, we assume that the bathymetry decreases with a Tanh function from the left to the right boundary in x-direction. Let 's place a Tsunami source at $(x_1,\;y_1) = (30\; m,\; 50\; m)$ and see what 's happening ...

```python
# Define depth profile h [m]
h0 = 50 - 45 * np.tanh((X-70.)/8.)

# Define initial eta [m]
eta0 = 0.5 * np.exp(-((X-30)**2/10)-((Y-50)**2/20))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + h0

# Maximum wave propagation time [s]
Tmax = 3.
dt = 1/8000.
nt = (int)(Tmax/dt)
```

```python
# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Use the operator
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling a tsunami in an ocean with 1D Tanh depth variation")
```

## Example IV: Tsunami in an ocean with a seamount

What happens if we have a constant bathymetry, except for a small seamount reaching 5 m below the water surface? Lets define the model parameters ...

```python
# Define constant ocean depth profile h = 50 m
h0 = 50. * np.ones_like(X)

# Adding seamount to seafloor topography
h0 -= 45. * np.exp(-((X-50)**2/20)-((Y-50)**2/20))

# Define initial eta [m]
eta0 = 0.5 * np.exp(-((X-30)**2/5)-((Y-50)**2/5))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + h0

# Maximum wave propagation time [s]
Tmax = 3.
dt = 1/8000.
nt = (int)(Tmax/dt)
```

```python
# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Use the operator
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling a tsunami in an ocean with a seamount")
```

## Example V: Tsunami in an ocean with random seafloor topography variations

Another modelling idea: what is the influence of the roughness of the seafloor topography. First, we add some random perturbations on the constant bathymetry model $h_0$ by using the `random.rand` function from the `NumPy` library. To smooth the random perturbations, we apply the `gaussian_filter` from the `SciPy`library:

```python
from scipy.ndimage import gaussian_filter

# Define constant ocean depth profile h = 30 m
h0 = 30. * np.ones_like(X)

# Add random seafloor perturbation of +- 5m
pert = 5.   # perturbation amplitude
r = 2.0 * (np.random.rand(ny, nx) - 0.5) * pert # create random number perturbations
r = gaussian_filter(r, sigma=8) # smooth random number perturbation
h0 = h0 * (1 + r) # add perturbations to constant seafloor

# Define initial eta [m]
eta0 = 0.5 * np.exp(-((X-30)**2/5)-((Y-50)**2/5))

# Define initial M and N
M0 = 100. * eta0
N0 = 0. * M0
D0 = eta0 + h0

# Maximum wave propagation time [s]
Tmax = 4.
dt = 1/8000.
nt = (int)(Tmax/dt)
```

```python
# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Compile the operator again to create a new code since the
# etasave factor is much higher than the one in example I.
optime = op_shallowWaterWave(etasave, eta, M, N, h, D, g, alpha)

# Use the operator
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling a tsunami in an ocean with\n random seafloor topography variations")
```

Notice how in this example I've had to regenerate the operator code. This wasn't necessary in the previous examples as the factor was smaller than the one in the first example. The one in the first example is 270, and in the second to fourth is 240, going to 320 in the fifth example. The etasave function takes snapshots from 270 index jumps, checking if the current time is divisible by 270.
When the factor goes above the 270 mark, as in the case of example V, the time goes up to 32000, which in the time 29700, the division 29700/270 = 101, exceding the time axis dimension of etasave of 100.
Since the factor is reduced in the examples II-IV, the last few snapshots are zeroed, since the time doesn't go above the 27000 thresho

## Example VI: 2D circular dam break problem

As a final modelling example, let's take a look at an (academic) engineering problem: a tsunami induced by the collapse of a circular dam in a lake with a constant bathymetry of 30 m. We only need to set the wave height in a circle with radius $r_0 = 5\; m$ to $\eta_0 = 0.5 \; m$ and to zero everywhere else. To avoid the occurence of high frequency artifacts in the wavefield, known as numerical grid dispersion, we apply a Gaussian filter to the initial wave height. To achieve a symmetric dam collapse, the initial discharge fluxes $M_0,N_0$ are set to equal values.

```python
# Define constant ocean depth profile h = 30 m
h0 = 30. * np.ones_like(X)

# Define initial eta [m]
eta0 = np.zeros_like(X)

# Define mask for circular dam location with radius r0
r0 = 5.
mask = np.where(np.sqrt((X-50)**2 + (Y-50)**2) <= r0)

# Set wave height in dam to 0.5 m
eta0[mask] = 0.5

# Smooth dam boundaries with gaussian filter
eta0 = gaussian_filter(eta0, sigma=8) # smooth random number perturbation

# Define initial M and N
M0 = 1. * eta0
N0 = 1. * M0
D0 = eta0 + h0

# Maximum wave propagation time [s]
Tmax = 3.
dt = 1/8000.
nt = (int)(Tmax/dt)
```

```python
# Inserting initial conditions
eta.data[0] = eta0.copy()
M.data[0]   = M0.copy()
N.data[0]   = N0.copy()
D.data[:]   = eta0 + h0
h.data[:]   = h0.copy()

# Setting up function to save the snapshots
factor = round(nt / nsnaps)
print(f"The nt/nsnaps factor is {factor}")
time_subsampled = ConditionalDimension('t_sub', parent=grid.time_dim, factor=factor)
etasave = TimeFunction(name='etasave', grid=grid, space_order=2,
                       save=nsnaps, time_dim=time_subsampled)

# Use the operator
optime(eta=eta, etasave=etasave, M=M, N=N, D=D, h=h, time=nt-2, dt=dt)

snaps2video(etasave, "Modeling a 2D circular dam break problem")
```

## What we learned:

* How to solve the 2D Shallow Water Equation using the FTCS finite difference scheme.

* Propagation of (multiple) Tsunamis in an ocean with constant bathymetry.

* Tsunamis reaching shallow waters near a coast will slow down, their wavelength decrease, while their wave height increases.

* The influence of a seamount and roughness of the seafloor topoghraphy on the Tsunami wavefield.

* Tsunami modelling for engineering problems, like the collapse of dams.


## Unevaluated Integrals

```
from sympy.plotting import plot
```

```
i = Integral(log((sin(x)**2+1)*sqrt(x**2+1)),(x,0,y))
```

```
i
```

```
i.evalf(subs={y:1})
```

```
plot(i,(y, 1, 5))
```

## Infinite Sums

```
s = summation(1/x**y,(x,1,oo))
```

```
s
```

```
plot(s, (y, 2, 10))
```

## Finite sums

```
p = plot(summation(1/x,(x,1,y)), (y, 2, 10))
```

```
p[0].only_integers = True

p[0].steps = True

p.show()
```


# Sine map explorations (Taylor and beyond)

Modified version of Logistic_map_explorations.ipynb, which in turn was adapted by Dick Furnstahl from the Ipython Cookbook by Cyrille Rossant.

Here we consider the *sine map*.  The defining function for the sine map is

$ f_r(x) = r \sin(\pi x) \;, $

where $r$ is the control parameter analogous to $\gamma$ for the damped, driven pendulum.  To study the pendulum, we looked at $\phi(t)$ at $t_0$, $t_0 + \tau$, $t_0 + 2\tau$, and so on, where $\tau$ is the driving frequency and we found $\phi(t_0 + \tau)$ from $\phi(t_0)$ by solving the differential equation for the pendulum.  After transients had died off, we characterized the value of $\gamma$ being considered by the periodicity of the $\phi(t_n)$ values: period one (all the same), period two (alternating), chaotic (no repeats) and so on.

Here instead of a differential equation telling us how to generate a trajectory, we use $f_r(x)$:

$\begin{align}
  x_1 = f_r(x_0) \ \rightarrow\ x_2 = f_r(x_1) \ \rightarrow\  x_3 = f_r(x_2) \ \rightarrow\  \ldots
\end{align}$

There will be transients at the beginning, but this sequence of $x_i$ values may reach a point $x = x^*$ such that $f(x^*) = x^*$. We call this a *fixed point*.  If instead it ends up bouncing between two values of $x$, it is period two and we call it a two-cycle.  We can have a cascade of period doubling, as found for the damped, driven pendulum, leading to chaos, which is characterized by the mapping never repeating.  We can make a corresponding bifurcation diagram and identify Lyapunov exponents for each initial condition and $r$.

```python
%matplotlib inline

# standard numpy and matplotlib imports
import numpy as np
import matplotlib.pyplot as plt

```

```python
def sine_map(r, x):
    """Sine map function: f_r(x) = r sin(pi x)
    """
    return r * np.sin(np.pi * x)

def sine_map_deriv(r, x):
    """Sine map derivative: f'(x) = r(1-2x)
    """
    return r * np.pi * np.cos(np.pi * x)
```

## Explore the sine map and its fixed points

Start with a simple plot of the sine function for $0.1 \leq r \leq 0.9$.

```python
x_pts, step = np.linspace(0,1, num=101, endpoint=True, retstep=True)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.plot(x_pts, x_pts, color='black')

r_pts = np.arange(.1, 1., .1)
for r in r_pts:
    ax.plot(x_pts, sine_map(r, x_pts), label=rf'r = {r:.1f}')
ax.legend()

ax.set_xlabel('x')
ax.set_ylabel('f(x)')
ax.set_title('sine map')

fig.tight_layout()
fig.savefig('problem_12.23a.png', bbox_inches='tight')
```

Make plots showing the approach to fixed points.

**Play with the number of steps taken and the values of $r$.**
1. **Increase the number of steps in the plots to more cleanly identify the long-time trend for that value of $r$.**
2. **Try smaller values of $r$.  Are there always non-zero fixed points for small $r$?**

```python
def plot_system(r, x0, n, ax=None):
    """Plot the function and the y=x diagonal line."""
    t = np.linspace(0,1, num=101)
    ax.plot(t, sine_map(r,t), 'k', lw=2)  # black, linewidth 2
    ax.plot([0,1], [0,1], 'k', lw=2)  # x is an array of 0 and 1,
                                      # y is the same array, so this plots
                                      #  a straight line from (0,0) to (1,1).

    # Recursively apply y=f(x) and plot two additional straight lines:
    #   line from (x, x) to (x, y)
    #   line from (x, y) to (y, y)
    x = x0
    for i in range(n):   # do n iterations, i = 0, 1, ..., n-1
        y = sine_map(r, x)
        # Plot the two lines
        ax.plot([x,x], [x,y], color='blue', lw=1)
        ax.plot([x,y], [y,y], color='blue', lw=1)
        # Plot the positions with increasing opacity of the circles
        ax.plot([x], [y], 'or', ms=10, alpha=(i+1)/n)
        x = y  # recursive: reset x to y for the next iteration
    ax.set_xlim(0,1)
    ax.set_ylim(0,1)
    ax.set_title(f'$r={r:.2f}$, $x_0={x0:.1f}$')
    ax.set_xlabel('x')

fig = plt.figure(figsize=(12,12))

ax1 = fig.add_subplot(2,2,1)
# start at 0.1, take n steps
plot_system(r=0.6, x0=0.1, n=10, ax=ax1)

ax2 = fig.add_subplot(2,2,2)
# start at 0.1, take n steps
plot_system(r=0.8, x0=0.1, n=20, ax=ax2)

ax3 = fig.add_subplot(2,2,3)
# start at 0.1, take n steps
plot_system(r=0.85, x0=0.1, n=40, ax=ax3)

ax4 = fig.add_subplot(2,2,4)
# start at 0.1, take n steps
plot_system(r=0.9, x0=0.1, n=40, ax=ax4)

fig.savefig('problem_12.23b.png', bbox_inches='tight')
```

**What periods do these exhibit?  Are any chaotic?**

$r=0.6$ is period one, $r=0.8$ is period 2, $r=0.85$ is period 4, $r=0.9$ is apparently chaotic.

## Find the unstable point numerically

To find the value of $r$ at which the second fixed point becomes unstable, we must solve simultaneously the equations (see Taylor 12.9 in the "A Test for Stability" subsection):

$\begin{align}
  &f_r(x^*) = x^* \\
  &f_r'(x^*) = -1
\end{align}$

where $f'$ means the derivative with respect to $x$, or

$\begin{align}
  &f_r(x^*) - x^* = 0 \\
  &f_r'(x^*) + 1 = 0
\end{align}$

It is the latter form of the equations that we will pass to the
scipy function `fsolve` to find the values of $r$ and $x*$.

```python
from scipy.optimize import fsolve  # Google this to learn more!

def equations(passed_args):
    """Return the two expressions that must equal to zero."""
    r, x = passed_args
    return (sine_map(r, x) - x, sine_map_deriv(r, x) + 1)

# Call fsolve with initial values for r and x
r, x = fsolve(equations, (0.5, 0.5))

print_string = f'x* = {x:.10f},  r = {r:.10f}'
print(print_string)
```

    x* = 0.6457736765,  r = 0.7199616829

**Verify analytically that these are the correct values for the logistic map.**  We use this same procedure for other maps where only numerical solutions are known.

## Now make the bifurcation diagram and calculate Lyapunov exponents

To make a bifurcation diagram, we:
* define an array of closely spaced $r$ values;
* for each $r$, iterate the map $x_{i+1} = f_r(x_i)$ a large number of times (e.g., 1000) to ensure convergence;
* plot the last 100 or so iterations as points at each $r$.

To think about how to calculate a Lyapunov exponent, consider two iterations of the map starting from two values of $x$ that are close together.  Call these initial values $x_0$ and $x_0 + \delta x_0$.  These are mapped by $f_r$ to $x_1$ and  $x_1 + \delta x_1$, then $x_2$ and $x_2 + \delta x_2$, up to $x_n + \delta x_n$.

How are the $\delta x_i$ related?  Each $\delta x_i$ is small, so use a Taylor expansion!
Expanding $f(x)$ about $x_n$:

$\begin{align}
  f(x_i &+ \delta x_i) = &f(x_i) + \delta x_i f'(x_i) + \frac{1}{2}(\delta x_i)^2 f''(x_i) + \cdots \\
\end{align}$

so, neglecting terms of order $(\delta x_i)^2$ and higher,

$\begin{align}
    x_{i+1} + \delta x_{i+1} \approx x_{i+1} + \delta x_i f'(x_i)
    \quad\mbox{or}\quad  \delta x_{i+1} \approx f'(x_i)\, \delta x_i \;.
\end{align}$

Iterating this result we get

$\begin{align}
  \delta x_1 &= f'(x_0)\, \delta x_0  \\
  \delta x_2 &= f'(x_1)\, \delta x_1 = f'(x_1)\times f'(x_0)\, \delta x_0  \\
  & \qquad\vdots  \\
  |\delta x_n| &= \left(\prod_{i=0}^{n-1} |f'(x_i)|\right) |\delta x_0| \;,
\end{align}$

where the last equation gives us the separation of two trajectories after $n$ steps $\delta x_n$ in terms of their
initial separation, $\delta x_0$.  (Why can we just take the absolute value of all the terms?)

We expect that this will vary exponentially at large $n$ like

$\begin{align}
  \left| \frac{\delta x_n}{\delta x_0} \right| = e^{\lambda n}
\end{align}$

and so we define the Lypunov exponent $\lambda$ by

$\begin{align}
  \lambda = \lim_{n\rightarrow\infty} \frac{1}{n} \sum_{i=1}^{n} \ln |f'(x_i)| \;,
\end{align}$

which in practice is well approximated by the sum for large $n$.

If $\lambda > 0$, then nearby trajectories diverge from each other exponentially at large $n$, which corresponds to chaos. However, if the
trajectories converge to a fixed point or a limit cycle, they will get closer together with increasing $n$, which corresponds to $\lambda<0$.

Ok, let's do it!

```python
n = 10000
# Here we'll use n values of r linearly spaced between 2.8 and 4.0
###  You may want to change the range of r for other maps
r = np.linspace(0.6, 1.0, n)

iterations = 1000   # iterations of logistic map; keep last iterations
last = 100          #  where results should have stabilized

x = 0.1 * np.ones(n)  # x_0 initial condition
                      ### (you may want to change for other maps)

lyapunov = np.zeros(n) # initialize vector used for the Lyapunov sums
```

```python
fig = plt.figure(figsize=(8,9))
ax1 = fig.add_subplot(2,1,1)   # bifurcation diagram
ax2 = fig.add_subplot(2,1,2)   # Lyapunov exponent

# Display the bifurcation diagram with one pixel per point x_n^(r) for last iterations
for i in range(iterations):
    x = sine_map(r,x)  # just iterate: x_{i+1} = f_r(x_i)

    # Compute the partial sum of the Lyapunov exponent, which is the sum
    #  of derivatives of the logistic function (absolute value)
    lyapunov += np.log(abs(sine_map_deriv(r,x)))
    # Display the bifurcation diagram.
    if i >= (iterations-last):  # only plot the last iterations
        ax1.plot(r, x, ',k', alpha=0.25)
ax1.set_xlim(0.6, 1.)
ax1.set_xlabel(r'$r$')
ax1.set_title("Bifurcation")

# Display the Lyapunov exponent

# Negative Lyapunov exponent
ax2.plot(r[lyapunov < 0],
         lyapunov[lyapunov < 0] / iterations, 'o',
         color='black', alpha=0.5, ms=0.5)
# Positive Lyapunov exponent
ax2.plot(r[lyapunov >= 0],
         lyapunov[lyapunov >= 0] / iterations, 'o',
         color='red', alpha=0.5, ms=0.5)

# Add a zero line (lightened with alpha=0.5)
ax2.axhline(0, color='k', lw=0.5, alpha=0.5)

ax2.set_xlim(0.6, 1.)
ax2.set_ylim(-2, 1.5)
ax2.set_xlabel(r'$r$')
ax2.set_title("Lyapunov exponent")

fig.tight_layout()
fig.savefig('problem_12.34.png', bbox_inches='tight')
```

We see there is a fixed point for $r < 3$, then two and four periods and a chaotic behavior when $r$ belongs to certain areas of the parameter space.  **Do the values of $r$ where these behaviors occur agree with what you found in the plots with particular $r$ values?**

As for the pendulum, the Lyapunov exponent is positive when the system is chaotic (in red).  **Do these regions look consistent with the characteristics of chaos?**

```python

```