# Foundations for Machine Learning - Linear Algebra

We've briefly touched on the three aspects of machine learning (ML) defined by Tom Mitchell in his book _Machine Learning_ (1997) in past lectures:

1. Task (T)
2. Performance Measure (P)
3. Experience (E)

Our goal in machine learning is to construct a formal (i.e. mathematical) framework for these three ideas, and this will constitute a machine learning approach. Of course, the definition above is very broad, and says nothing about whether we find the machine learning algorithm figuratively _useful_ or _informative_ in some way. For example, just because we write a formal description of a performance measure (P) and calculate its value under a task (T) by using some experience (E), doesn't mean the algorithm has to obtain the *best* value of P. Instead, just _improving_ according to P over the course of computing the algorithm could technically be called learning. To start delving into these ideas more, we need a simple example that at least complex *enough* in the sense that it can help us gain some understanding about how one generally constructs T, P, and E, but also that also performs something we might consider _useful_ or _informative_. Alternatively, some algorithms are mathematically complex, and require a bit of study before we begin to understand how they work. Instead of diving directly into a machine learning approach, for this assignment we will be getting familiar with some mathematical concepts and the python libraries that we will be using to implement those mathematical concepts. Once we have these tools in hand, we will be prepared to begin exploring how these three elements T, P, and E are constructed, and how one might use them to solve engineering problems.

## Classification Tasks and Vectors

Generally, a classification task involves sorting experiences amongst a discrete set of options. One might, for example, think about the last week and separate each day into the classes of "good days" and "bad days". Or maybe one might listen to a song to determine which genre it belongs (pop, rock, r-n-b, reggae, etc.). These are some potential tasks, T. Typically, you will use already-classified examples from the past (experience, E) to motivate your selection process and help you decide how to classify a new example. Being familiar with many rock songs might help one identify key features that are common to rock songs, and new songs which have these same features may be classified as rock then as well. However, different features might be used to identify pop songs, and if a song shares some features of both rock and pop then you will be forced to make a difficult decision. Songs which clearly have many features of the rock genre and few features of the pop genre might be desribed as more similar or _less distant_ to other songs in the rock genre, but also less similar or _more distant_ to songs in the pop genre. Notice that we are using the terms _similarity_ and _distance_ where one is generally taken to be roughly the inverse of the other. We often use the term _similarity_ to describe relationships between songs, and hence put similar songs into the same genre. However, due the inverse relationship distance has to similarity, it is also reasonable to say that we instead put songs that are less distant into the same genre. Using distance, it's easier to say if something does _not_ belong to a category. For example, if we say that a pop song doesn't sound like it belongs in the rock genre, we are saying that the song shares few features with other rock songs. That is, the pop song is distant from other rock songs. This concept is important because you have probably already studied and used some so-called _distance metrics_ in other math and CS courses, and we will be using these mathematical tools to describe relationships between experiences in machine learning tasks. For example, a common _distance metric_ is the Euclidean distance, defined between two vectors (or points) $\boldsymbol{x}$ and $\boldsymbol{y}$ to be: $\sqrt{\sum_{i=1}^{n}(x_{i}-y_{i})^2}$ where $\boldsymbol{x}$ and $\boldsymbol{y}$ are both vectors of length $n$.

Vectors play an important role in machine learning. We need a formal way to deliver experiences to a machine learning algorithm, and vectors provide a general mathematical way to do this, and they also help us understand how this mode of delivery shapes the learning and performance of the algorithms that we develop.

As a quick exercise, let's construct a couple of vectors and calculate the Euclidean distance between them:

```python
# Prep the environment
import numpy as np

import sympy as sp
sp.init_printing(use_latex=True)

# New stuff!!
import scipy.spatial.distance as ssd

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

```python
x = np.array([1,1])
print(x)
y = np.array([0.5,1.5])
print(y)
```

    [1 1]
    [0.5 1.5]

Notice that we have chosen to use two vectors ($\boldsymbol{x}$ and $\boldsymbol{y}$) with a length of $n=2$ for this example.
We create each by constructing a python list of the vector elements and the passing the list to numpy's `array()` function. The reason why we don't use standard python lists should become apparent very soon.

Now, we can calculate their distance...

```python
# Direct calculation with numpy
np.sqrt(np.sum(pow(x-y,2.0)))
```

Here we have utilized a nice property of numpy where arrays of the _same size/dimensions_ can have an operator or function apply to each of the *corresponding* elements in the arrays. For comparison, the $x-y$ operation would not be computable between two python lists (try it and you will see that it's _not supported_ under python). However, numpy recognizes that you would like to perform a subtraction operation for the two vectors _and_ that they have the same dimensions. Therefore, it knows to go through the corresponding elements in the vectors and subtract them one at a time, similar to if you had written a for-loop to iterate over the elements. It's a wonderful thing...

The end result of the subtraction operation is then another _temporary_ vector (let's call it $r$) of the same size ($n=2$) where $r_1 = x_1 - y_1$ and $r_2 = x_2 - y_2$. In a more python-like description, we would say `r[0] = x[0] - y[0]` and `r[1] = x[1] - y[1]` since numpy arrays are 0-indexed just like other python data structures.

Keep in mind that most mathematical descriptions that you see in these examples, as well as in textbooks, will usually be 1-indexed. You will sometimes need to take care to remember that the data structures are 0-indexed instead.

Additionally, the `pow()` function is also applied over the $r$ array, and then the `sum()` function understands that it should sum over the values in the array provided to produce a single _scalar_ value which is finally passed to the `sqrt()` function for evaluation of the square-root.

We'll get back to vector data manipulation soon, but before we move on, let's look at a utility function from the scipy.spatial.distance package which can make this even easier:

## Distance and Similarity Calculations

```python
# Using the scipy library spatial.distance
ssd.euclidean(x,y)
```

There are many functions in numpy and/or scipy which provide useful routines for common mathematical calculations like distances. You can see the list of distance functions [here](https://docs.scipy.org/doc/scipy/reference/spatial.distance.html).

Let's explore vectors and distance calculations a little more...

First note that the Euclidean distance is a special case of the so-called Minkowski distance which has an additional parameter, $p$:

${\left( \sum_{i=1}^{n} {\lvert x_i - y_i \rvert}^p \right)}^{\frac{1}{p}} = {\lVert \boldsymbol{x} - \boldsymbol{y} \rVert}_p$

If we set $p=2$, we obtain the Euclidean distance:

${\left( \sum_{i=1}^{n} {\vert x_i - y_i \rvert}^2 \right)}^{\frac{1}{2}} = \sqrt{\sum_{i=1}^{n}(x_{i}-y_{i})^2} = {\lVert \boldsymbol{x} - \boldsymbol{y} \rVert}_2$

The notation used here to denote the Minkowski distance is worth remembering.

Another common distance metric used in machine learning is the Manhattan distance, which is also a special case of the Minkowski distance when $p=1$:

${\left( \sum_{i=1}^{n} {\vert x_i - y_i \rvert}^1 \right)}^{\frac{1}{1}} = \sum_{i=1}^{n}\lvert x_{i}-y_{i} \rvert = {\lVert \boldsymbol{x} - \boldsymbol{y} \rVert}_1$

You can calculate these distances using the `minkowski()` function:

```python
print('Euclidean: %f'%ssd.minkowski(x,y,2))
print('Manhattan: %f'%ssd.minkowski(x,y,1))
```

    Euclidean: 0.707107
    Manhattan: 1.000000

Another commonly used metric is the _cosine similarity_. Note that this is not a distance function, but a __similarity__ function. For all of the metrics above, similar vectors have _low_ distance from one another and dissimilar vectors have _high_ distance from one another. However, under the cosine similarity metric, similar items will have _high_ similarity and dissimilar items will have _low_ similarity.

Cosine similarity is defined as follows:
$\cos{\theta} = \frac{\boldsymbol{x} \cdot \boldsymbol{y}}{{\lVert \boldsymbol{x} \rVert}_2 {\lVert \boldsymbol{y} \rVert}_2}$

where

${\lVert \boldsymbol{x} \lVert}_p = {\left( \sum_{i=1}^{n} {\lvert x_i \rvert}^p \right)}^\frac{1}{p}$ (so-called: p-norm)

and

$\boldsymbol{x} \cdot \boldsymbol{y} = \sum_{i=1}^{n} x_i y_i$ (so-called: dot-product)

and when $p=2$, we refer to this as the L2-norm of $\boldsymbol{x}$ (or somethimes simply the _length_ of the vector described by $\boldsymbol{x}$).

Let's calculate the cosine similarity between $\boldsymbol{x}$ and $\boldsymbol{y}$:

```python
np.sum(x*y)/(np.sqrt(np.dot(x,x))*np.sqrt(np.dot(y,y)))
```

An alternative way to formulate the L2-norm is to take the square-root of the dot product between a vector and itself:

${{\lVert \boldsymbol{x} \rVert}_2} = \sqrt{\boldsymbol{x} \cdot \boldsymbol{x}}$

The value, $\theta$, represents the angle between the two vectors measured in _radians_. However, instead of solving for $\theta$, we usually are only interested in $\cos{\theta}$ since this value will be in the range $[-1,1]$ where vectors with high similarity will have a $\cos{\theta}$ close to 1, _orthogonal_ vectors will have a $\cos{\theta}$ close to 0, and vectors which nearly lie on the same line but point in _opposite_ directions will have a $\cos{\theta}$ of near -1.

Due to this property, another common distance metric is the _cosine distance_ which is defined to be $1-\cos{\theta}$, or:

$1 - \frac{\boldsymbol{x} \cdot \boldsymbol{y}}{{\lVert \boldsymbol{x} \rVert}_2 {\lVert \boldsymbol{y} \rVert}_2}$

This formulation allows for vectors which point in similar directions to have a distance of _zero_, while _orthgonal_ vectors have a distance of _one_, and vectors pointing in opposite directions will have a distance of _two_ (this is however, the maximum distance between two vectors using cosine distance).

Also, the dot product, $\boldsymbol{x} \cdot \boldsymbol{y}$, alone has similar properties to cosine similarity so it is sometimes preferred due to the ease of computation using numpy's `dot()` function. For example, if you can know _a priori_ (beforehand, ahead of time) that your vectors will all be of length $n=1$ then the dot product is an _exact_ replacement for cosine similarity. We may investigate different ways to transform distance metrics into similarity metrics and vice-versa in future assignments.

Let's calculate cosine similarity and distance now...

```python
print('Cosine similarity: %f'%(np.dot(x,y) /
                               (np.sqrt(np.dot(x,x)) * np.sqrt(np.dot(y,y)))))
