# Multigrid examples

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

```python
from __future__ import print_function

import numpy as np

import mesh.boundary as bnd
import mesh.patch as patch
import multigrid.MG as MG
```

## Constant-coefficent Poisson equation

We want to solve

$$\phi_{xx} + \phi_{yy} = -2[(1-6x^2)y^2(1-y^2) + (1-6y^2)x^2(1-x^2)]$$

on

$$[0,1]\times [0,1]$$

with homogeneous Dirichlet boundary conditions (this example comes from "A Multigrid Tutorial").

This has the analytic solution
$$u(x,y) = (x^2 - x^4)(y^4 - y^2)$$

We start by setting up a multigrid object--this needs to know the number of zones our problem is defined on

```python
nx = ny = 256
mg = MG.CellCenterMG2d(nx, ny,
                       xl_BC_type="dirichlet", xr_BC_type="dirichlet",
                       yl_BC_type="dirichlet", yr_BC_type="dirichlet", verbose=1)
```

    cc data: nx = 2, ny = 2, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 4, ny = 4, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 8, ny = 8, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 16, ny = 16, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 32, ny = 32, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 64, ny = 64, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 128, ny = 128, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

    cc data: nx = 256, ny = 256, ng = 1
             nvars = 3
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

Next, we initialize the RHS.  To make life easier, the `CellCenterMG2d` object has the coordinates of the solution grid (including ghost cells) as `mg.x2d` and `mg.y2d` (these are two-dimensional arrays).

```python
def rhs(x, y):
    return -2.0*((1.0-6.0*x**2)*y**2*(1.0-y**2) + (1.0-6.0*y**2)*x**2*(1.0-x**2))

mg.init_RHS(rhs(mg.x2d, mg.y2d))
```

    Source norm =  1.097515813669473

The last setup step is to initialize the solution--this is the starting point for the solve.  Usually we just want to start with all zeros, so we use the `init_zeros()` method

```python
mg.init_zeros()
```

we can now solve -- there are actually two different techniques we can do here.  We can just do pure smoothing on the solution grid using `mg.smooth(mg.nlevels-1, N)`, where `N` is the number of smoothing iterations.  To get the solution `N` will need to be large and this will take a long time.

Multigrid accelerates the smoothing.  We can do a V-cycle multigrid solution using `mg.solve()`

```python
mg.solve()
```

    source norm =  1.097515813669473
    <<< beginning V-cycle (cycle 1) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.097515813669473
      after G-S, residual L2: 1.502308451578657

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.0616243965458263
      after G-S, residual L2: 1.4321452257629033

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.011366277976364
      after G-S, residual L2: 1.281872470375375

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.903531158162907
      after G-S, residual L2: 0.9607576999783505

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.6736112182020367
      after G-S, residual L2: 0.4439774050299674

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.30721142286171554
      after G-S, residual L2: 0.0727215591269748

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.04841813458618458
      after G-S, residual L2: 3.9610700301811246e-05

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 3.925006722484123e-05
      after G-S, residual L2: 1.0370099820862674e-09

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.07010129273961899
      after G-S, residual L2: 0.0008815704830693547

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.4307377377402105
      after G-S, residual L2: 0.007174899576794818

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.911086486792154
      after G-S, residual L2: 0.01618756602227813

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.1945438349788615
      after G-S, residual L2: 0.022021327892004925

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.313456560108626
      after G-S, residual L2: 0.02518650395173617

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.3618314516335004
      after G-S, residual L2: 0.026870007568672097

    cycle 1: relative err = 0.999999999999964, residual err = 0.02448256984911586

    <<< beginning V-cycle (cycle 2) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 0.026870007568672097
      after G-S, residual L2: 0.025790216249923552

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.018218080204017304
      after G-S, residual L2: 0.023654310121915274

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.01669077991582338
      after G-S, residual L2: 0.01977335201785163

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.013922595404814862
      after G-S, residual L2: 0.013577568890182053

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.009518306167970536
      after G-S, residual L2: 0.006115159484497302

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.004244630812032651
      after G-S, residual L2: 0.0010674120586864006

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.0007108144252738053
      after G-S, residual L2: 5.818246254772703e-07

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 5.765281065294632e-07
      after G-S, residual L2: 1.5231212503339452e-11

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.0010291471590693868
      after G-S, residual L2: 1.2950948742201083e-05

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.006239446983842889
      after G-S, residual L2: 0.00010483463130232172

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.014573363314854
      after G-S, residual L2: 0.00026233988398787004

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.021564270263952755
      after G-S, residual L2: 0.0003944827058086955

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.02579092712136628
      after G-S, residual L2: 0.00048636495715121916

      level: 7, grid: 256 x 256
      before G-S, residual L2: 0.028051324215592862
      after G-S, residual L2: 0.0005440874957950154

    cycle 2: relative err = 13.739483825281054, residual err = 0.0004957445615074047

    <<< beginning V-cycle (cycle 3) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 0.0005440874957950154
      after G-S, residual L2: 0.0005095844930046698

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.0003597879816772893
      after G-S, residual L2: 0.00044648485218937167

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.0003147892995472901
      after G-S, residual L2: 0.0003492541721056348

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.0002457276904804801
      after G-S, residual L2: 0.00022232862524233384

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.0001558932199490972
      after G-S, residual L2: 9.511093023364078e-05

      level: 2, grid: 8 x 8
      before G-S, residual L2: 6.616899520585456e-05
      after G-S, residual L2: 1.711006102346096e-05

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.139522901981679e-05
      after G-S, residual L2: 9.33004809910226e-09

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 9.245125097272049e-09
      after G-S, residual L2: 2.442311694447821e-13

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.64991725637487e-05
      after G-S, residual L2: 2.0771258971860784e-07

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.00010097720436460624
      after G-S, residual L2: 1.7241727900979902e-06

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.0002575410544503153
      after G-S, residual L2: 4.766282851613449e-06

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.00041133882050328275
      after G-S, residual L2: 7.600616845344458e-06

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.0005232809692242086
      after G-S, residual L2: 9.860758095018993e-06

      level: 7, grid: 256 x 256
      before G-S, residual L2: 0.0005945070122423073
      after G-S, residual L2: 1.1466134915427874e-05

    cycle 3: relative err = 34.347638624909216, residual err = 1.0447352805871284e-05

    <<< beginning V-cycle (cycle 4) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.1466134915427874e-05
      after G-S, residual L2: 1.054466722279011e-05

      level: 6, grid: 128 x 128
      before G-S, residual L2: 7.442814693866286e-06
      after G-S, residual L2: 8.955050475722099e-06

      level: 5, grid: 64 x 64
      before G-S, residual L2: 6.311313968968047e-06
      after G-S, residual L2: 6.734553609148436e-06

      level: 4, grid: 32 x 32
      before G-S, residual L2: 4.737984987500691e-06
      after G-S, residual L2: 4.091799997658277e-06

      level: 3, grid: 16 x 16
      before G-S, residual L2: 2.871028473858937e-06
      after G-S, residual L2: 1.6319551993366253e-06

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.1372178077508109e-06
      after G-S, residual L2: 2.961040430099916e-07

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.9721864323458624e-07
      after G-S, residual L2: 1.61503943872384e-10

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.6003411195777404e-10
      after G-S, residual L2: 4.2274326344473505e-15

      level: 2, grid: 8 x 8
      before G-S, residual L2: 2.855691101825338e-07
      after G-S, residual L2: 3.5961118754371857e-09

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.7893831203170535e-06
      after G-S, residual L2: 3.1136282101831173e-08

      level: 4, grid: 32 x 32
      before G-S, residual L2: 4.97129807196115e-06
      after G-S, residual L2: 9.544819739422644e-08

      level: 5, grid: 64 x 64
      before G-S, residual L2: 8.281644276572538e-06
      after G-S, residual L2: 1.56637783149839e-07

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.0888850082357996e-05
      after G-S, residual L2: 2.0777271327080248e-07

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.2717522622400765e-05
      after G-S, residual L2: 2.464531349025277e-07

    cycle 4: relative err = 0.17409776671446628, residual err = 2.24555429482631e-07

    <<< beginning V-cycle (cycle 5) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 2.464531349025277e-07
      after G-S, residual L2: 2.2491138140311698e-07

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.5874562191875262e-07
      after G-S, residual L2: 1.886249099391391e-07

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.3294481979637655e-07
      after G-S, residual L2: 1.397710191717015e-07

      level: 4, grid: 32 x 32
      before G-S, residual L2: 9.836928907527788e-08
      after G-S, residual L2: 8.269030961692836e-08

      level: 3, grid: 16 x 16
      before G-S, residual L2: 5.8062531341283565e-08
      after G-S, residual L2: 3.034725896415429e-08

      level: 2, grid: 8 x 8
      before G-S, residual L2: 2.116912379336852e-08
      after G-S, residual L2: 5.467519592468213e-09

      level: 1, grid: 4 x 4
      before G-S, residual L2: 3.6418116003284676e-09
      after G-S, residual L2: 2.982625229812215e-12

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 2.955484162036181e-12
      after G-S, residual L2: 7.806739482450516e-17

      level: 2, grid: 8 x 8
      before G-S, residual L2: 5.273610709946236e-09
      after G-S, residual L2: 6.642323465658688e-11

      level: 3, grid: 16 x 16
      before G-S, residual L2: 3.4146989205844565e-08
      after G-S, residual L2: 6.052228076583688e-10

      level: 4, grid: 32 x 32
      before G-S, residual L2: 1.031248597196911e-07
      after G-S, residual L2: 2.0541497445308587e-09

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.7585349306604133e-07
      after G-S, residual L2: 3.421022608879089e-09

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.3383756442516674e-07
      after G-S, residual L2: 4.552170797983864e-09

      level: 7, grid: 256 x 256
      before G-S, residual L2: 2.7592842790687426e-07
      after G-S, residual L2: 5.41488950707315e-09

    cycle 5: relative err = 0.005391244339065405, residual err = 4.933769007818501e-09

    <<< beginning V-cycle (cycle 6) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 5.41488950707315e-09
      after G-S, residual L2: 4.948141362729419e-09

      level: 6, grid: 128 x 128
      before G-S, residual L2: 3.4929583962703016e-09
      after G-S, residual L2: 4.154445183511443e-09

      level: 5, grid: 64 x 64
      before G-S, residual L2: 2.9288841397931397e-09
      after G-S, residual L2: 3.074779198797186e-09

      level: 4, grid: 32 x 32
      before G-S, residual L2: 2.164991235492634e-09
      after G-S, residual L2: 1.788028730183651e-09

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.2562223343389894e-09
      after G-S, residual L2: 6.021983813990021e-10

      level: 2, grid: 8 x 8
      before G-S, residual L2: 4.2028073688787063e-10
      after G-S, residual L2: 1.0655724637281067e-10

      level: 1, grid: 4 x 4
      before G-S, residual L2: 7.097871736854444e-11
      after G-S, residual L2: 5.813506543301849e-14

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 5.760611936011378e-14
      after G-S, residual L2: 1.521555112430923e-18

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.027891920456506e-10
      after G-S, residual L2: 1.294879454701896e-12

      level: 3, grid: 16 x 16
      before G-S, residual L2: 6.914011940773812e-10
      after G-S, residual L2: 1.2453691230551983e-11

      level: 4, grid: 32 x 32
      before G-S, residual L2: 2.2570491487662195e-09
      after G-S, residual L2: 4.639035392364569e-11

      level: 5, grid: 64 x 64
      before G-S, residual L2: 3.908967396962745e-09
      after G-S, residual L2: 7.803740782474827e-11

      level: 6, grid: 128 x 128
      before G-S, residual L2: 5.196394306272565e-09
      after G-S, residual L2: 1.033274523100204e-10

      level: 7, grid: 256 x 256
      before G-S, residual L2: 6.117636729623554e-09
      after G-S, residual L2: 1.2199402602477584e-10

    cycle 6: relative err = 7.51413991329132e-05, residual err = 1.111546863428753e-10

    <<< beginning V-cycle (cycle 7) >>>

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.2199402602477584e-10
      after G-S, residual L2: 1.121992266879251e-10

      level: 6, grid: 128 x 128
      before G-S, residual L2: 7.921861122082639e-11
      after G-S, residual L2: 9.493449600138316e-11

      level: 5, grid: 64 x 64
      before G-S, residual L2: 6.694993398453784e-11
      after G-S, residual L2: 7.050995614737483e-11

      level: 4, grid: 32 x 32
      before G-S, residual L2: 4.9666563586565975e-11
      after G-S, residual L2: 4.045094776680348e-11

      level: 3, grid: 16 x 16
      before G-S, residual L2: 2.843147343834713e-11
      after G-S, residual L2: 1.2576313722677801e-11

      level: 2, grid: 8 x 8
      before G-S, residual L2: 8.777954081387978e-12
      after G-S, residual L2: 2.170559196862902e-12

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.445876195415056e-12
      after G-S, residual L2: 1.1842925278593641e-15

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.1735184729034125e-15
      after G-S, residual L2: 3.0994757710835167e-20

      level: 2, grid: 8 x 8
      before G-S, residual L2: 2.094012660676073e-12
      after G-S, residual L2: 2.6382579574150587e-14

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.466147487151147e-11
      after G-S, residual L2: 2.6760553592700965e-13

      level: 4, grid: 32 x 32
      before G-S, residual L2: 5.130705216489902e-11
      after G-S, residual L2: 1.0810419626613159e-12

      level: 5, grid: 64 x 64
      before G-S, residual L2: 9.001551103280705e-11
      after G-S, residual L2: 1.8342879121275396e-12

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.1914921193827463e-10
      after G-S, residual L2: 2.4124327865487605e-12

      level: 7, grid: 256 x 256
      before G-S, residual L2: 1.3907209384461257e-10
      after G-S, residual L2: 2.8429898342353533e-12

    cycle 7: relative err = 7.062255558417692e-07, residual err = 2.590386214782638e-12

