This tutorial by Xiaozhou Li is licensed under a Creative Commons Attribution 4.0 International License.
All code examples are also licensed under the [MIT license](http://opensource.org/licenses/MIT).

# 1D Elliptic Equation
The first tast of this tutorial is solving the 1D eplliptic equation with Dirichlet boundary condition:
\begin{align}
   -\frac{d}{dx}\left(c(x)\frac{d u(x)}{dx}\right) & = f(x),\,\, a < x < b \\
   u(a) = g_a,\,\,u(b) & = g_b.
\end{align}
- The Elliptic equation is a easy example to look at the basic idea of the finite element methods.
- Other numerical methods for solving partial differential equations, such as finite difference methods, finite volume methods, discontinuous Galerkin methods, etc., each method has its advantages for particular problems.
- For example, the discontinuous Galerkin (DG) methods is the most popular methods used to solve hyperbolic equations, the details can be found in the [DG tutorial](http://).

## Finte Element Method
### Weak Formulation
To derive the weak formulation of the finite element methods, we first multiply a function $v(x)$ on both sides of the original equation and take the integration over interval $[a, b]$:
\begin{equation}
    \int_a^b \left(-\frac{d}{dx}\left(c(x)\frac{d u(x)}{dx}\right)\right)  = \int_a^b f(x)v(x),
\end{equation}
where $u(x)$ is called a trail function and $v(x)$ is called a test function.

Then, using the integration by parts, we obtain
\begin{equation}
    \int_a^b c(x) u'(x)v'(x) dx + c(b)u'(b)v(b) - c(a)u'(a)v(a) = \int_a^b f(x)v(x) dx.
\end{equation}
Here, since the Dirichlet boundary condtion gives $u(a) = g_a$ and $u(b) = g_b$ at the boundary points $x = a$ and $x = b$, we can choose the test function $v(x)$ such that $v(a) = v(b) = 0$.

Hence, we have
\begin{equation}
    \int_a^b c(x) u'(x)v'(x) dx = \int_a^b f(x)v(x) dx.
\end{equation}
By introducing the concept of Sobolev space, we can define the follows weak formulation.

__Weak Formulation:__

Let
\begin{equation}
    a(u, v)= \int_\Omega c u'v' dx\,\,\text{and }\, (f, v)=\int_Omega fv dx,
\end{equation}
find $u \in H^1(\Omega)$ such that
\begin{equation}
    a(u, v) = (f, v)
\end{equation}
for any $v \in H_0^1(\Omega)$ where $\Omega = (a, b)$.

### Weak Galerkin Formulation
However, the above Sobolev space $H^1(\Omega)$ is a __infinite__ dimensional space.  To numerically solve the solution, we usually have to consider a __finite__ dimensional approximation subspace $V_h$ belong to $H^1(\Omega)$.

__Weak Galerkin Formulation:__

Find $u_h \in V_h$ such that
\begin{equation}
    a(u_h, v_h) = (f, v_h)
\end{equation}
for any $v_h \in V_h$ where $\Omega = (a, b)$.

### Polynomial Approximation Space
A common way to construct the approximation space $V_h$ is considering a partition $\left\{x_j\right\}_0^N$ of $[a, b]$, such that
\begin{equation}
    a = x_0 < x_1 < \ldots < x_{N-1} < x_N = b,
\end{equation}
where $I_j = [x_{j-1}, x_j]$ denotes the $j$th element, $j = 1,\ldots,N$.

Define the polynomial finite element space:
\begin{equation}
    V_h = \left\{v \in \mathcal{C}[a,b]:  v|_{I_j} \in \mathbb{P}^k(I_j),\,\, j = 1,\ldots,N\right\}
\end{equation}
- $V_h$ is actually a piecewise polynomial space based on the generated mesh.
- In the practical engineering, the most common way is using the linear function ($k = 1$).

### More Reference
- For the detail of Sobolev space, one can refer to any finite element methods book, such as
    - Susanne C. Brenner and L. Ridgway Scott. _The mathematical theory of finite element methods_, volume 15 of Texts in Applied Mathematics. Springer, New York, third edition, 2008.
    - Philippe G. Ciarlet. _The finite element method for elliptic problems_, volume 40 of Classics in Applied Mathematics. Society for Industrial and Applied Mathematics (SIAM), Philadelphia, PA, 2002.

## Implementation
In general, there are two ways to implement the finite element method:
- __Loop of basis functions:__ We write the approximation $u_h$ as
\begin{equation}
    u_h(x) = \sum\limits_{j = 1}^{N_b}u_j\phi_j(x)
\end{equation}
where $N_b$ is the number of basis functions.  The implementation has main loops like:
```python
    for j in range(Nb_trial):
        for i in range(Nb_test):
            # main code (global integration: [a, b])
```
This approach has the follwing features:
    - It is a directly translation from the weak Galerkin formulation.
    - Degree of freedom is exactly $N_b$, and the unknown $\left\{u_j\right\}_{j=1}^{N_b}$ are independent.
    - Assembly of matrices (vectors) is straightforward.
    - The integrals is on the global interval $[a,b]$, each integration $\int_a^b c(x)\phi_j(x)\phi_i(x)dx$ represents a unique entry of the global matrix.
    - May be a little complicated for unstructured meshes in multi-dimensional cases.

- __Loop of elements:__ We write the approximation $u_h$ as
\begin{equation}
    u_h(x) = \sum\limits_{n = 1}^N\sum\limits_{i=1}^{N_{lb}}u_{n,i}\phi_{n,i}(x)
\end{equation}
where $N_{lb}$ is the number of local basis function on each element.  The implementation has main loops like:
```python
    for n in range(number_of_elements):
        for alpha in range(Nlb_trial):
            for beta in range(Nlb_test):
                # main code (local integration: [x_n, x_n+1])
```
This approach has the follwing features:
    - Degree of freedom is not $N\times N_{lb}$, and the unknown $\left\{u_{i,n}\right\}$ are dependent.
    - Assembly of matrices (vectors) needs some derivation, there is a local assembly to global assembly process.
    - The integral is on local element $I_n$ instead of the whole domain, each integration $\int_{I_n} c(x)\phi_{n,\alpha(x)}\phi_{n,\beta(x)}dx$ needs to add to correspond entry of the global matrix based on certain information.

__Note:__ It is rather a perference between the above two ways.  There is no definite answer which one is better.  Personally, I perfer the first way for  Isogeometric Analysis (IGA) methods, and the second way for DG methods.

```python
# environment setting, before any codes
import numpy as np
import numpy.polynomial.legendre as npleg
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
```

### Mesh information matrices
The first step is storing the mesh information for later computing usage.  The mesh information we needed is follows:
- Define global indices for all the mesh elements and mesh nodes. Let $N$ denote the number of mesh elements and $N_m$ denote the number of mesh nodes. Here $N_m = N + 1$.
- Define matrix $P$ to be an information matrix consisting of the coordinates of all mesh nodes.  The $j$th column of $P$ is the coordinate of the $j$th finite element node.
- Define matrix $T$ to be an information matrix consisting of the global node indices of the mesh nodes of all the mesh elements.  The $n$th column of $T$ stores the global indices of the vertices of the $n$th element.

Note:
- The input information is the left/right boundary and the number of elements
```python
    def generate_PT_1D(left_boundary, right_boundary, number_of_elements)
```
- Therefore $P[T[i,j]]$ gives the coordinate of the $i$th vertice on the $j$th element.
- This step is independent with the type of numerical methods, and for unstructure meshs in multi-dimensional space, this step is rather complicated.
- For one dimensional mesh, this step is very simple:
    - $P = [x_0, x_1, \ldots, x_N]$;
    - $T = \text{index}\left(\left[\begin{array}{cccc}x_0, & x_1, & \ldots, & x_{N-1}\\
                               x_1, & x_2, & \ldots, & x_N\end{array}\right]\right) = \left[\begin{array}{cccc}0, & 1, & \ldots, & N-1\\
                           1, & 2, & \ldots, & N\end{array}\right]$.

```python
def generate_PT_1D(left_boundary, right_boundary, number_of_elements):
    P = np.linspace(left_boundary, right_boundary, number_of_elements+1)
    T = np.zeros([2, number_of_elements], dtype=int)
    for j in range (number_of_elements):
        T[0,j] = j
        T[1,j] = j+1
    return P, T
```

### Finite element information matrices
The above mesh information matrices $P$ and $T$ are for the mesh nodes. We also need similar finite element information matrices $P_b$ and $T_b$ for the finite elements nodes, which are the nodes corresponding to the finite element basis functions.
- Let $N_b$ denote the total number of the finite element basis functions (= the number of unknowns = the total number of the finite element nodes). Here $N_b = N + 1$.
- Define matrix $P_b$ to be an information matrix consisting of the coordinates of all finite element nodes. The $j$th column of $P_b$ is the coordinate of the $j$th finite element node.
- Define matrix $T_b$ to be an information matrix consisting of the global node indices of the finite element nodes of all the mesh elements.  The $n$th column of $T$ stores the global indices of the nodes of the $n$th element.

Notes:
- The input information is the mesh information matrices $P$ and $T$, and the type of basis functions
```python
    def generate_NbPbTb_1D(P, T, basis_type)
```
- Unlike the mesh information matrices $P$ and $T$, the finite element information matrices $P_b$ and $T_b$ are dependent on the type of basis functions.  They can be quite complicated, especially for non-nodal basis functions.
- One has to update the code based on the type of basis functions.
- For simplicity, in this tutorial, we only consider the linear and quadratic functions.

__Linear basis function:__
- This is the simplest case (also the most popular one), the $P_b$ and $T_b$ are the same as the mesh information matrices $P$ and $T$.

__Quadratic basis function:__
- For using quadratic basis function, on each element, besides two vertices one still needs another node (the middle point of the element).  That is, let $x_{n+\frac{1}{2}} = \frac{1}{2}\left(x_n + x_{n+1}\right)$, the $P_b$ matrix is given by
\begin{equation}
    P_b = [x_0, x_{\frac{1}{2}}, x_1, \ldots, x_N],
\end{equation}
with $size(P_b) = 2N+1 = 2*size(P)-1$.
- For matrix $T_b$, on each element, we sort all the nodes in the order $x_n, x_{n+1}, x_{n+\frac{1}{2}}$.  Then, we have
\begin{equation}
    T_b = \text{index}\left(\left[\begin{array}{cccc}x_0, & x_1, & \ldots, & x_{N-1}\\
                               x_1, & x_2, & \ldots, & x_N \\
                               x_{\frac{1}{2}}, & x_{\frac{3}{2}}, &\ldots, & x_{N-\frac{1}{2}}\end{array}\right]\right) = \left[\begin{array}{cccc}0, & 2, & \ldots, & 2N-2\\
                           2, & 4, & \ldots, & 2N \\ 1, & 3, &\ldots, & 2N-1\end{array}\right]
\end{equation}

```python
def generate_NbPbTb_1D(P, T, basis_type):
    if (basis_type==101):
        Nb = np.size(P)
        Pb = np.copy(P)
        Tb = np.copy(T)
    elif (basis_type==102):
        Nb = 2*np.size(P)-1
        Pb = np.zeros(Nb)
        Pb[0::2] = P
        for i in range (np.size(P)-1):
            Pb[2*i+1] = 0.5*(Pb[2*i] + Pb[2*i+2])
        Tb = np.zeros([3,np.size(P)-1],dtype=int)
        Tb[0:2,:] = T*2
        Tb[2,:] = Tb[1,:]-1
    return Nb, Pb, Tb
```

```python
# local debugging cell: generate_PT_1D, generate_NbPbTb_1D
left_boundary = 0
right_boundary = 1
number_of_elements = 10
P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

basis_type = 102
Nb, Pb, Tb = generate_NbPbTb_1D(P, T, basis_type)
#print (P); print (T); print (Nb); print (Pb); print (Tb)
```

### Local Basis Function
- For local basis functions, the first thing needs to know is the number of local basis functions, $N_{lb}$
```python
    def generate_Nlb(basis_type)
```
- Then, for each index of the basis, one needs the evaluation function for the value of the basis function and its derivatives.
```python
    def local_basis_1D(x, vertices, basis_type, basis_index, der_order)
```
- Aagin, one has to update the code once new basis type is added.

```python
def generate_Nlb(basis_type):
    if (basis_type==101):
        Nlb = 2
    elif (basis_type==102):
        Nlb = 3
    return Nlb

def local_basis_1D(x, vertices, basis_type, basis_index, der_order):
    #basis_type==101: 1D linear basis functions
    #basis_type==102: 1D quadratic basis functions

    left = vertices[0]
    right = vertices[1]
    h = right - left
    if (basis_type==101):
        if (basis_index==0):
            if (der_order==0):
                r = (right - x)/h
            elif (der_order==1):
                r = -1/h
            elif (der_order>1):
                r = 0.
            else:
                print ("wrong der order")
        elif (basis_index==1):
            if (der_order==0):
                r = (x - left)/h
            elif (der_order==1):
                r = 1/h
            elif (der_order>1):
                r = 0
            else:
                print ("wrong der order")
    elif (basis_type==102):
        if (basis_index==0):
            if (der_order==0):
                r = 2*((x-left)/h)**2 - 3*(x-left)/h + 1
            elif (der_order==1):
                r = 4*(x-left)/h**2 - 3/h
            elif (der_order==2):
                r = 4/h**2
            elif (der_order>2):
                r = 0
            else:
                print ("wrong der order")
        elif (basis_index==1):
            if (der_order==0):
                r = 2*((x-left)/h)**2 - (x-left)/h
            elif (der_order==1):
                r = 4*(x-left)/h**2 - 1/h
            elif (der_order==2):
                r = 4/h**2
            elif (der_order>2):
                r = 0
            else:
                print ("wrong der order")
        elif (basis_index==2):
            if (der_order==0):
                r = -4*((x-left)/h)**2 + 4*(x-left)/h
            elif (der_order==1):
                r = -8*(x-left)/h**2 + 4/h
            elif (der_order==2):
                r = -8/h**2
            elif (der_order>2):
                r = 0
            else:
                print ("wrong der order")
    return r
```

### Local Assembly
- Now, we process the key part of this implemenation, __local assembly__.  Basically, this process takes two steps:
    1. Computing the integral $\int_{I_n} c(x){\phi'_{n,\alpha}}(x){\phi'_{n,\beta}}(x)dx$
    2. Adding the integral value to the correspond entry of the global matrix.
- Note, it is simiar to deal with the source term and assemble the global vector.

#### Gauss Quadrature
- However, to compute the integration in general, one needs use numerical quadrature methods.  Here, we choose the popular Gauss quadrature:
    - For each element, we need the corresponding Gauss points and weights based on the element vertices.
    ```python
        def generate_Gauss1D(vertices, Gauss_points_ref, Gauss_weights_ref)
    ```
    - One has to provide the number of Gauss quadrature points, $Gpn$, and the correspond Gauss points and weights on the reference interval $[-1,1]$.
    - In python, the Gauss-Legendre quadrature can be easily generated by
    ```python
        Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)
    ```

```python
def generate_Gauss1D(vertices, Gauss_points_ref, Gauss_weights_ref):
    mid  = 0.5*(vertices[0] + vertices[1])
    scale = 0.5*(vertices[1] - vertices[0])
    return mid + scale*Gauss_points_ref, scale*Gauss_weights_ref
```

```python
# local debugging cell: global testing part 1
# left, right boundary
left_boundary = 0
right_boundary = 1

# number of elements, also size(T[0])
number_of_elements = 4

# P, T matrice
P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

basis_type_trial = 102
basis_type_test = 102

# Nlb, number of local basis
Nlb_trial = generate_Nlb(basis_type_trial)
Nlb_test  = generate_Nlb(basis_type_test)
Nb_trial, Pb_trial, Tb_trial = generate_NbPbTb_1D(P, T, basis_type_trial)
Nb_test,  Pb_test,  Tb_test  = generate_NbPbTb_1D(P, T, basis_type_test)

# Gauss point number
Gpn = 6
Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)
#Gauss_points, Gauss_weights = generate_Gauss1D([0,1], Gauss_points_ref, Gauss_weights_ref)
```

#### Assemble Matrix (diffusion term)
One needs:
- Define the coefficient function $c(x)$
```python
def coeff_fun(x)
```
- Computing the integral $\int_{I_n} c(x){\phi'_{n,\alpha}}(x){\phi'_{n,\beta}}(x)dx$
```python
def Gauss_int_1D_trial_test(Gpn, Gauss_points, Gauss_weights, \
                coeff_fun, vertices, \
                der_order_trial, basis_type_trial, basis_index_trial,\
                der_order_test,  basis_type_test,  basis_index_test)
```
    - Here, in the above implementation, we allow the trial and test functions can come from different approximation space.
- Assemble the global matrix
```python
def assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref, \
           P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
           basis_type_trial, der_order_trial, \
           basis_type_test, der_order_test)
```
    - The correspond global index is provided by the $T_b$ matrix.
    - __Important__: the __trial__ function contributes to the __column__ entry and the __test__ function contributes to the __row__ entry of the global matrix.
    - Here, the derivative order is $1$.

```python
def coeff_fun(x):
    return np.exp(x)

def Gauss_int_1D_trial_test(Gpn, Gauss_points, Gauss_weights, coeff_fun, vertices, \
                            der_order_trial, basis_type_trial, basis_index_trial, \
                            der_order_test,  basis_type_test,  basis_index_test):
    int_value = 0.
    for k in range (Gpn):
        int_value += Gauss_weights[k]*coeff_fun(Gauss_points[k])*\
                        local_basis_1D(Gauss_points[k],vertices, basis_type_trial, basis_index_trial, der_order_trial)*\
                        local_basis_1D(Gauss_points[k],vertices, basis_type_test, basis_index_test, der_order_test)
    return int_value
```

```python
def assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref, P, T,\
                       Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, der_order_trial, basis_type_test, der_order_test):
    A = np.zeros([Nb_test, Nb_trial])
    for n in range(number_of_elements):
        vertices = P[T[0,n]:T[1,n]+1]
        Gauss_points, Gauss_weights=generate_Gauss1D(vertices, Gauss_points_ref, Gauss_weights_ref)
        for alpha in range(Nlb_trial):
            for beta in range(Nlb_test):
                int_value=Gauss_int_1D_trial_test(Gpn, Gauss_points, Gauss_weights, coeff_fun, vertices, \
                                                    der_order_trial, basis_type_trial, alpha, \
                                                    der_order_test,  basis_type_test,  beta)
                A[Tb_test[beta,n],Tb_trial[alpha,n]] += int_value
    return A
```

#### Assemble Vector (source term)
One needs:
- Define the source function $f(x)$
```python
def source_fun(x)
```
- Computing the integral $\int_{I_n} f(x){\phi_{n,\beta}}(x)dx$
```python
def Gauss_int_1D_source_test(Gpn, Gauss_points, Gauss_weights, \
                     source_fun, vertices, \
                     der_order_test,  basis_type_test,  basis_index_test)
```
- Assemble the global vector
```python
def assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, \
                       basis_type_test, der_order_test)
```
Note: it is a rather a simple version of asseble matrix process.

```python
def source_fun(x):
    #return 0.
    return -np.exp(x)*(np.cos(x)-2*np.sin(x)-x*np.cos(x)-x*np.sin(x))

def Gauss_int_1D_source_test(Gpn, Gauss_points, Gauss_weights, source_fun, vertices, \
                            der_order_test,  basis_type_test,  basis_index_test):
    int_value = 0.
    for k in range (Gpn):
        int_value += Gauss_weights[k]*source_fun(Gauss_points[k])*\
                        local_basis_1D(Gauss_points[k], vertices, basis_type_test, basis_index_test, der_order_test)
    return int_value

def assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, der_order_test):
    b = np.zeros(Nb_test)
    for n in range(number_of_elements):
        vertices = P[T[0,n]:T[1,n]+1]
        Gauss_points, Gauss_weights=generate_Gauss1D(vertices, Gauss_points_ref, Gauss_weights_ref)
        for beta in range(Nlb_test):
            int_value=Gauss_int_1D_source_test(Gpn, Gauss_points, Gauss_weights, source_fun, vertices, \
                                                der_order_test,  basis_type_test,  beta)
            b[Tb_test[beta,n]] += int_value
    return b
```

```python
# local debugging cell: assemble_matrix_1D, assemble_vector_1D, global testing part 2
#print (Nb_trial, Nb_test, np.shape(Tb_trial), np.shape(Tb_test))
A = assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref,
                       P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, 1, basis_type_test, 1)
b = assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, 0)
#print (A)
#print (b)
```

### Treat Boundary Conditions
#### Boundary information matrix
To deal with the boundary conditions in a general struture, one needs store the boundary information matrix $boundarynodes$:
- $boundarynodes[0,k]$ is the type of the $k$th boundary finite element node: Dirichlet, Neumann, Robin, etc.
- $boundarynodes[1,k]$ is the global node index of the kth boundary finite element node.
- Let $nbn$ denote the number of boundary finite element nodes.
- For more complicated methods, one may need to store more boundary information than the above.
```python
    def generate_boundarynodes_1D(Pb)
```

#### Dirichlet boundary conditions
- Define $g(x)$ to be the Dirichlet boundary function which satisfies $g(a)=g_a$ and $g(b)=g_b$.
- The Dirichlet boundary condition leads to the following equation:
\begin{align}
    u_h(a) & = \sum\limits_{n = 1}^N\sum\limits_{i=1}^{N_{lb}}u_{n,i}\phi_{n,i}(a) = g(a)\\
    u_h(b) & = \sum\limits_{n = 1}^N\sum\limits_{i=1}^{N_{lb}}u_{n,i}\phi_{n,i}(b) = g(b)
\end{align}
- However, only the coefficient of the Dirichlet boundary nodes have contribution to the final results:
    - Let $i = boundary[1,k]$, then $u_i = g(P_b[i])$, where $u_i$ is the $i$th unknown (global index).

```python
def generate_boundarynodes_1D(Pb):
    nbn = 2
    boundarynodes = np.zeros([2,nbn],dtype=int)
    # boundarynodes[0,i] = 0, Dirichlet
    boundarynodes[1,0] = 0
    boundarynodes[1,1] = np.size(Pb)-1
    return nbn, boundarynodes

def Dirichlet_fun(x):
    if (x <= left_boundary+1.e-10):
        return 0.
    elif (x >= right_boundary - 1.e-10):
        return np.cos(1)

def treat_Dirichlet_boundary(A, b, Pb, nbn, boundarynodes, Dirichlet_fun):
    for k in range(nbn):
        if (boundarynodes[0,k] == 0):
            i = boundarynodes[1,k]
            A[i,:] = 0
            A[i,i] = 1
            b[i] = Dirichlet_fun(Pb[i])
    return A, b
```

```python
# local debugging cell: global testing part3
nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)
#print (nbn)
#print (boundarynodes)

def exact_fun(x):
    return x*np.cos(x)

A, b = treat_Dirichlet_boundary(A, b, Pb_trial, nbn, boundarynodes, Dirichlet_fun)
#print (A, b)

u = np.linalg.solve(A,b)
#print (u)
err = np.zeros(np.size(Pb_trial))
for i in range (np.size(Pb_trial)):
    err[i] = np.abs(u[i] - exact_fun(Pb_trial[i]))
print (np.max(err))

plt.plot(Pb_trial, u, 'k')
plt.plot(Pb_trial, Pb_trial*np.cos(Pb_trial))
plt.show()
```

### Summary (Framework)
Now, we can put everthing together into the following framework:
```python
def Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
        basis_type, Gpn, coeff_fun, source_fun, Dirichlet_fun, exact_fun)
```
- Generate the mesh information matrices: $P, T, E$;
- Generate the finite element information matrices: $P_b, T_b, E_b$;
- Assembel the matrices and vectors: local assembly based on $P, T, E$ only, local to global needs $P_b, T_b, E_b$;
- Deal with the boundary conditions: boundary information matrix and local assembly;
- Solve linear systems: numerical linear algebra.

```python
def Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                     generate_boundarynodes_1D, basis_type,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, exact_fun):
    # P, T matrice
    P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

    basis_type_trial = basis_type
    basis_type_test  = basis_type

    # Nlb, number of local basis
    Nlb_trial = generate_Nlb(basis_type_trial)
    Nlb_test  = generate_Nlb(basis_type_test)
    Nb_trial, Pb_trial, Tb_trial = generate_NbPbTb_1D(P, T, basis_type_trial)
    Nb_test,  Pb_test,  Tb_test  = generate_NbPbTb_1D(P, T, basis_type_test)

    # Gauss point number
    Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)

    # Assemble local to global
    A = assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref,
                       P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, 1, basis_type_test, 1)
    b = assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, 0)

    # deal with boundary
    nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)
    # Dirichlet boundary
    A, b = treat_Dirichlet_boundary(A, b, Pb_trial, nbn, boundarynodes, Dirichlet_fun)

    # solve the solution
    u = np.linalg.solve(A,b)
    #print (u)

    # maximum error at nodes
    err = np.zeros(np.size(Pb_trial))
    for i in range (np.size(Pb_trial)):
        err[i] = np.abs(u[i] - exact_fun(Pb_trial[i]))

    return np.max(err), Pb_trial, u
#
err, Pb, u = Poisson1D_solver(left_boundary, right_boundary, number_of_elements,
                     generate_boundarynodes_1D, 102,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, exact_fun)
print ("maximum error:  %7.2e" % err)
```

    maximum error:  4.66e-05

__Example 1__

Use the 1D linear finite element method to solve the following equation:
\begin{align*}
     -\frac{d}{dx}\left(e^x\frac{d u(x)}{dx}\right) &= -e^x[\cos(x)-2\sin(x)-x\cos(x) - x\sin(x)],\,\, (0 \leq x \leq 1), \\
     u(0) &= 0,\,\,u(1) = \cos(1).
\end{align*}
- The analytic solution of this problem is $u = x\cos(x)$, which can be used to compute the error of the numerical solution.
- Second order convergence $\mathcal{O}(h^2)$ since the error is reduced by $1$ when $h$ is reduced by half.
- This matches the optimal approximation capability expected from piecewise linear functions.

```python
# function to output formatted order table
def print_OrderTable(number_coarse, err):
    print ("h      maximum error    order")
    for i in range(np.size(err)):
        number_of_elements = number_coarse*2**i
        if (i == 0):
            print ("1/%-3d    %7.2e        --" % (number_of_elements, err[i]))
        else:
            print ("1/%-3d    %7.2e       %4.2f" % (number_of_elements, err[i], np.log(err[i-1]/err[i])/np.log(2)))
```

```python
left_boundary_ex1 = 0
right_boundary_ex1 = 1

Gpn = 6

def coeff_fun_ex1(x):
    return np.exp(x)

def source_fun_ex1(x):
    return -np.exp(x)*(np.cos(x)-2*np.sin(x)-x*np.cos(x)-x*np.sin(x))

def generate_boundarynodes_1D_ex1(Pb):
    nbn = 2
    boundarynodes = np.zeros([2,nbn],dtype=int)
    # boundarynodes[0,i] = 0, Dirichlet
    boundarynodes[1,0] = 0
    boundarynodes[1,1] = np.size(Pb)-1
    return nbn, boundarynodes

def Dirichlet_fun_ex1(x):
    tiny = 1.e-12
    if (x <= left_boundary_ex1 + tiny):
        return 0.
    elif (x >= right_boundary_ex1 - tiny):
        return np.cos(1)

def exact_fun_ex1(x):
    return x*np.cos(x)

number_coarse = 4
basis_type = 101

err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                                     generate_boundarynodes_1D_ex1, basis_type,\
                                     Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex1, exact_fun_ex1)
print_OrderTable(number_coarse, err)
plt.plot(Pb, u)
plt.title("Example 1, Linear FEM, N = %3d" % number_of_elements)
plt.show()
```

__Example 2__

Use the 1D quadratric finite element method to solve the following equation:
\begin{align*}
    -\frac{d}{dx}\left(e^x\frac{d u(x)}{dx}\right) &= -e^x[\cos(x)-2\sin(x)-x\cos(x) - x\sin(x)],\,\, (0 \leq x \leq 1), \\
    u(0) &= 0,\,\,u(1) = \cos(1).
\end{align*}
- The analytic solution of this problem is $u = x\cos(x)$, which can be used to compute the error of the numerical solution.
- Third order convergence $\mathcal{O}(h^3)$ since the error is reduced by $1$ when $h$ is reduced by half.
- This matches the optimal approximation capability expected from piecewise quadratric functions.
- In fact, we observe superconvergence since the convergence order is almost $\mathcal{O}(h^4)$

```python
number_coarse = 4
basis_type = 102

err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                                    generate_boundarynodes_1D_ex1, basis_type,\
                                    Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex1, exact_fun_ex1)
print_OrderTable(number_coarse, err)
plt.plot(Pb, u)
plt.title("Example 2, Quadratic FEM, N = %3d" % number_of_elements)
plt.show()
```

### Extensions
#### Neumann boundary conditions
Consider 1D elliptic problem with Neumann boundary conditions:
\begin{align}
   -\frac{d}{dx}\left(c(x)\frac{d u(x)}{dx}\right) & = f(x),\,\, a < x < b \\
   u(a) = g_a,\,\,u'(b) & = r_b;\quad\text{or}\quad u'(a) = r_a,\,\,u(b) = g_b.
\end{align}
- Recall the weak formulation, we have:
\begin{equation}
    c(a)u'(a)v(a) - c(b)u'(b)v(b) + \int_a^b cu'v'dx = \int_a^b fv dx.
\end{equation}
- If $x = a$ is the Neumann boundary:
\begin{equation}
     \int_a^b cu'v'dx = \int_a^b fv dx - r_a*c(a)v(a).
\end{equation}
- If $x = b$ is the Neumann boundary:
\begin{equation}
     \int_a^b cu'v'dx = \int_a^b fv dx + r_b*c(b)v(b).
\end{equation}
- If both $x = a$ and $x = b$ are the Neumann boundary, what is __wrong__?
- Therefore, for Neumann boundary conditions, one only needs to change the assembly vector.
```python
    def Neumann_fun(x)
    def treat_Neumann_boundary(b, Pb, nbn, boundarynodes, \
                                coeff_fun, Neumann_fun)
```
Since the left and right boundaries are slightly different , $boundarynodes[2,:]$ needs to store the correspond information.

```python
def generate_boundarynodes_1D_ex3(Pb):
    nbn = 2
    boundarynodes = np.zeros([3,nbn],dtype=int)
    # boundarynodes[0,i] = 0, Dirichlet; boundarynodes[1,i] = 1, Neumann
    boundarynodes[1,0] = 0
    # assume right boundary is Neumann
    # add the third row to boundarynodes: 0, left; 1 right.
    boundarynodes[0,1] = 1
    boundarynodes[1,1] = np.size(Pb)-1
    boundarynodes[2,1] = 1
    return nbn, boundarynodes

nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)

def Dirichlet_fun(x):
    if (x <= left_boundary+1.e-10):
        return 0.

def Neumann_fun(x):
    if (x >= right_boundary-1.e-10):
        return np.cos(1) - np.sin(1)

def treat_Neumann_boundary(b, Pb, nbn, boundarynodes, coeff_fun, Neumann_fun):
    for k in range(nbn):
        if (boundarynodes[0,k] == 1):
            i = boundarynodes[1,k]
            if (boundarynodes[2,k] == 1):
                b[i] += coeff_fun(Pb[i])*Neumann_fun(Pb[i])
            elif (boundarynodes[2,k] == 0):
                b[i] += -coeff_fun(Pb[i])*Neumann_fun(Pb[i])
    return b
```

```python
def Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                     generate_boundarynodes_1D, basis_type,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, Neumann_fun, exact_fun):
    # P, T matrice
    P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

    basis_type_trial = basis_type
    basis_type_test  = basis_type

    # Nlb, number of local basis
    Nlb_trial = generate_Nlb(basis_type_trial)
    Nlb_test  = generate_Nlb(basis_type_test)
    Nb_trial, Pb_trial, Tb_trial = generate_NbPbTb_1D(P, T, basis_type_trial)
    Nb_test,  Pb_test,  Tb_test  = generate_NbPbTb_1D(P, T, basis_type_test)

    # Gauss point number
    Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)

    # Assemble local to global
    A = assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref,
                       P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, 1, basis_type_test, 1)
    b = assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, 0)

    # deal with boundary
    nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)
    # Dirichlet boundary
    A, b = treat_Dirichlet_boundary(A, b, Pb_trial, nbn, boundarynodes, Dirichlet_fun)
    # Neumann boundary
    b = treat_Neumann_boundary(b, Pb_trial, nbn, boundarynodes, coeff_fun, Neumann_fun)

    # solve the solution
    u = np.linalg.solve(A,b)
    #print (u)

    # maximum error at nodes
    err = np.zeros(np.size(Pb_trial))
    for i in range (np.size(Pb_trial)):
        err[i] = np.abs(u[i] - exact_fun(Pb_trial[i]))

    return np.max(err), Pb_trial, u
#
err, Pb, u = Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                     generate_boundarynodes_1D_ex3, 102,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, Neumann_fun, exact_fun)
print ("maximum error: %7.2e" % err)
```

    maximum error: 4.66e-11

__Example 3__

Use the 1D linear and quadratric finite element method to solve the following equation:
\begin{align*}
     -\frac{d}{dx}\left(e^x\frac{d u(x)}{dx}\right) &= -e^x[\cos(x)-2\sin(x)-x\cos(x) - x\sin(x)],\,\, (0 \leq x \leq 1), \\
     u(0) &= 0,\,\,u'(1) = \cos(1) - \sin(1).
\end{align*}
- The analytic solution of this problem is $u = x\cos(x)$, which can be used to compute the error of the numerical solution.

```python
def Dirichlet_fun_ex3(x):
    tiny = 1.e-12
    if (x <= left_boundary_ex1 + tiny):
        return 0.

def Neumann_fun_ex3(x):
    tiny = 1.e-12
    if (x >= right_boundary - tiny):
        return np.cos(1) - np.sin(1)

number_coarse = 4
basis_type = 101
print ("Linear Finite Element: ")
err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                        generate_boundarynodes_1D_ex3, basis_type,\
                        Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex3, Neumann_fun_ex3, exact_fun_ex1)
print_OrderTable(number_coarse, err)
fig = plt.figure(figsize=(10,4))
ax1 = fig.add_subplot(1,2,1)
ax1.plot(Pb, u)
ax1.set_title("Example 3, Linear FEM, N = %3d" % number_of_elements)

basis_type = 102
print ("Quadratic Finite Element: ")
err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                        generate_boundarynodes_1D_ex3, basis_type,\
                        Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex3, Neumann_fun_ex3, exact_fun_ex1)
print_OrderTable(number_coarse, err)
ax2 = fig.add_subplot(1,2,2)
ax2.plot(Pb, u)
ax2.set_title("Example 3, Quadratic FEM, N = %3d" % number_of_elements)
plt.show()
```

#### Robin boundary conditions
Consider 1D elliptic problem with Robin boundary conditions:
\begin{align}
   -\frac{d}{dx}\left(c(x)\frac{d u(x)}{dx}\right) & = f(x),\,\, a < x < b \\
   u'(a) + q_a u(a) = p_a,\,\,u(b) & = g_b;\quad\text{or}\quad u(a) = g_a,\,\,u'(b) + q_b u(b) = p_b.
\end{align}
- Recall the weak formulation, we have:
\begin{equation}
    c(a)u'(a)v(a) - c(b)u'(b)v(b) + \int_a^b cu'v'dx = \int_a^b fv dx.
\end{equation}
- If $x = a$ is the Robin boundary:
\begin{equation}
     \int_a^b cu'v'dx - q_a c(a)u(a)= \int_a^b fv dx - p_ac(a)v(a).
\end{equation}
- If $x = b$ is the Robin boundary:
\begin{equation}
     \int_a^b cu'v'dx + q_b c(b)u(b)= \int_a^b fv dx + p_bc(b)v(b).
\end{equation}
- Therefore, for Robin boundary conditions, one only needs to change the both assembly matrix and vector.
```python
    def Robin_fun(x)
    def Robin_coeff(x)
    def treat_Robin_boundary(A, b, Pb, nbn, boundarynodes, coeff_fun, \
                            Robin_fun, Robin_coeff)
```

```python
def generate_boundarynodes_1D_ex4(Pb):
    nbn = 2
    boundarynodes = np.zeros([3,nbn],dtype=int)
    # boundarynodes[0,i] = 0, Dirichlet; 1, Neumann; 2, Robin
    # assume left boundary: Robin, right boundary: Dirichlet
    # add the third row to boundarynodes: 0, left; 1 right.
    boundarynodes[0,0] = 2
    boundarynodes[2,0] = 0
    boundarynodes[1,0] = 0
    boundarynodes[1,1] = np.size(Pb)-1
    return nbn, boundarynodes

nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)

def Dirichlet_fun(x):
    if (x >= right_boundary-1.e-10):
        return np.cos(1)

def Robin_fun(x):
    if (x <= left_boundary+1.e-10):
        return 1.

def Robin_coeff(x):
    if (x <= left_boundary+1.e-10):
        return 1.

def treat_Robin_boundary(A, b, Pb, nbn, boundarynodes, coeff_fun, Robin_fun, Robin_coeff):
    for k in range(nbn):
        if (boundarynodes[0,k] == 2):
            i = boundarynodes[1,k]
            if (boundarynodes[2,k] == 0):
                A[i,i] += -Robin_coeff(Pb[i])*coeff_fun(Pb[i])
                b[i] += -coeff_fun(Pb[i])*Robin_fun(Pb[i])
            elif (boundarynodes[2,k] == 1):
                A[i,i] += Robin_coeff(Pb[i])*coeff_fun(Pb[i])
                b[i] += coeff_fun(Pb[i])*Robin_fun(Pb[i])
    return A, b
```

```python
def Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                     generate_boundarynodes_1D, basis_type,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, Neumann_fun, Robin_fun, Robin_coeff, exact_fun):
    # P, T matrice
    #print (number_of_elements)
    P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

    basis_type_trial = basis_type
    basis_type_test  = basis_type

    # Nlb, number of local basis
    Nlb_trial = generate_Nlb(basis_type_trial)
    Nlb_test  = generate_Nlb(basis_type_test)
    Nb_trial, Pb_trial, Tb_trial = generate_NbPbTb_1D(P, T, basis_type_trial)
    Nb_test,  Pb_test,  Tb_test  = generate_NbPbTb_1D(P, T, basis_type_test)

    # Gauss point number
    Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)

    # Assemble local to global
    A = assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref,
                       P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, 1, basis_type_test, 1)
    b = assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, 0)

    # deal with boundary
    nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)
    # Dirichlet boundary
    A, b = treat_Dirichlet_boundary(A, b, Pb_trial, nbn, boundarynodes, Dirichlet_fun)
    # Neumann boundary
    b = treat_Neumann_boundary(b, Pb_trial, nbn, boundarynodes, coeff_fun, Neumann_fun)
    # Robin boundary
    A, b = treat_Robin_boundary(A, b, Pb_trial, nbn, boundarynodes, coeff_fun, Robin_fun, Robin_coeff)

    # solve the solution
    u = np.linalg.solve(A,b)
    #print (u)

    # maximum error at nodes
    err = np.zeros(np.size(Pb_trial))
    for i in range (np.size(Pb_trial)):
        err[i] = np.abs(u[i] - exact_fun(Pb_trial[i]))

    return np.max(err), Pb_trial, u
#
err, Pb, u = Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                    generate_boundarynodes_1D_ex4, 102,\
                    Gpn, coeff_fun, source_fun, Dirichlet_fun, Neumann_fun, Robin_fun, Robin_coeff, exact_fun)
print ("maximum error: %7.2e" % err)
```

    maximum error: 3.08e-11

__Example 4__

Use the 1D linear and quadratric finite element method to solve the following equation:
\begin{align*}
     -\frac{d}{dx}\left(e^x\frac{d u(x)}{dx}\right) &= -e^x[\cos(x)-2\sin(x)-x\cos(x) - x\sin(x)],\,\, (0 \leq x \leq 1), \\
     u'(0) + u(0) &= 1,\,\,u(1) = \cos(1).
\end{align*}
- The analytic solution of this problem is $u = x\cos(x)$, which can be used to compute the error of the numerical solution.

```python
def Dirichlet_fun_ex4(x):
    tiny = 1.e-12
    if (x >= right_boundary_ex1 - tiny):
        return np.cos(1)

def Robin_fun_ex4(x):
    tiny = 1.e-12
    if (x <= left_boundary + tiny):
        return 1

def Robin_coeff_ex4(x):
    tiny = 1.e-12
    if (x <= left_boundary + tiny):
        return 1

number_coarse = 4
basis_type = 101
print ("Linear Finite Element: ")
err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                        generate_boundarynodes_1D_ex4, basis_type,\
                        Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex4, Neumann_fun_ex3, \
                        Robin_fun_ex4, Robin_coeff_ex4, exact_fun_ex1)
print_OrderTable(number_coarse, err)
fig = plt.figure(figsize=(10,4))
ax1 = fig.add_subplot(1,2,1)
ax1.plot(Pb, u)
ax1.set_title("Example 4, Linear FEM, N = %3d" % number_of_elements)

basis_type = 102
print ("Quadratic Finite Element: ")
err = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    err[i], Pb, u = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                            generate_boundarynodes_1D_ex4, basis_type,\
                            Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex4, Neumann_fun_ex3, \
                            Robin_fun_ex4, Robin_coeff_ex4, exact_fun_ex1)
print_OrderTable(number_coarse, err)
ax2 = fig.add_subplot(1,2,2)
ax2.plot(Pb, u)
ax2.set_title("Example 4, Quadratic FEM, N = %3d" % number_of_elements)
plt.show()
```

#### Measurements for Errors
In above examples, we have considered the maximum error of the approximation on the finite element __nodes__.  However, we should point out it is not a proper measurement for errors.  It is the reason that the quadratic basis funcions lead to convergence rate of $\mathcal{O}(h^4)$, the so-called superconvergence (sort of cheating).  The common measurements (norms) for errors are the following:
- $L^\infty$ norm error:
$$ \|u - u_h\|_{\infty} =\sup\limits_{x\in \Omega}|u(x)- u_h(x)|. $$
- $L^2$ norm error:
$$ \|u - u_h\|_{0} = \left(\int_{\Omega}|u - u_h|^2 dx\right)^{\frac{1}{2}}.$$
- $H^1$ semi-norm error:
$$ |u - u_h|_{1} = \left(\int_{\Omega}|u' - u'_h|^2 dx\right)^{\frac{1}{2}}.$$
- $H^1$ norm error:
$$ |u - u_h|_{1} = \left(\int_{\Omega}|u - u_h|^2 dx + \int_{\Omega}|u' - u'_h|^2 dx\right)^{\frac{1}{2}}.$$

#### Evaluation of Finite Element Solution
By solving the final linear system, we obtain the unknonw coefficients for the basis form of the approximation $u_h$,
\begin{equation}
    u_h(x) = \sum\limits_{j = 1}^{N_b}u_j\phi_j(x),
\end{equation}
however, the local basis functions we defined are for the local basis form of $u_h$
\begin{equation}
    u_h(x) = \sum\limits_{n = 1}^N\sum\limits_{i=1}^{N_{lb}}u_{n,i}\phi_{n,i}(x).
\end{equation}
Then, the question is how should we translate $\left\{u_j\right\}$ to $\left\{u_{n,i}\right\}$.
- Based on the information matrix $T_b$, we have the following relation:
    $$ u_{n,i} = u_{T_b[i,n]}.$$
- For $x \in I_n$, we have
    $$ u_h(x) = \sum\limits_{i=1}^{N_{lb}} u_{T_b[i,n]}\phi_{n,i}(x).$$

```python
def point_location(x, P):
    tiny = 1.e-10
    for n in range(np.size(P)-1):
        if (P[n] - tiny <= x and x <= P[n+1] -tiny):
            return n
    return np.size(P)-2

def FE_solution(x, u, P, T, Tb, basis_type, Nlb, der_order):
    result = 0.
    n = point_location(x, P)
    vertices = P[T[0,n]:T[1,n]+1]
    for i in range(Nlb):
        result += u[Tb[i,n]]*local_basis_1D(x, vertices, basis_type, i, der_order)
    return result
```

```python
def exact_fun_ex1(x, der_order):
    if (der_order == 0):
        return x*np.cos(x)
    elif (der_order == 1):
        return np.cos(x) - x*np.sin(x)
```

```python
def norm(u, P, T, Tb, basis_type, Nlb, exact_fun, Gpn, norm_type = 'L2'):
    Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)
    err = 0.
    for n in range(np.size(P)-1):
        vertices = P[T[0,n]:T[1,n]+1]
        Gauss_points, Gauss_weights=generate_Gauss1D(vertices, Gauss_points_ref, Gauss_weights_ref)
        if (norm_type == 'L2'):
            for k in range (Gpn):
                 err += Gauss_weights[k]*(exact_fun(Gauss_points[k],0) - \
                        FE_solution(Gauss_points[k], u, P, T, Tb, basis_type, Nlb, 0))**2
        elif (norm_type == 'H1'):
            for k in range (Gpn):
                 err += Gauss_weights[k]*(exact_fun(Gauss_points[k],1) - \
                        FE_solution(Gauss_points[k], u, P, T, Tb, basis_type, Nlb, 1))**2
        elif (norm_type == 'Linf'):
            for k in range (Gpn):
                err = max(err, np.abs(exact_fun(Gauss_points[k],0) - \
                        FE_solution(Gauss_points[k], u, P, T, Tb, basis_type, Nlb, 0)))
    if (norm_type == 'L2' or norm_type == 'H1'):
        return np.sqrt(err)
    elif (norm_type == 'Linf'):
        return err
```

```python
def Poisson1D_solver(left_boundary, right_boundary, number_of_elements, \
                     generate_boundarynodes_1D, basis_type,\
                     Gpn, coeff_fun, source_fun, Dirichlet_fun, Neumann_fun, Robin_fun, Robin_coeff):
    # P, T matrice
    #print (number_of_elements)
    P, T = generate_PT_1D(left_boundary, right_boundary, number_of_elements)

    basis_type_trial = basis_type
    basis_type_test  = basis_type

    # Nlb, number of local basis
    Nlb_trial = generate_Nlb(basis_type_trial)
    Nlb_test  = generate_Nlb(basis_type_test)
    Nb_trial, Pb_trial, Tb_trial = generate_NbPbTb_1D(P, T, basis_type_trial)
    Nb_test,  Pb_test,  Tb_test  = generate_NbPbTb_1D(P, T, basis_type_test)

    # Gauss point number
    Gauss_points_ref, Gauss_weights_ref = npleg.leggauss(Gpn)

    # Assemble local to global
    A = assemble_matrix_1D(coeff_fun, Gauss_points_ref, Gauss_weights_ref,
                       P, T, Tb_trial, Tb_test, Nlb_trial, Nlb_test, Nb_trial, Nb_test,\
                       basis_type_trial, 1, basis_type_test, 1)
    b = assemble_vector_1D(source_fun, Gauss_points_ref, Gauss_weights_ref, \
                       P, T, Tb_test, Nlb_test, Nb_test, basis_type_test, 0)

    # deal with boundary
    nbn, boundarynodes = generate_boundarynodes_1D(Pb_trial)
    # Dirichlet boundary
    A, b = treat_Dirichlet_boundary(A, b, Pb_trial, nbn, boundarynodes, Dirichlet_fun)
    # Neumann boundary
    b = treat_Neumann_boundary(b, Pb_trial, nbn, boundarynodes, coeff_fun, Neumann_fun)
    # Robin boundary
    A, b = treat_Robin_boundary(A, b, Pb_trial, nbn, boundarynodes, coeff_fun, Robin_fun, Robin_coeff)

    # solve the solution
    u = np.linalg.solve(A,b)
    #print (u)

    return u, P, T, Pb_trial, Tb_trial
#
u, P, T, Pb, Tb = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                    generate_boundarynodes_1D_ex1, 102,\
                    Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex1, Neumann_fun_ex3, Robin_fun_ex4, Robin_coeff_ex4)
#for i in range (np.size(Pb)):
#    print (u[i] - FE_solution(Pb[i], u, P, T, Tb, 102, 3, 0))
print (norm(u, P, T, Tb, 102, 3, exact_fun_ex1, Gpn, norm_type = 'Linf'))
```

    1.1115981131230857e-08

```python
# function to output formatted order table
def print_OrderTable3(number_coarse, erri, err2, err1):
    print (" h    Linf norm  order  L2 norm  order  H1 semi  order")
    for i in range(np.size(erri)):
        number_of_elements = number_coarse*2**i
        if (i == 0):
            print ("1/%-3d  %7.2e   --   %7.2e   --   %7.2e   --" % \
                   (number_of_elements, erri[i], err2[i], err1[i]))
        else:
            print ("1/%-3d  %7.2e  %4.2f  %7.2e  %4.2f  %7.2e  %4.2f" % \
                       (number_of_elements, \
                        erri[i], np.log(erri[i-1]/erri[i])/np.log(2),\
                        err2[i], np.log(err2[i-1]/err2[i])/np.log(2),\
                        err1[i], np.log(err1[i-1]/err1[i])/np.log(2)))
```

__Example 1 in Different Measurements__

```python
number_coarse = 4
basis_type = 101

err1 = np.zeros(6)
err2 = np.zeros(6)
erri = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    u, P, T, Pb, Tb = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                                     generate_boundarynodes_1D_ex1, basis_type,\
                                     Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex1, Neumann_fun_ex3, Robin_fun_ex4, Robin_coeff_ex4)
    err1[i] = norm(u, P, T, Tb, basis_type, 2, exact_fun_ex1, Gpn, norm_type = 'H1')
    err2[i] = norm(u, P, T, Tb, basis_type, 2, exact_fun_ex1, Gpn, norm_type = 'L2')
    erri[i] = norm(u, P, T, Tb, basis_type, 2, exact_fun_ex1, Gpn, norm_type = 'Linf')
print_OrderTable3(number_coarse, erri, err2, err1)
```

     h    Linf norm  order  L2 norm  order  H1 semi  order
    1/4    1.49e-02   --   7.20e-03   --   1.05e-01   --
    1/8    3.91e-03  1.93  1.80e-03  2.00  5.27e-02  1.00
    1/16   1.00e-03  1.97  4.49e-04  2.00  2.64e-02  1.00
    1/32   2.53e-04  1.98  1.12e-04  2.00  1.32e-02  1.00
    1/64   6.36e-05  1.99  2.80e-05  2.00  6.59e-03  1.00
    1/128  1.60e-05  2.00  7.01e-06  2.00  3.30e-03  1.00

__Example 2 in Different Measurements__

```python
number_coarse = 4
basis_type = 102

err1 = np.zeros(6)
err2 = np.zeros(6)
erri = np.zeros(6)
for i in range (6):
    number_of_elements = number_coarse*2**i
    u, P, T, Pb, Tb = Poisson1D_solver(left_boundary_ex1, right_boundary_ex1, number_of_elements, \
                                     generate_boundarynodes_1D_ex1, basis_type,\
                                     Gpn, coeff_fun_ex1, source_fun_ex1, Dirichlet_fun_ex1, Neumann_fun_ex3, Robin_fun_ex4, Robin_coeff_ex4)
    err1[i] = norm(u, P, T, Tb, basis_type, 3, exact_fun_ex1, Gpn, norm_type = 'H1')
    err2[i] = norm(u, P, T, Tb, basis_type, 3, exact_fun_ex1, Gpn, norm_type = 'L2')
    erri[i] = norm(u, P, T, Tb, basis_type, 3, exact_fun_ex1, Gpn, norm_type = 'Linf')
print_OrderTable3(number_coarse, erri, err2, err1)
```

     h    Linf norm  order  L2 norm  order  H1 semi  order
    1/4    3.86e-04   --   2.10e-04   --   5.42e-03   --
    1/8    4.70e-05  3.04  2.61e-05  3.01  1.35e-03  2.00
    1/16   5.78e-06  3.02  3.26e-06  3.00  3.38e-04  2.00
    1/32   7.16e-07  3.01  4.08e-07  3.00  8.45e-05  2.00
    1/64   8.91e-08  3.01  5.10e-08  3.00  2.11e-05  2.00
    1/128  1.11e-08  3.00  6.37e-09  3.00  5.28e-06  2.00

__Conclusion:__

For using polynomial basis functions of degree $k$, $\mathbb{P}^k$:
- The $L^2$ norm and $L^\infty$ norm errors have convergence rate of $\mathcal{O}(h^{k+1})$
- The $H^1$ norm and $H^1$ semi-norm errors have convergence rate of $\mathcal{O}(h^{k})$
- The superconvergence of node errors normally can not be seen in general measurements, for superconvergence extracing one can see [tutorial](http:\\)


We've been working on a [conference paper](https://github.com/gilbertgede/idetc-2013-paper) to demonstrate the ability to do multibody dynamics with Python. We've been calling this work flow [PyDy](http://pydy.org), short for Python Dynamics. Several pieces of the puzzle have come together lately to really demonstrate the power of the scientific python software packages to handle complex dynamic and controls problems (i.e. IPython notebooks, matplotlib animations, python-control, and our software package mechanics which is a part of SymPy). After writing the draft of our paper, which uses a general n-link pendulum as it's main example, I came across this [blog post by Wolfram](http://blog.wolfram.com/2011/03/01/stabilized-n-link-pendulum/) demonstrating their ability to symbolically derive the equations of motion for the n-link pendulum and stabilize it with an LQR controller. It inspired me to replicate the example as I realized that it was relatively easy to do with all free and open source software!

In this example problem we will derive the equations of motion of an n-link pendulum on a laterally sliding cart and then develop a controller to stabilize it. Balancing a single inverted pendulum is a classic problem that is many times a student's first experience with non-linear dynamics and control. The problem here is extended to a general n-link pendulum and as we will see the equations of motion quickly get messy with greater than 2 links.

The diagram below shows the general description of the problem.

```
from IPython.display import SVG
SVG(filename='n-pendulum-with-cart.svg')
```

I used these software versions for the following computations:

- IPython: 0.13.1.rc2
- matplotlib: 1.1.1
- NumPy: 1.6.2
- SciPy: 0.10.1
- SymPy: 0.7.2
- python-control: 0.6d

Equations of Motion
===================

We'll start by generating the equations of motion for the system with SymPy **[mechanics](http://docs.sympy.org/dev/modules/physics/mechanics/index.html)**. The functionality that mechanics provides is much more in depth than Mathematica's functionality. In the Mathematica example, Lagrangian mechanics were implemented manually with Mathematica's symbolic functionality. **mechanics** provides an assortment of functions and classes to derive the equations of motion for arbitrarily complex (i.e. configuration constraints, nonholonomic motion constraints, etc) multibody systems in a very natural way. First we import the necessary functionality from SymPy.

```
%pylab inline
from sympy import symbols
from sympy.physics.mechanics import *
```

    Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
    For more information, type 'help(pylab)'.

Now specify the number of links, $n$. I'll start with 5 since the Wolfram folks only showed four.

```
n = 5
```

**mechanics** will need the generalized coordinates, generalized speeds, and the input force which are all time dependent variables and the bob masses, link lengths, and acceleration due to gravity which are all constants. Time, $t$, is also made available because we will need to differentiate with respect to time.

```
q = dynamicsymbols('q:' + str(n + 1))  # Generalized coordinates
u = dynamicsymbols('u:' + str(n + 1))  # Generalized speeds
f = dynamicsymbols('f')                # Force applied to the cart

m = symbols('m:' + str(n + 1))         # Mass of each bob
l = symbols('l:' + str(n))             # Length of each link
g, t = symbols('g t')                  # Gravity and time
```

Now we can create and inertial reference frame $I$ and define the point, $O$, as the origin.

```
I = ReferenceFrame('I')                # Inertial reference frame
O = Point('O')                         # Origin point
O.set_vel(I, 0)                        # Origin's velocity is zero
```

Secondly, we define the define the first point of the pendulum as a particle which has mass. This point can only move laterally and represents the motion of the "cart".

```
P0 = Point('P0')                       # Hinge point of top link
P0.set_pos(O, q[0] * I.x)              # Set the position of P0
P0.set_vel(I, u[0] * I.x)              # Set the velocity of P0
Pa0 = Particle('Pa0', P0, m[0])        # Define a particle at P0
```

Now we can define the $n$ reference frames, particles, gravitational forces, and kinematical differential equations for each of the pendulum links. This is easily done with a loop.

```
frames = [I]                              # List to hold the n + 1 frames
points = [P0]                             # List to hold the n + 1 points
particles = [Pa0]                         # List to hold the n + 1 particles
forces = [(P0, f * I.x - m[0] * g * I.y)] # List to hold the n + 1 applied forces, including the input force, f
kindiffs = [q[0].diff(t) - u[0]]          # List to hold kinematic ODE's

for i in range(n):
    Bi = I.orientnew('B' + str(i), 'Axis', [q[i + 1], I.z])   # Create a new frame
    Bi.set_ang_vel(I, u[i + 1] * I.z)                         # Set angular velocity
    frames.append(Bi)                                         # Add it to the frames list

    Pi = points[-1].locatenew('P' + str(i + 1), l[i] * Bi.x)  # Create a new point
    Pi.v2pt_theory(points[-1], I, Bi)                         # Set the velocity
    points.append(Pi)                                         # Add it to the points list

    Pai = Particle('Pa' + str(i + 1), Pi, m[i + 1])           # Create a new particle
    particles.append(Pai)                                     # Add it to the particles list

    forces.append((Pi, -m[i + 1] * g * I.y))                  # Set the force applied at the point

    kindiffs.append(q[i + 1].diff(t) - u[i + 1])              # Define the kinematic ODE:  dq_i / dt - u_i = 0
```

With all of the necessary point velocities and particle masses defined, the `KanesMethod` class can be used to derive the equations of motion of the system automatically.

```
kane = KanesMethod(I, q_ind=q, u_ind=u, kd_eqs=kindiffs) # Initialize the object
fr, frstar = kane.kanes_equations(forces, particles)     # Generate EoM's fr + frstar = 0
```

The equations of motion are quite long as can been seen below. This is the general nature of most non-simple mutlibody problems. That is why a SymPy is so useful; no more mistakes in algegra, differentiation, or copying in hand written equations.

```
fr
```

    [                                                                                -f(t)]
    [g*l0*m1*cos(q1(t)) + g*l0*m2*cos(q1(t)) + g*l0*m3*cos(q1(t)) + g*l0*m4*cos(q1(t)) + g*l0*m5*cos(q1(t))]
    [                     g*l1*m2*cos(q2(t)) + g*l1*m3*cos(q2(t)) + g*l1*m4*cos(q2(t)) + g*l1*m5*cos(q2(t))]
    [                                          g*l2*m3*cos(q3(t)) + g*l2*m4*cos(q3(t)) + g*l2*m5*cos(q3(t))]
    [                                                               g*l3*m4*cos(q4(t)) + g*l3*m5*cos(q4(t))]
    [                                                                                g*l4*m5*cos(q5(t))]

```
frstar
```

    [                                                                                -l0*m1*u1(t)**2*cos(q1(t)) - l0*m2*u1(t)**2*cos(q1(t)) - l0*m3*u1(t)**2*cos(q1(t)) - l0*m4*u1(t)**2*cos(q1(t)) - l0*m5*u1(t)**2*cos(q1(t)) - l1*m2*u2(t)**2*cos(q2(t)) - l1*m3*u2(t)**2*cos(q2(t)) - l1*m4*u2(t)**2*cos(q2(t)) - l1*m5*u2(t)**2*cos(q2(t)) - l2*m3*u3(t)**2*cos(q3(t)) - l2*m4*u3(t)**2*cos(q3(t)) - l2*m5*u3(t)**2*cos(q3(t)) - l3*m4*u4(t)**2*cos(q4(t)) - l3*m5*u4(t)**2*cos(q4(t)) - l4*m5*u5(t)**2*cos(q5(t)) - l4*m5*sin(q5(t))*Derivative(u5(t), t) + (-l3*m4*sin(q4(t)) - l3*m5*sin(q4(t)))*Derivative(u4(t), t) + (-l2*m3*sin(q3(t)) - l2*m4*sin(q3(t)) - l2*m5*sin(q3(t)))*Derivative(u3(t), t) + (-l1*m2*sin(q2(t)) - l1*m3*sin(q2(t)) - l1*m4*sin(q2(t)) - l1*m5*sin(q2(t)))*Derivative(u2(t), t) + (-l0*m1*sin(q1(t)) - l0*m2*sin(q1(t)) - l0*m3*sin(q1(t)) - l0*m4*sin(q1(t)) - l0*m5*sin(q1(t)))*Derivative(u1(t), t) + (m0 + m1 + m2 + m3 + m4 + m5)*Derivative(u0(t), t)]
    [-l0*l1*m2*(-sin(q1(t))*cos(q2(t)) + sin(q2(t))*cos(q1(t)))*u2(t)**2 - l0*l1*m3*(-sin(q1(t))*cos(q2(t)) + sin(q2(t))*cos(q1(t)))*u2(t)**2 - l0*l1*m4*(-sin(q1(t))*cos(q2(t)) + sin(q2(t))*cos(q1(t)))*u2(t)**2 - l0*l1*m5*(-sin(q1(t))*cos(q2(t)) + sin(q2(t))*cos(q1(t)))*u2(t)**2 - l0*l2*m3*(-sin(q1(t))*cos(q3(t)) + sin(q3(t))*cos(q1(t)))*u3(t)**2 - l0*l2*m4*(-sin(q1(t))*cos(q3(t)) + sin(q3(t))*cos(q1(t)))*u3(t)**2 - l0*l2*m5*(-sin(q1(t))*cos(q3(t)) + sin(q3(t))*cos(q1(t)))*u3(t)**2 - l0*l3*m4*(-sin(q1(t))*cos(q4(t)) + sin(q4(t))*cos(q1(t)))*u4(t)**2 - l0*l3*m5*(-sin(q1(t))*cos(q4(t)) + sin(q4(t))*cos(q1(t)))*u4(t)**2 + l0*l4*m5*(sin(q1(t))*sin(q5(t)) + cos(q1(t))*cos(q5(t)))*Derivative(u5(t), t) - l0*l4*m5*(-sin(q1(t))*cos(q5(t)) + sin(q5(t))*cos(q1(t)))*u5(t)**2 + (l0*l3*m4*(sin(q1(t))*sin(q4(t)) + cos(q1(t))*cos(q4(t))) + l0*l3*m5*(sin(q1(t))*sin(q4(t)) + cos(q1(t))*cos(q4(t))))*Derivative(u4(t), t) + (l0*l2*m3*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))) + l0*l2*m4*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))) + l0*l2*m5*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))))*Derivative(u3(t), t) + (l0*l1*m2*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m3*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m4*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m5*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))))*Derivative(u2(t), t) + (l0**2*m1 + l0**2*m2 + l0**2*m3 + l0**2*m4 + l0**2*m5)*Derivative(u1(t), t) + (-l0*m1*sin(q1(t)) - l0*m2*sin(q1(t)) - l0*m3*sin(q1(t)) - l0*m4*sin(q1(t)) - l0*m5*sin(q1(t)))*Derivative(u0(t), t)]
    [                                  -l0*l1*m2*(sin(q1(t))*cos(q2(t)) - sin(q2(t))*cos(q1(t)))*u1(t)**2 - l0*l1*m3*(sin(q1(t))*cos(q2(t)) - sin(q2(t))*cos(q1(t)))*u1(t)**2 - l0*l1*m4*(sin(q1(t))*cos(q2(t)) - sin(q2(t))*cos(q1(t)))*u1(t)**2 - l0*l1*m5*(sin(q1(t))*cos(q2(t)) - sin(q2(t))*cos(q1(t)))*u1(t)**2 - l1*l2*m3*(-sin(q2(t))*cos(q3(t)) + sin(q3(t))*cos(q2(t)))*u3(t)**2 - l1*l2*m4*(-sin(q2(t))*cos(q3(t)) + sin(q3(t))*cos(q2(t)))*u3(t)**2 - l1*l2*m5*(-sin(q2(t))*cos(q3(t)) + sin(q3(t))*cos(q2(t)))*u3(t)**2 - l1*l3*m4*(-sin(q2(t))*cos(q4(t)) + sin(q4(t))*cos(q2(t)))*u4(t)**2 - l1*l3*m5*(-sin(q2(t))*cos(q4(t)) + sin(q4(t))*cos(q2(t)))*u4(t)**2 + l1*l4*m5*(sin(q2(t))*sin(q5(t)) + cos(q2(t))*cos(q5(t)))*Derivative(u5(t), t) - l1*l4*m5*(-sin(q2(t))*cos(q5(t)) + sin(q5(t))*cos(q2(t)))*u5(t)**2 + (l1*l3*m4*(sin(q2(t))*sin(q4(t)) + cos(q2(t))*cos(q4(t))) + l1*l3*m5*(sin(q2(t))*sin(q4(t)) + cos(q2(t))*cos(q4(t))))*Derivative(u4(t), t) + (l1*l2*m3*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))) + l1*l2*m4*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))) + l1*l2*m5*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))))*Derivative(u3(t), t) + (l1**2*m2 + l1**2*m3 + l1**2*m4 + l1**2*m5)*Derivative(u2(t), t) + (-l1*m2*sin(q2(t)) - l1*m3*sin(q2(t)) - l1*m4*sin(q2(t)) - l1*m5*sin(q2(t)))*Derivative(u0(t), t) + (l0*l1*m2*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m3*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m4*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))) + l0*l1*m5*(sin(q1(t))*sin(q2(t)) + cos(q1(t))*cos(q2(t))))*Derivative(u1(t), t)]
    [                                                                                -l0*l2*m3*(sin(q1(t))*cos(q3(t)) - sin(q3(t))*cos(q1(t)))*u1(t)**2 - l0*l2*m4*(sin(q1(t))*cos(q3(t)) - sin(q3(t))*cos(q1(t)))*u1(t)**2 - l0*l2*m5*(sin(q1(t))*cos(q3(t)) - sin(q3(t))*cos(q1(t)))*u1(t)**2 - l1*l2*m3*(sin(q2(t))*cos(q3(t)) - sin(q3(t))*cos(q2(t)))*u2(t)**2 - l1*l2*m4*(sin(q2(t))*cos(q3(t)) - sin(q3(t))*cos(q2(t)))*u2(t)**2 - l1*l2*m5*(sin(q2(t))*cos(q3(t)) - sin(q3(t))*cos(q2(t)))*u2(t)**2 - l2*l3*m4*(-sin(q3(t))*cos(q4(t)) + sin(q4(t))*cos(q3(t)))*u4(t)**2 - l2*l3*m5*(-sin(q3(t))*cos(q4(t)) + sin(q4(t))*cos(q3(t)))*u4(t)**2 + l2*l4*m5*(sin(q3(t))*sin(q5(t)) + cos(q3(t))*cos(q5(t)))*Derivative(u5(t), t) - l2*l4*m5*(-sin(q3(t))*cos(q5(t)) + sin(q5(t))*cos(q3(t)))*u5(t)**2 + (l2*l3*m4*(sin(q3(t))*sin(q4(t)) + cos(q3(t))*cos(q4(t))) + l2*l3*m5*(sin(q3(t))*sin(q4(t)) + cos(q3(t))*cos(q4(t))))*Derivative(u4(t), t) + (l2**2*m3 + l2**2*m4 + l2**2*m5)*Derivative(u3(t), t) + (-l2*m3*sin(q3(t)) - l2*m4*sin(q3(t)) - l2*m5*sin(q3(t)))*Derivative(u0(t), t) + (l0*l2*m3*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))) + l0*l2*m4*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))) + l0*l2*m5*(sin(q1(t))*sin(q3(t)) + cos(q1(t))*cos(q3(t))))*Derivative(u1(t), t) + (l1*l2*m3*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))) + l1*l2*m4*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))) + l1*l2*m5*(sin(q2(t))*sin(q3(t)) + cos(q2(t))*cos(q3(t))))*Derivative(u2(t), t)]
    [                                                                                -l0*l3*m4*(sin(q1(t))*cos(q4(t)) - sin(q4(t))*cos(q1(t)))*u1(t)**2 - l0*l3*m5*(sin(q1(t))*cos(q4(t)) - sin(q4(t))*cos(q1(t)))*u1(t)**2 - l1*l3*m4*(sin(q2(t))*cos(q4(t)) - sin(q4(t))*cos(q2(t)))*u2(t)**2 - l1*l3*m5*(sin(q2(t))*cos(q4(t)) - sin(q4(t))*cos(q2(t)))*u2(t)**2 - l2*l3*m4*(sin(q3(t))*cos(q4(t)) - sin(q4(t))*cos(q3(t)))*u3(t)**2 - l2*l3*m5*(sin(q3(t))*cos(q4(t)) - sin(q4(t))*cos(q3(t)))*u3(t)**2 + l3*l4*m5*(sin(q4(t))*sin(q5(t)) + cos(q4(t))*cos(q5(t)))*Derivative(u5(t), t) - l3*l4*m5*(-sin(q4(t))*cos(q5(t)) + sin(q5(t))*cos(q4(t)))*u5(t)**2 + (l3**2*m4 + l3**2*m5)*Derivative(u4(t), t) + (-l3*m4*sin(q4(t)) - l3*m5*sin(q4(t)))*Derivative(u0(t), t) + (l0*l3*m4*(sin(q1(t))*sin(q4(t)) + cos(q1(t))*cos(q4(t))) + l0*l3*m5*(sin(q1(t))*sin(q4(t)) + cos(q1(t))*cos(q4(t))))*Derivative(u1(t), t) + (l1*l3*m4*(sin(q2(t))*sin(q4(t)) + cos(q2(t))*cos(q4(t))) + l1*l3*m5*(sin(q2(t))*sin(q4(t)) + cos(q2(t))*cos(q4(t))))*Derivative(u2(t), t) + (l2*l3*m4*(sin(q3(t))*sin(q4(t)) + cos(q3(t))*cos(q4(t))) + l2*l3*m5*(sin(q3(t))*sin(q4(t)) + cos(q3(t))*cos(q4(t))))*Derivative(u3(t), t)]
    [                                                                                l0*l4*m5*(sin(q1(t))*sin(q5(t)) + cos(q1(t))*cos(q5(t)))*Derivative(u1(t), t) - l0*l4*m5*(sin(q1(t))*cos(q5(t)) - sin(q5(t))*cos(q1(t)))*u1(t)**2 + l1*l4*m5*(sin(q2(t))*sin(q5(t)) + cos(q2(t))*cos(q5(t)))*Derivative(u2(t), t) - l1*l4*m5*(sin(q2(t))*cos(q5(t)) - sin(q5(t))*cos(q2(t)))*u2(t)**2 + l2*l4*m5*(sin(q3(t))*sin(q5(t)) + cos(q3(t))*cos(q5(t)))*Derivative(u3(t), t) - l2*l4*m5*(sin(q3(t))*cos(q5(t)) - sin(q5(t))*cos(q3(t)))*u3(t)**2 + l3*l4*m5*(sin(q4(t))*sin(q5(t)) + cos(q4(t))*cos(q5(t)))*Derivative(u4(t), t) - l3*l4*m5*(sin(q4(t))*cos(q5(t)) - sin(q5(t))*cos(q4(t)))*u4(t)**2 + l4**2*m5*Derivative(u5(t), t) - l4*m5*sin(q5(t))*Derivative(u0(t), t)]

