## Basics of Calculus

Calculus is the study of how things change. It provides a framework to construct relatively simple quantitative models of change, and to deduce their consequences. If you graph a quadratic you will notice that you do not get a straight line. Think of this like you moving on earth. Though the earth is round, as we walk down the street it looks pretty flat to us.

If you look at a quadratic function $f$ at some particular argument, call it $z$, and very close to $z$, then $f$ will look like a straight line.  This slope is often written as

$f'\left( x \right) = y' = \frac{{df}}{{dx}} = \frac{{dy}}{{dx}} = \frac{d}{{dx}}\left( {f\left( x \right)} \right) = \frac{d}{{dx}}\left( y \right)$

### What is the rate of change?

On if the important applications of derivatives is finding the rate of change. That is the fact that $f'\left( x \right)$ represents the rate of change of $f\left( x \right)$.

We can think of it by using an example. Think of a car, in finding the velocity of the car, we are given the position of the car at time $t$. To compute the velocity of the car we just need find the rate at which the position is changing. This can be cacluated using the function below.

$\begin{align*}Velocity & = \frac{{{\mbox{change in position}}}}{{{\mbox{time traveled}}}}\end{align*}$

$\begin{align*}Rate  of  Change = \frac{{f\left( t \right) - f\left( a \right)}}{{t - a}}\end{align*}$

```python
from IPython.display import YouTubeVideo
YouTubeVideo('K2jQ0AGbYaA', width=860, height=460)
```

```python
import numpy as np
import matplotlib.pyplot as plt
from sympy import *

x = symbols('x')
expr = cos(x)

print("Expression : {}".format(expr))

limit_expr = limit(expr, x, 0)

print("Limit of expression tends to 0 : {}".format(limit_expr))

```

    Expression : cos(x)
    Limit of expression tends to 0 : 1

### Introduction to differentiation

So far we've learned how to evaluate limits for points on a line. Now we're going to build on that knowledge and look at a calculus technique called differentiation. In differentiation, we use our knowledge of limits to calculate the derivative of a function in order to determine the rate of change at an individual point on its line. The word differentiation is derived from the word difference (subtraction). The known difference of function $f(x)$ at a point $x$ given a chnage of $\Delta x$ is simply the difference between the values of the function at those points:

The derivative of $f\left( x \right)$ with respect to x is the function $f'\left( x \right)$  and is defined as, $\begin{equation}f'\left( x \right) = \mathop {\lim }\limits_{h \to 0} \frac{{f\left( {x + h} \right) - f\left( x \right)}}{h} \label{eq:eq2}\end{equation}$

```python
from IPython.display import YouTubeVideo
YouTubeVideo('a5WVw9vmGHU', width=860, height=460)
```

```python
import numpy as np
import matplotlib.pyplot as plt
from sympy import *

x = symbols('x')
expr = diff(x**2 +x , x)

print("Expression : {}".format(expr))
print(expr.subs(x,3))

expr = diff(cos(x) +x , x)
print("Expression : {}".format(expr))
print(expr.subs(x,0))
```

    Expression : 2*x + 1
    7
    Expression : 1 - sin(x)
    1

### Introduction to Integration

Lets say we want to find the area under a function between the function line and the X axis. This is called the integral of the function and it's expressed like this. $\int{{f\left( x \right)\,dx}}$ . To calculate the integral of $f(x)$ with respect to $x$, we need to find the anti-derivative of $f(x)$ , which means we needs a function whose derivative is $f(x)$ . By calculating the parallel in reverse, we can figure out that this is one half X squared.

To do so there are many techniques and methods. Lets discuss them in the video in detail.

```python
from IPython.display import YouTubeVideo
YouTubeVideo('5pwZchmmgF0', width=860, height=460)
```

```python
x_range = np.linspace(0,2,1000)
f = lambda x:np.exp(x)

plt.plot(x_range, f(x_range))
plt.fill_between(x_range, 0, f(x_range), alpha = 0.25)
plt.title("Area below the curve")
```

```python
import numpy as np
import matplotlib.pyplot as plt
from sympy import *

x = symbols('x')
expr = integrate(x**2 +x , x)

print("Expression : {}".format(expr))
print(expr.subs(x,3))

expr = integrate(cos(x) +x , x)
print("Expression : {}".format(expr))
print(expr.subs(x,3))
```

    Expression : x**3/3 + x**2/2
    27/2
    Expression : x**2/2 + sin(x)
    sin(3) + 9/2

### Exercises

1. Determine where the following function is increasing and decreasing : $ A\left( t \right) = 17{t^3} - 45{t^2} - 130{t} + 150$. Integration can be used to find areas, volumes, central points and many useful things.

2. Sketch the graph of $f\left( x \right) = 3 - {\frac{1}{2}}x $ and identify all the minima and maxima of the function on each of the following intervals.

a). $\left( { - \infty ,\infty } \right)$

b). $\left[ { - 1,1} \right]$

c).  $\left[ {1,4} \right]$

3. Evaluate the following indefinite integrals:  $\displaystyle \int{{7{x^6} - 12{x^3} - 5\,dx}}$