We can access the solution on the finest grid using `get_solution()`

```python
phi = mg.get_solution()
```

```python
plt.imshow(np.transpose(phi.v()), origin="lower")
```

we can also get the gradient of the solution

```python
gx, gy = mg.get_solution_gradient()
```

```python
plt.subplot(121)
plt.imshow(np.transpose(gx.v()), origin="lower")
plt.subplot(122)
plt.imshow(np.transpose(gy.v()), origin="lower")
```

## General linear elliptic equation

The `GeneralMG2d` class implements support for a general elliptic equation of the form:
$$\alpha \phi + \nabla \cdot (\beta \nabla \phi) + \gamma \cdot \nabla \phi = f$$

with inhomogeneous boundary condtions.

It subclasses the `CellCenterMG2d` class, and the basic interface is the same

We will solve the above with
\begin{align}
\alpha &= 10 \\
\beta &= xy + 1 \\
\gamma &= \hat{x} + \hat{y}
\end{align}
and
\begin{align}
f = &-\frac{\pi}{2}(x + 1)\sin\left(\frac{\pi y}{2}\right) \cos\left(\frac{\pi x}{2}\right ) \\
    &-\frac{\pi}{2}(y + 1)\sin\left(\frac{\pi x}{2}\right) \cos\left(\frac{\pi y}{2}\right ) \\
    &+\left(\frac{-\pi^2 (xy+1)}{2} + 10\right) \cos\left(\frac{\pi x}{2}\right) \cos\left(\frac{\pi y}{2}\right)
\end{align}
on $[0, 1] \times [0,1]$ with boundary conditions:
\begin{align}
\phi(x=0) &= \cos(\pi y/2) \\
\phi(x=1) &= 0 \\
\phi(y=0) &= \cos(\pi x/2) \\
\phi(y=1) &= 0
\end{align}

This has the exact solution:
$$\phi = \cos(\pi x/2) \cos(\pi y/2)$$

```python
import multigrid.general_MG as gMG
```

For reference, we'll define a function providing the analytic solution

```python
def true(x,y):
    return np.cos(np.pi*x/2.0)*np.cos(np.pi*y/2.0)
```

Now the coefficents--note that since $\gamma$ is a vector, we have a different function for each component

```python
def alpha(x,y):
    return 10.0*np.ones_like(x)

def beta(x,y):
    return x*y + 1.0

def gamma_x(x,y):
    return np.ones_like(x)

def gamma_y(x,y):
    return np.ones_like(x)
```

and the righthand side function

```python
def f(x,y):
    return -0.5*np.pi*(x + 1.0)*np.sin(np.pi*y/2.0)*np.cos(np.pi*x/2.0) - \
            0.5*np.pi*(y + 1.0)*np.sin(np.pi*x/2.0)*np.cos(np.pi*y/2.0) + \
            (-np.pi**2*(x*y+1.0)/2.0 + 10.0)*np.cos(np.pi*x/2.0)*np.cos(np.pi*y/2.0)
```

Our inhomogeneous boundary conditions require a function that can be evaluated on the boundary to give the value

```python
def xl_func(y):
    return np.cos(np.pi*y/2.0)

def yl_func(x):
    return np.cos(np.pi*x/2.0)
```

Now we can setup our grid object and the coefficients, which are stored as a `CellCenter2d` object.  Note, the coefficients do not need to have the same boundary conditions as $\phi$ (and for real problems, they may not).  The one that matters the most is $\beta$, since that will need to be averaged to the edges of the domain, so the boundary conditions on the coefficients are important.

Here we use Neumann boundary conditions

```python
import mesh.patch as patch

nx = ny = 128

g = patch.Grid2d(nx, ny, ng=1)
d = patch.CellCenterData2d(g)

bc_c = bnd.BC(xlb="neumann", xrb="neumann",
              ylb="neumann", yrb="neumann")

d.register_var("alpha", bc_c)
d.register_var("beta", bc_c)
d.register_var("gamma_x", bc_c)
d.register_var("gamma_y", bc_c)
d.create()

a = d.get_var("alpha")
a[:,:] = alpha(g.x2d, g.y2d)

b = d.get_var("beta")
b[:,:] = beta(g.x2d, g.y2d)

gx = d.get_var("gamma_x")
gx[:,:] = gamma_x(g.x2d, g.y2d)

gy = d.get_var("gamma_y")
gy[:,:] = gamma_y(g.x2d, g.y2d)
```

Now we can setup the multigrid object

```python
a = gMG.GeneralMG2d(nx, ny,
                    xl_BC_type="dirichlet", yl_BC_type="dirichlet",
                    xr_BC_type="dirichlet", yr_BC_type="dirichlet",
                    xl_BC=xl_func,
                    yl_BC=yl_func,
                    coeffs=d,
                    verbose=1, vis=0, true_function=true)
```

    cc data: nx = 2, ny = 2, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 4, ny = 4, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 8, ny = 8, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 16, ny = 16, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 32, ny = 32, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 64, ny = 64, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

    cc data: nx = 128, ny = 128, ng = 1
             nvars = 7
             variables:
                   v: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   f: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
                   r: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               alpha: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
                beta: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_x: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
             gamma_y: min:    0.0000000000    max:    0.0000000000
                      BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

just as before, we specify the righthand side and initialize the solution

```python
a.init_zeros()
a.init_RHS(f(a.x2d, a.y2d))
```

    Source norm =  1.775181492337501

and we can solve it