print('Cosine distance: %f'%ssd.cosine(x,y))
```

    Cosine similarity: 0.894427
    Cosine distance: 0.105573

As mentioned above, we can rewrite cosine similarity as follows:

$\boldsymbol{x} \cdot \boldsymbol{y} = {{\lVert \boldsymbol{x} \rVert}_2 {\lVert \boldsymbol{y} \rVert}_2} \cos {\theta}$

This means that the dot product ($\boldsymbol{x} \cdot \boldsymbol{y}$) is an _unscaled_ or _unnormalized_ version of the cosine similarity metric. You can compute it in two different ways:

```python
# The dot product
print(np.sum(x*y))
print(np.dot(x,y))
```

    2.0
    2.0

The `dot()` function actually is very useful for working with matrices and tensors as well, and will come in handy later. For now, if you wanted to learn a little more about its capabilities, you could just ask for help in the notebook. This will look up the basic documentation provided for the function:

```python
help(np.dot)
```

    Help on function dot in module numpy:

    dot(...)
        dot(a, b, out=None)

        Dot product of two arrays. Specifically,

        - If both `a` and `b` are 1-D arrays, it is inner product of vectors
          (without complex conjugation).

        - If both `a` and `b` are 2-D arrays, it is matrix multiplication,
          but using :func:`matmul` or ``a @ b`` is preferred.

        - If either `a` or `b` is 0-D (scalar), it is equivalent to :func:`multiply`
          and using ``numpy.multiply(a, b)`` or ``a * b`` is preferred.

        - If `a` is an N-D array and `b` is a 1-D array, it is a sum product over
          the last axis of `a` and `b`.

        - If `a` is an N-D array and `b` is an M-D array (where ``M>=2``), it is a
          sum product over the last axis of `a` and the second-to-last axis of `b`::

            dot(a, b)[i,j,k,m] = sum(a[i,j,:] * b[k,:,m])

        Parameters
        ----------
        a : array_like
            First argument.
        b : array_like
            Second argument.
        out : ndarray, optional
            Output argument. This must have the exact kind that would be returned
            if it was not used. In particular, it must have the right type, must be
            C-contiguous, and its dtype must be the dtype that would be returned
            for `dot(a,b)`. This is a performance feature. Therefore, if these
            conditions are not met, an exception is raised, instead of attempting
            to be flexible.

        Returns
        -------
        output : ndarray
            Returns the dot product of `a` and `b`.  If `a` and `b` are both
            scalars or both 1-D arrays then a scalar is returned; otherwise
            an array is returned.
            If `out` is given, then it is returned.

        Raises
        ------
        ValueError
            If the last dimension of `a` is not the same size as
            the second-to-last dimension of `b`.

        See Also
        --------
        vdot : Complex-conjugating dot product.
        tensordot : Sum products over arbitrary axes.
        einsum : Einstein summation convention.
        matmul : '@' operator as method with out parameter.

        Examples
        --------
        >>> np.dot(3, 4)
        12

        Neither argument is complex-conjugated:

        >>> np.dot([2j, 3j], [2j, 3j])
        (-13+0j)

        For 2-D arrays it is the matrix product:

        >>> a = [[1, 0], [0, 1]]
        >>> b = [[4, 1], [2, 2]]
        >>> np.dot(a, b)
        array([[4, 1],
               [2, 2]])

        >>> a = np.arange(3*4*5*6).reshape((3,4,5,6))
        >>> b = np.arange(3*4*5*6)[::-1].reshape((5,4,6,3))
        >>> np.dot(a, b)[2,3,2,1,2,2]
        499128
        >>> sum(a[2,3,2,:] * b[1,2,:,2])
        499128

## Matrices and Vectors

Now that we have worked with vectors a little, we will find ourselves needing to work with many of them at the same time. For that, we usually stack our vectors into a matrix so that we can utilize them for matrix-vector and matrix-matrix multiplication operations.

Let's make a new vector, $\boldsymbol{z}$, and then combine each of the 2-element vectors $\boldsymbol{x},\boldsymbol{y},\boldsymbol{z}$ into a 3-row matrix.

The result (we will call it: data) will be a 3x2 matrix.

```python
# Make another vector
z = np.array([1.5,0.25])

# Combine all vectors -by row- to form a matrix
data = np.array([x,y,z])
data
```

    array([[1.  , 1.  ],
           [0.5 , 1.5 ],
           [1.5 , 0.25]])

If you prefer, you can use sympy to print matrices and vectors for $\LaTeX$ style rendering. Note that you will get column-wise vectors by default. You can always see the transpose instead using the `T` member of the Matrix class.

```python
# Note that Matrix() was imported from sympy
sp.Matrix(data)
```

```python
# Same for vectors - but you will get a column-matrix
sp.Matrix(x)
```

```python
# Maybe you want the transpose - use T?
sp.Matrix(x).T
```

## Pairwise Distance Matrices

A common kind of matrix used in machine learning is the *pairwise distance* matrix. This matrix consists of distance calculations for $m$ different vectors obtained from an $m$ x $n$ data matrix.

Our matrix was 3x2, which means we have $m=3$ vectors, each of dimension $n=2$. What we want to do is compare each of the 3 vectors to itself and the other two. After doing this for all 3 vectors, we will have a 3x3 matrix which contains the distance from vector $i$ to $j$ where $i=1...n$ and $j=1...n$.

The `pdist()` function from sypy.spatial is useful for making this calculation, but to get the full $m$ x $m$ to display, we then convert the result into a square matrix using the `squareform()` function. (Since the matrix is symmetric, the internal storage of the matrix prefers to just keep the lower triangle instead of the entire thing, but _printing_ the square form is better supported.)

Here are some examples using the data we created above for a few distance metrics:

```python
ssd.squareform(ssd.pdist(data,metric='euclidean'))
```

    array([[0.        , 0.70710678, 0.90138782],
           [0.70710678, 0.        , 1.60078106],
           [0.90138782, 1.60078106, 0.        ]])

```python
ssd.squareform(ssd.pdist(data,metric='minkowski',p=2))
```

    array([[0.        , 0.70710678, 0.90138782],
           [0.70710678, 0.        , 1.60078106],
           [0.90138782, 1.60078106, 0.        ]])

```python
ssd.squareform(ssd.pdist(data,metric='minkowski',p=1))
```

    array([[0.  , 1.  , 1.25],
           [1.  , 0.  , 2.25],
           [1.25, 2.25, 0.  ]])

```python
ssd.squareform(ssd.pdist(data,metric='cosine'))
```

    array([[0.        , 0.10557281, 0.18626653],
           [0.10557281, 0.        , 0.53211228],
           [0.18626653, 0.53211228, 0.        ]])

```python
# Just to make it pretty...
sp.Matrix(ssd.squareform(ssd.pdist(data,metric='cosine')))
```

# Matrix-Vector Operations

We will be using common linear algebra operations often for neural network computations.

One common operations are the matrix-vector product. I will show how to print the expression we are wanting to compute using sympy, but most of the time, I will _not_ ask you to do this for your assignments. Sympy, by default, will simplify expressions automatically, so it takes some work to turn off the simplication engine in order to use it for printing.

Nevertheless, let's try to compute a matrix-vector product using the numpy `dot()` function...

```python
# Let's use sympy to print a pretty version of the operation..
sp.Eq(sp.MatMul(sp.Matrix(data),sp.Matrix(x),evaluate=False),
      sp.Matrix(np.dot(data,x)),evaluate=False)
```

```python
# Most of the time, you will simply use:
np.dot(data,x)
```

    array([2.  , 2.  , 1.75])

```python
# And a simple pretty-print of the results...
sp.Matrix(np.dot(data,x))
```

Note how numpy knows that `data` is a 3x2 array and `x` is a 2-dimensional vector. This means that it treats the vector as a 2x1 matrix for this operation. The result is therefore a 3x1, which is expressed more clearly in the pretty-printed version.

This property of numpy which allows it match vector, matrix, and other arrays of arbitrary dimension (tensors - we will see them later) by correspondingly sized dimensions will be useful.

Note what happens if we reverse the order of `data` and `x`:

```python
np.dot(x,data)
```

While numpy is smart enough to match corresponding dimensions when requested, you still have to formulate your computation in the order that makes sense for the array and matrix you are using.

In the case above, since numpy sees the `x` vector as a column vector, it has size 2x1, and trying to multply a 2x1 by a 3x2 doesn't work (the "inner" numbers would need to match in the order of calculations, but 1 and 3 are not the same).

So, just remember, numpy is *great*, but _order matters_...

## Matrix-Matrix operations

Numpy is also good for performing matrix-matrix operations (as well as vector-tensor, matrix-tensor, and tensor-tensor... we will do those later).

For now, we will calculate `data` times its transpose. That's a 3x2 times a 2x3, so the result is a 3x3. Again, I will use sympy to show the math, but most of the time the `dot()` function is what you will be using...

```python
sp.Eq(sp.MatMul(sp.Matrix(data),sp.Matrix(data).T,evaluate=False),
      sp.Matrix(np.dot(data,data.T)),evaluate=False)
```

```python
# Let's use numpy!
sp.Matrix(np.dot(data,data.T))
```

Notice, this is equivalent to calculating the __pairwise dot-product__ between all three vectors!

With a little more work, we can calculate the pairwise cosine distances like we did using the `pdist()` function and the `metric=cosine` argument, but using only pure numpy...

```python
def mynorm(a_vector):
    return np.sqrt(np.dot(a_vector,a_vector))

A = np.round(
    1.0 - (np.dot(data,data.T) / np.dot(
    np.reshape(np.apply_along_axis(mynorm,1,data),[3,1]),
    np.reshape(np.apply_along_axis(mynorm,1,data),[1,3]))),12)