**Note** : The following tutorial is a brief introduction to Calculus. You Can dive more deeper using the various resources and books availabe [online](https://www.sydney.edu.au/stuserv/documents/maths_learning_centre/)


# Symbolic Calculation of Optimal Ribosomal Allocation
(c) This work is licensed under a Creative Commons Attribution License CC-BY 4.0. All code contained herein is licensed under an MIT license

```python
import sympy as sp
```

In the main text of this work, we present analytical solutions for the steady-state growth rate and potential ribosomal allocations as functions of the simple model parameters. While we drive almost all of these explicitly in the Supplementary Text, we state that the optimal allocation towards ribosomes was computed through differentiation of a complex function. Here, we perform this calculation symbolically to yield an identical result as Eq. S30 of the supplemental text.

We begin by definiting the necessary symbols we will use in [SymPy](https://www.sympy.org/en/index.html), the symbolic computing library for Python.

```python
# Define symbols
gamma_max = sp.Symbol(r'{{\gamma_{max}}}') # Translation rate
nu_max = sp.Symbol(r'{{\nu_{max}}}') # Maximal metabolic rate
phi_Rb = sp.Symbol(r'{{\phi_R}}') # Allocation towards ribosomes
phi_O = sp.Symbol(r'{{\phi_O}}') # Allocation towards other proteins
K_M = sp.Symbol(r'{{K_M^{c_{pc}}}}')  # Michaelis-Menten constant for translatio
cpc = sp.Symbol(r'{{c_{pc}}}') # Concentration of precursors
```

$\displaystyle \frac{\lambda \left(\lambda {{K_M^{c_{pc}}}} - \lambda + {{\gamma_{max}}} {{\phi_R}}\right)}{\lambda {{\phi_R}} - \lambda - {{\gamma_{max}}} {{\phi_R}}^{2} + {{\gamma_{max}}} {{\phi_R}}}$

We can now define the necessary equations. Specifically, we define here the equation defined in Eq. S24,
including the short-hand notation for the maximal metabolic output $\mathsf{N}$ and maximal translational outpu $\Gamma$.

```python
# Define the metbolic outputs
Nu = nu_max * (1 - phi_O - phi_Rb);
Gamma = gamma_max * phi_Rb;

# Define the equation for the steady-state growth rate piecewise using the
# quadratic form, Eq. S22.
a =  (1 - K_M)
b = Nu + Gamma
c = Nu * Gamma
lam = (-b + sp.sqrt(b**2 - 4 * a * c)) / (2 * a)
lam
```

$\displaystyle \frac{- {{\gamma_{max}}} {{\phi_R}} - {{\nu_{max}}} \left(- {{\phi_O}} - {{\phi_R}} + 1\right) + \sqrt{- {{\gamma_{max}}} {{\nu_{max}}} {{\phi_R}} \left(4 - 4 {{K_M^{c_{pc}}}}\right) \left(- {{\phi_O}} - {{\phi_R}} + 1\right) + \left({{\gamma_{max}}} {{\phi_R}} + {{\nu_{max}}} \left(- {{\phi_O}} - {{\phi_R}} + 1\right)\right)^{2}}}{2 - 2 {{K_M^{c_{pc}}}}}$

With the steady-state growth rate now defined, we can compute the derivative with respect to $\phi_{Rb}$,

```python
dlam_dphiRb = lam.diff(phi_Rb)
dlam_dphiRb
```

$\displaystyle \frac{- {{\gamma_{max}}} + {{\nu_{max}}} + \frac{\frac{{{\gamma_{max}}} {{\nu_{max}}} {{\phi_R}} \left(4 - 4 {{K_M^{c_{pc}}}}\right)}{2} - \frac{{{\gamma_{max}}} {{\nu_{max}}} \left(4 - 4 {{K_M^{c_{pc}}}}\right) \left(- {{\phi_O}} - {{\phi_R}} + 1\right)}{2} + \frac{\left(2 {{\gamma_{max}}} - 2 {{\nu_{max}}}\right) \left({{\gamma_{max}}} {{\phi_R}} + {{\nu_{max}}} \left(- {{\phi_O}} - {{\phi_R}} + 1\right)\right)}{2}}{\sqrt{- {{\gamma_{max}}} {{\nu_{max}}} {{\phi_R}} \left(4 - 4 {{K_M^{c_{pc}}}}\right) \left(- {{\phi_O}} - {{\phi_R}} + 1\right) + \left({{\gamma_{max}}} {{\phi_R}} + {{\nu_{max}}} \left(- {{\phi_O}} - {{\phi_R}} + 1\right)\right)^{2}}}}{2 - 2 {{K_M^{c_{pc}}}}}$

which is equivalent to Eq. S28 upon some manual rearrangement. We can now solve this derivative for the value of $\phi_{Rb}$ when $\tfrac{\partial \lambda}{\partial \phi_{Rb}} = 0$.

```python
# Solve for the optimal phiRb
opt_phiRb = sp.solve(dlam_dphiRb, phi_Rb)[0]
opt_phiRb
```

$\displaystyle \frac{\left({{\phi_O}} - 1\right) \left(- {{\nu_{max}}} \left(- 2 {{K_M^{c_{pc}}}} {{\gamma_{max}}} + {{\gamma_{max}}} + {{\nu_{max}}}\right) + \sqrt{{{K_M^{c_{pc}}}} {{\gamma_{max}}} {{\nu_{max}}}} \left(- {{\gamma_{max}}} + {{\nu_{max}}}\right)\right)}{- 4 {{K_M^{c_{pc}}}} {{\gamma_{max}}} {{\nu_{max}}} + {{\gamma_{max}}}^{2} + 2 {{\gamma_{max}}} {{\nu_{max}}} + {{\nu_{max}}}^{2}}$

The above equation is idential to Eq. S30 upon some algebraic rearrangement.


```python
from numpy import array
from sympy import symbols, Eq, Matrix, diff, Derivative, simplify, factor, expand, latex, init_printing, collect
init_printing()
from IPython.display import display, Math
```

# General Procedure
The stiffness matrix [K] for an MITC4 element is defined using Equation 5.27 of Reference 1 (all references are listed at the bottom of this notebook):

$[k] = \int_A [B]^T[C][B] dA$

This integral will be extremely complex, so integration over the area (A) of the element will be carried out numerically using a procedure called Gaussian Quadrature as outlined in Reference 3 Section 10.4. The greatest difficulty in deriving this element is in coming up with the [B] matrix.

The equation for [k] above will need to be evaluated for bending, shear, and membrane forces separately. The resulting [k] matrices will then be summed to obtain the overall stiffness:

$[k] = [k_\kappa] + [k_\gamma] + [k_m]$

# Step 1: Define the Interpolation Functions
To obtain the [B] matrix, we'll need to define what are known as interpolation functions. Their purpose will become clear below. The interpolation functions are given in Figure 5.4 of Reference 1:

```python
x1, y1, x2, y2, x3, y3, x4, y4 = symbols('x_1, y_1, x_2, y_2, x_3, y_3, x_4, y_4')
r, s = symbols('r, s')

# Define the interpolation functions
h1 = factor(1/4*(1+r)*(1+s))
h2 = factor(1/4*(1-r)*(1+s))
h3 = factor(1/4*(1-r)*(1-s))
h4 = factor(1/4*(1+r)*(1-s))

display(Math('h_1 = ' + latex(h1)))
display(Math('h_2 = ' + latex(h2)))
display(Math('h_2 = ' + latex(h3)))
display(Math('h_2 = ' + latex(h4)))
```

# Step 2: Relate the (x, y) Local Coordinate System to the (r, s) Natural Coordinate System
MITC4 elements use 2 coordinate systems shown in Figure 5.26 of Reference 1. The natural coordinate system has its origin at the plate's centroid where r = 0 and s = 0, and goes from -1 to 1 along each axis. For example (r, s) = (-1, -1) is the bottom left corner of the plate. (r, s) = (1, 0) is the center of the right edge of the plate. Using the (r, s) system greatly simplifies integration over the volume of the plate. The interpolation functions are used to relate (or map) the real world (x, y) system to the natural (r, s) system using Equation 5.18 of Reference 1:

$x = \sum \limits _{i=1} ^{4} {h_ix_i}$     $y = \sum \limits _{i=1} ^{4} {h_iy_i}$

```python
x = h1*x1 + h2*x2 + h3*x3 + h4*x4
y = h1*y1 + h2*y2 + h3*y3 + h4*y4

display(Math('x = ' + latex(x)))
display(Math('y = ' + latex(y)))
```

# Step 3: Find the Jacobian Matrix
The Jacobian matrix converts derivatives with respect to x and y into derivatives with respect to r and s. It will be used for deriving the $[B]$ matrix. It should also be noted that he term $dA$ in the equation for $[k]$ above can be rewritten as:

$dA = |J|*dr*ds$

$J = \begin{bmatrix} \frac{dx}{dr} & \frac{dy}{dr} \\ \frac{dx}{ds} & \frac{dy}{ds} \end{bmatrix}$

```python
J = Matrix([[diff(x, r), diff(y, r)],
            [diff(x, s), diff(y, s)]])

display(Math('J = (1/4)' + latex(factor(J*4))))
```

# Step 4: Find the $[B]$ Matrix for Bending

The plate curvatures are shown in Reference 1 Figure 5.25.

These curvatures can be obtained from Equation 5.99, which uses the interpolation functions and the nodal rotations to define curvature. We will also need the derivatives of these curvatures:

$\beta_x = -\sum \limits_{i=1}^{4} h_i \theta_y^i$   $\therefore$   $\frac{d\beta_x}{dx} = -\sum \limits_{i=1}^{4}\frac{dh_i}{dx}\theta_y^i$    and   $\frac{d\beta_x}{dy} = -\sum \limits_{i=1}^{4}\frac{dh_i}{dy}\theta_y^i$

$\beta_y = \sum \limits_{i=1}^{4} h_i \theta_x^i$   $\therefore$   $\frac{d\beta_y}{dy} = \sum \limits_{i=1}^{4}\frac{dh_i}{dy}\theta_x^i$    and   $\frac{d\beta_y}{dx} = \sum \limits_{i=1}^{4}\frac{dh_i}{dx}\theta_x^i$

Plugging these equations into Reference 1 Equation 5.95 we can write an expression for the bending strains:

$\kappa = \begin{bmatrix} \frac{d\beta_x}{dx} \\ \frac{d\beta_y}{dy} \\ \frac{d\beta_x}{dy} + \frac{d\beta_y}{dx}\end{bmatrix} = [B_\kappa][u] = \begin{bmatrix} 0 & 0 & -\frac{h_1}{dx} & 0 & 0 & -\frac{h_2}{dx} & ... \\ 0 & \frac{h_1}{dy} & 0 & 0 & \frac{h_2}{dy} & 0 & ... \\ 0 & \frac{h_1}{dx} & -\frac{h_1}{dy} & 0 & \frac{h_2}{dx} & -\frac{h_2}{dy} & ... \end{bmatrix} \begin{bmatrix} w_1 \\ \theta_{x1} \\ \theta_{y1} \\ w_2 \\ \theta_{x2} \\ \theta_{y2} \\ . \\ . \\ . \end{bmatrix}$

where it can be seen that

$[B_\kappa] = \begin{bmatrix} 0 & 0 & -\frac{h_1}{dx} & 0 & 0 & -\frac{h_2}{dx} & ... \\ 0 & \frac{h_1}{dy} & 0 & 0 & \frac{h_2}{dy} & 0 & ... \\ 0 & \frac{h_1}{dx} & -\frac{h_1}{dy} & 0 & \frac{h_2}{dx} & -\frac{h_2}{dy} & ... \end{bmatrix}$

Using the Jacobian matrix as shown in Reference 1, Equation 5.25, we can change these derivatives with respect to x and y into derivatives with respect to r and s.

$\begin{bmatrix} \frac{d}{dx} \\ \frac{d}{dy} \end{bmatrix} = J^{-1} \begin{bmatrix} \frac{d}{dr} \\ \frac{d}{ds} \end{bmatrix}$

Using the Jacobian to evaluate the derivatives of the shape functions ($dH$) with respect to x and y yields the following in terms of r and s:

$\begin{bmatrix} \frac{dH}{dx} \\ \frac{dH}{dy} \end{bmatrix} = \begin{bmatrix} \frac{dh_1}{dx} & \frac{dh_2}{dx} & \frac{dh_3}{dx} & \frac{dh_4}{dx} \\ \frac{dh_1}{dy} & \frac{dh_2}{dy} & \frac{dh_3}{dy} & \frac{dh_4}{dy} \end{bmatrix} = J^{-1} \begin{bmatrix} \frac{dh_1}{dr} & \frac{dh_2}{dr} & \frac{dh_3}{dr} & \frac{dh_4}{dr} \\ \frac{dh_1}{ds} & \frac{dh_2}{ds} & \frac{dh_3}{ds} & \frac{dh_4}{ds} \end{bmatrix}$

Evaluating this equation gives the following:

```python
dH_drs = Matrix([[diff(h1, r), diff(h2, r), diff(h3, r), diff(h4, r)],
                 [diff(h1, s), diff(h2, s), diff(h3, s), diff(h4, s)]])

display(Math(r'\begin{bmatrix} \frac{dH}{dx} \\ \frac{dH}{dy} \end{bmatrix} = J^{-1}(1/4)' + latex(dH_drs*4)))
```

We can now calculate this expression numerically in the program and plug the terms into the expression we derived for $[B_{\kappa}]$. Note that the matrix is a function of r and s. This matrix will be different for any (r, s) point on the quad element. Later on, four Guass points will be selected for numerical integration by Guassian quadrature of the expression for $[k]$.

# Step 5: Find the $[B]$ Matrix for In-Plane Forces

The procedure for the $[B_m]$ matrix is nearly identical to that for the $[B_{\kappa}]$ matrix. Instead of an equation for the bending strains $\kappa$, we have an equation for in-plane strains $\epsilon$:

$\epsilon = \begin{bmatrix} \epsilon_{xx} \\ \epsilon_{yy} \\ \gamma_{xy} \end{bmatrix} = \begin{bmatrix} \frac{du}{dx} \\ \frac{dv}{dy} \\ \frac{du}{dy} + \frac{dv}{dx}\end{bmatrix} = [B_m][u] = \begin{bmatrix} \frac{dh_1}{dx} & 0 & \frac{dh_2}{dx} & 0 & ... \\ 0 & \frac{dh_1}{dy} & 0 & \frac{dh_2}{dy} & ... \\ \frac{dh_1}{dy} & \frac{dh_1}{dx} & \frac{dh_2}{dy} & \frac{dh_2}{dx} & ... \end{bmatrix} \begin{bmatrix} u_1 \\ v_1 \\ u_2 \\ v_2 \\ . \\ . \\ . \end{bmatrix}$

As before, to obtain the $[B_m]$ matrix, we can substitute in the terms from the follwing expression into the expression for $[B_m]$:

```python
display(Math(r'\begin{bmatrix} \frac{dH}{dx} \\ \frac{dH}{dy} \end{bmatrix} = J^{-1}(1/4)' + latex(dH_drs*4)))
```

# Step 6: Find the $[B]$ Matrix for Shear

Theoretically the shear matrix could be obtained the same way, but when we do that we tend to get spurious shear stresses due to a phenomenon known as shear locking. The MITC4 element is formulated differently to eliminate the shear locking problem.

First, the tensor shear strain components can be calculated using Reference 1, equations 5.102:

$\gamma_{xz}=\gamma_{rz}sin{\beta}-\gamma_{sz}sin{\alpha}$

$\gamma_{yz}=-\gamma_{rz}cos{\beta}+\gamma_{sz}cos{\alpha}$

The values for $\gamma_{rz}$ and $\gamma_{sz}$ can be found from equations 5.103:

$\gamma_{rz}=\frac{\sqrt{(C_x+rB_x)^2+(C_y+rB_y)^2}}{8|J|}([1+s][\frac{w_1-w_2}{2}+\frac{x_1-x_2}{4}(\theta_y^1+\theta_y^2)-\frac{y_1-y_2}{4}(\theta_x^1+\theta_x^2)]+[1-s][\frac{w_4-w_3}{2}+\frac{x_4-x_3}{4}(\theta_y^4+\theta_y^3)-\frac{y_4-y_3}{4}(\theta_x^4+\theta_x^3)])$

$\gamma_{sz}=\frac{\sqrt{(A_x+sB_x)^2+(A_y+sB_y)^2}}{8|J|}([1+r][\frac{w_1-w_4}{2}+\frac{x_1-x_4}{4}(\theta_y^1+\theta_y^4)-\frac{y_1-y_4}{4}(\theta_x^1+\theta_x^4)]+[1-r][\frac{w_2-w_3}{2}+\frac{x_2-x_3}{4}(\theta_y^2+\theta_y^3)-\frac{y_2-y_3}{4}(\theta_x^2+\theta_x^3)])$

By rearranging the equation and factoring out the displacement terms, we can rewrite equations 5.103 in matrix form as follows:

$\gamma_{rz} = [B_{\gamma_{rz}}][u] = \frac{\sqrt{(C_x+rB_x)^2+(C_y+rB_y)^2}}{8|J|} \begin{bmatrix} \frac{1+s}{2} & -\frac{y_1-y_2}{4}(1+s) & \frac{x_1-x_2}{4}(1+s) & -\frac{1+s}{2} & -\frac{y_1-y_2}{4}(1+s) & \frac{x_1-x_2}{4}(1+s) & -\frac{1-s}{2} & -\frac{y_4-y_3}{4}(1-s) & \frac{x_4-x_3}{4}(1-s) & \frac{1-s}{2} & -\frac{y_4-y_3}{4}(1-s) & \frac{x_4-x_3}{4}(1-s) \end{bmatrix} [u]$

$\gamma_{sz} = [B_{\gamma_{sz}}][u] = \frac{\sqrt{(A_x+sB_x)^2+(A_y+sB_y)^2}}{8|J|} \begin{bmatrix} \frac{1+r}{2} & -\frac{y_1-y_4}{4}(1 + r) & \frac{x_1-x_4}{4}(1+r) & \frac{1-r}{2} &  -\frac{y_2-y_3}{4}(1-r) & \frac{x_2-x_3}{4}(1-r) & -\frac{1-r}{2} & -\frac{y_2-y_3}{4}(1-r) & \frac{x_2-x_3}{4}(1-r) & -\frac{1+r}{2} & -\frac{y_1-y_4}{4}(1+r) & \frac{x_1-x_4}{4}(1+r) \end{bmatrix} [u]$

where

$u = \begin{bmatrix} w_1 & \theta_x^1 & \theta_y^1 & w_2 & \theta_x^2 & \theta_y^2 & w_3 & \theta_x^3 & \theta_y^3 & w_4 & \theta_x^4 & \theta_y^4 \end{bmatrix}^T$

Equations 5.102 can now be rewritten as:

$\gamma_{xz}=[B_{\gamma_{rz}}][d]sin{\beta}-[B_{\gamma_{sz}}][u]sin{\alpha}=([B_{\gamma_{rz}}]sin{\beta}-[B_{\gamma_{sz}}]sin{\alpha})[u]$

$\gamma_{yz}=-[B_{\gamma_{rz}}][d]cos{\beta}+[B_{\gamma_{sz}}][u]cos{\alpha}=(-[B_{\gamma_{rz}}]cos{\beta}+[B_{\gamma_{sz}}]cos{\alpha})[u]$

Or more concisely:

$\gamma = [B_{\gamma}][u] = \begin{bmatrix} B_{\gamma_{xz}} \\ B_{\gamma_{yz}} \end{bmatrix}[u]$

# References

The following references were used in the formulation of this element. The primary reference was the first one because it had a section directly relating to isoparametric general plate bending elements, but all three were used. The first reference is a free download from MIT's website.

1. "Finite Element Procedures, 2nd Edition", Klaus-Jurgen Bathe
2. "Finite Element Analysis Fundamentals", Richard H. Gallagher
3. "A First Course in the Finite Element Method, 4th Edition", Daryl L. Logan


We are solving 1D Poisson equation:
$$
{d^2\over dx^2}V(x) = -4\pi n(x)
$$
Using Fast Fourier Transform (FFT). And then calculating the Hartree energy using:
$$
E_H = 2\pi \sum_{{\bf G}\ne 0} {|n({\bf G})|^2\over G^2}
$$

```
%pylab inline
from pylab import plot, semilogy
from numpy import loadtxt, linspace
from numpy.fft import fft, fftfreq
from sympy import exp, sin, pi, var, Integral
```

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

Let's setup the right hand side:

```
var("x")
L = 2 # domain [0, L]
rho = exp(sin(2*pi*x/L))
#rho = sin(2*pi*x/L)
integ = Integral(rho, (x, 0, L)).n()
rho -= integ / L
print "rho(x) =", rho
print "Integral of rho =", Integral(rho, (x, 0, L)).n()
```

    rho(x) = exp(sin(pi*x)) - 1.26606587775201
    Integral of rho = -1.41156085310944e-16

```
D = []
for m in range(1, 15):
    #2m+1 terms -Gmax, ..., 0, ..., Gmax
    n = 2*m+1
    xj = linspace(0, L, n+1)[:-1]
    rhoj = array([float(rho.subs(x, val).n(20)) for val in xj])
    nr = fft(rhoj) / n
    # re-order -> -Gmax, ... Gmax
    f = fftfreq(n)
    idx = f.argsort()
    f = f[idx]
    nr = nr[idx]
    nr[m] = 0  # set G=0 term = 0
    G = array([float((2*pi*j/L)) for j in range(-m, m+1)])
    G[m] = 1
    tmp = array([complex(2*pi*abs(nr[j])**2/G[j]**2) for j in range(n)])
    E = sum(tmp).real * L
    print n, E
    D.append((n, E))
D = array(D)
```

    3 0.857623650249
    5 0.825420490259
    7 0.825230159883
    9 0.825229196177
    11 0.825229191862
    13 0.825229191846
    15 0.825229191846
    17 0.825229191846
    19 0.825229191846
    21 0.825229191846
    23 0.825229191846
    25 0.825229191846
    27 0.825229191846
    29 0.825229191846

```
N = D[:, 0]
E = D[:, 1]
E_exact = E[-1]
figure(figsize=(8, 6), dpi=80)
semilogy(N, abs(E-E_exact), "bo-")
grid()
title("Convergence of an FFT Poisson solver (semilogy)")
xlabel("N (number of PW in each direction)")
ylabel("E  -  E_exact  [a.u.]")

figure(figsize=(8, 6), dpi=80)
loglog(N, abs(E-E_exact), "bo-", label="calculated")
grid()
title("Convergence of an FFT Poisson solver (loglog)")
xlabel("N (number of PW in each direction)")
ylabel("E  -  E_exact  [a.u.]")
```

```

```


```python
import sympy
from  sympy import symbols, sqrt
sympy.init_printing()
```

```python
rho, kappa, r =  symbols('rho kappa r')
```

```python
z = rho ** 2/(r*(1 + sqrt(1 - kappa*(rho/r) ** 2)))
z
```

```python
z.diff(rho).expand().simplify().subs(kappa, 1)
```

```python
zs = z.radsimp()
zs
```

```python
zs.diff(rho)
```


```python
from decodes.core import *
from decodes.io.jupyter_out import JupyterOut
out = JupyterOut.unit_square( )
```

# Alternate Coordinate Geometry
todo

\begin{align}
x = r \ cos\theta \\
y = r \ sin\theta
\end{align}

### Cylindrical Coordinates

\begin{eqnarray}
x &=& r \ cos\theta \\
y &=& r \ sin\theta \\
z &=& z
\end{eqnarray}

```python
"""
Cylindrical Evaluation of an Orthonormal CS
Returns a Point relative to this CS given three cylindrical coordinates.
"""
def eval_cyl(self, radius, radians, z):
    pt = Point( radius * cos(radians), radius * sin(radians), z)
    return self.eval(pt)
```

### Spherical Coordinates

\begin{eqnarray}
x &=& \varrho \ sin\varphi \ cos\theta \\
y &=& \varrho \ sin\varphi \ sin\theta \\
z &=& \varrho \ cos\varphi
\end{eqnarray}

```python
"""
Spherical Evaluation of an Orthonormal CS
Returns a Point relative to this CS given three spherical coordinates.
"""
def eval_sph(self, rho, phi, theta):
    x = rho * sin(phi) * cos(theta)
    y = rho * sin(phi) * sin(theta)
    z = rho * cos(phi)
    return self.eval(Point(x,y,z))

```

```python

```

103 - Points, Vectors, and Coordinate Systems/109 - Alternate Coordinate Geometry.ipynb


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

# Exercises

## Q1: integrating a sampled vs. analytic function

Numerical integration methods work differently depending on whether you have the analytic function available (in which case you can evaluate it freely at any point you please) or if it is sampled for you.

Create a function to integrate, and use NumPy to sample it at $N$ points.  Compare the answer you get from integrating the function directly (using `integrate.quad` to the integral of the sampled function (using `integrate.simps`).

To get a better sense of the accuracy, vary $N$, and look at how the error changes (if you plot the error vs. $N$, you can measure the _convergence_).

```python

```

## Q2: Condition number

For a linear system, ${\bf A x} = {\bf b}$, we can only solve for $x$ if the determinant of the matrix ${\bf A}$ is non-zero.  If the determinant is zero, then we call the matrix _singular_.  The _condition number_ of a matrix is a measure of how close we are to being singular.  The formal definition is:
\begin{equation}
\mathrm{cond}({\bf A}) = \| {\bf A}\| \| {\bf A}^{-1} \|
\end{equation}
But we can think of it as a measure of how much ${\bf x}$ would change due to a small change in ${\bf b}$.  A large condition number means that our solution for ${\bf x}$ could be inaccurate.

A _Hilbert matrix_ has $H_{ij} = (i + j + 1)^{-1}$, and is known to have a large condition number.  Here's a routine to generate a Hilbert matrix

```python
def hilbert(n):
    """ return a Hilbert matrix, H_ij = (i + j - 1)^{-1} """

    H = np.zeros((n,n), dtype=np.float64)

    for i in range(1, n+1):
        for j in range(1, n+1):
            H[i-1,j-1] = 1.0/(i + j - 1.0)
    return H
```

Let's solve ${\bf Hx} ={\bf b}$.  Create a linear system by picking an ${\bf x}$ and generating a ${\bf b}$ by multiplying by the matrix ${\bf H}$.  Then use the `scipy.linalg.solve()` function to recover ${\bf x}$.  Compute the error in ${\bf x}$ as a function of the size of the matrix.

You won't need a large matrix, $n \sim 13$ or so, will start showing big errors.

You can compute the condition number with `numpy.linalg.cond()`

There are methods that can do a better job with nearly-singular matricies.  Take a look at `scipy.linalg.lstsq()` for example.

```python

```

## Q3: damped driven pendulum and chaos

There are a large class of ODE integration methods available through the `scipy.integrate.ode()` function.  Not all of them provide _dense output_ -- most will just give you the value at the end of the integration.

The explicit `dopri5` integrator will store the solution at intermediate points and allow you to access them.  We'll use that here.  You'll need to use the `set_solout()` method to define a function that takes the current integration solution and store it).

The damped driven pendulum obeys the following equations:
$$\dot{\theta} = \omega$$
$$\dot{\omega} = -q \omega - \sin \theta + b \cos \omega_d t$$
here, $\theta$ is the angle of the pendulum from vertical and $\omega$ is the angular velocity.  $q$ is a damping coefficient, $b$ is a forcing amplitude, and $\omega_d$ is a driving frequency.

Choose $q = 0.5$ and $\omega_d = 2/3$.

Integrate the system for different values of $b$ (start with $b = 0.9$ and increase by $0.05$, and plot the results ($\theta$ vs. $t$).  Here's a RHS function to get you started:

```python
def rhs(t, Y, q, omega_d, b):
        """ damped driven pendulum system derivatives.  Here, Y = (theta, omega) are
        the solution variables. """
        f = np.zeros_like(Y)

        f[0] = Y[1]
        f[1] = -q*Y[1] - np.sin(Y[0]) + b*np.cos(omega_d*t)

        return f
```

Note that the pendulum can flip over, giving values of $\theta$ outside of $[-\pi, \pi]$.  The following function can be used to restrict it back to $[-\pi, \pi]$ for plotting.

```python
def restrict_theta(theta):
    """ convert theta to be restricted to lie between -pi and pi"""
    tnew = theta + np.pi
    tnew += -2.0*np.pi*np.floor(tnew/(2.0*np.pi))
    tnew -= np.pi
    return tnew
```

Write a function that takes an initial angle, $\theta_0$, and integrates the system and returns the solution.  For the parameters that are part of the `rhs()` function, you'll need to use the `set_f_params()` method.

Some values of $b$ will show very non-periodic behavior.  To see chaos, integrate two different pendula that are the same except for $\theta_0$, with only a small difference between then (like 60 degrees and 60.0001 degrees.  You'll see the solutions track for a while, but then diverge.

```python

```

## Q4: Let's find the errors on our fit

We looked at fits, but not what the errors are on the fit.  Look at `scipy.optimize.curve_fit()`.  This is a simplified wrapper on the least squares fitting.  It can return the convariance matrix, the diagonals of which can give the error of the fit for the parameters.

Make up some data that models a non-linear function (by introducing some random noise) and perform a fit and find the errors on the parameters.

```python

```


# Using Python for maths

## Introduction

This notebook lists examples of simple Python commands and programs that can be used to help with the understanding of maths, essentially at A level.

I will introduce simpler GCSE level materials later. the new A-Level syllabus.

This is a Jupyter notebook created from within the Anaconda data science platform. Anaconda can be downloaded here:
https://www.anaconda.com

Python is an open-source programming language used extensively around the world. You can use almost for anything: game, scripts to automate repetititve task, mapping, statistical analysis, machine learning, scientific computation, ...
https://www.python.org/about/gettingstarted/

To execute commands in Jupyter, either Run the cell or hit SHIFT + ENTER.

The notation should self-explanatory: x**2 is $x^2$.
To write nice mathematical equation, I use LaTex and the text is written in a Markdown language similar to HTML, the language use to write web pages, but you do not have to worry about that for now.

This idea is that if you want to solve a problem you

> 1) Insert a new cell

2) Copy the cell that solve a problem similar to yours

3) Hit SHIFT + ENTER

If you spot any mistakes, please feel free to correct.

## 1. Surds and Indices

1) Simplify $(2 + 2 \sqrt{2})(-\sqrt{2} + 4)$