```python
a.solve(rtol=1.e-10)
```

    source norm =  1.775181492337501
    <<< beginning V-cycle (cycle 1) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.775181492337501
      after G-S, residual L2: 188.9332667507471

      level: 5, grid: 64 x 64
      before G-S, residual L2: 129.93801550392874
      after G-S, residual L2: 56.28708770794368

      level: 4, grid: 32 x 32
      before G-S, residual L2: 38.88692621665778
      after G-S, residual L2: 18.722754099081875

      level: 3, grid: 16 x 16
      before G-S, residual L2: 12.92606814051491
      after G-S, residual L2: 6.741858401611561

      level: 2, grid: 8 x 8
      before G-S, residual L2: 4.646478379380238
      after G-S, residual L2: 2.065126154146587

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.3745334259197384
      after G-S, residual L2: 0.02244519721859215

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.031252520872477096
      after G-S, residual L2: 8.232822131685586e-05

      level: 2, grid: 8 x 8
      before G-S, residual L2: 2.8059768631102893
      after G-S, residual L2: 0.07481536016730024

      level: 3, grid: 16 x 16
      before G-S, residual L2: 8.772402436595382
      after G-S, residual L2: 0.24361942694526875

      level: 4, grid: 32 x 32
      before G-S, residual L2: 19.591011324351037
      after G-S, residual L2: 0.5448263647958976

      level: 5, grid: 64 x 64
      before G-S, residual L2: 50.4641088994847
      after G-S, residual L2: 1.3597629173942398

      level: 6, grid: 128 x 128
      before G-S, residual L2: 160.2131163846867
      after G-S, residual L2: 4.125142056231141

    cycle 1: relative err = 0.9999999999999981, residual err = 2.3237860883730193

    <<< beginning V-cycle (cycle 2) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 4.125142056231141
      after G-S, residual L2: 2.4247311846143957

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.6915411385849393
      after G-S, residual L2: 1.0486241094402862

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.7283416353571861
      after G-S, residual L2: 0.45548181093652995

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.3165327512850198
      after G-S, residual L2: 0.22128563126748008

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.15332496186655512
      after G-S, residual L2: 0.0747196881784426

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.04974939187294444
      after G-S, residual L2: 0.0008133572860410457

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.0011325179143730458
      after G-S, residual L2: 2.98337783917788e-06

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.10152627387884022
      after G-S, residual L2: 0.0027007047002410374

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.29814672415595245
      after G-S, residual L2: 0.00819910795226899

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.5218848114624619
      after G-S, residual L2: 0.014956130961989498

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.9910630869231989
      after G-S, residual L2: 0.028422939317571984

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.044187745817752
      after G-S, residual L2: 0.058293826018797935

    cycle 2: relative err = 0.036315310129800826, residual err = 0.032838234439926776

    <<< beginning V-cycle (cycle 3) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.058293826018797935
      after G-S, residual L2: 0.0417201187072595

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.029246699093099564
      after G-S, residual L2: 0.023356326397591495

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.016306296792818056
      after G-S, residual L2: 0.012906629461195234

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.009011110787953703
      after G-S, residual L2: 0.007315262938908486

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.005081499522859323
      after G-S, residual L2: 0.002562526517155576

      level: 1, grid: 4 x 4
      before G-S, residual L2: 0.0017064130732665692
      after G-S, residual L2: 2.7912387046731474e-05

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 3.886526925433118e-05
      after G-S, residual L2: 1.0238217009484441e-07

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.0034819145217789937
      after G-S, residual L2: 9.252096659805176e-05

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.01006499034870321
      after G-S, residual L2: 0.0002744054418255884

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.016032310448838724
      after G-S, residual L2: 0.0004558226543272663

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.024303743880186898
      after G-S, residual L2: 0.0007098551729201239

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.037775318915862
      after G-S, residual L2: 0.0011035122819927912

    cycle 3: relative err = 0.0012532978372415335, residual err = 0.0006216334987470617

    <<< beginning V-cycle (cycle 4) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.0011035122819927912
      after G-S, residual L2: 0.0008898317346917108

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.0006257398720776081
      after G-S, residual L2: 0.000607740119084607

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.00042604165447901086
      after G-S, residual L2: 0.00039767401825608673

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.0002784624522907369
      after G-S, residual L2: 0.00024268300992319052

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.0001688184030119159
      after G-S, residual L2: 8.63435239999583e-05

      level: 1, grid: 4 x 4
      before G-S, residual L2: 5.750132804390505e-05
      after G-S, residual L2: 9.407985171344554e-07

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.3099714803222558e-06
      after G-S, residual L2: 3.450833950914012e-09

      level: 2, grid: 8 x 8
      before G-S, residual L2: 0.00011732421042687768
      after G-S, residual L2: 3.1157531467636086e-06

      level: 3, grid: 16 x 16
      before G-S, residual L2: 0.00033850867119400885
      after G-S, residual L2: 9.17760188796962e-06

      level: 4, grid: 32 x 32
      before G-S, residual L2: 0.0005249527904418192
      after G-S, residual L2: 1.4651643230958405e-05

      level: 5, grid: 64 x 64
      before G-S, residual L2: 0.0007080871923387015
      after G-S, residual L2: 2.0290645679943462e-05

      level: 6, grid: 128 x 128
      before G-S, residual L2: 0.0009185166830535544
      after G-S, residual L2: 2.6570300453995103e-05

    cycle 4: relative err = 4.2574662963457396e-05, residual err = 1.4967652923762853e-05

    <<< beginning V-cycle (cycle 5) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.6570300453995103e-05
      after G-S, residual L2: 2.3098223923757352e-05

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.6274857395354832e-05
      after G-S, residual L2: 1.7906142642175535e-05

      level: 4, grid: 32 x 32
      before G-S, residual L2: 1.258588239896169e-05
      after G-S, residual L2: 1.2880701433730278e-05

      level: 3, grid: 16 x 16
      before G-S, residual L2: 9.035061892671461e-06
      after G-S, residual L2: 8.10300318788889e-06

      level: 2, grid: 8 x 8
      before G-S, residual L2: 5.641504287378599e-06
      after G-S, residual L2: 2.9012129063955126e-06

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.932169517574082e-06
      after G-S, residual L2: 3.161675601835735e-08

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 4.4023320992879136e-08
      after G-S, residual L2: 1.1596974313938014e-10

      level: 2, grid: 8 x 8
      before G-S, residual L2: 3.9422658747144435e-06
      after G-S, residual L2: 1.0466257645445924e-07

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.1405869020431955e-05
      after G-S, residual L2: 3.0819546585464564e-07

      level: 4, grid: 32 x 32
      before G-S, residual L2: 1.7696025211842327e-05
      after G-S, residual L2: 4.853326074858634e-07

      level: 5, grid: 64 x 64
      before G-S, residual L2: 2.281722184794443e-05
      after G-S, residual L2: 6.339093026629609e-07

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.7204506586512792e-05
      after G-S, residual L2: 7.61736677407384e-07

    cycle 5: relative err = 1.4372233555992132e-06, residual err = 4.2910354839513e-07

    <<< beginning V-cycle (cycle 6) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 7.61736677407384e-07
      after G-S, residual L2: 6.887955287148536e-07

      level: 5, grid: 64 x 64
      before G-S, residual L2: 4.858303580829294e-07
      after G-S, residual L2: 5.698844682533653e-07

      level: 4, grid: 32 x 32
      before G-S, residual L2: 4.011448592273346e-07
      after G-S, residual L2: 4.2887305175998083e-07

      level: 3, grid: 16 x 16
      before G-S, residual L2: 3.011320287970724e-07
      after G-S, residual L2: 2.7229135972437344e-07

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.8967555884605451e-07
      after G-S, residual L2: 9.770491553515245e-08

      level: 1, grid: 4 x 4
      before G-S, residual L2: 6.507167357899105e-08
      after G-S, residual L2: 1.0648579116334552e-09

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.4827137294363792e-09
      after G-S, residual L2: 3.9058805523605475e-12

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.3276705475319977e-07
      after G-S, residual L2: 3.524245793876337e-09

      level: 3, grid: 16 x 16
      before G-S, residual L2: 3.8563144896921417e-07
      after G-S, residual L2: 1.0398885077513769e-08

      level: 4, grid: 32 x 32
      before G-S, residual L2: 6.038836850187365e-07
      after G-S, residual L2: 1.6338312481157817e-08

      level: 5, grid: 64 x 64
      before G-S, residual L2: 7.682416346530921e-07
      after G-S, residual L2: 2.0772116210685317e-08

      level: 6, grid: 128 x 128
      before G-S, residual L2: 8.865086230602598e-07
      after G-S, residual L2: 2.401923227919822e-08

    cycle 6: relative err = 4.8492598977484135e-08, residual err = 1.353057835656594e-08

    <<< beginning V-cycle (cycle 7) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.401923227919822e-08
      after G-S, residual L2: 2.2125290070425652e-08

      level: 5, grid: 64 x 64
      before G-S, residual L2: 1.5613809613835955e-08
      after G-S, residual L2: 1.8869606239963252e-08

      level: 4, grid: 32 x 32
      before G-S, residual L2: 1.3292687837677291e-08
      after G-S, residual L2: 1.4485742520315527e-08

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.0177212111802273e-08
      after G-S, residual L2: 9.198083791538658e-09

      level: 2, grid: 8 x 8
      before G-S, residual L2: 6.409467335640698e-09
      after G-S, residual L2: 3.3018379633629456e-09

      level: 1, grid: 4 x 4
      before G-S, residual L2: 2.1990607567876347e-09
      after G-S, residual L2: 3.598750197454369e-11

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 5.010919630110133e-11
      after G-S, residual L2: 1.3200151156453123e-13

      level: 2, grid: 8 x 8
      before G-S, residual L2: 4.48679228107323e-09
      after G-S, residual L2: 1.1908945622999935e-10

      level: 3, grid: 16 x 16
      before G-S, residual L2: 1.3081162779667808e-08
      after G-S, residual L2: 3.522982496836639e-10

      level: 4, grid: 32 x 32
      before G-S, residual L2: 2.0705037621548675e-08
      after G-S, residual L2: 5.546643639307605e-10

      level: 5, grid: 64 x 64
      before G-S, residual L2: 2.6280822057541362e-08
      after G-S, residual L2: 6.964954384251476e-10

      level: 6, grid: 128 x 128
      before G-S, residual L2: 2.994449911367404e-08
      after G-S, residual L2: 7.914383325620475e-10

    cycle 7: relative err = 1.6392150533299687e-09, residual err = 4.4583516444840087e-10

    <<< beginning V-cycle (cycle 8) >>>

      level: 6, grid: 128 x 128
      before G-S, residual L2: 7.914383325620475e-10
      after G-S, residual L2: 7.355629304356289e-10

      level: 5, grid: 64 x 64
      before G-S, residual L2: 5.19218220597571e-10
      after G-S, residual L2: 6.364663261794707e-10

      level: 4, grid: 32 x 32
      before G-S, residual L2: 4.485504928535875e-10
      after G-S, residual L2: 4.928237246176745e-10

      level: 3, grid: 16 x 16
      before G-S, residual L2: 3.4637122000064977e-10
      after G-S, residual L2: 3.1194162913950586e-10

      level: 2, grid: 8 x 8
      before G-S, residual L2: 2.174181615639314e-10
      after G-S, residual L2: 1.1194514367241423e-10

      level: 1, grid: 4 x 4
      before G-S, residual L2: 7.455734323986808e-11
      after G-S, residual L2: 1.2201499216239134e-12

      bottom solve:
      level: 0, grid: 2 x 2

      level: 1, grid: 4 x 4
      before G-S, residual L2: 1.6989436916357301e-12
      after G-S, residual L2: 4.475487188247986e-15

      level: 2, grid: 8 x 8
      before G-S, residual L2: 1.521214490284944e-10
      after G-S, residual L2: 4.037434677870943e-12

      level: 3, grid: 16 x 16
      before G-S, residual L2: 4.4491498629640967e-10
      after G-S, residual L2: 1.197248120085576e-11

      level: 4, grid: 32 x 32
      before G-S, residual L2: 7.109792371905777e-10
      after G-S, residual L2: 1.8912335700376235e-11

      level: 5, grid: 64 x 64
      before G-S, residual L2: 9.034017109357381e-10
      after G-S, residual L2: 2.3606466325271617e-11

      level: 6, grid: 128 x 128
      before G-S, residual L2: 1.0238349148814258e-09
      after G-S, residual L2: 2.678477889744364e-11

    cycle 8: relative err = 5.555107077431201e-11, residual err = 1.5088473495842003e-11