# Notice how the object returned by final line of code
# in a code cell gets printed?
sp.Matrix(A)
```

There are a lot of new functions being used here. Some are fairly straight-forward to understand, such as `round()` which simply rounds numbers to the specified number of digits following the decimal point (in this case, 12). Also, the `dot()` and `T` operations you have already seen.

However, another useful tool is the `apply_along_axis()` function. This function allows us to call a function and apply it to individual vectors (or even sub-matrices if we are using tensors) in a matrix. In the case of the cosine distance, we need to calculate the vector norms in the denominator of the equation. First, we create a function, `mynorm` which takes a vector as input, and calculates the norm (a scalar) for that vector. We use this as the first argument to apply_along_axis since this is the function we need to apply to the vectors in `data`. Next, we know we need to apply the function to the vectors in each _row_ and the first dimension of a matrix specifies the number of _rows_ in that matrix. Thus, if we want to apply the function to each row, we select the first dimension (0) as the second argument. Finally, we supply the matrix containing the vectors we are performing the operation on: `data`. The end result is a 3-element vector which contains the _lengths_ (vector norms) of each of the 3 vectors in the matrix.

The `reshape()` function then comes in handy for forming the 3 elements into first a 3x1 matrix, and then a 1x3 matrix (one matrix for each call). By multiplying the two matrices together with `dot()` we have calculated all pairs of vector norms (a 3x3 matrix - the denominators in the cosine distance equation).

We then use `dot()` to calculate the dot products for all vectors and divide it by the pairwise norm matrix before we finally subtract the result from 1, and pass it to `round()` to eliminate some numerical round-off error accumulation. (Feel free to remove the `round()` function to observe the left-over values along the diagonal, the values of which are very near, but not quite, zero.

## Matrix Decompositions

Finally, let's perform some matrix factorizations (or often matrix decomposition). This is similar to finding the factors of a number, like how you can get $12=4*3$ or $12=2*6$. Each pair is a factorization of the number $12$.

For matrix factorization, we want a combination of matrices that can be multiplied together to obtain a given matrix, $\boldsymbol{A}$.

We will explore how to use these operations in neural networks and machine learning in the future. For now, just learn how to compute them.

Let's start with *Eigen decomposition*, where we want to factor a square and (in most cases) symmetric matrix $\boldsymbol{A}$ (which we computed above) into:

$\boldsymbol{A} = \boldsymbol{Q} \boldsymbol{\Lambda} \boldsymbol{Q}^\intercal$

```python
L,Q = np.linalg.eig(A)

sp.MatMul(sp.Matrix(Q),sp.Matrix(np.diag(L)),sp.Matrix(Q).T,evaluate=False)
```

The result is a 3x3 matrix of eigen vectors, $\boldsymbol{Q}$, and a _diagonal_ matrix of eigen values, $\boldsymbol{\Lambda}$ (which I have assigned to L for simplicity).

You can obtain the original matrix, $A$, (within a little numerical rounding error) by multiplying the parts together as prescribed above:

```python
sp.Matrix(np.round(np.dot(np.dot(Q,np.diag(L)),Q.T),12))
```

Another common decomposition is the *singular value decomposition*, which can be applied to $m$ x $n$ (i.e. rectangular) matrices:

$\boldsymbol{A} = \boldsymbol{U} \boldsymbol{\Sigma} \boldsymbol{V}^\intercal$

```python
U,S,V = np.linalg.svd(A,full_matrices=True)

sp.MatMul(sp.Matrix(U),sp.Matrix(np.diag(S)),sp.Matrix(V),evaluate=False)
```

The result is a 3x3 matrix of left-singular vectors, $\boldsymbol{U}$, and a _diagonal_ matrix of singular values, $\boldsymbol{\Sigma}$ (which I have assigned to S for simplicity), and a 3x3 matrix of right-singular vectors, $\boldsymbol{V}$. (However, note that V is already in transposed form when returned by numpy.)

You can obtain the original matrix, $\boldsymbol{A}$, (within a little numerical rounding error) by multiplying the parts together as prescribed above:

```python
sp.Matrix(np.round(np.dot(np.dot(U,np.diag(S)),V),12))
```


# Laplace's equation for electrostatics

- toc:false
- branch: master
- badges: true
- comments: false
- categories: [mathematics, numerical recipes]
- hide: true

-----
Questions:
- How do I use a finite difference method to calculate derivatives?
- How do I use the relaxation method to solve Laplace's equation?

Objectives:
- Use the finite difference method to convert Laplace's equation into a set of linear simultaneous equations
- Use the relaxation method to solve Laplace's equation

-----

### The method of finite differences is often used to solve partial differential equations

Consider the two-dimensional Laplace equation for the electric potential $\phi$ subject to appropriate boundary conditions:

\begin{equation}
\frac{\partial^2\phi}{\partial x^2} + \frac{\partial^2\phi}{\partial y^2} = 0
\end{equation}

Real physical problems are in three dimensions, but we can more easily visualise the method of finite differences - and the extension to three dimensions is straight forward.

The method of finite differences involves dividing the space into a grid of discrete points $[x,y]$ and calculating numerical derivatives or at each of these points.

In this case we consider a 2-dimensional sheet with a fixed voltage $V$ at the top side, and all other sides fixed at $0V$.  But how do we calculate these numerical derivatives?

### A numerical derivative can be calculated using the forward, backward or central difference

The standard definition of a derivative is

\begin{equation}
\frac{\mathrm{d} f}{\mathrm{d} x} = \lim_{h\to0}\frac{f(x+h)-f(x)}{h}.
\end{equation}

To calculate the derivative numerically we make $h$ very small and calculate

\begin{equation}
\frac{\mathrm{d} f}{\mathrm{d} x} \simeq \frac{f(x+h)-f(x)}{h}.
\end{equation}

This is the forward difference because it is measured in the forward direction from $x$.

The backward difference is measured in the backward direction from $x$:

\begin{equation}
\frac{\mathrm{d} f}{\mathrm{d} x} \simeq \frac{f(x)-f(x-h)}{h},
\end{equation}

and the central difference uses both the forwards and backwards directions around $x$:

\begin{equation}
\frac{\mathrm{d} f}{\mathrm{d} x} \simeq \frac{f(x+\frac{h}{h2})-f(x-\frac{h}{2})}{h},
\end{equation}

> Important: The central difference is in general more accurate than the forward or backward differences, but im all cases we need to carefully converge with respect to the step size $h$

### Second-order derivatives, partial derivatives and the Laplacian can be calculated using the same approach

The second derivative is a derivative of a derivative, and so we can calculate it be applying the first derivative formulas twice. The resulting expression (after application of central differences) is:

\begin{equation}
\frac{\mathrm{d} ^2f}{\mathrm{d} x^2} \simeq \frac{f(x+h)-2f(x)+f(x-h)}{h^2}.
\end{equation}

The extension to partial derivatives is straight-forward:

\begin{equation}
\frac{\partial f}{\partial x} \simeq \frac{f(x+\frac{h}{2},y)-f(x-\frac{h}{2},y)}{h},
\end{equation}

\begin{equation}
\frac{\partial f}{\partial y} \simeq \frac{f(x,y+\frac{h}{2})-f(x,y-\frac{h}{2})}{h},
\end{equation}

\begin{equation}
\frac{\partial ^2f}{\partial x^2} \simeq \frac{f(x+h,y)-2f(x,y)+f(x-h,y)}{h^2},
\end{equation}

\begin{equation}
\frac{\partial ^2f}{\partial y^2} \simeq \frac{f(x,y+h)-2f(x,y)+f(x,y-h)}{h^2}.
\end{equation}

By adding the two equations above, the Laplacian in two dimensions is:

\begin{equation}
\frac{\partial ^2f}{\partial x^2} + \frac{\partial ^2f}{\partial y^2} \simeq \frac{f(x+h,y)+f(x-h,y)+f(x,y+h)+f(x,y-h)-4f(x,y)}{h^2},
\end{equation}

The expression above is known as a five-point stencil as it uses five points to calculate the Laplacian.

### The finite difference method turns our partial differential equation into a set of linear simulatenous equation

Returning to our Laplace equation for for the electric potential $\phi$:

\begin{equation}
\frac{\partial^2\phi}{\partial x^2} + \frac{\partial^2\phi}{\partial y^2} = 0
\end{equation}

The numerical Laplacian can be substituted into the equation above, giving us a set of $n$ simulatenous equations for the $n$ grid points.

\begin{equation}
 \frac{\phi(x+h,y)+\phi(x-h,y)+\phi(x,y+h)+\phi(x,y-h)-4\phi(x,y)}{h^2} = 0,
\end{equation}

where $h$ is the distance between each grid point.

### To  solve this set of equations we use the relaxation method

To calculate $\phi(x,y)$ we use the relaxation method, also known as the Jacobi method in the context of the Laplace equation.
First we re-arrange the equation above:

\begin{equation}
 \phi(x,y)=\frac{1}{4}\left(\phi(x+h,y)+\phi(x-h,y)+\phi(x,y+h)+\phi(x,y-h)\right).
\end{equation}

 Note that because we set the Laplacian equal to zero in Equation 13 (for this particular example), the $h^2$ term has dropped out of the expression - this might not be the case for other examples.

This tells us that $\phi(x,y)$ is the average of the surrounding grid points, which can be represented visually as:

 Second, we fix $\phi(x,y)$ at the boundaries using the boundary conditions. Third, we guess the initial values of the interior $\phi(x,y)$ points - our guesses do not need to be good, and can be zero.

Finally we use Equation 14 to calculate new values of $\phi'(x,y)$ at all points in space. We take these new $\phi'(x,y)$ values and feed them into Equation 14 again to calculate new values. We repeat this iterative process until the $\phi(x,y)$ values converge, and that is our solution.

Convergence can be tested by specifying what the maximum difference should be between iterations. For example, that $\phi'(x,y)-\phi(x,y)< 1e-5$ for all grid points.

### The relaxation and finite difference methods can be applied using the Python skills we have developed

We will now use our Python Skillz to solve Laplace's equation with the boundary conditions outlined above. Let's also imagine that the sheet is 1m along each side and that we want a grid spacing of 1cm. First let's import the libraries we will use:

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

Now let's specify our simulation parameters...

```python
grid_width = 101 # number of grid points for width of simulation
grid_height = 101 # number of grid points for height of simulation
target = 1e-6 # target accuracy to complete convergence
```

...and our boundary conditions:

```python
V_top = 1.0 # top wall voltage, in volts
V_left = 0.0 # left wall voltage
V_right = 0.0 # right wall voltage
V_bottom = 0.0 # bottom wall voltage
```

Next let's create a NumPy array to hold our $\phi(x,y)$ and $\phi'(x,y)$ values:

```python
phi = np.zeros([grid_height,grid_width], float)
phi_prime = np.empty([grid_height,grid_width], float)
```

Now we apply the boundary conditions to our array:

```python
phi[0,:] = V_top
```

Now we write a function which uses the finite difference method to calculate an updated $\phi'(x,y)$ .

```python
def finite_difference(phi):

    for i in range(grid_height): # for each grid point
        for j in range(grid_width):
            if i==0 or i==grid_height-1 or j==0 or j==grid_width-1:
                phi_prime[i,j] = phi[i,j] # if at boundary, keep fixed
            else: # otherwise apply finite difference
                phi_prime[i,j] = (phi[i+1,j]+phi[i-1,j]+phi[i,j+1]+phi[i,j-1]) / 4

    return phi_prime