Simulation
==========

Now that the symbolic equations of motion are available we can simulate the pendulum's motion. We will need some more SymPy functionality and several NumPy functions, and most importantly the integration function from SciPy, `odeint`.

```
from sympy import Dummy, lambdify
from numpy import array, hstack, zeros, linspace, pi
from numpy.linalg import solve
from scipy.integrate import odeint
```

First, define some numeric values for all of the constant parameters in the problem.

```
arm_length = 1. / n                          # The maximum length of the pendulum is 1 meter
bob_mass = 0.01 / n                          # The maximum mass of the bobs is 10 grams
parameters = [g, m[0]]                       # Parameter definitions starting with gravity and the first bob
parameter_vals = [9.81, 0.01 / n]            # Numerical values for the first two
for i in range(n):                           # Then each mass and length
    parameters += [l[i], m[i + 1]]
    parameter_vals += [arm_length, bob_mass]
```

Mathematica has a really nice `NDSolve` function for quickly integrating their symbolic differential equations. We have plans to develop something similar for SymPy but haven't found the development time yet to do it properly. So the next bit isn't as clean as we'd like but you can make use of SymPy's lambdify function to create functions that will evaluate the mass matrix, $M$, and forcing vector, $\bar{f}$ from $M\dot{u} = \bar{f}(q, \dot{q}, u, t)$ as a NumPy function. We make use of dummy symbols to replace the time varying functions in the SymPy equations a simple dummy symbol.