We can compare to the true solution

```python
v = a.get_solution()
b = true(a.x2d, a.y2d)
e = v - b
```

The norm of the error is

```python
e.norm()
```

    1.6719344048744095e-05

```python

```


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
from copy import deepcopy
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        1,              #  1
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    objc = deepcopy(obj)
    try:
        tmp = DX(str(float(objc)))
        objc = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(objc)):
            tmp = DX(str(float(objc[i])))
            objc[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return objc

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm, deg) = ( m/1000, m/100, pi/180)
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton

half = S(1)/2

# Header ends here.
#
# https://colab.research.google.com/github/kassbohm/wb-snippets/blob/master/ipynb/HTM_03/Uebungen/3.1_cc.ipynb

from mpmath import radians, degrees, pi

l1, l2, l3, b = 1*m, 2*m, 2*m, 2*m
p1 = radians(30)
prec = 0.001

p2, p3 = var("φ₂ φ₃")
dp2, dp3 = var("δφ₂ δφ₃")

c1, s1 = cos(p1), sin(p1)
c2, s2 = cos(p2), sin(p2)
c3, s3 = cos(p3), sin(p3)

xdev = ( l1*c1 + l2*c2 - l3*c3 - b ) / m
ydev = ( l1*s1 + l2*s2 - l3*s3 ) / m

J11, J12 = diff(xdev, p2), diff(xdev, p3)
J21, J22 = diff(ydev, p2), diff(ydev, p3)

# Jacobian:
J = Matrix([[J11, J12],[J21, J22]])

pprint("\n--- Numerical Solution using SymPy's nsolve ---")

# Initial Values:
pprint("\nInitial (φ₂ φ₃) / deg:")
pprint(Matrix([45, 90]))
p2i, p3i = radians(45), radians(90)

sol = nsolve( [xdev, ydev], [p2, p3], [p2i , p3i], dict=True )
sol = sol[0]
sol =  Matrix([sol[p2], sol[p3]])
pprint("\nnsolve-solution:")
pprint("\n(φ₂ φ₃) / deg:")
tmp = degrees(sol)
pprint(iso_round(tmp,prec))

pprint("\n--- Step-by-Step Newton-Solution ---")
pprint("\n1: Jacobian:")
pprint(J)

# n = 1 Iteration:
pprint("\n2: Inital (φ₂ φ₃) / rad:")
p = Matrix([p2i, p3i])
pprint(iso_round(p, prec))

F = Matrix([xdev, ydev])

sub_list = [
    (p2, p2i),
    (p3, p3i),
    ]

pprint("\n3: Jacobian for (φ₂ φ₃):")
Jn = J.subs(sub_list)
# pprint(Jn)
pprint(iso_round(Jn, prec))

pprint("\n4: Deviation Phi for (φ₂ φ₃) / rad:")
Fn = F.subs(sub_list)
pprint(iso_round(Fn, prec))

# Newton-Step:
pprint("\n5: Increment (δφ₂ δφ₃) / rad:")
dp = Matrix([dp2, dp3])
eq = Eq(Jn*dp, -Fn)
sol = solve(eq,[dp2, dp3], dict=True)
sol = sol[0]
dp = Matrix([sol[dp2], sol[dp3]])
pprint(iso_round(dp, prec))
p += dp

pprint("\n6: At end of iteration step:")
pprint("\n(φ₂ φ₃) / rad:")
pprint(iso_round(p, prec))

pprint("\n(φ₂ φ₃) / deg:")
pprint(iso_round(degrees(p), prec))

# --- Numerical Solution using SymPy's nsolve ---
#
# Initial (φ₂ φ₃) / deg:
# ⎡45⎤
# ⎢  ⎥
# ⎣90⎦
#
# nsolve-solution:
#
# (φ₂ φ₃) / deg:
# ⎡48.157⎤
# ⎢      ⎥
# ⎣84.255⎦
#
# --- Step-by-Step Newton-Solution ---
#
# 1: Jacobian:
# ⎡-2⋅sin(φ₂)  2⋅sin(φ₃) ⎤
# ⎢                      ⎥
# ⎣2⋅cos(φ₂)   -2⋅cos(φ₃)⎦
#
# 2: Inital (φ₂ φ₃) / rad:
# ⎡0.785⎤
# ⎢     ⎥
# ⎣1.571⎦
#
# 3: Jacobian for (φ₂ φ₃):
# ⎡-1.414  2.0⎤
# ⎢           ⎥
# ⎣1.414   0.0⎦
#
# 4: Deviation Phi for (φ₂ φ₃) / rad:
# ⎡ 0.28 ⎤
# ⎢      ⎥
# ⎣-0.086⎦
#
# 5: Increment (δφ₂ δφ₃) / rad:
# ⎡0.061 ⎤
# ⎢      ⎥
# ⎣-0.097⎦
#
# 6: At end of iteration step:
#
# (φ₂ φ₃) / rad:
# ⎡0.846⎤
# ⎢     ⎥
# ⎣1.474⎦
#
# (φ₂ φ₃) / deg:
# ⎡48.476⎤
# ⎢      ⎥
# ⎣84.429⎦

```


## Smith-Hutton Problem

We want to solve the following PDE:

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

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

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

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

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

```
from nangs.pde import PDE
from nangs.bocos import PeriodicBoco, DirichletBoco, NeumannBoco
from nangs.solutions import MLP

class MyPDE(PDE):
    def __init__(self, inputs=None, outputs=None):
        super().__init__(inputs, outputs)
    def computePDELoss(self, grads, inputs, outputs, params):
        dpdx, dpdy = grads['p']['x'], grads['p']['y']
        x, y = inputs['x'], inputs['y']
        u, v = 2*y*(1-x**2), -2*x*(1-y**2)
        return [u*dpdx + v*dpdy]

# instanciate pde
pde = MyPDE(inputs=['x', 'y'], outputs=['p'])

# define input values
x = np.linspace(-1,1,60)
y = np.linspace(0,1,30)

pde.setValues({'x': x, 'y': y})

x_v = np.linspace(-1,1,30)
y_v = np.linspace(0,1,15)
pde.setValues({'x': x_v, 'y': y_v}, train=False)
```

Boundary conditions.

```
# left and rigth b.c
ALPHA = 10
x1, x2 = np.array([-1, 1]), np.array([1])
p = np.zeros(2*len(y))
for i in range(2*len(y)): p[i] = 1. - math.tanh(ALPHA)
boco = DirichletBoco('left_rigth', {'x': x1, 'y': y}, {'p': p})
pde.addBoco(boco)

# top b.c
y2 = np.array([1])
p = np.zeros(len(x))
for i in range(len(x)): p[i] = 1. - math.tanh(ALPHA)
boco = DirichletBoco('top', {'x': x, 'y': y2}, {'p': p})
pde.addBoco(boco)

# bottom b.c
y1 = np.array([0])
x1, x2, p1 = [], [], []
for i in range(len(x)):
    if x[i] < 0:
        x1.append(x[i])
        p1.append(1.+math.tanh(ALPHA*(2*x[i]+1)))
    else:
        x2.append(x[i])
x1, x2, p1 = np.array(x1), np.array(x2), np.array(p1)

# bottom left b.c
boco = DirichletBoco('bottom_left', {'x': x1, 'y': y1}, {'p': p1})
pde.addBoco(boco)