```

Finally let's use the relaxation method. We repeatedly call the function `finite_difference` until all values of $\phi(x,y)$ are converged.

```python
delta = 1.0 # create delta. It can take any value larger than the target accuracy

while delta > 1e-6:  # keep running the following code until delta < 1e-6
    phi_prime = finite_difference(phi)  # calculate phi_prime
    delta = np.max(np.abs(phi-phi_prime)) # calculate the maximum difference between phi and phi_prime
    phi,phi_prime = phi_prime,phi # Swap phi and phi-prime, ready for the next iteration
```

We can visualise our result using the function `matplotlib.pyplot.imshow` which displays our data as an image:

```python
plt.imshow(phi_prime)
```

This result makes sense: there is a region of high electric potential around the top side of the sheet, where the voltage is fixed at 1V, and regions of low potential around the other three walls. If we would like a colour bar to indicate the $\phi(x,y)$ values across the image then we can use the function `matplotlib.pyplot.contourf` to produce a filled contour plot. Note that this function flips our image (plotting the values held in the array from left to right, top to bottom) so we use `numpy.flip` to achieve the expected result.

```python
plt.contourf(np.flip(phi_prime),levels=np.linspace(0,1,101))
plt.colorbar().set_label("Electric potential")
```

---
Keypoints:
- The finite difference method for numerical derivatives is often used to solve partial differential equations
- A numerical derivative can be calculated using the forward, backward or central difference
- Second-order derivatives, partial derivatives and the Laplacian can be calculated using the same approach
- The finite difference method turns our partial differential equation into a set of linear simulatenous equation
- To solve this set of equations we use the relaxation method
- The relaxation and finite difference methods can be applied using the Python skills we have developed

---

Do [the quick-test](https://nu-cem.github.io/CompPhys/2021/08/02/Finite-Difference-Qs.html).

Back to [Modelling with Partial Differential Equations](https://nu-cem.github.io/CompPhys/2021/08/02/PDEs.html).

---


```python
# Set up to be able to use python for symbolic math help.
from sympy import *
from sympy.assumptions.assume import global_assumptions
from IPython.display import display, Math, Latex # Latex printing in notebook

l, t, m1, m2, g = symbols("l t m1 m2 g", real=True, positive=True)
p1x = Function("p1x", real=True)(t)
theta = Function("theta", real=True)(t)
global_assumptions.add(Q.real(theta.diff(t)))
global_assumptions.add(Q.real(p1x.diff(t)))
thetaDot = theta.diff(t, real=True)
```

Suppose we have a mass attached to a cart depicted in following diagram:

We wish to control the system such that the $m2$ mass will remain in the upright and vertical position.
To keep the $m2$ mass in the upright vertical position i.e. keeping $\theta = 0$, we will be applying some force $f(t)$ on the $m1$ mass in the horizontal direction. To determine the magnitude and direction of the force needed to keep the $m2$ mass balanced, we will be using a full state feedback controller.

In order create a state feedback controller to balance our $m2$ mass, we must first come up with a mathematical model of our system, a.k.a the equations of motion (EOM). For this exercise, we opted to use [Lagrangian mechanics](https://en.wikipedia.org/wiki/Lagrangian_mechanics) to derive our EOM.

> Note: We could have derived the EOMs using Newton's 2nd law, but found the math to be much more simple
> when using the Lagrangian.

In order to use the Lagrangian, we must fully describe the kinetic and potential energies of the system. This is more easily achieved if we add a few position vectors to our system diagram:

With the vectors $\vec{p_1}$ and $\vec{p_2}$ we can now describe the kinetic and potential energies as follows:

$$
KE = \frac{1}{2} m_1 \lvert \dot {\vec p_1} \rvert ^2 + \frac{1}{2} m_2 \lvert \dot {\vec p_2} \rvert ^2 \\
\tag{1}
\label{keNotUseful}
$$

$$
PE = m_2 g l cos(\theta) \\
\tag{2}
\label{pe}
$$

While the equation listed at $\eqref{keNotUseful}$ is very true, it is not very helpful. When we go to implement, we will not be able to directly measure or approximate $\vec p_2$ or $\dot{\vec p_2}$. We can however solve for $\vec p_2$ and $\dot{\vec p_2}$ in terms of other variables we can measure and approximate such as $\vec p_1$ and $\dot {\vec p_1}$.

> Note: In the controls world, we typically call the variables that we can measure and/or calculate in real-time 'states'.

Consider our system but while only looking at the following vectors:

We can say that
$$
\vec{p_2} = \vec{p_1} + \Delta \vec{p} \\
\tag{3}
$$

Using the vector diagram, we can substitute useful relationships for $\vec{p_1}$ and $\Delta \vec{p}$ to represent $\vec{p_2}$ in terms of states that we can measure or approximate.

```python
p1Vec = Matrix([p1x,
                0], real=True)
display(Math(r"\vec{p_1} = {}"), p1Vec)
```

$\displaystyle \vec{p_1} = {}$

$\displaystyle \left[\begin{matrix}\operatorname{p1x}{\left(t \right)}\\0\end{matrix}\right]$

```python
deltaPVec = Matrix([-l*sin(theta),
                   l*cos(theta)], real=True)
display(Math(r"\Delta \vec{p} ="), deltaPVec)
```

$\displaystyle \Delta \vec{p} =$

$\displaystyle \left[\begin{matrix}- l \sin{\left(\theta{\left(t \right)} \right)}\\l \cos{\left(\theta{\left(t \right)} \right)}\end{matrix}\right]$

```python
p2Vec = p1Vec + deltaPVec
display(Math(r"\vec{p_2} = \vec{p_1} + \Delta \vec{p} = "), p2Vec)
```

$\displaystyle \vec{p_2} = \vec{p_1} + \Delta \vec{p} = $

$\displaystyle \left[\begin{matrix}- l \sin{\left(\theta{\left(t \right)} \right)} + \operatorname{p1x}{\left(t \right)}\\l \cos{\left(\theta{\left(t \right)} \right)}\end{matrix}\right]$

We can now use these relationships for $\vec{p_1}$ and $\vec{p_2}$ to determine $\lvert \dot{\vec{p_1}} \rvert$ and $\lvert \dot{\vec{p_2}} \rvert$ in terms of known constants and measurable values.

```python
p1VecDotNorm = p1Vec.diff(t).norm()
display(Math(r"\lvert \dot{\vec{p_1}} \rvert = "),p1VecDotNorm)
```

$\displaystyle \lvert \dot{\vec{p_1}} \rvert = $

$\displaystyle \left|{\frac{d}{d t} \operatorname{p1x}{\left(t \right)}}\right|$

```python
p2VecDotNorm = simplify(refine(p2Vec.diff(t).norm()))
display(Math(r"\lvert \dot{\vec{p_2}} \rvert = "), p2VecDotNorm)
```

$\displaystyle \lvert \dot{\vec{p_2}} \rvert = $

$\displaystyle \sqrt{l^{2} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} - 2 l \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \operatorname{p1x}{\left(t \right)} \frac{d}{d t} \theta{\left(t \right)} + \left(\frac{d}{d t} \operatorname{p1x}{\left(t \right)}\right)^{2}}$

Now that we have $\vec{p_1}, \lvert \dot{\vec{p_1}} \rvert, \vec{p_2}, \lvert \dot{\vec{p_2}} \rvert$ all in terms of useful states, we can express our Lagrangian entirely in terms of useful states.

```python
ke = 1/2*m1*p1VecDotNorm**2  + 1/2*m2*p2VecDotNorm**2
pe = m2*g*l*cos(theta)
L = simplify(refine(ke - pe))
display(Math(r"L ="),L)
```

$\displaystyle L =$

$\displaystyle - g l m_{2} \cos{\left(\theta{\left(t \right)} \right)} + 0.5 m_{1} \left(\frac{d}{d t} \operatorname{p1x}{\left(t \right)}\right)^{2} + 0.5 m_{2} \left(l^{2} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} - 2 l \cos{\left(\theta{\left(t \right)} \right)} \frac{d}{d t} \operatorname{p1x}{\left(t \right)} \frac{d}{d t} \theta{\left(t \right)} + \left(\frac{d}{d t} \operatorname{p1x}{\left(t \right)}\right)^{2}\right)$

Now that we have our Lagrangian, we can get our EOM by evaluating the following:

$$
\frac{d}{dt}\left\lgroup \frac{\partial L}{\partial \dot x}\right\rgroup-\frac{\partial L}{\partial x}=f(t) \\
\tag{4}
\label{eom1}
$$

$$
\frac{d}{dt}\left\lgroup \frac{\partial L}{\partial \dot \theta}\right\rgroup-\frac{\partial L}{\partial \theta}=0 \\
\tag{5}
\label{eom2}
$$

> Note: If you need a refresher of Lagrangian mechanics, we found the following videos to be useful:
> - <https://youtu.be/4uJaKJASKnY>
> - <https://youtu.be/uFnTRJ2be7I>

```python
eom1, eom2 = euler_equations(L, [p1x, theta], t)
f = Function("f")
eom1 = eom1.subs(0, -f(t))
display(simplify(eom1))
```

$\displaystyle f{\left(t \right)} = 1.0 m_{1} \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)} + m_{2} \left(l \sin{\left(\theta{\left(t \right)} \right)} \left(\frac{d}{d t} \theta{\left(t \right)}\right)^{2} - l \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} + \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)}\right)$

$$
= \frac{d}{dt}\left\lgroup \frac{\partial L}{\partial \dot x}\right\rgroup-\frac{\partial L}{\partial x}=f(t)
$$

```python
display(simplify(eom2))
```

$\displaystyle l m_{2} \left(g \sin{\left(\theta{\left(t \right)} \right)} - l \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} + \cos{\left(\theta{\left(t \right)} \right)} \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)}\right) = 0$

$$
= \frac{d}{dt}\left\lgroup \frac{\partial L}{\partial \dot \theta}\right\rgroup-\frac{\partial L}{\partial \theta}=0
$$

Now that we have our EOM, we want to represent our system in a way that makes it easy to control.
Since we have decided to use a full-state feedback controller, we want to represent our system in the following form:

$$
\dot{\vec{x}}= A \vec x + Bu \\
\tag{6}
\label{stateEq}
$$

$$
y = C \vec x + Du \\
\tag{7}
\label{outputEq}
$$

> Note: If the **state equation** $\eqref{stateEq}$ or the **output equation** $\eqref{outputEq}$ seem unfamiliar, then we highly suggest watching the following video:
> - <https://youtu.be/hpeKrMG-WP0>

In order to simplify our lives and be able to express `A` in terms of only constants, we decide to apply the following simplifications:

> Note: The following approximations are justified for $-5 ^{\circ} \leq \theta \leq 5 ^{\circ} $:
$$
cos\theta \approxeq1 \\
sin\theta\approxeq\theta \\
$$

> Note: The following approximation is valid since $\dot\theta$ will be relatively small:
$$
\dot\theta^2\approxeq 0
$$

```python
linearizations = [
    (sin(theta), theta),
    (cos(theta), 1),
    (thetaDot**2, 0)
]
eom1 = eom1.subs(linearizations)
eom1 = simplify(eom1)