```
dynamic = q + u                                                # Make a list of the states
dynamic.append(f)                                              # Add the input force
dummy_symbols = [Dummy() for i in dynamic]                     # Create a dummy symbol for each variable
dummy_dict = dict(zip(dynamic, dummy_symbols))
kindiff_dict = kane.kindiffdict()                              # Get the solved kinematical differential equations
M = kane.mass_matrix_full.subs(kindiff_dict).subs(dummy_dict)  # Substitute into the mass matrix
F = kane.forcing_full.subs(kindiff_dict).subs(dummy_dict)      # Substitute into the forcing vector
M_func = lambdify(dummy_symbols + parameters, M)               # Create a callable function to evaluate the mass matrix
F_func = lambdify(dummy_symbols + parameters, F)               # Create a callable function to evaluate the forcing vector
```

To integrate the ODE's we need to define a function that returns the derivatives of the states given the current state and time.

```
def right_hand_side(x, t, args):
    """Returns the derivatives of the states.

    Parameters
    ----------
    x : ndarray, shape(2 * (n + 1))
        The current state vector.
    t : float
        The current time.
    args : ndarray
        The constants.

    Returns
    -------
    dx : ndarray, shape(2 * (n + 1))
        The derivative of the state.

    """
    u = 0.0                              # The input force is always zero
    arguments = hstack((x, u, args))     # States, input, and parameters
    dx = array(solve(M_func(*arguments), # Solving for the derivatives
        F_func(*arguments))).T[0]

    return dx
```