2) Simplify $\frac{6}{5 \sqrt{2}}$

3) Simplify $(2-3 \sqrt{5})^{10}$

```python
# Load functions from the SYMbolic PYthon module.
from sympy import sqrt, simplify
simplify((2 + 2 * sqrt(2))*(-sqrt(2) + 4))
```

```python
simplify(6 / (5 * sqrt(2)))
```

```python
simplify((2 - 3 * sqrt(5))**10)
```

Find, to 3 significant figures, the value of  $x$ for which $8^x=0.8$

```python
from sympy import Symbol, solve
x = Symbol('x')
sol = solve(8**x - 0.8, x)
# Zero-based indexing in Python: sol[0] is the first solution.
# The other two roots are complex.
# Format the solution.
print('The real solution is {:0.3g},'.format(sol[0]), 'to 3 sig. figs.')
```

## 2. Quadratic Functions

(i) Determine the roots of the equation: $x^2+3x-5=0$

(ii) Plot the quadratic function: $y=x^2+3x-54$

```python
from sympy import solve, Symbol
x = Symbol('x')
solve(x**2 + 3 * x - 5, x)
```

```python
# Use the alias np instead of numpy (NUMerical PYthon).
# Use the alias plt for matplotlib.pyplot.
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# x ranges from -8 to 5 in 100 intervals.
x = np.linspace(-8, 5, 100)
plt.plot(x, x**2 + 3 * x - 5)
plt.xlabel('x')
plt.ylabel('y')
# Show the plot.
plt.show()
```

## Print dependences
This is required to record the computational environment in order to share the results online and for reproducibility.

```python
%load_ext watermark

# python, ipython, packages, and machine characteristics
%watermark -v -m -p simpy,matplotlib,pandas,numpy,watermark

# date
print (" ")
%watermark -u -n -t -z
```

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

# evenly sampled time at 200ms intervals
t = np.arange(0., 5., 0.2)