# bottom right b.c
boco = NeumannBoco('bottom_right', {'x': x2, 'y': y1}, grads={'p': 'y'})
pde.addBoco(boco)
```

```
plt.plot(x1,p1)
plt.xlabel('x')
plt.ylabel('p      ', rotation=np.pi/2)
plt.title('Solution at y=0')
plt.grid()
plt.show()
```

The objective is to reproduce the inflow ($y$=0 and $x<0$) at the outlet ($y$=0 and $x>0$).

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

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

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

Epoch 1/30 Losses 0.30990
 PDE  [ 0.04466 ]
 left_rigth 0.01039
 top 0.00669
 bottom_left 0.23285
 bottom_right 0.01531
 Val [ 0.01753 ]
Epoch 2/30 Losses 0.02908
 PDE  [ 0.01289 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.01435
 bottom_right 0.00182
 Val [ 0.01281 ]
Epoch 3/30 Losses 0.02360
 PDE  [ 0.01174 ]
 left_rigth 0.00005
 top 0.00005
 bottom_left 0.00978
 bottom_right 0.00199
 Val [ 0.01805 ]
Epoch 4/30 Losses 0.02615
 PDE  [ 0.01195 ]
 left_rigth 0.00003
 top 0.00003
 bottom_left 0.00835
 bottom_right 0.00580
 Val [ 0.01723 ]
Epoch 5/30 Losses 0.02226
 PDE  [ 0.00913 ]
 left_rigth 0.00005
 top 0.00005
 bottom_left 0.01035
 bottom_right 0.00269
 Val [ 0.00418 ]
Epoch 6/30 Losses 0.01823
 PDE  [ 0.00990 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00560
 bottom_right 0.00271
 Val [ 0.00497 ]
Epoch 7/30 Losses 0.01427
 PDE  [ 0.00787 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00499
 bottom_right 0.00140
 Val [ 0.00568 ]
Epoch 8/30 Losses 0.01062
 PDE  [ 0.00575 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00300
 bottom_right 0.00187
 Val [ 0.00744 ]
Epoch 9/30 Losses 0.01321
 PDE  [ 0.00670 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00458
 bottom_right 0.00191
 Val [ 0.00967 ]
Epoch 10/30 Losses 0.01061
 PDE  [ 0.00467 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00296
 bottom_right 0.00298
 Val [ 0.00430 ]
Epoch 11/30 Losses 0.01304
 PDE  [ 0.00811 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00356
 bottom_right 0.00136
 Val [ 0.01640 ]
Epoch 12/30 Losses 0.07422
 PDE  [ 0.00865 ]
 left_rigth 0.00014
 top 0.00015
 bottom_left 0.06247
 bottom_right 0.00281
 Val [ 0.00622 ]
Epoch 13/30 Losses 0.01126
 PDE  [ 0.00718 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00315
 bottom_right 0.00090
 Val [ 0.00405 ]
Epoch 14/30 Losses 0.00873
 PDE  [ 0.00559 ]
 left_rigth 0.00005
 top 0.00005
 bottom_left 0.00258
 bottom_right 0.00045
 Val [ 0.00814 ]
Epoch 15/30 Losses 0.00722
 PDE  [ 0.00442 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00190
 bottom_right 0.00087
 Val [ 0.00686 ]
Epoch 16/30 Losses 0.01224
 PDE  [ 0.00654 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00388
 bottom_right 0.00180
 Val [ 0.00587 ]
Epoch 17/30 Losses 0.00849
 PDE  [ 0.00523 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00225
 bottom_right 0.00100
 Val [ 0.00574 ]
Epoch 18/30 Losses 0.00718
 PDE  [ 0.00477 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00172
 bottom_right 0.00069
 Val [ 0.01079 ]
Epoch 19/30 Losses 0.00618
 PDE  [ 0.00376 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00157
 bottom_right 0.00085
 Val [ 0.00625 ]
Epoch 20/30 Losses 0.00639
 PDE  [ 0.00395 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00148
 bottom_right 0.00096
 Val [ 0.00610 ]
Epoch 21/30 Losses 0.00882
 PDE  [ 0.00482 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00223
 bottom_right 0.00177
 Val [ 0.00588 ]
Epoch 22/30 Losses 0.00987
 PDE  [ 0.00500 ]
 left_rigth 0.00001
 top 0.00000
 bottom_left 0.00367
 bottom_right 0.00119
 Val [ 0.00324 ]
Epoch 23/30 Losses 0.01038
 PDE  [ 0.00594 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00321
 bottom_right 0.00122
 Val [ 0.00298 ]
Epoch 24/30 Losses 0.00954
 PDE  [ 0.00515 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00279
 bottom_right 0.00158
 Val [ 0.00381 ]
Epoch 25/30 Losses 0.00604
 PDE  [ 0.00354 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00160
 bottom_right 0.00089
 Val [ 0.00314 ]
Epoch 26/30 Losses 0.00521
 PDE  [ 0.00305 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00133
 bottom_right 0.00082
 Val [ 0.00501 ]
Epoch 27/30 Losses 0.00660
 PDE  [ 0.00452 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00153
 bottom_right 0.00055
 Val [ 0.00556 ]
Epoch 28/30 Losses 0.01040
 PDE  [ 0.00567 ]
 left_rigth 0.00001
 top 0.00001
 bottom_left 0.00340
 bottom_right 0.00132
 Val [ 0.00520 ]
Epoch 29/30 Losses 0.00550
 PDE  [ 0.00305 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00161
 bottom_right 0.00084
 Val [ 0.00283 ]
Epoch 30/30 Losses 0.00419
 PDE  [ 0.00293 ]
 left_rigth 0.00000
 top 0.00000
 bottom_left 0.00091
 bottom_right 0.00034
 Val [ 0.00324 ]

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

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

Finally, we can evaluate our solution.

```
# evaluate the solution
x = np.linspace(-1,1,100)
y = np.linspace(0,1,50)
pde.evaluate({'x': x, 'y': y})
p = pde.outputs['p']
plt.imshow(p.reshape((len(y),len(x))), vmin=p.min(), vmax=p.max(), origin='lower',
           extent=[x.min(), x.max(), y.min(), y.max()])
plt.colorbar()
plt.show()
```

```
x = np.linspace(-1,1,200)
y = np.array([0])
pde.evaluate({'x': x, 'y': y})
p = pde.outputs['p']
plt.plot(x1,p1)
plt.plot(x1+1,np.flip(p1), label="exact")
plt.plot(x, p, '.k', label="predicted")
plt.legend()
plt.xlabel('x')
plt.ylabel('p      ', rotation=np.pi/2)
plt.title('Solution at y=0')
plt.grid()
plt.show()
```

```

```

```

```


```python
import holoviews as hv
hv.extension('bokeh')
hv.opts.defaults(hv.opts.Curve(width=500))
```

```python
import numpy as np
import scipy.signal
```

# Efectos del muestreo y fenómeno de aliasing

## El espectro de una señal discreta

### ¿Qué le ocurre al espectro de una señal continua cuando la muestreamos?

Muestrear es equivalente a multiplicar una señal continua por un tren de impulsos, también conocido como "peineta de Dirac". La definición matemática del tren de impulsos es

$$
\upuparrows(t) = \sum_{m=-\infty}^\infty \delta[t - m / F_s],
$$

donde $F_s$ es la frecuencia de muestreo, es decir el recíproco de $T_s$.

La siguiente figura representa graficamente el proceso de muestreo:

que matemáticamente sería

$$
s(t) ~ \cdot \upuparrows(t) = \{s_m\}
$$

donde $\{s_m\}$ es la secuencia muestreada a partir de $s(t)$.

Nos interesa entender que ocurre con $S(f)$, el espectro de $s(t)$, cuando se realiza el muestreo. Para verificar esto matematicamente necesitamos conocer la transformada de Fourier del tren de impulsos, que es

$$
\mathbb{FT}[\upuparrows(t)] =  F_s \sum_{m=-\infty}^\infty  \delta(f - m F_s)
$$

es decir **otro tren de impulsos** pero en frecuencia en lugar de tiempo. Pueden revisar la demostración de la transformada anterior [aquí](http://www.thefouriertransform.com/pairs/shah.php)

Con esto tenemos todos los ingredientes para calcular la transformada de Fourier de la señal discreta

$$
\begin{align}
\mathbb{FT}[s(t) \cdot \upuparrows(t)] &= \mathbb{FT}[s(t)] * \mathbb{FT}[\upuparrows(t)] \nonumber \\
&= S(f) *  F_s \sum_{m=-\infty}^\infty  \delta(f - m F_s) \nonumber \\
&= F_s \sum_{m = -\infty}^{\infty} S(f - m F_s) \nonumber
\end{align}
$$

:::{note}

El espectro de la señal discreta es equivalente a una repetición infinita del espectro de la señal continua. Las repeticiones del espectro están separadas en $F_s$ [Hz]

:::

Para obtener el espectro de la señal discreta hemos usado una importante propiedad de la transformada de Fourier: La transformada de Fourier de una multiplicación de señales corresponde a la convolución entre las transformadas de Fourier de cada uno de los términos.

A continuación veremos en mayor detalle que es la operación de convolución y que significa convolucionar con un tren de impulsos

### Convolucón con un tren de impulsos

La convolución entre dos señales discretas (de una dimensión) se define matematicamente como

$$
(f * g)[n] = \sum_{m=-\infty}^\infty f[m] g[n-m] = \sum_{m=-\infty}^\infty f[n-m] g[m]
$$

La variable $m$ se suele llamar (*lag*). La convolución consiste en sumar la multiplicación de $f$ con versiones retrasadas de $g$ (o viceverza). El resultado de la convolución es una nueva función

En el caso de que $f$ sea un tren de impulsos ocurrirá una "repetición" de $g$ (o viceverza). La siguiente animación ejemplifica lo anterior con la convolución entre una señal gaussiana y un tren de impulsos con $F_s=2$ [Hz]

```python
def señal(t, mu=0, sigma=0.2):
    return np.exp(-0.5*((t-mu)/sigma)**2)

def peineta(t, Fs=2000):
    s = np.zeros_like(t)
    s[::Fs] = 1
    return s

x = np.arange(-2, 2+1e-3, step=1e-3)
conv_s = np.convolve(señal(x), peineta(x), mode='same')
```

```python
%%output holomap='gif' fps=5

hmap1 = hv.HoloMap(kdims=['frame'])
hmap2 = hv.HoloMap(kdims=['frame'])
tren_plot = hv.Curve((x, peineta(x)), label='Tren de impulsos')
conv_plot = hv.Curve((x, conv_s), 'Tiempo [s]', 'Convolución')
for frame in range(0, 4001, 250):
    hmap1[frame] = hv.Curve((x, señal(x, mu=x[frame])), 'Tiempo [s]', 'Señal', label='Gaussiana')
    hmap2[frame] = hv.Scatter((x[frame], conv_s[frame])).opts(color='k', size=10)