Now that we have the right hand side function, the initial conditions are set such that the pendulum is in the vertical equilibrium and a slight initial rate is set for each speed to ensure the pendulum falls. The equations can then be integrated with SciPy's `odeint` function given a time series.

```
x0 = hstack(( 0, pi / 2 * ones(len(q) - 1), 1e-3 * ones(len(u)) )) # Initial conditions, q and u
t = linspace(0, 10, 1000)                                          # Time vector
y = odeint(right_hand_side, x0, t, args=(parameter_vals,))         # Actual integration
```

Plotting
========

The results of the simulation can be plotted with matplotlib.

```
lines = plot(t, y[:, :y.shape[1] / 2])
lab = xlabel('Time [sec]')
leg = legend(dynamic[:y.shape[1] / 2])
```

```
lines = plot(t, y[:, y.shape[1] / 2:])
lab = xlabel('Time [sec]')
leg = legend(dynamic[y.shape[1] / 2:])
```

Animation
=========

matplotlib now includes very nice animation functions for animating matplotlib plots. First we import the necessary functions for creating the animation.

```
from numpy import zeros, cos, sin, arange, around
from matplotlib import pyplot as plt
from matplotlib import animation
from matplotlib.patches import Rectangle
```

The following function was modeled from Jake Vanderplas's [post on matplotlib animations](http://jakevdp.github.com/blog/2012/08/18/matplotlib-animation-tutorial/).