# red dashes, blue squares and green triangles
plt.plot(t, t, 'r--', t, t**2, 'bs', t, t**3, 'g^')
plt.show()
```

```python
import matplotlib.pyplot as plt
plt.plot([1,2,3,4], [1,4,9,16], 'ro')
plt.axis([0, 6, 0, 20])
plt.show()
```

```python

```


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

from sympy import *
init_printing()
```

```python
x,y =var("x y")
x,y
```

```python
f=x**3-3*x**2-24*x+32
plot(f)
```

    <IPython.core.display.Javascript object>

    <sympy.plotting.plot.Plot at 0x244510897b8>

```python
df=f.diff()
df
```

```python
intf=f.integrate()
intf
```

```python
df2=f.diff(x,2)
df2
```

```python
pc=solve(df)
pc
```

```python
ddf=df.diff()
ddf.subs(x,pc[0]),ddf.subs(x,pc[1])
```

ejercicio:
se desea que combinaciones de medidas da el area maxima

```python
P=Eq(x+2*y,2400)
A=x*y
(P,A)
```

```python
y2=-x*(0.5)+1200
y2
```

```python
f_=A.subs(y,y2[0])
f_
```

```python
df_=f_.diff()
df_
```

```python
pc=solve(df_)
pc
```

```python
Sol=y2.subs(x,1200)
Sol
```

```python
x
```

```python
type(y2)
```

    sympy.core.add.Add

$Optimizacion-Con-Dos-Variables$

```python
f= x**2+y**2
f
```

```python
dfx=f.diff(x)
dfy=f.diff(y)

pc=solve([dfx,dfy],[x,y])
pc
```

```python
dfxx=dfx.diff(x)
dfxy=dfx.diff(y)
dfyx=dfy.diff(x)
dfyy=dfy.diff(y)

D=dfxx*dfyy-dfxy
D.subs(pc),dfxx.subs(pc)
```

```python

```


# 变量和数据类型

## 数字及其运算

- 计算身高体重指数
- BMI(Body Mass Index) 公式
  \begin{equation}BMI = \frac{weight}{height^{2}}\end{equation}

```python
height = 1.79 # 身高1米79
```

```python
weight = 68.7 # 体重68.7公斤
```

```python
bmi = weight / height ** 2
```

```python
bmi
```

    21.44127836209856

- 数据类型
- float/int
- str
- bool

```python
type(bmi)
```

    float

```python
type(2)
```

    int

```python
type("hello world")
```

    str

```python
type(True)
```

    bool

```python
2 + True # 数据类型转换
```

    3

```python
"hello" + 2 # 运算是有类型的
```

```python
"hello" + "world" # + : 字符串连接
```

```python
2 + 22.4 # + ： 加法; 数据类型转换 int → float
```

    24.4

```python
77 % 17 # 取模
```

    9

```python
77 // 17 # 求商
```

    4

## 字符串及其运算
>字符串是字符的集合

```python
he = "他"
didnt = "没"
verb = "上"
obj = "上海的车"
# 用字符串组成另一个字符串
he + didnt + verb * 3 + obj
```

```python
he = "他"
didnt = "没"
verb = "上"
obj = "上海的车"
# 用字符串组成另一个字符串
he + didnt + verb * 3 + obj
```

    '他没上上上上海的车'

- 如果 + * 代表往字符串集合中增加元素
- 那么 - / 运算是取字符串集合的子集？

```python
obj - verb
```

```python
obj / verb
```

```python
obj[1]
```

    '海'

```python
obj[0]
```

    '上'

```python
(he + didnt + verb * 3 + obj)[2:6] # 包括第2，但不包括第6，从第0开始
```

## 布尔型及其运算

```python
bmi > 25
```

    False

```python
type(bmi > 25)
```

    bool

```python
bmi > 20
```

    True

```python
bmi > 20 and not bmi > 25 # and, or, not
```

    True

```python
bmi > 20 and bmi < 25  # 更符合阅读习惯的写法
```

    True

```python
bmi == bmi or bmi != bmi # 完全正确
```

    True

```python
bmi == he or bmi != he
```

    True

## 分支

```python
bmi = 40
if bmi > 40:                 # Python用缩进区分block
    print("hyper obese")     # 注意这行的开头有TAB
```

```python
bmi = 45                       # 改变bmi，观察分支选择
if bmi > 40:
    print("hyper obese")
else:
    print("not hyper obese")
```

    hyper obese

```python
# 对bmi值进行分类
if bmi > 40:
    print("hyper obese")
elif bmi > 35 and bmi <= 40:
    print("super obese")
elif bmi > 30 and bmi <= 35:
    print("obese")
elif bmi > 25 and bmi <= 30:
    print("over weight")
elif bmi > 18.5 and bmi <= 25:
    print("normal")
elif bmi > 16 and bmi <= 18.5:
    print("under weight")
else :
    print("slim") # https://en.wikipedia.org/wiki/Body_mass_index
```

    hyper obese

#### 根据[肥胖分类定义](https://en.wikipedia.org/wiki/Body_mass_index)补全上面的分类代码

| type                      | from |   to |
|---------------------------|------|------|
| very severely underweight |      |   15 |
| severely underweight      |   15 |   16 |
| underweight               |   16 | 18.5 |
| normal                    | 18.5 |   25 |
| overweight                |   25 |   30 |
| moderately obese          |   30 |   35 |
| severely obese            |   35 |   40 |
| very severely obese       |   40 |   45 |
| morbidly obese            |   45 |   50 |
| super obese               |   50 |   60 |
| hyper obese               |   60 |      |

>可见，各个种族，地区，年龄段对于肥胖的定义是有很大差别的

# 列表和循环

## 列表是数据的容器
>已经学过一种容器：str

## 列表及其运算
- 从通用List开始
- 以后基本只关注数值类型容器
- 注意程序语言和数学语言的映射

```python
ret = he + didnt + verb * 3 + obj # 用一个变量保存计算结果
l = list(ret)                     # 转换为列表
l                                 # 可以把一个字符串转换为列表，反之？
```

    ['他', '没', '上', '上', '上', '上', '海', '的', '车']

```python
type(l)
```

    list

```python
l[2:6]  # 重复之前的操作，取出'上'
```

    ['上', '上', '上', '上']

```python
l[:2]   # 到第2个为止，不包括第2个，从第0个开始
```

    ['他', '没']

```python
l[2:]   # 从第2个开始
```

    ['上', '上', '上', '上', '海', '的', '车']

```python
l[-1]   # 最后一个，或者，从右边开始第1个，注意不是从第0个开始
```

    '车'

##### 负数索引

|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|他|没|上|上|上|上|海|的|车|他|没|上|上|上|上|海|的|车|

```python
l[-2:-1] # 虽然索引是从右往左计数的，但范围和这些数字的排列顺序是一致的
```

    ['的']

|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|他|没|上|上|上|上|海|的|车|他|没|上|上|上|上|海|的|车|

```python
l[-2:]
```

    ['的', '车']

|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|他|没|上|上|上|上|海|的|车|他|没|上|上|上|上|海|的|车|

```python
l[-7:-3]
```

    ['上', '上', '上', '上']

|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|他|没|上|上|上|上|海|的|车|他|没|上|上|上|上|海|的|车|

```python
l[:]
```

    ['他', '没', '上', '上', '上', '上', '海', '的', '车']

|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|
|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|-|
|他|没|上|上|上|上|海|的|车|他|没|上|上|上|上|海|的|车|

```python
len(l)
```

    9

```python
c = [6, 7, 3, 3, 3, 3, 10, 8, 4] # 特征：笔画数
f = l + c
len(f)
```

    18

```python
f
```

#### 列表里面的数据可以是不同类型的

```python
g = f      # g只是f的另一个名字
g[9] = 0   # 改变列表
f          # f也会跟着变化
```

    ['他', '没', '上', '上', '上', '上', '海', '的', '车', 0, 7, 3, 3, 3, 3, 10, 8, 4]

```python
g = list(f) # 新创建一个列表
f[9] = 6    # 改回去
g           # g[9]仍然是0
```

    ['他', '没', '上', '上', '上', '上', '海', '的', '车', 0, 7, 3, 3, 3, 3, 10, 8, 4]

```python
del g[9]   # 删掉第9个元素
g
```

    ['他', '没', '上', '上', '上', '上', '海', '的', '车', 7, 3, 3, 3, 3, 10, 8, 4]

```python
len(g)
```

    17

```python
len(f)
```

    18

## 循环

```python
for v in f:
    print(type(v)) # 注意这行的开头有TAB
```

    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'str'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>
    <class 'int'>

```python
len(range(1)) # 只生成一个数
```

    1

```python
range(1)[0]   # 这个数是0
```

    0

```python
range(len(f)) # f → f的索引
```

    range(0, 18)

```python
for i in range(len(f)): # 以索引做循环变量
    print(i, type(f[i]))
```

    0 <class 'str'>
    1 <class 'str'>
    2 <class 'str'>
    3 <class 'str'>
    4 <class 'str'>
    5 <class 'str'>
    6 <class 'str'>
    7 <class 'str'>
    8 <class 'str'>
    9 <class 'int'>
    10 <class 'int'>
    11 <class 'int'>
    12 <class 'int'>
    13 <class 'int'>
    14 <class 'int'>
    15 <class 'int'>
    16 <class 'int'>
    17 <class 'int'>

```python
for i, v in enumerate(f): # 上例的简化版本
    print(i, type(v))
```

    0 <class 'str'>
    1 <class 'str'>
    2 <class 'str'>
    3 <class 'str'>
    4 <class 'str'>
    5 <class 'str'>
    6 <class 'str'>
    7 <class 'str'>
    8 <class 'str'>
    9 <class 'int'>
    10 <class 'int'>
    11 <class 'int'>
    12 <class 'int'>
    13 <class 'int'>
    14 <class 'int'>
    15 <class 'int'>
    16 <class 'int'>
    17 <class 'int'>

基础教程/A1-Python与基础知识/Python基础/02_变量和数据类型.ipynb


# Point source on top  of a halfspace

To illustrate the use of the package we are going to play with the solutions
for a concentrated force located on top of a halfspace. The origin,
$\mathbf{x} = (0,0,0)$, is placed on the free surface
and positive $z$ is inside the medium. This problem is of interest when
modeling the deformation/stress around a localized load, e.g., the
load caused by the weigth of a building on top of a soil.

The derivations for the strain and stress tensors are not too difficult,
but it can get cumbersome really fast because of the lengthy calculations.
Using the package we can simplify the whole process.

```python
import numpy as np
from sympy import init_printing, symbols, lambdify, S, simplify
from sympy import pi, Matrix, sqrt, oo
from continuum_mechanics.solids import sym_grad, strain_stress
```

```python
%matplotlib notebook
import matplotlib.pyplot as plt
from matplotlib import colors
```

The following snippet allows to format the graphs.

```python
repo = "https://raw.githubusercontent.com/nicoguaro/matplotlib_styles/master"
style = repo + "/styles/minimalist.mplstyle"
plt.style.use(style)

init_printing()
```

```python
x, y, z, r, E, nu, Fx, Fy, Fz = symbols('x y z r E nu F_x F_y F_z')
```