main_plot = ((hmap1 * tren_plot) + (hmap2 * conv_plot)).cols(1)
main_plot.opts(hv.opts.Curve(height=200, xlim=(-2.1,2.1)))
```

:::{note}

Cada repetición de la gaussiana calza con uno de los impulsos del tren de impulsos

:::

### Ejemplo: Espectro de una señal Gaussiana discreta, parte 1

La señal Gaussiana con media cero es

$$
s(t) = \exp \left ( -\frac{t^2}{2\sigma^2} \right)
$$

y su transformada de Fourier es

$$
S(f) = \mathbb{FT}[s(t)] = \sqrt{2\pi}\sigma \cdot \exp \left ( -2\sigma^2 \pi^2 f^2 \right).
$$

Puedes ver la demostración de esta transformada [aquí](http://www.thefouriertransform.com/applications/gaussian.php)

Si prestamos atención a las fórmulas podemos notar que el espectro de una gaussiana es otra gaussiana pero con ancho inversamente proporcional a la original. Es decir que mientras más "ancha" sea la gaussiana en el tiempo ($\sigma$ pequeño) más "angosto" será su espectro en frecuencia, como muestra la siguiente gráfica

```python
x = np.arange(-5, 5, step=0.001)
gauss_time = lambda time, sigma: np.exp(-0.5*(time/sigma)**2)
gauss_freq = lambda freq, sigma: np.exp(-2*(np.pi*freq*sigma)**2)#*(np.sqrt(2*np.pi)*sigma)
```

```python
gt = hv.HoloMap(kdims=['sigma'])
gf = hv.HoloMap(kdims=['sigma'])

for sigma in [0.25, 1/np.sqrt(2*np.pi), 1]:
    gt[sigma] = hv.Curve((x, gauss_time(x, sigma)), 'Tiempo [s]', 's(t)')
    gf[sigma] = hv.Curve((x, gauss_freq(x, sigma)), 'Frecuencia [Hz]', 'S(f)')

(gt + gf).opts(hv.opts.Curve(width=300))
```

Como vimos anteriormente el espectro de una señal discreta es el espectro de la señal continua convolucionado con un tren de impulsos, donde los impulsos están separados entre sí por  $F_s$ [Hz].

La siguiente figura interactiva muestra la convolución del espectro teórico de la señal gaussiana con un tren de impulsos de $F_s=2$ [Hz] para distintos valores de $\sigma$.

```python
def periodic_gauss_freq(f, sigma, Fs=2):
    # Equivalente a convolucionar la gaussiana con un tren de impulsos
    S = np.zeros_like(f)
    for m in range(-20, 20):
        S += gauss_freq(f - Fs*m, sigma)
    return S

x = np.arange(-5, 5, step=0.001)
```

```python
espectro_continuo = hv.HoloMap(kdims=['sigma'], sort=False)
espectro_discreto = hv.HoloMap(kdims=['sigma'], sort=False)

for sigma in [1, 1/np.sqrt(2*np.pi), 0.25]:
    espectro_continuo[sigma] = hv.Curve((x, gauss_freq(x, sigma)),
                                        'Frecuencia [Hz]', 'Espectro', label='Continuo').opts(line_width=4)
    espectro_discreto[sigma] = hv.Curve((x, periodic_gauss_freq(x, sigma)),
                                        'Frecuencia [Hz]', label='Discreto')

(espectro_continuo * espectro_discreto * hv.Box(0, 0.5, (2, 1)).opts(line_dash='dashed', line_width=2, alpha=0.5))
```

:::{important}

Para poder recuperar perfectamente el espectro continuo (azul) a partir del espectro discreto (rojo) necesitamos que todas sus frecuencias distintas de cero estén dentro del rango $[-F_s/2, F_s/2]$ (negro punteado)

:::

Esta es la base del siguiente teorema fundamental

## Teorema del muestreo

El siguiente resultado se conoce como el Teorema del muestreo

> Sea una señal continua $s(t)$ muestreada a $F_s$ [Hz] produciendo una señal digital $s[n] = s(t = n/F_s)$
>
> La señal continua puede ser recuperada sin pérdidas a partir de las muestras digitales siempre y cuando
>
>$$
f_{\text{max}} < \frac{F_s}{2},
$$
>
> donde $f_{\text{max}}$ es la componente de frecuencia más alta presente en $s(t)$

La recuperación de la señal continua está dada por

$$
s(t) = \sum_{n=-\infty}^{\infty} s[n] \text{sinc}(\pi F_s (t - n /F_s) )
$$

La frecuencia

$$
F_{Nyq} = \frac{F_s}{2}
$$

se conoce como **Frecuencia de Nyquist**, en honor a [Harry Nyquist](https://en.wikipedia.org/wiki/Harry_Nyquist)

### Ejemplo: Espectro de una señal Gaussiana discreta, parte 2

Asumamos que la frecuencia de muestreo se mantiene en $F_s=2$ [Hz]. La frecuencia máxima de la gaussiana es la "última" frecuencia donde el espectro es distinto de cero

A continuación se muestra la misma figura anterior pero marcando con un círculo negro la posición aproximada de la frecuencia máxima.

```python
espectro_continuo = hv.HoloMap(kdims=['sigma'], sort=False)
espectro_discreto = hv.HoloMap(kdims=['sigma'], sort=False)
fmax_plot = hv.HoloMap(kdims=['sigma'], sort=False)

for sigma in [1, 1/np.sqrt(2*np.pi), 0.25]:
    espectro_continuo[sigma] = hv.Curve((x, gauss_freq(x, sigma)),
                                        'Frecuencia [Hz]', 'Espectro', label='Continuo').opts(line_width=4)
    espectro_discreto[sigma] = hv.Curve((x, periodic_gauss_freq(x, sigma)),
                                        'Frecuencia [Hz]', label='Discreto')
    fmax = x[gauss_freq(x, sigma)>1e-2][-1]
    fmax_plot[sigma] = hv.Scatter(([-fmax, fmax], [0, 0])).opts(size=10, color='k')

(espectro_continuo * espectro_discreto * fmax_plot * hv.Box(0, 0.5, (2, 1)).opts(line_dash='dashed', line_width=2, alpha=0.5))
```

Cuando la frecuencia máxima sale del cuadrado negro se produce un "solapamiento" de las gaussianas en el espectro discreto

:::{note}

Debido al solapamiento en el espectro discreto (rojo) se vuelve imposible recuperar el espectro original (azul) sin alteraciones.

:::

El solapamiento espectral se llama **aliasing**

## Aliasing

Como vimos anteriormente el espectro de una señal muestreada es periódico en $F_s$. Esto significa que si originalmente la señal tenía componentes con frecuencias mayores a $\frac{F_s}{2}$ se produce un "traslape" o "solapamiento" espectral

Este fenómeno se llama *aliasing* y los componentes traslapados se denominan *aliases*

:::{important}

Cuando existe aliasing veremos que no es posible reconstruir la señal original sin ambiguedad

:::

### Aliasing en el espectro de una sinusoide

Consideremos por ejemplo la siguiente señal sinusoidal

$$
s(t) = \cos(2\pi f_0 t)
$$

La transformada de Fourier de coseno es un impulso en $f_0$ y otro en $-f_0$

$$
S(f) = \frac{1}{2} \left(\delta(f-f_0) + \delta(f+f_0) \right)
$$

Puedes ver la demostración de esta transformada [aquí](http://www.thefouriertransform.com/pairs/sinusoids.php)

Si muestreamos la señal a una frecuencia $F_s$ que sea menor a $2 f_0$ entonces habrá traslape en el espectro discreto. Por ejemplo consideremos $f_0 = 1.23$ [Hz] y $F_s = 2$ [Hz]. Digamos además que observamos la señal por $100$ [s]

¿Cómo se ve el espectro de amplitud?

```python
import scipy.fft as sfft

f0 = 1.23 # Frecuencia de la sinusoide
T = 100 # Largo temporal
Fs = 2 # Frecuencia de muestreo

time = np.arange(0, T, step=1/Fs)
signal = np.cos(2.0*np.pi*f0*time)
SA = sfft.fftshift(np.absolute(sfft.fft(signal)))
freq = sfft.fftshift(sfft.fftfreq(n=len(time), d=1/Fs))
```

```python
hv.Curve((freq, SA), 'Frecuencia [Hz]', 'Espectro de amplitud')
```

:::{note}

La señal era de $1.23$ [Hz], sin embargo en el espectro aparecen *peaks* en $\pm 0.77$ [Hz]

:::

Recordemos que el espectro de la señal de la señal continua se repite cada $F_s$ [Hz] debido al muestreo. La siguiente figura muestra como se ve el espectro discreto de esta señal

Lo que observamos al calcular la FFT está en el rango

$$
\left[-\frac{F_s}{2}, \frac{F_s}{2} \right]
$$

que en este caso es $[-1,1]$ Hz, es decir que no estamos viendo el espectro original (verde) sino un alias (rojo)

### Frecuencia de los aliases

Hasta ahora no hemos explicado porqué el alias en este caso tiene una frecuencia de $\pm 0.77$ Hz

Del diagrama anterior podemos ver que para una sinusoide con frecuencia fundamental $f_0> \frac{F_s}{2}$, su espectro discreto tendrá infinitos aliases con las siguientes frecuencias

- $f_m = f_0  + m F_s$ [Hz] donde $m$ es un número natural
- $f_m = m F_s - f_0$ [Hz] donde $m$ es un número natural

Según esta fórmula el "primer" alias positivo tiene una frecuencia de

$$
f_m = F_s - f_0 = 2 - 1.23 = 0.773 [Hz],
$$

que es exactamente la frecuencia que vimos cuando calculamos la FFT de la señal

### Aliasing y reconstrucción de señales

La siguiente figura muestra simula el alias que ocurre cuando muestreamos la señal sinusoidal con frecuencia $f_0=1.23$ [Hz] usando una frecuencia de muestreo de $F_s=2$ [Hz]

```python
f0 = 1.23
T = 5
Fs = 2