eom2 = eom2.subs(linearizations)
eom2 = simplify(eom2)
```

```python
print("eom1 after linearization:")
display(eom1)
```

    eom1 after linearization

$\displaystyle f{\left(t \right)} = 1.0 m_{1} \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)} - m_{2} \left(l \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} - \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)}\right)$

```python
print("eom2 after linearization:")
display(eom2)
```

    eom2 after linearization

$\displaystyle l m_{2} \left(g \theta{\left(t \right)} - l \frac{d^{2}}{d t^{2}} \theta{\left(t \right)} + \frac{d^{2}}{d t^{2}} \operatorname{p1x}{\left(t \right)}\right) = 0$

Now that we have linearized our EOM, let us explicitly say what our states are:
$$
\vec x = \left [
\begin{matrix}
p_{1x} \\
\dot p_{1x} \\
\theta \\
\dot\theta \\
\end{matrix}
\right ] \implies
\dot{\vec x} = \left [
\begin{matrix}
\dot{p_{1x}} \\
\ddot{p_{1x}} \\
\dot{\theta} \\
\ddot{\theta}\\
\end{matrix}
\right ]
$$

We will now solve our equations of motion for $\ddot{\theta}$ and $\ddot{p_{1x}}$. This gets us one step closer to be able to represent our system in the following form:
$$
\dot{\vec{x}}= A \vec x + Bu \\
y = C \vec x + Du
$$

```python
# Defining symbolic variables
thetaDotDot = thetaDot.diff(t, real=True)
p1xDotDot = p1x.diff(t, real=True).diff(t, real=True)

# Solving for thetaDotDot and p1xDotDot
solution = solve([eom1, eom2], [thetaDotDot, p1xDotDot])

# Getting the solutions of thetaDotDot and p1XDotDot out
# of a dictionary
thetaDotDot = simplify(solution[thetaDotDot])
p1xDotDot = simplify(solution[p1xDotDot])
```

```python
display(Math(r"\ddot{\theta} ="), thetaDotDot)
```

$\displaystyle \ddot{\theta} =$

$\displaystyle \frac{g m_{1} \theta{\left(t \right)} + g m_{2} \theta{\left(t \right)} + f{\left(t \right)}}{l m_{1}}$

```python
display(Math(r"\ddot{p_{1x}} = "), p1xDotDot)
```

$\displaystyle \ddot{p_{1x}} = $

$\displaystyle \frac{g m_{2} \theta{\left(t \right)} + f{\left(t \right)}}{m_{1}}$

Now that we have expressions for $\ddot{\theta}$ and $\ddot{p_{1x}}$ in terms of our states, we are ready to represent the system in terms of the **state equation** and the **output equation**.

```python
p1xDot = p1x.diff(t)
x = [p1x, p1xDot, theta, thetaDot]
xDot = [p1xDot, p1xDotDot, thetaDot, thetaDotDot]

# type conversions are needed for the linear_eq_to_matrix function call to work
typeConversions = [
    (p1xDot, Symbol("p1xDot")),
    (thetaDot, Symbol("thetaDot")),
    (p1x, Symbol("p1x")),
    (theta, Symbol("theta")),
    (f(t), Symbol("f(t)"))
]

for i in range(len(xDot)):
    xDot[i] = xDot[i].subs(typeConversions)
    x[i] = x[i].subs(typeConversions)

# Gives the variables in the following form
# xDot = A*x - B*u
APlant, B = linear_eq_to_matrix(xDot, x)

# Changing to the form
# xDot = A*x + B*u
B = -1*B

# Getting f(t) out of the B matrix
B = B.subs(Symbol("f(t)"), 1)
```

```python
display(Math(r"A ="), APlant)
```

$\displaystyle A =$

$\displaystyle \left[\begin{matrix}0 & 1 & 0 & 0\\0 & 0 & \frac{g m_{2}}{m_{1}} & 0\\0 & 0 & 0 & 1\\0 & 0 & \frac{g m_{1} + g m_{2}}{l m_{1}} & 0\end{matrix}\right]$

```python
display(Math(r"B ="), B)
```

$\displaystyle B =$

$\displaystyle \left[\begin{matrix}0\\\frac{1}{m_{1}}\\0\\\frac{1}{l m_{1}}\end{matrix}\right]$

```python
C = Matrix([[1, 0, 0, 0],
            [0, 0, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 0]])

display(Math(r"C ="), C)
```

$\displaystyle C =$

$\displaystyle \left[\begin{matrix}1 & 0 & 0 & 0\\0 & 0 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0 & 0 & 0\end{matrix}\right]$

```python
D = Matrix([0,
            0,
            0,
            0])
display(Math(r"D = "), D)
```

$\displaystyle D = $

$\displaystyle \left[\begin{matrix}0\\0\\0\\0\end{matrix}\right]$

Now is a good time to substitue some actual values for our constants and do some sanity checks. We want to plot the poles of the system and check that at least one is in the right half plane.

```python
from control.matlab import *
import matplotlib.pyplot as plt
import numpy as np
from math import pi

measuredValues = [
    (g, 9.81),
    (m1, 1.0),
    (m2, 2.0),
    (l, 1.0)
]
APlant = APlant.subs(measuredValues)
B = B.subs(measuredValues)