```
def animate_pendulum(t, states, length, filename=None):
    """Animates the n-pendulum and optionally saves it to file.

    Parameters
    ----------
    t : ndarray, shape(m)
        Time array.
    states: ndarray, shape(m,p)
        State time history.
    length: float
        The length of the pendulum links.
    filename: string or None, optional
        If true a movie file will be saved of the animation. This may take some time.

    Returns
    -------
    fig : matplotlib.Figure
        The figure.
    anim : matplotlib.FuncAnimation
        The animation.

    """
    # the number of pendulum bobs
    numpoints = states.shape[1] / 2

    # first set up the figure, the axis, and the plot elements we want to animate
    fig = plt.figure()

    # some dimesions
    cart_width = 0.4
    cart_height = 0.2

    # set the limits based on the motion
    xmin = around(states[:, 0].min() - cart_width / 2.0, 1)
    xmax = around(states[:, 0].max() + cart_width / 2.0, 1)

    # create the axes
    ax = plt.axes(xlim=(xmin, xmax), ylim=(-1.1, 1.1), aspect='equal')

    # display the current time
    time_text = ax.text(0.04, 0.9, '', transform=ax.transAxes)

    # create a rectangular cart
    rect = Rectangle([states[0, 0] - cart_width / 2.0, -cart_height / 2],
        cart_width, cart_height, fill=True, color='red', ec='black')
    ax.add_patch(rect)

    # blank line for the pendulum
    line, = ax.plot([], [], lw=2, marker='o', markersize=6)

    # initialization function: plot the background of each frame
    def init():
        time_text.set_text('')
        rect.set_xy((0.0, 0.0))
        line.set_data([], [])
        return time_text, rect, line,

    # animation function: update the objects
    def animate(i):
        time_text.set_text('time = {:2.2f}'.format(t[i]))
        rect.set_xy((states[i, 0] - cart_width / 2.0, -cart_height / 2))
        x = hstack((states[i, 0], zeros((numpoints - 1))))
        y = zeros((numpoints))
        for j in arange(1, numpoints):
            x[j] = x[j - 1] + length * cos(states[i, j])
            y[j] = y[j - 1] + length * sin(states[i, j])
        line.set_data(x, y)
        return time_text, rect, line,

    # call the animator function
    anim = animation.FuncAnimation(fig, animate, frames=len(t), init_func=init,
            interval=t[-1] / len(t) * 1000, blit=True, repeat=False)

    # save the animation if a filename is given
    if filename is not None:
        anim.save(filename, fps=30, codec='libx264')
```