t_c = np.arange(0, T, step=1e-4)
t_d = np.arange(0, T, step=1/Fs)
s_c = np.cos(2.0*np.pi*f0*t_c)
s_d = np.cos(2.0*np.pi*f0*t_d)
s_a = np.cos(2.0*np.pi*(Fs-f0)*t_c)
```

```python
p_continuo = hv.Curve((t_c, s_c), 'Tiempo [s]', 'Señal', label='Original').opts(color='g')
p_discreto = hv.Scatter((t_d, s_d), 'Tiempo [s]', label='Muestreada').opts(size=10, color='k')
p_alias = hv.Curve((t_c, s_a), 'Tiempo [s]', label='Alias').opts(line_dash='dashed', color='r')
(p_continuo * p_alias * p_discreto)
```

Si sólo tenemos la señal muestreada (puntos negros) no es posible distinguir si la sinusoide real es la original (verde) o el alias (roja punteada)

:::{important}

Debido al aliasing ya no podemos reconstruir la señal sin ambigüedad

:::

### ¿Cómo eliminamos el aliasing?

Para que no haya aliasing necesitamos que todas las componentes frecuenciales con amplitud distinta de cero de la señal cumplan con $ |f| < \frac{Fs}{2}$

Luego, para eliminar el aliasing podemos

- Modificar $F_s$: Podemos aumentar $F_s$ tal que sea dos veces mayor que la frecuencia máxima de interés. Para esto necesitamos saber *a priori* cuál es la frecuencia máxima
- Filtrar: Podemos eliminar las frecuencias mayores a $\frac{F_s}{2}$ antes de muestrear

En la próxima unidad veremos en detalle como filtrar señales

## Apéndice: Principio de incertidumbre para señales

Anteriormente vimos que el ancho en frecuencia y en tiempo de la gaussina son inversamentes proporcionales. Pero en realidad esto es algo que se cumple de forma general para todas las señales.

Recordemos primero el principio de incertudimbre "original", es decir el de mecánica cuántica

El [principio de incertidumbre de Heisenberg](https://es.wikipedia.org/wiki/Relaci%C3%B3n_de_indeterminaci%C3%B3n_de_Heisenberg) nos dice que la precisión (certeza) con que medimos la posición de una particula es inversamente proporcional a la precisión con que medimos su momentum lineal:

$$
\Delta x \Delta p \geq \frac{h}{4\pi},
$$

donde $h$ es la constante de Planck

:::{note}

**En señales existe un principio análogo:** No podemos especificar con infinita precisión la localización temporal y frecuencial de una señal al mismo tiempo.

:::

Denis Gabor (1946) fue el primero en darse cuenta de que el principio de incertidumbre aplica para señales.

Su teorema dice que para una señal con energía finita

$$
E = \int |s(t)|^2 dt
$$

con valor medio temporal

$$
\langle t \rangle = \frac{1}{E} \int t |s(t)|^2 dt,
$$

y varianza (ancho) temporal

$$
(\Delta t)^2 = \frac{1}{E} \int (t - \langle t \rangle)^2 |s(t)|^2 dt,
$$

cuya transformada de Fourier $\mathbb{FT}[s(t)] = S(\omega)$ tiene un valor medio en frecuencia

$$
\langle \omega \rangle = \frac{1}{E} \int (\omega - \langle \omega \rangle) |S (\omega)|^2 d \omega
$$

y varianza frecuencial

$$
(\Delta \omega)^2 = \frac{1}{E} \int (\omega - \langle \omega \rangle)^2 |S(\omega)|^2 d\omega
$$

se cumple que

$$
\Delta t \Delta \omega \geq \frac{1}{2},
$$

es decir $\Delta t$ y $\Delta \omega$ no pueden ser arbitrariamente pequeños

:::{important}

El ancho temporal y el ancho frecuencial están inversamente correlacionados sin importar la señal en particular

:::

```python

```


```python
# Header starts here.
from sympy.physics.units import *
from sympy import *

# Rounding:
import decimal
from decimal import Decimal as DX
from copy import deepcopy
def iso_round(obj, pv, rounding=decimal.ROUND_HALF_EVEN):
    import sympy
    """
    Rounding acc. to DIN EN ISO 80000-1:2013-08
    place value = Rundestellenwert
    """
    assert pv in set([
        # place value   #  round to:
        1,              #  1
        0.1,            #  1st digit after decimal
        0.01,           #  2nd
        0.001,          #  3rd
        0.0001,         #  4th
        0.00001,        #  5th
        0.000001,       #  6th
        0.0000001,      #  7th
        0.00000001,     #  8th
        0.000000001,    #  9th
        0.0000000001,   # 10th
        ])
    objc = deepcopy(obj)
    try:
        tmp = DX(str(float(objc)))
        objc = tmp.quantize(DX(str(pv)), rounding=rounding)
    except:
        for i in range(len(objc)):
            tmp = DX(str(float(objc[i])))
            objc[i] = tmp.quantize(DX(str(pv)), rounding=rounding)
    return objc

# LateX:
kwargs = {}
kwargs["mat_str"] = "bmatrix"
kwargs["mat_delim"] = ""
# kwargs["symbol_names"] = {FB: "F^{\mathsf B}", }

# Units:
(k, M, G ) = ( 10**3, 10**6, 10**9 )
(mm, cm) = ( m/1000, m/100 )
Newton = kg*m/s**2
Pa     = Newton/m**2
MPa    = M*Pa
GPa    = G*Pa
kN     = k*Newton
deg    = pi/180

half = S(1)/2

# Header ends here.
#
# https://colab.research.google.com/github/kassbohm/tm-snippets/blob/master/ipynb/TM_A/TM_2/rod-lin_cc.ipynb

# Input:
(l, lp) = ( 10 *cm, 12.5 *cm )
alpha = 30 * deg
e = Matrix([1, 0])

pprint("\n(l, l') / cm:")
tmp = Matrix([l, lp])
tmp /= cm
tmp = iso_round(tmp, 0.001)
pprint(tmp)

pprint("\nα / deg:")
tmp = alpha / deg
pprint(tmp)
alpha = N(alpha, 50)
ca, sa = cos(alpha), sin(alpha)

pprint("\nr / cm:")
r = Matrix([l, 0])
tmp = r
tmp /= cm
tmp = iso_round(tmp, 0.001)
pprint(tmp)

pprint("\nr' / cm:")
rp = lp*Matrix([ca, sa])
tmp = rp
tmp /= cm
tmp = iso_round(tmp, 0.001)
pprint(tmp)

pprint("\nΔℓ / cm:")
dell = lp - l
tmp = dell
tmp /= cm
tmp = iso_round(tmp, 0.001)
pprint(tmp)

pprint("\nΔl / cm:")
dl = e.dot(rp - r)
tmp = dl
tmp /= cm
tmp = iso_round(tmp, 0.001)
pprint(tmp)

pprint("\n|Δℓ - Δl | / l:")
tmp = abs(dell - dl)/l
tmp = iso_round(tmp, 0.001)
pprint(tmp)

# (l, l') / cm:
# ⎡10.0⎤
# ⎢    ⎥
# ⎣12.5⎦
#
# α / deg:
# 30
#
# r / cm:
# ⎡10.0⎤
# ⎢    ⎥
# ⎣0.0 ⎦
#
# r' / cm:
# ⎡10.825⎤
# ⎢      ⎥
# ⎣ 6.25 ⎦
#
# Δℓ / cm:
# 2.500
#
# Δl / cm:
# 0.825
#
# |Δℓ - Δl | / l:
# 0.167

```


# Removing `if` Statements from Expressions

## Author: Patrick Nelson

### NRPy+ Source Code for this module:
* [Min_Max_and_Piecewise_Expressions.py](../edit/Min_Max_and_Piecewise_Expressions.py) Contains functions that can be used to compute the minimum or maximum of two values and to implement piecewise-defined expressions

## Introduction:

Conditional statements are a critical tool in programming, allowing us to control the flow through a program to avoid pitfalls, code piecewise-defined functions, and so forth. However, there are times when it is useful to work around them. It takes a processor time to evaluate the whether or not to execute the code block, so for some expressions, performance can be improved by rewriting the expression to use an absolute value function in a manner upon which we will expand in this tutorial. Even more relevant to NRPy+ are piecewise-defined functions. These inherently involve `if` statements, but NRPy+'s automatic code generation cannot handle these by itself, requiring hand-coding to be done. However, if it is possible to rewrite the expression in terms of absolute values, then NRPy+ can handle the entire thing itself.

The absolute value is a function that simply returns the magnitude of its argument, a positive value. That is,
\begin{align}
|x|&= \left \{ \begin{array}{lll}x & \mbox{if} & x \geq 0 \\
-x & \mbox{if} & x \leq 0 \end{array} \right. \\
\end{align}

In C, this is implemented as `fabs()`, which merely has to make the first bit of a double-precision floating point number 0, and is thus quite fast.

There are myriad uses for these tricks in practice. One example comes from GRMHD (and, by extension, the special cases of GRFFE and GRHD), in which it is necessary to limit the velocity of the plasma in order to keep the simulations stable. This is done by calculating the Lorentz factor $\Gamma$ of the plasma and comparing to some predefined maximum $\Gamma_\max$. Then, if
$$
R = 1-\frac{1}{\Gamma^2} > 1-\frac{1}{\Gamma_{\max}^2} = R_\max,
$$
we rescale the velocities by $\sqrt{R_\max/R}$. In NRPy+, we instead always rescale by
$$
\sqrt{\frac{\min(R,R_\max)}{R+\epsilon}},
$$
which has the same effect while allowing the entire process to be handled by NRPy+'s automatic code generation. ($\epsilon$ is some small number chosen to avoid division by zero without affecting the results otherwise.) See [here](Tutorial-GRHD_Equations-Cartesian.ipynb#convertvtou) for more information on this.

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

This notebook is organized as follows

1. [Step 1](#min_max): Minimum and Maximum
    1. [Step 1.a](#confirm): Confirm that these work for real numbers
1. [Step 2](#piecewise): Piecewise-defined functions
1. [Step 3](#sympy): Rewrite functions to work with symbolic expressions
1. [Step 4](#validation): Validation against `Min_Max_and_Piecewise_Expressions` NRPy+ module
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Minimum and Maximum \[Back to [top](#toc)\]
$$\label{min_max}$$

Our first job will be to rewrite minimum and maximum functions without if statements. For example, the typical implementation of `min(a,b)` will be something like this:
```python
def min(a,b):
    if a<b:
        return a
    else:
        return b