The components of the displacement vector are given by [[1]](#References)

\begin{align}
&u_x = \frac{(1 + \nu)}{2 \pi E}  \left\{\left[\frac{xz}{r^3} - \frac{(1 - 2\nu)x}{r(r + z)}\right]F_z +
  \frac{2(1 - \nu)r +z}{r(r + z)}F_x  +\frac{[2r(\nu r + z) + z^2]x}{r^3(r + z)^2}(xF_x + y F_y)\right\}\, ,\\
&u_y = \frac{(1 + \nu)}{2 \pi E}  \left\{\left[\frac{yz}{r^3} - \frac{(1 - 2\nu)y}{r(r + z)}\right]F_z +
  \frac{2(1 - \nu)r +z}{r(r + z)}F_y  +\frac{[2r(\nu r + z) + z^2]y}{r^3(r + z)^2}(xF_x + y F_y)\right\}\, ,\\
&u_z = \frac{(1 + \nu)}{2 \pi E}  \left\{\left[\frac{2(1 - \nu)}{r} - \frac{z^2}{r^3}\right]F_z  +\left[\frac{1 - 2\nu}{r(r + z)} + \frac{z}{r^3}\right](xF_x +  y F_y)\right\}\, ,
\end{align}

with $r = \sqrt{x^2 + y^2 + z^2}$.

```python
ux = (1+nu)/(2*pi*E)*((x*z/r**3 - (1-2*nu)*x/(r*(r + z)))*Fz +
                       (2*(1 - nu)*r + z)/(r*(r + z))*Fx +
                       ((2*r*(nu*r + z) + z**2)*x)/(r**3*(r + z)**2)*(x*Fx + y*Fy))
ux
```

```python
uy = (1+nu)/(2*pi*E)*((y*z/r**3 - (1-2*nu)*y/(r*(r + z)))*Fz +
                       (2*(1 - nu)*r + z)/(r*(r + z))*Fy +
                       ((2*r*(nu*r + z) + z**2)*y)/(r**3*(r + z)**2)*(x*Fx + y*Fy))
uy
```

```python
uz = (1+nu)/(2*pi*E)*((2*(1 - nu)/r + z**2/r**3)*Fz +
                       ((1 - 2*nu)/(r*(r + z)) + z/r**3)*(x*Fx + y*Fy))
uz
```

Withouth loss of generality we can assume that $F_y=0$, this is equivalent a rotate the axes until the force is in the plane $y=0$.

```python
ux = ux.subs(Fy, 0)
ux
```

```python
uy = ux.subs(Fy, 0)
uy
```

```python
uz = uz.subs(Fy, 0)
uz
```

The displacement vector is then

```python
u = Matrix([ux, uy, uz]).subs(r, sqrt(x**2 + y**2 + z**2))
```

We can check that the displacement vanish when $x,y,z \rightarrow \infty$

```python
u.limit(x, oo)

```

```python
u.limit(y, oo)

```

```python
u.limit(z, oo)
```

We can compute the strain and stress tensors using the
symmetric gradient ([**vector.sym_grad()**](https://continuum-mechanics.readthedocs.io/en/latest/modules.html#vector.sym_grad)) and stress-to-stress ([**solids.stres_stress()**](https://continuum-mechanics.readthedocs.io/en/latest/modules.html#solids.strain_stress))
functions.

```python
lamda = E*nu/((1 + nu)*(1 - 2*nu))
mu = E/(2*(1 - nu))
strain = sym_grad(u)
stress = strain_stress(strain, [lamda, mu])
```

The expressions for strains and stresses are lengthy and difficult to work with.
Nevertheless, we can work with them. For example, we can evaluate the stress
tensor at a point $\mathbf{x} = (1, 0, 1)$ for a vertical load and a
Poisson coefficient $\nu = 1/4$.

```python
simplify(stress.subs({x: 1, y: 0, z:1, nu: S(1)/4, Fx: 0}))
```

## Visualization of the fields

Since it is difficult to handle these lengthy expressions we can
visualize them. For that, we define a grid where to evaluate the
expressions,

$$ (x, z) \in [-2, 2]\times[0, 5]\, ,$$

in this case.

```python
x_vec, z_vec = np.mgrid[-2:2:100j, 0:5:100j]
```

We can use [**lambdify()**](https://docs.sympy.org/1.5.1/modules/utilities/lambdify.html)
to turn the SymPy expressions to evaluatable functions.

```python
def field_plot(expr, x_vec, y_vec, z_vec, E_val, nu_val, Fx_val, Fz_val, title=''):
    """Plot the field"""

    # Lambdify the function
    expr_fun = lambdify((x, y, z, E, nu, Fx, Fz), expr, "numpy")
    expr_vec = expr_fun(x_vec, y_vec, z_vec, E_val, nu_val, Fx_val, Fz_val)

    # Determine extrema
    vmin = np.min(expr_vec)
    vmax = np.max(expr_vec)
    print("Minimum value in the domain: {:g}".format(vmin))
    print("Maximum value in the domain: {:g}".format(vmax))
    vmax = max(np.abs(vmax), np.abs(vmin))

    # Plotting
    fig = plt.gcf()
    levels = np.logspace(-1, np.log10(vmax), 10)
    levels = np.hstack((-levels[-1::-1], [0], levels))
    cbar_ticks = ["{:.2g}".format(level) for level in levels]
    cont = plt.contourf(x_vec, z_vec, expr_vec, levels=levels,
                        cmap="RdYlBu_r", norm=colors.SymLogNorm(0.1))
    cbar = fig.colorbar(cont, ticks=levels[::2])
    cbar.ax.set_yticklabels(cbar_ticks[::2])
    plt.axis("image")
    plt.gca().invert_yaxis()
    plt.xlabel(r"$x$")
    plt.ylabel(r"$z$")
    plt.title(title)
    return cont
```

### Displacements

```python
plt.figure()
field_plot(u.norm(), x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: 0.0881197
    Maximum value in the domain: 15.4645

```python
plt.figure()
field_plot(u[0], x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -4.09665
    Maximum value in the domain: 4.09665

```python
plt.figure()
field_plot(u[2], x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: 0.0869101
    Maximum value in the domain: 14.3383

### Stresses

We can plot the components of stress

```python
for row in range(0, 3):
    for col in range(row, 3):
        plt.figure()
        field_plot(stress[row,col], x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0,
                   title=r"$\sigma_{%i%i}$"%(row+1, col+1))

plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -41.4274
    Maximum value in the domain: 406.682

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -12.0021
    Maximum value in the domain: 144.846

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -95.9472
    Maximum value in the domain: 95.9472

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -59.0538
    Maximum value in the domain: 116.991

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -506.96
    Maximum value in the domain: 506.96

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -243.272
    Maximum value in the domain: 116.991

### Stress invariants

We can also plot the invariants of the stress tensor

```python
I1 = S(1)/3 * stress.trace()
I2 = S(1)/2 * (stress.trace()**2 + (stress**2).trace())
I3 = stress.det()
Mises = sqrt(((stress[0,0] - stress[1,1])**2 + (stress[1,1] - stress[2,2])**2 +
             (stress[2,2] - stress[0,0])**2 +
             6*(stress[0,1]**2 + stress[1,2]**2 + stress[0,2]**2))/2)
```

```python
plt.figure()
field_plot(I1, x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -107.797
    Maximum value in the domain: 213.555

```python
plt.figure()
field_plot(I2, x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: 0.000977492
    Maximum value in the domain: 579596

```python
plt.figure()
field_plot(I3, x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: -1.01409e+08
    Maximum value in the domain: 419218

```python
plt.figure()
field_plot(Mises, x_vec, 0, z_vec, 1.0, 0.3, 0.0, 1.0)
plt.show()
```

    <IPython.core.display.Javascript object>

    Minimum value in the domain: 0.0274784
    Maximum value in the domain: 958.065

## References

1. Landau, L. D., Kosevich, A. M., Pitaevskii, L. P., & Lifshitz, E. M. (1986). Theory of elasticity.

```python

```


# Similarity Solutions for Steady Two-dimensional Flow

Starting with the two-dimensional, incompressible form of the Navier-Stokes equations, we can use dimensional analysis to derive a set of simplified equations for laminar flow over a flat-plate. The fundamental scaling arguments of the boundary layer approximations is that the shear layer is thin, $\delta << x$, which is true for large Reynolds number flows, $\textrm{Re}_x >> 1$.  Using this observation, we can make the following scaling arguments

\begin{align}
& u, T, x = \mathcal{O}(1) \\
& v, y = \mathcal{O}(\textrm{Re}_x^{-1/2})
\end{align}

which result in

\begin{align}
v &<< u \\
\frac{\partial u}{\partial x} &<< \frac{\partial u}{\partial y} \\
\frac{\partial v}{\partial x} &<< \frac{\partial v}{\partial y}
\end{align}

Applying these scaling arguments to the Navier-Stokes equations, we can make the following observations

- The continuity equation is not affected.

- The $\partial u^2 / \partial x^2$ term in the $x$-momentum equation can be neglected.

- The pressure gradient in the $y$-direction is approximately zero. In other words, the pressure distribution, $p(x)$, is impressed upon the boundary layer and determined by the outer inviscid flow.

- In the non-dimensional form, the Reynolds number does not appear. In this coordinate system, all thicknesses have been scaled to unity.

- All second derivatives with respect to $x$ are removed, and the resulting differential equations are parabolic instead of elliptic.

The boundary layer equations are accurate for large Reynolds numbers, $\textrm{Re}_x > 1000$, but too large such that the transistion to turbulence occurs. Likewise, if the flow separates, i.e., by an adverse pressure gradient, the boundary layer equations are also no longer valid. Ignoring the affects of bouyancy and neglecting the unsteady terms, the steady, laminar boundary layer equations are

\begin{align}
& \frac{ \partial u }{ \partial x } + \frac{ \partial v }{ \partial y } = 0 \\
& u \frac{ \partial u }{ \partial x } + v \frac{ \partial u }{ \partial y } = U \frac{ \partial U }{ \partial x } + \nu \frac{ \partial^2 u }{ \partial y^2 }
\end{align}

where the $x-$ and $y-$ components of the velocity vector, $u$ and $v$, are functions of the Cartesian coordinates, where for a flate-plate, the $x-$component is directed along the surface flat plate, and the $y-$component is orientated normal to the surface. The boundary layer equations are subject to the following boundary conditions

\begin{align}
& u(x,0) = v(x,0) = 0 \\
& u(x,\infty) = U(x)
\end{align}

which describe the no-slip velocity condition and the *matching* boundary condition requiring that the viscous boundary layer flow matches the flow of the outer inviscid flow.

## Concept of self-similarity

As we saw above, dimensional analysis can be useful for understanding the relative scales of physical phenonemon and be used to make informed approximations of the Navier-Stokes equations. Another important concept in seeking solutions to differential equations is **self-similarity**. Self-similarity occurs when the solution is similar to itself (self similar) when the independent variables are conveniently scaled. In other words, if we can find an approprate scaling factor or function, we can transoform the governing equations in such a way to exploit the symmetry of the problem and reduce the number of independent variables necessary to describe the problem.

If this concept seems new to you, it isn't. Think of geometrical similarity. A triangle can be described by the lengths of its sides, $x$, $y$, and $z$. What if the triangle is equilateral and all the sides are equal? If we define a equilateral triangle we unit-length sides, then we can describe all other equilateral triangles with a single scale factor. We use the concept of self-similarity to simplify the boundary layer equations.

## Blasius Solution for Flat-plate Flow

If the boundary layer is thin $\textrm{Re} >> 1$, the outer-flow velocity, $U$, is constant, i.e., not accelerating $\partial U / \partial x$, then the resulting set of governing equations has no characteristic length, $L$. All local velocity profiles must have the same dimensionless shape,

\begin{equation}
\frac{u}{U} = f\left( \frac{y}{\delta} \right)
\end{equation}

where $\delta$ is scaled-height of the boundary layer with the scale

\begin{equation}
\delta \approx  \left( \frac{ \nu x }{ U } \right)^{\frac{1}{2}}
\end{equation}

We can then introduce a dimensionless similarity variable,

\begin{equation}
\eta = y \sqrt{\frac{U}{2 \nu x}}
\end{equation}

Introducing the stream function $\Psi$, which has the following definition

\begin{equation}
\Psi = \int u \, dy |_{x = \textrm{const} }
\end{equation}

and has the following nondimensional form

\begin{equation}
\Psi = \sqrt{ 2 \nu U x } f(\eta)
\end{equation}

where the function, $f$, is to be determined and can be thought of as a "shape"-function for the dimensionless boundary layer velocity profile. From the definition of the stream function,

\begin{align}
& u =  \frac{ \partial \Psi }{ \partial y } = U f^{\prime} \\
& v = -\frac{ \partial \Psi }{ \partial x } = \sqrt{ \frac{ \nu U }{ 2 x } } \left( \eta f^{\prime} - f \right)
\end{align}

where the prime denotes differentiation with respect to $\eta$. We see that (as expected) $u$ is of order $U$ and $v$ is of smaller order, $ U \, \textrm{Re}_x^{-1/2}$. Plugging these equations for $u$ and $v$ into the boundary-layer momentum equation, we get the following third-order differential equation

\begin{equation}
f^{'''} + f f^{''} = 0
\end{equation}

subject to the no-slip conditions $u(x,0) = v(x,0) = 0$ and the freestream-merge condition, $u(x,\infty) = U$, which transform to

\begin{align}
& f^{'}(0) = f(0) = 0 \\
& f^{'}(\infty) = 1
\end{align}

## Solution Procedure

The Blasius equation is a third-order, non-linear differential equation. To solve this equation, let $f^{\prime \prime} = y_1$, $f^{\prime} = y_2$, and $f = y_3$. Using these definitions, we can write the Blasius equation as a system of first-order, ordinary differential equations,

\begin{align}
& F^{\prime}_0 = y_1 \\
& F^{\prime}_1 = y_2 \\
& F^{\prime}_2 = -y_0 y_2
\end{align}

which are subject to the following boundary values

\begin{align}
& f(0) = y_3(0) = 0 \\
& f^{\prime}(0) = y_2(0) = 0
\end{align}

The more difficult problem is finding the correct value for $f^{\prime \prime}(0) = y_2$ that results in satisfying the matching boundary condition, $f^{'} = y_1 = 1$ as $\eta \to \infty$. To do this, we use a shooting-method. Let $\tau_w$ be the value of $f^{\prime \prime}(0) = y_1 = \tau_w$. Note that physically, $f^{\prime \prime}(0)$ corresponds to the wall shear stress. The shooting method is an iterative procedure with the objective to minimize a boundary value error function. We can define this error function as

\begin{equation}
\phi(\tau_w) = y_1(\eta_{\infty};\tau_w) - 1
\end{equation}

where the value of $y_1$ is paramatized by the value of $\tau_w$, which is updated each iteration and used to drive the error function, $\phi(\tau_w)$, to zero. We can use [Newton's method](https://en.wikipedia.org/wiki/Newton%27s_method) to iterative solve for the correct value of $\tau_w$. Let $n$ be the iteration count, then

\begin{equation}
\tau_w^{n+1} = \tau_w^{n} - \phi(\tau_w^n) \left( \frac{ \textrm{d} \phi }{ \textrm{d}\tau_w } \right)^{-1}
\end{equation}

where the derivative of $\phi$ with respect to $\tau_w$ is

\begin{equation}
\frac{ \textrm{d} \phi }{ \textrm{d} \tau_w } = \frac{ \phi\left(\tau_w^{n}\right) - \phi \left( \tau_w^{n-1} \right) }{\tau_w^n - \tau_w^{n-1} }
\end{equation}

# Solution

```python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
```

```python
def fblasius(t, y):
    """
    The Blasius equation for the solution to a laminar boundary layer
    converted into a system of first-order ODE's.

    Args:
        t (float) : Indepdent variable.
        y (array_like) :
    """
    return [y[1], y[2], -y[0]*y[2]]
```

```python
def delta_tau(phi_prev, phi_curr, tau_prev, tau_curr):
    """
    Use Newton's method to update the value of the wall shear stress so that
    the outer-edge boundary condition is satisifed.
    """
    return -phi_curr * (tau_curr - tau_prev) / (phi_curr - phi_prev)
```

```python
def boundary_value_error(f_infinity):
    """
    Compute the error at the boundary.
    """
    return f_infinity - 1.0
```

```python
def euler(fun, y0, t_span):
    """
    Euler's method for solving a system of ordinary differential equations.
    The method  is first-order accurate with the global error proportional
    to the step-size.

    Args:
        fun (callable) : Right-hand side of the ODE system. The calling signature
            is `fun(t,y)`, where `t` is a scalar.
        y0 (array_like) : Initial state.
        t_span (array_like) : Interval of integration (t0, tf).
    """

    y = np.zeros((t_span.size, y0.size))
    y[0,:] = y0

    for i in range(len(t_span)-1):
        dt = t_span[i+1] - t_span[i]
        y[i+1,:] = y[i,:] + np.dot(dt, fun(t_span[i], y[i,:]))

    return y
```

```python
N = 400
eta = np.linspace(0, 5.75, N+1)

# Set the bounds on the values of the wall shear stress for our
# shooting method
tau = [0.1, 0.8]

# Define an array for storing the value of the boundary error
# function -- we need to save both current and previous values.
phi = [0.0, 0.0]

PREV = 0
CURRENT = 1

# Initial conditions for system
y0 = np.zeros((3,))
y0[2] = tau[0]

# Solve
f = euler(fblasius, y0, eta)

# Compute the error function
phi[PREV] = boundary_value_error(f[-1,1])

# Save the solution history
solution_history = [f]

# Iterate until we reduce the boundary value error
error = 1.0
eps = 1.0e-3
while error > eps:

    # Update value of f''
    y0[2] = tau[CURRENT]

    # Solve the system
    f = euler(fblasius, y0, eta)
    solution_history.append(f)

    # Compute the new error function
    phi[CURRENT] = boundary_value_error(f[-1,1])

    # Update the value of f'' and cache previous values
    #
    dtau = delta_tau(phi[PREV], phi[CURRENT], tau[PREV], tau[CURRENT])

    tau[PREV] = tau[CURRENT]
    tau[CURRENT] += dtau
    phi[PREV] = phi[CURRENT]

    # Quit when tau is no longer changing
    error = abs(dtau)
```

```python
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(9, 6))
fig.tight_layout(rect=(0.05,0.05,0.9,0.9))

line = ax.plot(f[:,1], eta, lw=2, color='b', label=r'$f^{\prime}(\eta)$ - velocity')
line = ax.plot(f[:,2], eta, lw=2, color='k', label=r'$f^{\prime\prime}(\eta)$ - wall shear stress')

ax.set_xlim((-0.01, 1.01))
ax.set_ylim((0, 5.75))

ax.set_title("Solution to the Blasius equation", fontsize=15)
ax.legend(loc=4, fontsize=15)
ax.set_xlabel(r'$u / U, \tau_w$', fontsize=15.0)
ax.set_ylabel(r'$\eta$', fontsize=15.0)
```

## Creating an animation of the convergence history

If you get the following error `RuntimeError: Requested MovieWriter (ffmpeg) not available`, then you may need to install `ffmpeg`. You can do this by running `conda install ffmpeg`.

```python
from matplotlib import animation, rc
from IPython.display import HTML

# On my system, I also had to add the path to the ffmpeg binary.
#
plt.rcParams['animation.ffmpeg_path'] = '/Applications/anaconda3/bin/ffmpeg'
```

```python
# First set up the figure, the axis, and the plot element we want to animate
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(9, 6))
fig.tight_layout(rect=(0.05,0.05,0.9,0.9))

ax.set_xlim((0, 1.5))
ax.set_ylim((0, 5.75))

plotlays, plotcols = [2], ["blue","black"]
labels = [r'$f^{\prime}(\eta)$ - velocity', r'$f^{\prime\prime}(\eta)$ - wall shear stress']
lines = []
for index in range(2):
    lobj = ax.plot([],[],lw=2,color=plotcols[index],label=labels[index])[0]
    lines.append(lobj)
ax.set_title("Convergence History", fontsize=15)
ax.legend(loc=4, fontsize=15)
ax.set_xlabel(r'$u / U, \tau_w$', fontsize=15.0)
ax.set_ylabel(r'$\eta$', fontsize=15.0)

ax.spines['top'].set_linewidth(1.0)
ax.spines['bottom'].set_linewidth(1.0)
ax.spines['left'].set_linewidth(1.0)
ax.spines['right'].set_linewidth(1.0)
ax.minorticks_on()
ax.tick_params(which='major', length=3, width=1, labelsize=12.0, pad=5)
ax.tick_params(which='minor', length=2, width=1)
```

```python
# initialization function: plot the background of each frame
def init():
    for line in lines:
        line.set_data([],[])
    return lines
```

```python
# animation function. This is called sequentially
def animate(i):
    fn = solution_history[i]
    n = 1
    for line in lines:
        line.set_data(fn[:,n], eta)
        n += 1
    return lines
```

```python
# call the animator. blit=True means only re-draw the parts that have changed.
anim = animation.FuncAnimation(fig, animate, init_func=init, frames=len(solution_history),
                              interval=800, blit=True, repeat_delay=80000)
```

```python
HTML(anim.to_html5_video())
```

# References

The data and discussion of the Blasius equation for laminar, boundary-layer flow used information from the following references:

- White, Frank M. *Viscous Fluid Flow.* McGraw-Hill, International Edition, 2nd ed., (1991) p. 233-234.

- Hellevik, Leif Rune. *Numerical Methods for Engineers* (2020), http://folk.ntnu.no/leifh/teaching/tkt4140/._main000.html


# Exercises

We have four exercises in this section:
1. Rewrite the CART method to the Gini index.
2. Use pydot do draw the tree for C4.5 example.
3. Implement the reduced error pruning (REP).
4. Plot OC1 tree, but instead of elements id, print the feature id it was split by.

## Exercise 1: Rewrite the CART method to use Gini index as shown in the lecture

Use Gini index can be calculated with the following equation:
\begin{equation}
I_{G}(X)=1-\sum_{i=1}^{m}p^{2}_{i},
\end{equation}
and
\begin{equation}
I_{G}(\text{feature})=1-\sum_{i=1}^{n}p_{i}*I_{G}(X_{i}).
\end{equation}

You need to fill the ``calculate_gini`` function and change the ``build`` function a bit.

```python
# block for future ploting

from IPython.display import Image

def my_plot_tree(split_history, file_name):
    tree = pydot.Dot(graph_type='graph')
    for split in split_history:
        new_edge = pydot.Edge(split[0], split[1], fillcolor = 'red')
        tree.add_edge(new_edge)
    tree.write(file_name, format='png')
```

```python
import math
import numpy as np
import pydot
import copy

class BinaryLeaf:

    def __init__(self, elements, labels, ids):
        self.L = None
        self.R = None
        self.elements = elements
        self.split_feature = None
        self.labels = labels
        self.completed = False
        self.ids = ids

    def set_R(self, Rleaf):
        self.R = Rleaf

    def set_L(self, Lleaf):
        self.L = Lleaf

    def set_elements(self, elements):
        self.elements = elements

    def get_elements(self):
        return self.elements

    def set_p(self, threshold):
        self.p = threshold

    def get_L(self):
        return self.L

    def get_R(self):
        return self.R

    def set_completed(self):
        self.completed = True

    def is_completed(self):
        return self.completed

    def get_labels(self):
        return self.labels

    def set_split(self, feature):
        self.split_feature = feature

    def get_split(self):
        return self.split_feature

    def set_ids(self, ids):
        self.ids = ids

    def get_ids(self):
        return self.ids

labels = [1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]
data_set = [[1, 1, 2, 2], [2, 1, 2, 2], [1, 1, 1, 2], [1, 2, 1, 2], [2, 3, 2, 2],
            [2, 2, 1, 2], [3, 2, 2, 1], [1, 3, 2, 2], [3, 3, 2, 1], [2, 3, 1, 2],
            [3, 1, 1, 1], [1, 2, 1, 1], [2, 3, 1, 1], [2, 1, 1, 2], [2, 2, 1, 1]]

labels_count = len(np.unique(labels))

ids = list(range(len(data_set)))
root = BinaryLeaf(data_set, labels, ids)
current_node = root

## helper methods:

def get_unique_labels(labels):
    return np.unique(np.array(labels)).tolist()

def get_unique_values(elements):
    features_number = len(elements[0])
    unique = []
    for i in range(features_number):
        features_list = []
        for j in range(len(elements)):
            features_list.append(elements[j][i])
        unique.append(np.unique(np.array(features_list)))
    return unique

def is_leaf_completed(node):
    if node.is_completed():
        if node.get_L() != None and not node.get_L().is_completed():
            return node.get_L()
        elif node.get_R() != None and not node.get_R().is_completed():
            return node.get_R()
        elif node.get_L() == None and node.get_R() == None:
            return None
        elif node.get_L().is_completed() or node.get_R().is_completed():
            new_node = is_leaf_completed(node.get_L())
            if new_node == None:
                return is_leaf_completed(node.get_R())
            else:
                return new_node
        else:
            return None
    return node

def find_leaf_not_completed(root):
    return is_leaf_completed(root)

## CART methods:

def get_split_candidates(unique_values):
    split_list = []
    for i in range(len(unique_values)):
        current_list = []
        temp_list = copy.deepcopy(unique_values)
        current_list.append(temp_list[i])
        del temp_list[i]
        current_list.append(temp_list)
        split_list.append(current_list)
    return split_list

def get_number_of_labels_for_value(elements, column_id, label):
    count = 0
    if not isinstance(elements, list):
        elements_list = [elements]
    else:
        elements_list = elements

    column_elements = get_node_elements_column(column_id)

    for i in range(len(elements_list)):
        for j in range(len(column_elements)):
            if column_elements[j] == elements_list[i]:
                if current_node.labels[j] == label:
                    count = count + 1
    return count

def get_node_elements_column(column_id):
    return np.array(current_node.elements)[..., column_id].tolist()

def count_number_of_elements(elements, column_id):
    count = 0
    if isinstance(elements, list):
        column_elements = get_node_elements_column(column_id)
        for i in range(len(elements)):
            count = count + column_elements.count(elements[i])
    else:
        count = count + get_node_elements_column(column_id).count(elements)
    return count

def check_completed(labels, elements):
    ratio = len(get_unique_labels(labels))
    if ratio == 1:
        return True
    elements = sorted(elements)
    duplicated = [elements[i] for i in range(len(elements)) if i == 0 or elements[i] != elements[i - 1]]
    if len(duplicated) == 1:
        return True
    return False

def split_node(current_node, value, split_id, split_history):
    left_leaf = []
    left_leaf_labels = []
    left_leaf_ids = []
    right_leaf = []
    right_leaf_labels = []
    right_leaf_ids = []
    for i in range(len(current_node.elements)):
        if current_node.elements[i][split_id] == value:
            left_leaf.append(current_node.elements[i])
            left_leaf_labels.append(current_node.labels[i])
            left_leaf_ids.append(current_node.ids[i])
        else:
            right_leaf.append(current_node.elements[i])
            right_leaf_labels.append(current_node.labels[i])
            right_leaf_ids.append(current_node.ids[i])
    if len(right_leaf_labels) == 0 or len(left_leaf_labels) == 0:
        current_node.set_completed()
        return current_node, split_history
    split_history.append([str(current_node.ids), str(left_leaf_ids)])
    split_history.append([str(current_node.ids), str(right_leaf_ids)])
    current_node.set_L(BinaryLeaf(left_leaf, left_leaf_labels, left_leaf_ids))
    current_node.set_R(BinaryLeaf(right_leaf, right_leaf_labels, right_leaf_ids))
    current_node.set_split(split_id)
    current_node.set_completed()
    if check_completed(left_leaf_labels, left_leaf):
        current_node.L.set_completed()
    if check_completed(right_leaf_labels, right_leaf):
        current_node.R.set_completed()
    return current_node, split_history

def get_current_node():
    return find_leaf_not_completed()

# fill:
def calculate_gini(elements, column_id): # candidates, build_i
    # prepare data
    elems_count = [count_number_of_elements(elems, column_id) for elems in elements]
    num_of_labels_0 = [get_number_of_labels_for_value(elems, column_id, 1) for elems in elements]
    num_of_labels_1 = [get_number_of_labels_for_value(elems, column_id, -1) for elems in elements]
    # calculate
    ig = [1 - (num_of_labels_0[i]/elems_count[i])**2 - (num_of_labels_1[i]/elems_count[i])**2 for i in [0,1]]
    gini = 1 - (ig[0] * elems_count[0]/(elems_count[0] + elems_count[1]) + ig[1] * elems_count[1] / (elems_count[0] + elems_count[1]) )
    return gini

def build(root_node):
    current_node = root_node
    stop_criterion = False
    split_history = []
    while stop_criterion == False:
        unique_values = get_unique_values(current_node.get_elements())
        max_unique_id = 0
        max_split_id = 0
        max_value = 0
#         print('uniq 0', unique_values)
        for i in range(len(unique_values)):  # co gdy len(unique) == 1 ?
            if len(unique_values[i]) == 1:
                continue
#             print('uniq', unique_values[i].tolist())
            split_candidates = get_split_candidates(unique_values[i].tolist())
#             print('sc', split_candidates)
            for j in range(len(split_candidates)):
                # change here:
#                 current_value = calculate_omega(split_candidates[j], i)
                current_value = calculate_gini(split_candidates[j], i)
                ###
                if max_value < current_value:
                    max_unique_id = i
                    max_split_id = j
                    max_value = current_value
        current_node, split_history = split_node(current_node, unique_values[max_unique_id][max_split_id], max_unique_id, split_history)
        new_node = find_leaf_not_completed(root_node)
        if new_node != None:
            current_node = new_node
        else:
            stop_criterion = True
    return root_node, split_history

def plot_tree(split_history):
    tree = pydot.Dot(graph_type='graph')
    for split in split_history:
        new_edge = pydot.Edge(split[0], split[1], fillcolor = 'red')
        tree.add_edge(new_edge)
    tree.write('cart_tree.png', format='png')

cart_tree, split_history = build(current_node)
plot_tree(split_history)
Image(filename='cart_tree.png')
```

## Exercise 2: Use pydot do draw the tree for C4.5 example

Fill the code below.

```python
%store -r split_history_c45
```

```python
def plot_tree(split_history):
    tree = pydot.Dot(graph_type='graph')
    for split in split_history:
        new_edge = pydot.Edge(split[0], split[1])
        tree.add_edge(new_edge)
    tree.write('c45_tree.png', format='png')

plot_tree(split_history_c45)
# plot_tree(split_history)
# plot_tree(split_history_cart)

from IPython.display import Image
Image(filename='c45_tree.png')
```

## Exercise 3: Implement the reduced error pruning (REP)

Implement the REP method and use it on the tree created with OC1 classifier. You need to the following:
- implement the accuracy function,
- manipulate the data to go through the path of the tree.

The REP method checkes the accuracy at each node and prune the node if the split gives a lower accuracy. Check the node as ``validated`` when checked and ``completed`` when pruned. When pruned set also the leafs to ``None``.

```python
# Prep
import math
import numpy as np
import copy

test_labels = [1,1,-1,-1,1,1,1,-1]
test_data_set = [[1,1,2,2],[3,2,1,2],[2,3,1,2],
                [2,2,1,2],[1,3,2,2],[2,1,1,2],
                [3,1,2,1],[2,1,2,2]]

labels = [1,1,-1,1,-1,-1,1,1,1,1,1,1,-1,-1,-1]
data_set = [[1,1,2,2],[2,1,2,2],[1,1,1,2],[1,2,1,2],[2,3,2,2],
                [2,2,1,2],[3,2,2,1],[1,3,2,2],[3,3,2,1],[2,3,1,2],
                [3,1,1,1],[1,2,1,1],[2,3,1,1],[2,1,1,2],[2,2,1,1]]

class BinaryLeaf:

    def __init__(self, elements, labels, ids):
        self.L = None
        self.R = None
        self.elements = elements
        self.split_feature = None
        self.split_value = None
        self.labels = labels
        self.completed = False
        self.ids = ids
        self.validated = False

    def set_R(self, Rleaf):
        self.R = Rleaf

    def set_L(self, Lleaf):
        self.L = Lleaf

    def set_elements(self, elements):
        self.elements = elements

    def get_elements(self):
        return self.elements

    def set_p(self, threshold):
        self.p = threshold

    def get_L(self):
        return self.L

    def get_R(self):
        return self.R

    def set_completed(self):
        self.completed = True

    def is_completed(self):
        return self.completed

    def get_labels(self):
        return self.labels

    def set_split(self, feature):
        self.split_feature = feature

    def get_split(self):
        return self.split_feature

    def set_split_value(self, value):
        self.split_value = value

    def get_split_value(self):
        return self.split_value

    def set_validated(self):
        self.validated = True

    def is_validated(self):
        return self.validated

    def set_ids(self, ids):
        self.ids = ids

    def get_ids(self):
        return self.ids

labels_count = len(np.unique(labels))

ids = list(range(len(data_set)))
root = BinaryLeaf(data_set, labels, ids)
current_node = root

def get_unique_labels(labels):
    return np.unique(np.array(labels)).tolist()

def get_unique_values(elements):
    features_number = len(elements[0])
    unique = []
    for i in range(features_number):
        features_list = []
        for j in range(len(elements)):
            features_list.append(elements[j][i])
        unique.append(np.unique(np.array(features_list)))
    return unique

def is_leaf_completed(node):
    if node.is_completed():
        if node.get_L() != None and not node.get_L().is_completed():
            return node.get_L()
        elif node.get_R() != None and not node.get_R().is_completed():
            return node.get_R()
        elif node.get_L() == None and node.get_R() == None:
            return None
        elif node.get_L().is_completed() or node.get_R().is_completed():
            new_node = is_leaf_completed(node.get_L())
            if new_node == None:
                return is_leaf_completed(node.get_R())
            else:
                return new_node
        else:
            return None
    return node

def find_leaf_not_completed(root):
    return is_leaf_completed(root)

def get_split_candidates(unique_values):
    split_list = []
    for i in range(len(unique_values)):
        current_list = []
        temp_list = copy.deepcopy(unique_values)
        current_list.append(temp_list[i])
        del temp_list[i]
        current_list.append(temp_list)
        split_list.append(current_list)
    return split_list

def get_number_of_labels_for_value(elements, column_id, label):
    count = 0
    if not isinstance(elements, list):
        elements_list = [elements]
    else:
        elements_list = elements

    column_elements = get_node_elements_column(column_id)

    for i in range(len(elements_list)):
        for j in range(len(column_elements)):
            if column_elements[j] == elements_list[i]:
                if current_node.labels[j] == label:
                    count = count + 1
    return count

def get_node_elements_column(column_id):
    return np.array(current_node.elements)[..., column_id].tolist()

def count_number_of_elements(elements, column_id):
    count = 0
    if isinstance(elements, list):
        column_elements = get_node_elements_column(column_id)
        for i in range(len(elements)):
            count = count + column_elements.count(elements[i])
    else:
        count = count + get_node_elements_column(column_id).count(elements)
    return count

def calculate_omega(elements, column_id):
    t_l = count_number_of_elements(elements[0], column_id)
    t_r = count_number_of_elements(elements[1], column_id)
    p_l = t_l * 1.0 / len(current_node.elements) * 1.0
    p_r = t_r * 1.0 / len(current_node.elements) * 1.0

    sum_p = 0
    labels = get_unique_labels(current_node.labels)
    for i in range(labels_count):
        p_class_t_l = (get_number_of_labels_for_value(elements[0], column_id, labels[i]) * 1.0) / (
                count_number_of_elements(elements[0], column_id) * 1.0)
        p_class_t_r = (get_number_of_labels_for_value(elements[1], column_id, labels[i]) * 1.0) / (
                count_number_of_elements(elements[1], column_id) * 1.0)
        sum_p = sum_p + math.fabs(p_class_t_l - p_class_t_r)
    return 2.0 * p_l * p_r * sum_p

def check_completed(labels, elements):
    ratio = len(get_unique_labels(labels))
    if ratio == 1:
        return True
    elements = sorted(elements)
    duplicated = [elements[i] for i in range(len(elements)) if i == 0 or elements[i] != elements[i - 1]]
    if len(duplicated) == 1:
        return True
    return False

def split_node(current_node, value, split_id, split_history):
    left_leaf = []
    left_leaf_labels = []
    left_leaf_ids = []
    right_leaf = []
    right_leaf_labels = []
    right_leaf_ids = []
    for i in range(len(current_node.elements)):
        if current_node.elements[i][split_id] == value:
            left_leaf.append(current_node.elements[i])
            left_leaf_labels.append(current_node.labels[i])
            left_leaf_ids.append(current_node.ids[i])
        else:
            right_leaf.append(current_node.elements[i])
            right_leaf_labels.append(current_node.labels[i])
            right_leaf_ids.append(current_node.ids[i])
    if len(right_leaf_labels) == 0 or len(left_leaf_labels) == 0:
        current_node.set_completed()
        return current_node, split_history
    split_history.append([str(current_node.ids), str(left_leaf_ids)])
    split_history.append([str(current_node.ids), str(right_leaf_ids)])
    current_node.set_L(BinaryLeaf(left_leaf, left_leaf_labels, left_leaf_ids))
    current_node.set_R(BinaryLeaf(right_leaf, right_leaf_labels, right_leaf_ids))
    current_node.set_split(split_id)
    current_node.set_split_value(value)
    current_node.set_completed()
    if check_completed(left_leaf_labels, left_leaf):
        current_node.L.set_completed()
    if check_completed(right_leaf_labels, right_leaf):
        current_node.R.set_completed()
    return current_node, split_history

def get_current_node():
    return find_leaf_not_completed()

def build(root_node):
    current_node = root_node
    stop_criterion = False
    split_history = []
    while stop_criterion == False:
        unique_values = get_unique_values(current_node.get_elements())
        max_unique_id = 0
        max_split_id = 0
        max_value = 0
        for i in range(len(unique_values)):
            if len(unique_values[i]) == 1:
                continue
            split_candidates = get_split_candidates(unique_values[i].tolist())
            for j in range(len(split_candidates)):
                current_value = calculate_omega(split_candidates[j], i)
                if max_value < current_value:
                    max_unique_id = i
                    max_split_id = j
                    max_value = current_value
        current_node, split_history = split_node(current_node, unique_values[max_unique_id][max_split_id],
                                                 max_unique_id, split_history)
        new_node = find_leaf_not_completed(root_node)
        if new_node != None:
            current_node = new_node
        else:
            stop_criterion = True
    return root_node, split_history

cart_tree, split_history_cart = build(current_node)
```

```python
my_plot_tree(split_history_cart, 'tree3_0.png')
Image(filename='tree3_0.png')
```

Fill the gap below:

```python
def get_curent_level(node):
    if type(node) != list: # BinaryLeaf
        return [node.get_R(),node.get_L()]
    level = []
    for leaf in node:
        if leaf.get_R() is not None:
            level.append(leaf.get_R())
        if leaf.get_L() is not None:
            level.append(leaf.get_L())
    return level

def get_accuracy(labels):
    label = labels[0]
    for _label in labels:
        if _label != label:
            return 0
    return -1

def validate_rep(cart_tree, test_data_set, test_labels):
#     level = [cart_tree.get_R(),cart_tree.get_L()]
    level = get_curent_level(cart_tree)
    old_accuracy = 0.0
    while level != None and len(level) > 0:
#         print('level', level)
        for leaf in level:
#             print('leaf.elements', leaf.get_elements())
#             print('leaf.ids', leaf.get_ids())
#             print('leaf.labels', leaf.get_labels())
            accuracy = get_accuracy(leaf.get_labels())
#             print('acc', accuracy)
            if accuracy < old_accuracy:
                print("To be pruned:")
                print('elements', leaf.elements)
                print('labels', leaf.labels)
                leaf.set_completed()
                leaf.set_R(None)
                leaf.set_L(None)
            else:
#                 old_accuracy = accuracy
                leaf.set_validated()
        level = get_curent_level(level)
```

```python
cart_tree, split_history_cart = build(current_node)
# print(1, cart_tree)
# print(2, test_data_set)
# print(3, test_labels)
validate_rep(cart_tree, test_data_set, test_labels)
```

    To be pruned:
    elements [[1, 1, 2, 2], [3, 2, 2, 1], [1, 3, 2, 2], [3, 3, 2, 1]]
    labels [1, 1, 1, 1]
    To be pruned:
    elements [[3, 1, 1, 1], [1, 2, 1, 1]]
    labels [1, 1]
    To be pruned:
    elements [[2, 3, 1, 1], [2, 2, 1, 1]]
    labels [-1, -1]

```python
# cart_tree, split_history_cart = build(cart_tree)
# my_plot_tree(split_history_cart, 'tree3_1.png')
# Image(filename='tree3_1.png')
```

## Exercise 4: Plot OC1 tree, but instead of elements id, print the feature id it was split by

To make the task done, you need to change the functions:
- ``build_level``,
- update the ``BinaryLeaf`` in two places to add the setters/getters and the feature and feature value split data.

```python
import random
# from random import randint, random
random.seed(30)

class BinaryLeaf:

    def __init__(self, elements, labels, ids):
        self.L = None
        self.R = None
        self.p = None
        self.elements = elements
        # add here features and values
        self.feature = None
        self.labels = labels
        self.completed = False
        self.ids = ids

    def set_R(self, Rleaf):
        self.R = Rleaf

    def set_L(self, Lleaf):
        self.L = Lleaf

    def set_elements(self, elements):
        self.elements = elements

    def get_elements(self):
        return self.elements

    def set_p(self, threshold):
        self.p = threshold

    def get_L(self):
        return self.L

    def get_R(self):
        return self.R

    def set_completed(self):
        self.completed = True

    def is_completed(self):
        return self.completed

    def get_labels(self):
        return self.labels

    # fill the methods here
    def set_feature(self, feature):
        self.feature = feature

    def get_feature(self):
        return self.feature

    def set_ids(self, ids):
        self.ids = ids

    def get_ids(self):
        return self.ids

labels = [1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, -1, -1, -1]
data_set = [[1, 1, 2, 2], [2, 1, 2, 2], [1, 1, 1, 2], [1, 2, 1, 2], [2, 3, 2, 2],
            [2, 2, 1, 2], [3, 2, 2, 1], [1, 3, 2, 2], [3, 3, 2, 1], [2, 3, 1, 2],
            [3, 1, 1, 1], [1, 2, 1, 1], [2, 3, 1, 1], [2, 1, 1, 2], [2, 2, 1, 1]]

labels_count = len(np.unique(labels))

ids = list(range(len(data_set)))
root = BinaryLeaf(data_set, labels, ids)
current_node = root
R = 10

## helper methods:

def compute_v(element, scv):
    return np.sum(np.multiply(element, scv[:-1])) + scv[-1]

def compare_two_leafs(leaf1, leaf2):
    labels1 = leaf1.labels
    labels2 = leaf2.labels
    if len(labels1) == len(labels2):
        for i in range(len(labels1)):
            if labels1[i] != labels2[i]:
                return False
        return True
    return False

def is_leaf_completed(node):
    if node.is_completed():
        if node.get_L() != None and not node.get_L().is_completed():
            return node.get_L()
        elif node.get_R() != None and not node.get_R().is_completed():
            return node.get_R()
        elif node.get_L() == None and node.get_R() == None:
            return None
        elif node.get_L().is_completed() or node.get_R().is_completed():
            new_node = is_leaf_completed(node.get_L())
            if new_node == None:
                return is_leaf_completed(node.get_R())
            else:
                return new_node
        else:
            return None
    return node

## oc1

def calculate_gini(labels):
    unique_labels = np.unique(labels)
    gini = 0
    for label in unique_labels:
        found = np.where(labels == label)
        gini = gini + len(found)/len(labels)
    return np.square(gini)

def get_all_possible_splits_by_gini(leaf):
    leaf_elements = leaf.elements
    labels = leaf.labels
    ginis = []
    for i in range(len(leaf_elements[0])):
        feature_ginis = []
        feature_column = np.array(leaf_elements)[:, i]
        for feature in feature_column:
            distinguish = feature_column <= feature
            left_labels  = np.array(labels)[distinguish]
            right_labels = np.array(labels)[~distinguish]
            gini = 1 - calculate_gini(left_labels) - calculate_gini(right_labels)
            feature_ginis.append([feature,gini])
        ginis.append(min(feature_ginis))
    return ginis

def find_current_level_data(root):
    return is_leaf_completed(root)

def divide_data_hiperplane(leaf,scv):
    below = []
    above = []
    below_labels = []
    above_labels = []
    below_ids = []
    above_ids = []
    for i in range(len(leaf.elements)):
        v = compute_v(leaf.elements[i],scv) > 0
        if v:
            above.append(leaf.elements[i])
            above_labels.append(leaf.labels[i])
            above_ids.append(leaf.ids[i])
        else:
            below.append(leaf.elements[i])
            below_labels.append(leaf.labels[i])
            below_ids.append(leaf.ids[i])
    return np.array(below), np.array(above), np.array(below_labels), np.array(above_labels), below_ids, above_ids

def get_coefficiency(splits):
    splits = np.array(splits)
    scv = np.zeros(len(splits)+1)
    min_split_index = np.argmin(splits[:,1])
    scv[min_split_index] = 1
    scv[-1] = -splits[min_split_index][1]
    return scv

def compute_u(element, scv, feature):
    return (scv[feature] * element[feature] - compute_v(element, scv)) / element[feature]

def sort_u(element):
    return np.sort(element)

def perturb(leaf, scv, feature, old_gini):
    u=[]
    for element in leaf.elements:
        u.append(compute_u(element, scv, feature))
    splits = sort_u(np.array(u))
    am = []
    for split in splits:
        new_scv = scv
        new_scv[feature] = split
        below, above, below_label, above_label, below_ids, above_ids = divide_data_hiperplane(leaf, scv)
        gini = 1 - (calculate_gini(below_label) - calculate_gini(above_label))
        am.append([new_scv, gini])
    am = np.array(am)
    best_split_index = np.argmin(am[:,1])
    if am[best_split_index][1] < old_gini:
        return am[best_split_index][1], am[best_split_index][0]
    elif am[best_split_index][1] == old_gini:
        if random.random() < 0.3:
            return am[best_split_index][1], am[best_split_index][0]
    return old_gini, scv

def build_level(root, split_history):
    leaf = find_current_level_data(root)
    if leaf == None:
        return
    splits = get_all_possible_splits_by_gini(leaf)
#     print(splits)
    split_coefficiency_vector = get_coefficiency(splits)
#     print(split_coefficiency_vector)
    below,above, below_label, above_label, below_ids, above_ids = divide_data_hiperplane(leaf,split_coefficiency_vector)
#     print('ai1 ',below_ids, above_ids)
    gini = 1 - calculate_gini(below_label) - calculate_gini(above_label)
    for c in range(R):
        feature = random.randint(0,len(leaf.elements[0])-1)
        gini, split_coefficiency_vector = perturb(leaf, split_coefficiency_vector, feature, gini)
        below, above, below_label, above_label, below_ids, above_ids = divide_data_hiperplane(leaf,split_coefficiency_vector)
#     print('ai2 ',below_ids, above_ids)
#     print('fe', feature)
#     print('sv', split_coefficiency_vector)
    left_leaf = BinaryLeaf(below, below_label, below_ids)
    right_leaf = BinaryLeaf(above, above_label, above_ids)

    left_id = str(left_leaf.get_ids())
    right_id = str(right_leaf.get_ids())

    base_node = pydot.Node(str(leaf.get_ids()), label = feature)
    left_node = pydot.Node(left_id, label = left_id)
    right_node = pydot.Node(right_id, label = right_id)

    split_history.append([base_node, left_node])
    split_history.append([base_node, right_node])

    leaf.set_completed()
    leaf.set_feature(feature)
    # change here
    ###
    if len(np.unique(below_label)) == 1:
        left_leaf.set_completed()
    if len(np.unique(above_label)) == 1:
        right_leaf.set_completed()
    if compare_two_leafs(leaf, left_leaf) or compare_two_leafs(leaf,right_leaf):
        leaf.set_completed()
    else:
        leaf.set_R(right_leaf)
        leaf.set_L(left_leaf)
    build_level(root, split_history)
    return root, split_history

def build(root):
    split_history = []
    return build_level(root, split_history)

def x_plot_tree(split_history):
    tree = pydot.Dot(graph_type='graph')
    for split in split_history:
        tree.add_node(split[0])
        tree.add_node(split[1])
        new_edge = pydot.Edge(split[0], split[1], fillcolor = 'red')
        tree.add_edge(new_edge)
    tree.write('oc1_tree.png', format='png')

oc1_tree, split_history = build(root)
x_plot_tree(split_history)

Image(filename='oc1_tree.png')
```

```python
Image(filename='oc1_tree_oryginal.png')
```

```python

```


```python
import numpy as np
import matplotlib.pylab as plt
from numpy.lib.stride_tricks import sliding_window_view
```

## Implementing a Function to Compute the Local Binary Pattern

Local Binary Pattern (LBP) is a simple yet very efficient texture feature commonly used in image processing.

\begin{align}
LBP(p) = \sum_{i=0}^{7} [f(n_i) > f(p)]2^i$
\end{align}

```python
def local_binary_pattern_list_comprehension(img):
    neighbors = [(0,2),(1,2),(2,2),\
                 (2,1), (2,0), (1,0),\
                 (0,0),(0,1)]
    H,W = img.shape
    aux_img = np.zeros((H+2,W+2))
    aux_img[1:-1,1:-1] = img

    return sum((aux_img[x:H+x,y:W+y] > img)*2**i for (i,(x,y)) in enumerate(neighbors))
```

```python
def local_binary_pattern_loop(img):
    neighbors = [(0,2),(1,2),(2,2),\
                 (2,1), (2,0), (1,0),\
                 (0,0),(0,1)]
    H,W = img.shape
    aux_img = np.zeros((H+2,W+2))
    aux_img[1:-1,1:-1] = img
    lbp = np.zeros(img.shape)
    for i, (x,y) in enumerate(neighbors):
        lbp+= (aux_img[x:H+x,y:W+y] > img)*2**i
    return lbp
```

```python
def lbp_sliding_window_view(img):
    codebook  = np.array([[[[64,128,1],\
                            [32,0,2],\
                            [16,8,4]]]], dtype = np.uint8)
    H,W = img.shape
    aux_img = np.zeros((H+2,W+2))
    aux_img[1:-1,1:-1] = img
    aux_img_view = sliding_window_view(aux_img, (3,3))
    lbp = ((aux_img_view > img[:,:,np.newaxis,np.newaxis])*codebook).sum(axis = (2,3))
    return lbp
```

```python
from PIL import Image

img = np.array(Image.open('../Figures/lena.png').convert('L'))
```

```python
%%timeit

local_binary_pattern_loop(img)
```

    8.39 ms ± 414 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

```python
%%timeit

lbp_sliding_window_view(img)
```

    24.7 ms ± 1.05 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

```python
%%timeit

local_binary_pattern_list_comprehension(img)
```

    8.86 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)