Now we can create the animation of the pendulum. This animation will show the open loop dynamics.

```
animate_pendulum(t, y, arm_length, filename="open-loop.ogv")
animate_pendulum(t, y, arm_length, filename="open-loop.mp4")
```

```
from IPython.display import HTML
h = \
"""

"""
HTML(h)
```

Controller Design
=================

The n-link pendulum can be balanced such that all of the links are inverted above the cart by applying the correct lateral force to the cart. We can design a full state feedback controller based off of a linear model of the pendulum about its upright equilibrium point. We'll start by specifying the equilibrium point and parameters in dictionaries.

```
equilibrium_point = hstack(( 0, pi / 2 * ones(len(q) - 1), zeros(len(u)) ))
equilibrium_dict = dict(zip(q + u, equilibrium_point))
parameter_dict = dict(zip(parameters, parameter_vals))
```

The `KanesMethod` class has method that linearizes the forcing vector about generic state and input perturbation vectors. The equilibrium point and numerical constants can then be substituted in to give the linear system in this form: $M\dot{x}=F_Ax+F_Bu$. The state and input matrices, $A$ and $B$, can then be computed by left side multiplication by the inverse of the mass matrix: $A=M^{-1}F_A$ and $B=M^{-1}F_B$.

```
# symbolically linearize about arbitrary equilibrium
linear_state_matrix, linear_input_matrix, inputs = kane.linearize()
# sub in the equilibrium point and the parameters
f_A_lin = linear_state_matrix.subs(parameter_dict).subs(equilibrium_dict)
f_B_lin = linear_input_matrix.subs(parameter_dict).subs(equilibrium_dict)
m_mat = kane.mass_matrix_full.subs(parameter_dict).subs(equilibrium_dict)
# compute A and B
from numpy import matrix
A = matrix(m_mat.inv() * f_A_lin)
B = matrix(m_mat.inv() * f_B_lin)
```