```
However, to take full advantage of NRPy+'s automated function generation capabilities, we want to write this without the `if` statements, replacing them with calls to `fabs()`. We will define these functions in the following way:
$$\boxed{
\min(a,b) = \tfrac{1}{2} \left( a+b - \lvert a-b \rvert \right)\\
\max(a,b) = \tfrac{1}{2} \left( a+b + \lvert a-b \rvert \right).}
$$

## Step 1.a: Confirm that these work for real numbers \[Back to [top](#toc)\]
$$\label{confirm}$$

For real numbers, these operate exactly as expected. In the case $a>b$,
\begin{align}
\min(a,b) &= \tfrac{1}{2} \left( a+b - (a-b) \right) = b \\
\max(a,b) &= \tfrac{1}{2} \left( a+b + (a-b) \right) = a, \\
\end{align}
and in the case $a<b$, the reverse holds:
\begin{align}
\min(a,b) &= \tfrac{1}{2} \left( a+b - (b-a) \right) = a \\
\max(a,b) &= \tfrac{1}{2} \left( a+b + (b-a) \right) = b, \\
\end{align}

In code, we will represent this as:
```
min_noif(a,b) = sp.Rational(1,2)*(a+b-nrpyAbs(a-b))
max_noif(a,b) = sp.Rational(1,2)*(a+b+nrpyAbs(a-b))
```
For demonstration purposes, we will use `np.absolute()` and floating point numbers.

```python
import numpy as np               # NumPy: Python module specializing in numerical computations
import matplotlib.pyplot as plt  # matplotlib: Python module specializing in plotting capabilities

thismodule = "Min_Max_and_Piecewise_Expressions"
# First, we'll write the functions. Note that we are not using sympy right now. For NRPy+ code generation,
# use the expressions above.
def min_noif(a,b):
    return 0.5 * (a+b-np.absolute(a-b))

def max_noif(a,b):
    return 0.5 * (a+b+np.absolute(a-b))

# Now, let's put these through their paces.
a_number = 5.0
another_number = 10.0
print("The minimum of "+str(a_number)+" and "+str(another_number)+" is "+str(min_noif(a_number,another_number)))
```

    The minimum of 5.0 and 10.0 is 5.0

Feel free to test other cases above if you'd like. Note that we use a suffix, `_noif`, to avoid conflicts with other functions. When using this in NRPy+, make sure you use `sp.Rational()` and the `nrpyAbs()` function, which will always be interpreted as the C function `fabs()` (Sympy's `sp.Abs()` may get interpreted as $\sqrt{zz^*}$, for instance).

# Step 2: Piecewise-defined functions \[Back to [top](#toc)\]
$$\label{piecewise}$$

Next, we'll define functions to represent branches of a piecewise-defined function. For example, consider the function
\begin{align}
f(x) &= \left \{ \begin{array}{lll} \frac{1}{10}x^2+1 & \mbox{if} & x \leq 0 \\
\exp(\frac{x}{5}) & \mbox{if} & x > 0 \end{array} \right. , \\
\end{align}
which is continuous, but not differentiable at $x=0$.

To solve this problem, let's add the two parts together, multiplying each part by a function that is either one or zero depending on $x$. To define $x \leq 0$, this can be done by multiplying by the minimum of $x$ and $0$. We also will need to normalize this. To avoid putting a zero in the denominator, however, we will add some small $\epsilon$ to the denominator, i.e.,
$$
\frac{\min(x,0)}{x-\epsilon}
$$
This $\epsilon$ corresponds `TINYDOUBLE` in NRPy+; so, we will define the variable here with its default value, `1e-100`. Additionally, to get the correct behavior on the boundary, we shift the boundary by $\epsilon$, giving us
$$
\frac{\min(x-\epsilon,0)}{x-\epsilon}
$$

The corresponding expression for $x > 0$ can be written as
$$
\frac{\max(x,0)}{x+\epsilon},
$$
using a positive small number to once again avoid division by zero.
When using these for numerical relativity codes, it is important to consider the relationship between $\epsilon$, or `TINYDOUBLE`, and the gridpoints in the simulation. As long as $\epsilon$ is positive and large enough to avoid catastrophic cancellation, these functional forms avoid division by zero, as proven [below](#proof).

So, we'll code NumPy versions of these expressions below. Naturally, there are many circumstances in which one will want the boundary between two pieces of a function to be something other than 0; if we let that boundary be $x^*$, this can easily be done by passing $x-x^*$ to the maximum/minimum functions. For the sake of code readability, we will write the functions to pass $x$ and $x^*$ as separate arguments. Additionally, we code separate functions for $\leq$ and $<$, and likewise for $\geq$ and $>$. The "or equal to" versions add a small offset to the boundary to give the proper behavior on the desired boundary.

```python
TINYDOUBLE = 1.0e-100
def coord_leq_bound(x,xstar):
    # Returns 1.0 if x <= xstar, 0.0 otherwise.
    # Requires appropriately defined TINYDOUBLE
    return min_noif(x-xstar-TINYDOUBLE,0.0)/(x-xstar-TINYDOUBLE)

def coord_geq_bound(x,xstar):
    # Returns 1.0 if x >= xstar, 0.0 otherwise.
    # Requires appropriately defined TINYDOUBLE
    return max_noif(x-xstar+TINYDOUBLE,0.0)/(x-xstar+TINYDOUBLE)

def coord_less_bound(x,xstar):
    # Returns 1.0 if x < xstar, 0.0 otherwise.
    # Requires appropriately defined TINYDOUBLE
    return min_noif(x-xstar,0.0)/(x-xstar-TINYDOUBLE)

def coord_greater_bound(x,xstar):
    # Returns 1.0 if x > xstar, 0.0 otherwise.
    # Requires appropriately defined TINYDOUBLE
    return max_noif(x-xstar,0.0)/(x-xstar+TINYDOUBLE)

# Now, define our the equation and plot it.
x_data = np.arange(start = -10.0, stop = 11.0, step = 1.0)
y_data = coord_less_bound(x_data,0.0)*(0.1*x_data**2.0+1.0)\
        +coord_geq_bound(x_data,0.0)*np.exp(x_data/5.0)

plt.figure()
a = plt.plot(x_data,y_data,'k',label="Piecewise function")
b = plt.plot(x_data,0.1*x_data**2.0+1.0,'b.',label="y=0.1*x^2+1")
c = plt.plot(x_data,np.exp(x_data/5.0),'g.',label="y=exp(x/5)")
plt.legend()
plt.xlabel("x")
plt.ylabel("y")
plt.show()

```

The plot above shows the expected piecewise-defined function. It is important in applying these functions that each greater-than be paired with a less-than-or-equal-to, or vice versa. Otherwise, the way these are written, a point on the boundary will be set to zero or twice the expected value.

These functions can be easily combined for more complicated piecewise-defined functions; if a piece of a function is defined as $f(x)$ on $x^*_- \leq x < x^*_+$, for instance, simply multiply by both functions, e.g.
```
coord_geq_bound(x,x_star_minus)*coord_less_bound(x,x_star_plus)*f(x)
```

# Step 3: Rewrite functions to work with symbolic expressions \[Back to [top](#toc)\]
$$\label{sympy}$$

In order to use this with sympy expressions in NRPy+, we will need to rewrite the `min` and `max` functions with slightly different syntax. Critically, we will change `0.5` to `sp.Rational(1,2)` and calls to `np.absolute()` to `nrpyAbs()`. We will also need to import `outputC.py` here for access to `nrpyAbs()`. The other functions will not require redefinition, because they only call specific combinations of the `min` and `max` function.

In practice, we want to use `nrpyAbs()` and *not* `sp.Abs()` with our symbolic expressions, which will force `outputC` to use the C function `fabs()`, and not try to multiply the argument by its complex conjugate and then take the square root.

```python
from outputC import nrpyAbs     # NRPy+: Core C code output module

def min_noif(a,b):
    # Returns the minimum of a and b
    if a==sp.sympify(0):
        return sp.Rational(1,2) * (b-nrpyAbs(b))
    if b==sp.sympify(0):
        return sp.Rational(1,2) * (a-nrpyAbs(a))
    return sp.Rational(1,2) * (a+b-nrpyAbs(a-b))

def max_noif(a,b):
    # Returns the maximum of a and b
    if a==sp.sympify(0):
        return sp.Rational(1,2) * (b+nrpyAbs(b))
    if b==sp.sympify(0):
        return sp.Rational(1,2) * (a+nrpyAbs(a))
    return sp.Rational(1,2) * (a+b+nrpyAbs(a-b))
```

# Step 4: Validation against `Min_Max_and_Piecewise_Expressions` NRPy+ module \[Back to [top](#toc)\]
$$\label{validation}$$

As a code validation check, we will verify agreement in the SymPy expressions for plane-wave initial data for the Scalar Wave equation between
1. this tutorial and
2. the NRPy+ [Min_Max_and_Piecewise_Expressions](../edit/Min_Max_and_Piecewise_Expressions.py) module.

```python
# Reset & redefine TINYDOUBLE for proper comparison
%reset_selective -f TINYDOUBLE

import sympy as sp               # SymPy: The Python computer algebra package upon which NRPy+ depends
import NRPy_param_funcs as par   # NRPy+: parameter interface
TINYDOUBLE = par.Cparameters("REAL", thismodule, "TINYDOUBLE", 1e-100)

import Min_Max_and_Piecewise_Expressions as noif
all_passed=0

def comp_func(expr1,expr2,basename,prefixname2="noif."):
    passed = 0
    if str(expr1-expr2)!="0":
        print(basename+" - "+prefixname2+basename+" = "+ str(expr1-expr2))
        passed = 1
    return passed

a,b = sp.symbols("a b")

here = min_noif(a,b)
there = noif.min_noif(a,b)
all_passed += comp_func(here,there,"min_noif")

here = max_noif(a,b)
there = noif.max_noif(a,b)
all_passed += comp_func(here,there,"max_noif")

here = coord_leq_bound(a,b)
there = noif.coord_leq_bound(a,b)
all_passed += comp_func(here,there,"coord_leq_bound")

here = coord_geq_bound(a,b)
there = noif.coord_geq_bound(a,b)
all_passed += comp_func(here,there,"coord_geq_bound")

here = coord_less_bound(a,b)
there = noif.coord_less_bound(a,b)
all_passed += comp_func(here,there,"coord_less_bound")

here = coord_greater_bound(a,b)
there = noif.coord_greater_bound(a,b)
all_passed += comp_func(here,there,"coord_greater_bound")

import sys
if all_passed==0:
    print("ALL TESTS PASSED!")
else:
    print("ERROR: AT LEAST ONE TEST DID NOT PASS")
    sys.exit(1)
```

    ALL TESTS PASSED!

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

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

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

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