sys = StateSpace(APlant, B, C, D)
fig0 = plt.figure()
p0 = pzmap(sys)
```

As seen in the figure above a pole is in the right half plane, meaning the system is inherently unstable. This makes sense, since the pendulum would not remain in the upright vertical position if we were not actively trying to balance the pendulum.

The objective of our statespace controller should be to move the pole in the right half plane to the left half.

In general, state feeback controllers take a plant as illustrated in the figure below:

The state is 'fed back' to the input while applying a individual gains `K` to each state variable. Applying these gains and feedback ultimately is what moves the poles of the overall system. A figure illustrating the state feedback controller being applied to the plant can be found below:

One question you may be asking yourself is how do we determine each of the gains for `K` such that the system's poles get moved to the left hand plane?

Probably the best way to show how we determine the gains for `K` is through a little derivation.
We will prove:
1. The **state equation** and the **output equation** can be represented as a transfer function.
2. The eigenvalues of the `A` matrix in the **state equation** represent the poles of our system.

Let's begin with proving the **state equation** $\dot{\vec{x}} = A \vec x + Bu$ and the **output equation** $y = C\vec x + Du$ can be represented as a transfer function.

First let's take the Laplace transfrom of both the **state equation** and the **output equation**:

$$
sX(s) = A X(s) + B U(s) \\
Y(s) = C X(s) + D U(s)
$$

Now let's isolate for $X(s)$ in the **state equation**:

$$
s X(s) - A X(s) = B U(s) \\
X(s)(sI - A) = B U(s) \\
X(s) = (sI - A)^{-1}B U(s)
$$

In many cases (including our inverted pendulum) we choose our $D = \mathbf{0}$. Let's express this simplification in our **output equation**:

$$
Y(s) = CX(s)
$$

Let's substitute $X(s)$ for the expression we derived in the **state equation**:

$$
Y(s) = C(sI - A)^{-1}B U(s)
$$

Since $U(s)$ is a 1x1, we can solve for the transfer function by dividing $U(s)$ to both sides of the equation:

$$
\frac{Y(s)}{U(s)} = G(s) \\
= C(sI - A)^{-1}B \\
$$
$$
= C \frac{\mathrm{adj}(sI - A)}{\det(sI - A)} B \label{transferFunction} \tag{8}
$$

As seen in equation $\eqref{transferFunction}$ the poles for the system can be determined by setting the $\det(sI -A) = 0$. As it turns out, solving for the values of $s$ that cause the denominator to be zero is the same as if we had determined the eigenvalues for A.

> An aside: The relationship between the poles of our system and the eigenvalues of the A matrix become apparent if we look at the definition of an eigenvector and try to solve for the eigenvalues:
> $$
A\vec{v} = \lambda \vec{v} \\
\lambda \vec{v} - A\vec{v} = \mathbf{0} \\
=(\lambda I - A)\vec{v} = \mathbf{0}
$$
>
> We would then solve for the eigenvalues by finding the $\det(\lambda I - A) = 0$. Notice that the $\det(\lambda I -A) = 0$ would be identical to the denominator of equation $\eqref{transferFunction}$ if we substitute $\lambda$ for $s$.
>
> If you are uncertain why we took the $\det(\lambda I - A)$ and set it equal to $\mathbf{0}$ then consider watching the following video:
> - <https://youtu.be/rfm0wQObxjk>

Okay, we have proven that the state space representation our system can be converted into a transfer function and the poles of that transfer function are the eigenvalues of our $A$ matrix. Lets now look at applying feedback and how that affects the $A$ matrix of the closed-loop system. Looking back at our closed-loop model:

Let's now substitute $u=r-K\vec x$ into our **state equation**:

$$
\dot{\vec x} = A \vec x + B (r - K\vec x)
$$
Let's see if we can expand and factor out our $\vec x$.

$$
\dot{\vec x} = A \vec x + Br - BK\vec x \\
= A \vec x - BK\vec x + Br
$$$$
= (A - BK)\vec x + Br \label{aClosedLoop} \tag{9}
$$

Equation $\eqref{aClosedLoop}$ should look very familiar. If we substitute $(A-BK)$ for $A_{closed loop}$, we will see equation $\eqref{aClosedLoop}$ is in the exact same form as our original **state equation**:

$$
\dot{\vec{x}} = A_{closed loop} \vec x + Br
$$

We can now say that the eigenvalues for the overall system are at the eigenvalues of $A_{closed loop}$. We can solve for our $K$ matrix by setting the characteristic equation of $A_{closed loop}$ to our desired characteristic equation:
$$
\det(A_{closed loop}) = \mathrm{desiredCharEq}
$$

```python
k1, k2, k3, k4 = symbols(["k1", "k2", "k3", "k4"])
s = Symbol("s")
K = Matrix([[k1, k2, k3, k4]])
AClosedLoop = APlant - B*K
charEq = det(s*eye(4) - AClosedLoop)
```

To find our gain values we must choose some performance criteria for the transient response, namely the maximum settling time and percent overshoot. This produces a 2nd-order characteristic polynomial to which we add two more terms to make the overall 'desired' polynomial a 4th-order. The added terms simply place then two remaining roots (poles) at 5x the distance of the other two on the real axis. We can then set this equal to our actual characteristic equatioin and solve for our gains by matching like-terms:
$$
\omega_n=\frac{4}{\zeta T_s} \\
\zeta=\frac{-\ln(\frac{\%OS}{100})}{\sqrt{\pi^2+\ln^2(\frac{\%OS}{100})}}
\\
\mathrm{desiredCharEq} = (s^2+2\zeta\omega_ns+\omega_n^2)(s+5\zeta\omega_n)^2 = \det(A_{closed loop})
$$

```python
# Requirements we are trying to achieve
os = 0.10 # Overshoot
TSetSeconds = 2.0 # Settling time

zet = -ln(os)/sqrt(pi**2 + ln(os)**2)
wn = 4/(zet*TSetSeconds)

desiredCharEq = (s + zet*wn + I*wn*sqrt(1 - zet**2)) * (s + zet*wn - I*wn*sqrt(1 - zet**2)) * (s + 5*wn*zet)**2

gains = solve(Eq(charEq, desiredCharEq), K)
gainsSub = [
    (k1, gains[k1]),
    (k2, gains[k2]),
    (k3, gains[k3]),
    (k4, gains[k4])
]
```

Now that we have solved for the gains that move the poles to the desired locations. We should plot where the poles are and confirm they are at their desired locations.

```python
AClosedLoop = AClosedLoop.subs(gainsSub)

initialStates = [0, # p1x (m)
                 0, # p1xdot (m/s)
                 5 * pi / 180, # theta (rad)
                 0] # thetadot (rad/sec)

sys = StateSpace(AClosedLoop, B, C, D)
fig1 = plt.figure()
p1 = pzmap(sys)
```

Okay, we have reason to believe the system is now stable. Let's see what happens to $\theta$ when we give it an intial displacement of $5^\circ$.

```python
fig2 = plt.figure()
dt = 0.01
T = np.arange(0, 3, dt)
y, t = initial(sys, T, initialStates)
thetaDeg = y[:, 2] * 180/pi
p1 = plt.plot(t, thetaDeg)
plt.title("$\Theta$ vs t")
plt.ylabel("$\Theta$ ($\degree$)")
plt.ylim([-4, 5.5])
plt.xlabel("t (sec)")
plt.grid("on")
```

As seen in the plot above, $\theta$ will approch $0$ as $t \to \infty$. This gives us reason to believe our control theory is correct and we are ready to begin implementation.


```python
import numpy as np
import scipy as sp
from scipy import linalg
from scipy import optimize
from scipy import interpolate
import sympy as sm

%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D
```

# Durable purchases

Consider a **household** living in two periods.

In the **second period** it gets utility from **non-durable consumption**, $c$, and **durable consumption**, $d+\chi x$:

$$
\begin{aligned}
v_{2}(m_{2},d)&= \max_{c}\frac{(c^{\alpha}(d+\chi x)^{1-\alpha})^{1-\rho}}{1-\rho}\\
\text{s.t.} \\
x &= m_{2}-c \\
c &\in [0,m_{2}]
\end{aligned}
$$

where

* $m_2$ is cash-on-hand in the beginning of period 2
* $c$ is non-durable consumption
* $d$ is pre-commited durable consumption
* $x = m_2 - c$ is extra durable consumption
* $\rho > 1$ is the risk aversion coefficient
* $\alpha \in (0,1)$ is the utility weight on non-durable consumption
* $\chi \in (0,1)$ implies that extra durable consumption is *less* valuable than pre-comitted durable consumption
* the second constraint ensures the household *cannot* die in debt

The **value function** $v_2(m_2,d)$ measures the household's value of having $m_2$ at the beginning of period 2 with precomitted durable consumption of $d$. The optimal choice of non-durable consumption is denoted $c^{\ast}(m_2,d)$. The optimal extra durable consumption function is $x^{\ast}(m_2,d) = m_2-c^{\ast}(m_2,d)$.

Define the so-called **end-of-period 1 value function** as:

$$
\begin{aligned}
w(a,d)&\equiv\beta\mathbb{E}_{1}\left[v_2(m_2,d)\right]
\end{aligned}
$$

where

$$
\begin{aligned}
m_2&= (1+r)a+y \\
y &= \begin{cases}
1-\Delta & \text{with prob. }\frac{1}{3}\\
1 & \text{with prob. }\frac{1}{3}\\
1+\Delta & \text{with prob. }\frac{1}{3}
\end{cases}\\
\end{aligned}
$$

and

* $a$ is assets at the end of period 1
* $\beta > 0$ is the discount factor
* $\mathbb{E}_1$ is the expectation operator conditional on information in period 1
* $y$ is income in period 2
* $\Delta \in (0,1)$ is the level of income risk (mean-preserving)
* $r$ is the return on savings

In the **first period**, the household chooses it's pre-comitted level of durable consumption for the next-period,

$$
\begin{aligned}
v_{1}(m_{1})&=\max_{d} w(a,d)\\&\text{s.t.}&\\
a&= m_{1}-d \\
d&\in [0,m_{1}]\\
\end{aligned}
$$

where $m_1$ is cash-on-hand in period 1. The second constraint ensures the household *cannot* borrow. The **value function** $v_1(m_1)$ measures the household's value of having $m_1$ at the beginning of period 1. The optimal choice of pre-committed durable consumption is denoted $d^{\ast}(m_1)$.

The **parameters** and **grids** for $m_1$, $m_2$ and $d$ should be:

```python
# a. parameters
rho = 2
alpha = 0.8
beta = 0.96
r = 0.04
Delta = 0.25

# b. grids
m1_vec = np.linspace(1e-8,10,100)
m2_vec = np.linspace(1e-8,10,100)
d_vec = np.linspace(1e-8,5,100)
```

**Question 1:** Find and plot the functions $v_{2}(m_{2},d)$, $c^{\ast}(m_2,d)$, and $x^{\ast}(m_2,d)$. Comment.

**Question 2:** Find and plot the functions $v_{1}(m_{1})$ and $d^{\ast}(m_1)$. Comment.

**Hint:** For interpolation of $v_2(m_2,d)$ consider using `interpolate.RegularGridInterpolator([GRID-VECTOR1,GRID-VECTOR2],VALUE-MATRIX,bounds_error=False,fill_value=None)`.

Next, consider an **extension** of the model, where there is also a **period 0**. In this period, the household makes a choice whether to stick with the level of durables it has, $z = 0$, or adjust its stock of durables, $z = 1$. If adjusting, the household loses a part of the value of its durable stock; more specificaly it incurs a proportional loss of $\Lambda \in (0,1)$.

Mathematically, the **household problem in period 0** is:

$$
\begin{aligned}
v_{0}(m_{0},d_{0}) &= \max_{z\in\{0,1\}} \begin{cases}
w(m_{0},d_{0}) & \text{if } z = 0\\
v_1(m_0+(1-\Lambda) d_{0}) & \text{if } z = 1\\
\end{cases}\\
\end{aligned}
$$

The **parameters** and **grids** for $m_0$ and $d_0$ should be:

```python
Lambda = 0.2
m0_vec = np.linspace(1e-8,6,100)
d0_vec = np.linspace(1e-8,3,100)
```

**Question 3:** For which values of $m_0$ and  $d_0$ is the optimal choice not to adjust, i.e. $z = 0$? Show this in a plot. Give an interpretion of your results.

```python

```

# Problem set 7

```python
rho = 2
alpha = 0.8
r = 0.04
beta = 0.96
Delta = 0.25

# b. solve
# write your code here

"""
# a. parameters
rho = 2
alpha = 0.8
beta = 0.96
r = 0.04
Delta = 0.25

# b. grids
m1_vec = np.linspace(1e-8,10,100)
m2_vec = np.linspace(1e-8,10,100)
d_vec = np.linspace(1e-8,5,100)
"""