Now that we have a linear system, the python-control package can be used to design an optimal controller for the system.

```
import control
from numpy import dot, rank
from numpy.linalg import matrix_rank
```

First we can check to see if the system is, in fact, controllable.

```
assert matrix_rank(control.ctrb(A, B)) == A.shape[0]
```

The control matrix is full rank, so now we can compute the optimal gains with a linear quadratic regulator. I chose identity matrices for the weightings for simplicity.

```
K, X, E = control.lqr(A, B, ones(A.shape), 1);
```

The gains can now be used to define the required input during simulation to stabilize the system. The input $u$ is simply the gain vector multiplied by the error in the state vector from the equilibrium point, $u(t)=K(x_{eq} - x(t))$.

```
def right_hand_side(x, t, args):
    """Returns the derivatives of the states.

    Parameters
    ----------
    x : ndarray, shape(2 * (n + 1))
        The current state vector.
    t : float
        The current time.
    args : ndarray
        The constants.

    Returns
    -------
    dx : ndarray, shape(2 * (n + 1))
        The derivative of the state.

    """
    u = dot(K, equilibrium_point - x)    # The controller
    arguments = hstack((x, u, args))     # States, input, and parameters
    dx = array(solve(M_func(*arguments), # Solving for the derivatives
        F_func(*arguments))).T[0]

    return dx
```

Now we can simulate and animate the system to see if the controller works.

```
x0 = hstack(( 0, pi / 2 * ones(len(q) - 1), 1 * ones(len(u)) )) # Initial conditions, q and u
t = linspace(0, 10, 1000)                                       # Time vector
y = odeint(right_hand_side, x0, t, args=(parameter_vals,))      # Actual integration
```

The plots show that we seem to have a stable system.

```
lines = plot(t, y[:, :y.shape[1] / 2])
lab = xlabel('Time [sec]')
leg = legend(dynamic[:y.shape[1] / 2])
```

```
lines = plot(t, y[:, y.shape[1] / 2:])
lab = xlabel('Time [sec]')
leg = legend(dynamic[y.shape[1] / 2:])
```

```
animate_pendulum(t, y, arm_length, filename="closed-loop.ogv")
animate_pendulum(t, y, arm_length, filename="closed-loop.mp4")
```

```
from IPython.display import HTML
h = \
"""

"""
HTML(h)
```

The video clearly shows that our controller can balance all $n$ of the pendulum links. The weightings in the lqr design can be tweaked to give different performance if needed.

This example shows that the free and open source scientific Python tools for dynamics are easily comparable in ability and quality a commercial package such as Mathematica. Besides the current installation hurdles for Python, I'd like to claim that it may better than commercial packages, due to our much more robust SymPy **mechanics** package and the fact that all of the code is liberally licensed for reuse and hacking.

The IPython notebook for this example can be downloaded from https://github.com/gilbertgede/idetc-2013-paper/blob/master/n-pendulum-control.ipynb. Yo ucan try out different $n$ values. I've gotten the equations of motion to compute and an open loop simulation of 10 links. My computer ran out of memory when I tried to compute for $n=50$. The controller weightings and initial conditions will probably have to be adjusted for better performance for $n>5$, but it should work. Let me know the resuls if you play with it.


# Composition mathématique avec LaTeX

Cela ne servira que de brève introduction à la composition mathématique avec LaTeX et ne couvrira pas beaucoup d'autres aspects du langage de composition. Ce tutoriel couvrira les fondements de quelques erreurs courantes et comment les résoudre, et vous fournira un lien vers un aide-mémoire.

1. Pour mettre une variable en ligne, entourez-la de signes dollar comme: `$x$`, qui rend et $x$.
1. Pour composer une équation, entourez-la (sur une nouvelle ligne) en double dollar: `$$equation$$`
1. vous pouvez également utiliser cette syntaxe
```latex
\begin{equation}
       votre_équation_ici
\end{equation}
```
1. Les symboles spéciaux commencent par une barre oblique inverse (`\`). Par exemple, la lettre grecque $\alpha$ est tapée comme `$\alpha$`.

1. Les fractions peuvent être composées avec `\frac{numerator}{denominator}`
1. Les racines carrées sont `\sqrt{stuff inside square root}`

Si vous voulez taper des variables en ligne, c'est aussi simple que $x$. Si vous voulez des symboles spéciaux en ligne, appelez-les simplement par leur nom, par exemple $\alpha$, $\gamma$, $\epsilon$, $\Delta$, $\nabla$ et ainsi de suite sont appelés en tapant «$\alpha$», `$\nabla$` etc. [Ce lien mène](http://tug.ctan.org/info/symbols/comprehensive/symbols-a4.pdf) vers une liste complète de symboles et leurs commandes LaTeX. Bien sûr, une recherche Google est parfois plus rapide.

# Bases du LaTeX

#### Double-clickez sur moi
Vous pouvez facilement commencer des équations
$$ c  = \sqrt{a^2 + b^2}$$
remarquez comment dans la liste ci-dessus, les variables apparaissant sous la racine carrée sont entourées par des accolades (`{ }`), et les puissances sont faites avec des carottes (`^`).

#### Double-clickez sur moi

Vous pouvez également taper des équations en utilisant cette syntaxe
\begin{equation}
a = \frac{x^3 +\Gamma}{\int e^{-x^2} dx}
\end{equation}

Remarquez comment le numérateur de la fraction apparaît entre accolades et le dénominateur apparaît entre accolades séparées. Notez que les lettres majuscules grecques sont trouvées en utilisant une majuscule comme première lettre de votre commande LaTeX.

#### Double-clickez sur moi

Notez également l'exposant, cette fois nous avons inclus des accolades. C'est parce que sans eux, seulement le premier caractère deviendra un exposant, comme démontré ci-dessous.

\begin{equation}
a = x^-10
\end{equation}

#### Double-clickez sur moi

Ce qui précède ne rend pas comme prévu. Cependant, nous pouvons résoudre ce problème en ajoutant des accolades autour de l'exposant comme suit:
\begin{equation}
a = x^{-10}
\end{equation}

#### Double-clickez sur moi

Vous pouvez également inclure des vecteurs sur des variables
\begin{equation}
\vec{F} = m \vec{a}
\end{equation}

#### Double-clickez sur moi

Le symbole astérisque peut ne pas rendre comment vous l'espérez pour la multiplication en LaTeX, et il est souvent préférable d'utiliser `\times`. Par exemple, comparez

\begin{equation}
a * b = ab
\end{equation}

contre

\begin{equation}
a \times b = ab
\end{equation}

#### Double Click Me

Si vous avez besoin de mettre des parenthèses autour d'une fraction, votre équation peut parfois ne pas être bien rendue, par exemple

\begin{equation}
f(x) = a \times ( \frac{ x^2 }{ \sqrt{ x^2 + \tan(x) } } )
\end{equation}

Pour augmenter la taille de vos parenthèses, vous devez inclure les commandes `\left` et` \right` comme suit:

\begin{equation}
f(x) = a \times \left( \frac{ x^2 }{ \sqrt{ x^2 + \tan(x) } } \right)
\end{equation}

# Erreurs communes au LaTeX

### Les indices longs doivent être entourés d'accolades pour être affichés correctement

##### Double-clickez sur moi

Notez comment l'équation
\begin{equation}
a = x_10
\end{equation}

Ne rend pas ce que vous voulez, mais
\begin{equation}
a = x_{10}
\end{equation}
le fait une fois que vous incluez les accolades.

### Les espaces dans les équations sont ignorés

##### Double-clickez sur moi
Notez ce qui arrive aux mots inclus dans l'environnement mathématique
$$
Si vous utilisez des mots comme variable, ils deviennent difficiles à lire = x_a^2
$$

Si vous devez inclure des mots dans vos équations, vous pouvez le faire avec `\text{}`
$$
\text{Maintenant, nous pouvons lire ceci facilement une fois que nous avons rendu le calcul} = x_a^2
$$

### Vous ne pouvez pas inclure de signes de pourcentage sans un peu de travail supplémentaire

##### Double-clickez sur moi

Si vous voulez inclure un signe de pourcentage dans une équation mathématique, vous devez y échapper avec un `\` parce qu'un signe de pourcentage indique un commentaire dans LaTeX

$$
100 % C'est parce qu'un signe de pourcentage est un commentaire en Latex!
$$
Pour échapper le commentaire, ajoutez simplement une barre oblique inverse
$$ 100 \% $$

### Manquez une accolade

Si vous manquez une accolade lorsque vous utilisez une commande LaTeX, par exemple dans une fraction, votre équation ne sera pas rendue, comme dans un exemple simple ci-dessous.

\begin{equation}
x = \frac{ a } { y
\end{equation}

Pour résoudre ce problème, ajoutez simplement l'accolade manquante

\begin{equation}
x = \frac{ a } { y }
\end{equation}

Notez cependant que chaque fois que vous rencontrez une erreur dans votre LaTeX, il sera tout simplement pas rendu comme indiqué ci-dessus, et parfois c'est un peu difficile de trouver l'accolade manquante.

Le LaTeX est très étendu pour la composition mathématique, et nous ne pouvons pas tout couvrir ici. Cependant, il est assez intuitif et, avec un peu de pratique, un peu plus rapide que d'utiliser les éditeurs d'équations dans d'autres outils. N'hésitez pas d'essayer de créer vos propres équations maintenant.

---
### Des exercices
1. Corrigez ce qui suit pour représenter correctement la formule pour calculer l'aire de surface d'une sphère

$$
Area = 4 pi r2
$$

2. Corrigez ce qui suit pour représenter correctement la formule pour calculer le volume d'une sphère
\begin{equation}
Volume = \frac{4} / {3} pi r3
\end{equation}
3. Assurez-vous que pi apparaît comme un symbole dans les questions 1 et 2.
4. Dans cette phrase, modifiez les variables x, Y et y pour vous assurer qu'elles apparaissent sous forme de variables LaTeX.

[](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)