# c. plot
# write your code here
```

    '\n# a. parameters\nrho = 2\nalpha = 0.8\nbeta = 0.96\nr = 0.04\nDelta = 0.25\n\n# b. grids\nm1_vec = np.linspace(1e-8,10,100)\nm2_vec = np.linspace(1e-8,10,100)\nd_vec = np.linspace(1e-8,5,100)\n'

```python
def utility(c,d,alpha,rho):
    return ( ( (c**alpha) * (d**(1-alpha)) ) /(1-rho) )**(1-rho)

def v2(c2,d2,m2,alpha,rho):
    return utility(c2,d2,alpha,rho)

def v1(d2,m1,alpha,rho,beta,r,Delta,v2_interp):

    # a. v2 value, if low income
    m2_low = (1+r)*(m1-d2) + 1-Delta
    v2_low = v2_interp([m2_low,d2])[0]

    # b. v2 value, if medium income
    m2_med = (1+r)*(m1-d2) + 1
    v2_med = v2_interp([m2_med,d2])[0]

    # c. v2 value, if high income
    m2_high = (1+r)*(m1-d2) + 1+Delta
    v2_high = v2_interp([m2_high,d2])[0]

    # c. expected v2 value
    v2 = 1/3*v2_low + 1/3*v2_med + 1/3*v2_high

    # d. total value
    return beta*v2
```

```python
def solve_period_2(alpha,rho,Delta):

    # a. grids
    m2_vec = np.linspace(1e-8,10,100)
    d2_vec = np.linspace(1e-8,10,100)
    v2_grid = np.empty((100,100))
    c2_grid = np.empty((100,100))

    # b. solve for each m2 in grid
    for i,m2 in enumerate(m2_vec):
        for j,d2 in enumerate(d2_vec):

            # i. objective
            obj = lambda c2: -v2(c2,d2,m2,alpha,rho)

            # ii. initial value (consume half)
            x0 = m2/2

            # iii. optimizer
            result = optimize.minimize_scalar(obj,x0,method='bounded',bounds=[1e-8,m2])

            # iv. save
            v2_grid[i,j] = -result.fun
            c2_grid[i,j] = result.x

    return m2_vec,d2_vec,v2_grid,c2_grid
```

```python
# a. solve
m2_vec,d2_vec,v2_grid,c2_grid = solve_period_2(alpha,rho,Delta)

# b. grids
m2_grid,d2_grid = np.meshgrid(m2_vec,d2_vec,indexing='ij')

# c. main
fig = plt.figure()
ax = fig.add_subplot(1,1,1,projection='3d')
cs = ax.plot_surface(m2_grid,d2_grid,c2_grid,cmap=cm.jet)

# d. add labels
ax.set_xlabel('$m_2$')
ax.set_ylabel('$d_2$')
ax.set_zlabel('$c_2$')

# e. invert xaxis
ax.invert_xaxis()

# f. add colorbar
fig.colorbar(cs);
```

```python
# a. define solve function
def solve_period_1(alpha,rho,beta,r,Delta,v1,v2_interp):

    # a. grids
    m1_vec = np.linspace(1e-4,4,100)
    v1_vec = np.empty(100)
    #c1_vec = np.empty(100)
    d2_vec = np.empty(100)

    # b. solve for each m1 in grid
    for i,m1 in enumerate(m1_vec):

        # i. objective
        obj = lambda x: -v1(x[0],m1,alpha,rho,beta,r,Delta,v2_interp)

        # ii. initial guess
        x0 = [m1*1/3, m1*1/3]

        # iii. bounds and constraitns
        bound = (1e-8,m1-1e-8)
        bounds = (bound, bound)
        ineq_con = {'type': 'ineq', 'fun': lambda x: m1-x[0]}

        # iv. optimize
        result = optimize.minimize(obj,x0, method='SLSQP',
                                   bounds=bounds,
                                   constraints=[ineq_con])

        #result = optimize.minimize(obj,x0, method='Nelder-Mead')

        # v. save
        v1_vec[i] = -result.fun
        d2_vec[i] = result.x[0]

    return m1_vec,v1_vec,d2_vec

# b. construct interpolator
v2_interp = interpolate.RegularGridInterpolator((m2_vec,d2_vec), v2_grid,
                                                bounds_error=False,fill_value=None)

# c. solve period 1
m1_vec,v1_vec,d2_vec = solve_period_1(alpha,rho,beta,r,Delta,v1,v2_interp)

# d. plot
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot(m1_vec,v1_vec,label='v1 utility')
ax.plot(m1_vec,d2_vec,label='durable consumption')
ax.legend(loc='upper left')
ax.set_xlabel('$m_1$')
ax.set_xlim([0,4])
ax.set_ylim([-2.5,2.5]);
```

```python

```


# ODE

We will solve the following linear Cauchy model

\begin{align}
y^{\prime}(t) &= \lambda y(t)\\
y(0) & = 1
\end{align}

whose exact solution is

$$
y(t) = e^{\lambda t}
$$

```python
%matplotlib inline
from numpy import *
from matplotlib.pyplot import *
import scipy.linalg
import numpy.linalg

l = -5.
t0 = 0.
tf = 10.
y0 = 1.

s = linspace(t0,tf,5000)

exact = lambda x: exp(l*x)

```

### Forward Euler

$$
\frac{y_{n}-y_{n-1}}{h} = f(y_{n-1}, t_{n-1})
$$

```python
def fe(l,y0,t0,tf,h):
    timesteps = arange(t0,tf+1e-10, h)
    sol = zeros_like(timesteps)
    sol[0] = y0
    for i in range(1,len(sol)):
        sol[i] = sol[i-1]*(1+l*h)

    return sol, timesteps

y, t = fe(l,y0,t0,tf,0.1)

_ = plot(t,y, 'o-')
_ = plot(s,exact(s))

error = numpy.linalg.norm(exact(t) - y, 2)
print(error)
```

### Backward Euler

$$
\frac{y_{n}-y_{n-1}}{h} = f(y_{n}, t_{n})
$$

```python
def be(l,y0,t0,tf,h):
    pass # TODO
```

### $\theta$-method

$$
\frac{y_{n}-y_{n-1}}{h} = \theta\, f(y_{n}, t_{n}) + (1-\theta)\,f(y_{n-1}, t_{n-1})
$$

```python
def tm(theta,l,y0,t0,tf,h):
    pass # TODO
```

### Simple adaptive time stepper

For each time step:
- Compute solution with CN
- Compute solution with BE
- Check the difference
- If the difference satisfy a given tolerance:
    - keep the solution of higher order
    - double the step size
    - go to the next step
- Else:
    - half the step size and repeat the time step

```python
def adaptive(l,y0,t0,tf,h0, hmax=0.9,tol=1e-3):
    sol = []
    sol.append(y0)
    t = []
    t.append(t0)
    h = h0
    while t[-1] < tf:
        #print 'current t =', t[-1], '            h=', h
        current_sol = sol[-1]
        current_t = t[-1]
        sol_cn, _ = tm(0.5,l,current_sol,current_t, current_t + h, h)
        sol_be, _ = tm(1.,l,current_sol,current_t, current_t + h, h)

        if (abs(sol_cn[-1] - sol_be[-1]) < tol): #accept
            sol.append(sol_cn[-1])
            t.append(current_t+h)
            h *= 2.
            if h > hmax:
                h=hmax
        else:
            h /= 2.

    return sol, t

y,t = adaptive(l,y0,t0,tf,0.9)
_ = plot(t,y, 'o-')
_ = plot(s,exact(array(s)))

error = numpy.linalg.norm(exact(array(t)) - y, infty)
print error, len(y)
```

```python

```

```python

```


# AMATH 301 - HW1
University of Washington\
Due 10/6/2021

```python
# Import Block
import numpy as np
import sympy as sp
sp.init_printing() # for pretty print with LaTeX/Mathjax
```

## Part 1

```python
# Define absolute error function that takes initial value as input
def abserr(x):
    for j in range(0,x*10):
        x = x - 0.1
    return abs(0 - x)

# Answers
A1 = abserr(1000)
A2 = abserr(10000)
A3 = abserr(100000)

# Debug displays below (comment/uncomment blocks for debug)

# display(A1)
# display(A2)
# display(A3)
```

## Part 2

```python
# Define matrices
A = np.matrix([[1,2],[-1,1]])
B = np.eye(2).astype(int)*2
C = np.matrix([[2,0,-3],[0,0,-1]])
D = np.matrix([[1,2],[2,3],[-1,0]])
x = np.matrix([[1],[0]])
y = np.matrix([[0],[1]])
z = D[:,0]

# Answers
# using numpy.dot() avoids value error from dimensions when multiplying
A4 = A + B
A5 = 3*x - 4*y
A6 = A.dot(x)
A7 = B.dot(x-y)
A8 = D.dot(x)
A9 = D.dot(y) + z
A10 = A.dot(B)
A11 = B.dot(C)
A12 = C.dot(D)

# Debug displays below (comment/uncomment blocks for debug)

# display(sp.Matrix(A4))
# display(sp.Matrix(A5))
# display(sp.Matrix(A6))
# display(sp.Matrix(A7))
# display(sp.Matrix(A8))
# display(sp.Matrix(A9))
# display(sp.Matrix(A10))
display(sp.Matrix(A11))
display(sp.Matrix(A12))
```

$\displaystyle \left[\begin{matrix}4 & 0 & -6\\0 & 0 & -2\end{matrix}\right]$

$\displaystyle \left[\begin{matrix}5 & 4\\1 & 0\end{matrix}\right]$

## Part 3

```python
# Define parameters

p = [0.8,1.5,2.8,3.2,3.5,3.65]
Aarray = np.matrix(np.ones(6)*0.5)

# Loop through arrays
for j in range(2,51):
#     i = np.insert(i,len(i),j,axis=0)
    Aarray = np.insert(Aarray,len(Aarray),np.zeros(len(p)),axis=0)
    for k in range(len(p)):
        xn = Aarray[len(Aarray)-2,k]
        Aarray[len(Aarray)-1,k] = p[k]*xn*(1-xn)

# Full array for debugging (Comment/uncomment below for debug)

# i = np.matrix(range(1,51)).reshape(50,1) # iterations
# labels = sp.Matrix([["x", "A13(x)", "A14(x)", "A15(x)", "A16(x)", "A17(x)", "A18(x)"]])
# Part3 = labels.row_insert(1,sp.Matrix(np.append(i,Aarray,axis=1)))
# display(Part3)
```

```python
# Part 3 Answers
A13 = Aarray[:,0]
A14 = Aarray[:,1]
A15 = Aarray[:,2]
A16 = Aarray[:,3]
A17 = Aarray[:,4]
A18 = Aarray[:,5]

# Debug display (comment/uncomment below for debug)

# display(sp.Matrix(A13))
```


# 16 Partial Differential Equations — 1 (Students)

## Solving Laplace's or Poisson's equation

**Poisson's equation** for the electric potential $\Phi(\mathbf{r})$ and the charge density $\rho(\mathbf{r})$:

$$
\nabla^2 \Phi(x, y, z) = -4\pi\rho(x, y, z)\\
$$

For a region of space without charges ($\rho = 0$) this reduces to **Laplace's equation**

$$
\nabla^2 \Phi(x, y, z) = 0
$$

Solutions depend on the **boundary conditions**: the value of the potential on the *boundary* (or the electric field normal to the surface, which directly follows from the charge distribution).

### Example: 2D Laplace equation
$$
\frac{\partial^2 \Phi(x,y)}{\partial x^2} + \frac{\partial^2 \Phi(x,y)}{\partial y^2} = 0
$$
("elliptic PDE")

Boundary conditions:
* square area surrounded by wires
* three wires at ground (0 V), one wire at 100 V

## Finite difference algorithm for Poisson's equation
Discretize space on a lattice (2D) and solve for $\Phi$ on each lattice site.

Taylor-expansion of the four neighbors of $\Phi(x, y)$:

\begin{align}
\Phi(x \pm \Delta x, y) &= \Phi(x, y) \pm \Phi_x \Delta x + \frac{1}{2} \Phi_{xx} \Delta x^2 + \dots\\
\Phi(x, y \pm \Delta y) &= \Phi(x, y) \pm \Phi_y \Delta x + \frac{1}{2} \Phi_{yy} \Delta x^2 + \dots\\
\end{align}

Add equations in pairs: odd terms cancel, and **central difference approximation** for 2nd order partial derivatives (to $\mathcal{O}(\Delta^4)$):

\begin{align}
\Phi_{xx}(x,y) = \frac{\partial^2 \Phi}{\partial x^2} & \approx
  \frac{\Phi(x+\Delta x,y) + \Phi(x-\Delta x,y) - 2\Phi(x,y)}{\Delta x^2} \\
\Phi_{yy}(x,y) = \frac{\partial^2 \Phi}{\partial y^2} &\approx
  \frac{\Phi(x,y+\Delta y) + \Phi(x,y-\Delta y) - 2\Phi(x,y)}{\Delta y^2}
\end{align}

Take $x$ and $y$ grids of equal spacing $\Delta$: Discretized Poisson equation

$$
\Phi(x+\Delta x,y) + \Phi(x-\Delta x,y) + \Phi(x,y+\Delta y) + \Phi(x,y-\Delta y) - 4\Phi(x,y) = -4\pi\rho(x,y)\,\Delta^2
$$

Defines a system of $N_x \times N_y$ simultaneous algebraic equations for $\Phi_{ij}$ to be solved.

Can be solved directly via matrix approaches (and then is the best solution) but can be unwieldy for large grids.

Alternatively: **iterative solution**:

$$
4\Phi(x,y) = \Phi(x+\Delta x,y) + \Phi(x-\Delta x,y) + \Phi(x,y+\Delta y) + \Phi(x,y-\Delta y) + 4\pi\rho(x,y)\,\Delta^2
$$

Or written for lattice sites $(i, j)$ where

$$
x = x_0 + i\Delta\quad\text{and}\quad y = y_0 + j\Delta, \quad 0 \leq i,j < N_\text{max}
$$

$$
\Phi_{i,j} = \frac{1}{4}\Big(\Phi_{i+1,j} + \Phi_{i-1,j} + \Phi_{i,j+1} + \Phi_{i,j-1}\Big)
     + \pi\rho(i\Delta, j\Delta) \Delta^2
$$

* Converged solution at $(i, j)$ will be the average potential from the four neighbor sites + charge density contribution.
* *Not a direct solution*: iterate and hope for convergence.

#### Jacobi method
Do not change $\Phi_{i,j}$ until a complete sweep has been completed.

#### Gauss-Seidel method
Immediately use updated new values for $\Phi_{i-1, j}$ and $\Phi_{i, j-1}$ (if starting from $\Phi_{1, 1}$).

Leads to *accelerated convergence* and therefore *less round-off error* (but distorts symmetry of boundary conditions... hopefully irrelevant when converged but check!)

## Example: wire in a box: Solution via relaxation (Gauss-Seidel)

Solve the box-wire problem on a lattice: The wire at $x=0$ (the $y$-axis) is at 100 V, the other three sides of the box are grounded (0 V).

Note: $\rho=0$ inside the box.

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
#%matplotlib inline
%matplotlib notebook
```

* set boundary conditions
* Implement Gauss-Seidel algorithm
* visualize solution
* does it make sense?
* try higher `Max_iter`

```python
Nmax = 100
Max_iter = 70
Phi = np.zeros((Nmax, Nmax), dtype=np.float64)

# initialize boundaries
# everything starts out zero so nothing special for the grounded wires

# wire at x=0 at 100 V
raise NotImplementedError

for n_iter in range(Max_iter):
    for xi in range(1, Phi.shape[0]-1):
        for yj in range(1, Phi.shape[1]-1):
            raise NotImplementedError
```

```python
# plot Phi(x,y)
x = np.arange(Phi.shape[0])
y = np.arange(Phi.shape[1])
X, Y = np.meshgrid(x, y)

Z = Phi[X, Y]
```

### Simple wireframe plot

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.plot_wireframe(X, Y, Z)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel(r'potential $\Phi$ (V)')
```

(Note: The coordinate system has Y running in the opposite direction compared to the normal coordinate system because numpy arrays "have 0,0 in the upper left hand corner" and we decided to associate rows with X and columns with Y.)

### Surfaces and 2D contours

Nicer plot (use this code for other projects):

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

ax.plot_wireframe(X, Y, Z, rstride=2, cstride=2, linewidth=0.5, color="gray")
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, alpha=0.6)
cset = ax.contourf(X, Y, Z, zdir='z', offset=-50, cmap=plt.cm.coolwarm)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel(r'potential $\Phi$ (V)')
ax.set_zlim(-50, 100)
ax.view_init(elev=40, azim=-65)

cb = fig.colorbar(surf, shrink=0.5, aspect=5)
cb.set_label(r"potential $\Phi$ (V)")
```

## Example: Parallel plate capacitor in a box
* two thin sheets of conducting materials at +100 V and -100 V
* width 50, distance 20, box 100
* box is grounded

### Implement solution with Gauss-Seidel
* implement boundary conditions (plates!)
* Gauss-Seidel algorithm
* convergence criterion: change in Frobenius norm (`np.linalg.norm(A)` = $||A|| = \sqrt{\sum_{i,j} a_{ij}^2}$) should be less than tol:
  $$
  || \Phi^{\text{new}} - \Phi^{\text{old}} || < \epsilon
  $$

```python
Nmax = 100
Max_iter = 200
tol = 1e-2
Phi = np.zeros((Nmax, Nmax), dtype=np.float64)
Phi_old = np.zeros_like(Phi)

def set_boundaries(Phi, w=50, d=20):
    # box (edges) always at 0 V
    Phi[:, 0] = Phi[:, -1] = 0
    Phi[0, :] = Phi[-1, :] = 0
    # plate 1 always at +100 V, plate 2 always -100V
    Nx, Ny = Phi.shape

    raise NotImplementedError

    return Phi

set_boundaries(Phi)
Phi_old[:, :] = Phi
print("Starting...")
for n_iter in range(Max_iter):
    if n_iter % 10 == 0:
        print("Iteration {0:5d}".format(n_iter), end="\r")
    for xi in range(1, Nmax-1):
        for yj in range(1, Nmax-1):
            raise NotImplementeError

    # enforce boundary conditions
    set_boundaries(Phi)

    # check convergence (using the Frobenius matrix norm, delta_norm)

    raise NotImplementeError

    Phi_old[:, :] = Phi
else:
    print("\nDid not converge in {0} steps, Delta FrobeniusNorm(Phi) = {1} "
          " > {2}".format(n_iter, delta_norm, tol))
```

### Check convergence and Visualize
* check initial results
* increase `Max_iter` if necessary

```python
x = np.arange(Nmax)
y = np.arange(Nmax)
X, Y = np.meshgrid(x, y)

Z = Phi[X, Y]
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, alpha=0.3)

cset = ax.contour(X, Y, Z, 20, zdir='z', offset=-100, cmap=plt.cm.coolwarm)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel(r'potential $\Phi$ (V)')
ax.set_zlim(-110, 100)

fig.colorbar(surf, shrink=0.5, aspect=5)
```

## Successive Over Relaxation (SOR)
Accelerate convergence with the scheme

$$
r_{i, j} = \Phi_{i,j}^\text{new} - \Phi_{i, j}^\text{old}\\
\Phi_{i,j}^\text{new} = \Phi_{i,j}^\text{old} + \omega r_{i,j}
$$

Values of $1 \leq \omega \leq 2$ may work well, $\omega > 2$ can lead to numerical instabilities. Experiment!

### Implement SOR with the capacitor problem
* use code above and add SOR
* try $\omega = 1.4$ for a start

```python
# plate capacitor with SOR
raise NotImplementedError
```

#### Check convergence and Visualize
* Find $\omega$ so that it converges in about half the number of steps than with Gauss-Seidel.
* Visualize

```python
x = np.arange(Nmax)
y = np.arange(Nmax)
X, Y = np.meshgrid(x, y)

Z = Phi[X, Y]
```

```python
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, alpha=0.3)

cset = ax.contour(X, Y, Z, 20, zdir='z', offset=-100, cmap=plt.cm.coolwarm)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel(r'potential $\Phi$ (V)')
ax.set_zlim(-110, 100)

fig.colorbar(surf, shrink=0.5, aspect=5)
```