# 12 Root Finding

An important tool in the computational tool box is to find roots of equations for which no closed form solutions exist:

We want to find the roots $x_0$ of

$$
f(x_0) = 0
$$

## Problem: Projectile range
The equations of motion for the projectile with linear air resistance (see *11 ODE applications*) can be solved exactly.

As a reminder: the linear drag force is
$$
\mathbf{F}_1 = -b_1 \mathbf{v}\\
b := \frac{b_1}{m}
$$

Equations of motion with force due to gravity $\mathbf{g} = -g \hat{\mathbf{e}}_y$

\begin{align}
\frac{d\mathbf{r}}{dt} &= \mathbf{v}\\
\frac{d\mathbf{v}}{dt} &= - g \hat{\mathbf{e}}_y -b \mathbf{v}
\end{align}

### Analytical solution of the equations of motion
(Following Wang Ch 3.3.2)

Solve $x$ component of the velocity

$$
\frac{dv_x}{dt} = -b v_x
$$

by integration:

$$
v_x(t) = v_{0x} \exp(-bt)
$$

The drag force reduces the forward velocity to 0.

Integrate again to get the $x(t)$ component

$$
x(t) = x_0 + \frac{v_{0x}}{b} \left[1 - \exp(-bt)\right]
$$

Integrating the $y$ component of the velocity

$$
\frac{dv_y}{dt} = -g - b v_y
$$

gives

$$
v_y = \left(v_{0y} + \frac{g}{b}\right) \exp(-bt) - \frac{g}{b}
$$

and integrating again

$$
y(t) = y_0 + \frac{v_{0y} + \frac{g}{b}}{b} \left[1 - \exp(-bt)\right] - \frac{g}{b} t
$$

(Note: This shows immediately that the *terminal velocity* is

$$
\lim_{t\rightarrow\infty} v_y(t) = - \frac{g}{b},
$$

i.e., the force of gravity is balanced by the drag force.)

#### Analytical trajectory

To obtain the **trajectory $y(x)$** eliminate time (and for convenience, using the origin as the initial starting point, $x_0 = 0$ and $y_0 = 0$. Solve $x(t)$ for $t$

$$
t = -\frac{1}{b} \ln \left(1 - \frac{bx}{v_{0x}}\right)
$$

and insert into $y(t)$:

$$
y(x) = \frac{x}{v_{0x}} \left( v_{0y} + \frac{g}{b} \right) + \frac{g}{b^2} \ln \left(1 - \frac{bx}{v_{0x}}\right)
$$

#### Plot

Plot the analytical solution $y(x)$ for $\theta = 30^\circ$ and $v_0 = 100$ m/s.

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.style.use('ggplot')
```

The function `y_lindrag()` should compute $y(x)$.

```python
def y_lindrag(x, v0, b1=0.2, g=9.81, m=0.5):
    b = b1/m
    v0x, v0y = v0
    return x/v0x * (v0y + g/b) + g/(b*b) * np.log(1 - b*x/v0x)

def initial_v(v, theta):
    x = np.deg2rad(theta)
    return v * np.array([np.cos(x), np.sin(x)])
```

The analytical function drops *very* rapidly towards the end ($> 42$ m – found by manual trial-and-error plotting...) so in order to nicely plot the function we use a fairly coarse sampling of points along $x$ for the range $0 \le x < 42$ and very fine sampling for the last 2 m ($42 \le x < 45$):

```python
X = np.concatenate([np.linspace(0, 42, 100), np.linspace(42, 45, 10000)])
```

Evaluate the function for all $x$ values:

```python
Y = y_lindrag(X, initial_v(100, 30), b1=1)
```

    /var/folders/sm/37rm_wm16tq9qsf4n5md98ph0000gp/T/ipykernel_28716/482518341.py:4: RuntimeWarning: invalid value encountered in log
      return x/v0x * (v0y + g/b) + g/(b*b) * np.log(1 - b*x/v0x)

(The warning can be ignored, it just means that some of our `X` values were not approriate and outside the range of validity – when the argument of the logarithm becomes ≤0).

To indicate the ground we also plot a dashed black line: note that the analytical solution goes below the dashed line.

```python
plt.plot(X, Y)
plt.xlabel("$x$ (m)")
plt.ylabel("$y$ (m)")
plt.hlines([0], X[0], X[-1], colors="k", linestyles="--");
```

Compare to the numerical solution (from **11 ODE Applications**):

```python
import ode

def simulate(v0, h=0.01, b1=0.2, g=9.81, m=0.5):
    def f(t, y):
        # y = [x, y, vx, vy]
        return np.array([y[2], y[3], -b1/m * y[2], -g - b1/m * y[3]])

    vx, vy = v0
    t = 0
    positions = []
    y = np.array([0, 0, vx, vy], dtype=np.float64)

    while y[1] >= 0:
        positions.append([t, y[0], y[1]])  # record t, x and y
        y[:] = ode.rk4(y, f, t, h)
        t += h

    return np.array(positions)
```

```python
r = simulate(initial_v(100, 30), h=0.01, b1=1)
```

```python
plt.plot(X, Y, lw=2, label="analytical")
plt.plot(r[:, 1], r[:, 2], '--', label="RK4")
plt.legend(loc="best")
plt.xlabel("$x$ (m)"); plt.ylabel("$y$ (m)")
plt.hlines([0], X[0], X[-1], colors="k", linestyles="--");
```

The RK4 solution tracks the analytical solution perfectly (and we also programmed it to not go below ground...)

OPTIONAL: Show the residual

$$
r = y_\text{numerical}(x) - y_\text{analytical}
$$

```python
residual = r[:, 2] - y_lindrag(r[:, 1], initial_v(100, 30), b1=1)
```

```python
plt.plot(r[:, 1], residual)
plt.xlabel("$x$ (m)"); plt.ylabel("residual $r$ (m)");
```

### Predict the range $R$
How far does the ball or projectile fly, i.e., that value $x=R$ where $y(R) = 0$:

$$
\frac{R}{v_{0x}} \left( v_{0y} + \frac{g}{b} \right) + \frac{g}{b^2} \ln \left(1 - \frac{bR}{v_{0x}}\right) = 0
$$

This *transcendental equation* can not be solved in terms of elementary functions.

Use a **root finding** algorithm.

## Root-finding with the Bisection algorithm
**Bisection** is the simplest (but very robust) root finding algorithm that uses trial-and-error:

* bracket the root
* refine the brackets
* see first part in [12_rootfinding.pdf (PDF)](12_rootfinding.pdf)

More specifically:
1. determine a bracket that contains the root: $a < x_0 < b$ (i.e., an interval $[a, b]$ with $f(a) > 0$ and $f(b) < 0$ or $f(a) < 0$ and $f(b) > 0$)
2. cut bracket in half: $x' = \frac{1}{2}(a + b)$
3. determine in which half the root lies: either in $[a, x']$ or in $[x', b]$: If $f(a) f(x') > 0$ then the root lies in the right half $[x', b]$, otherwise the left half $[a, x']$.
4. Change the boundaries $a$ or $b$.
5. repeat until $|f(x')| < \epsilon$.

### Implementation of Bisection

- Test that the initial bracket contains a root; if not, return `None` (and possibly print a warning).
- If either of the bracket points is a root then return the bracket point.
- Allow `Nmax` iterations or until the convergence criterion `eps` is reached.
- Bonus: print a message if no root was found after `Nmax` iterations, but print the best guess and the error (but return `None`).

```python
def bisection(f, a, b, Nmax=100, eps=1e-14):
    fa, fb = f(a), f(b)
    if (fa*fb) > 0:
        print("bisect: Initial bracket [{0}, {1}] "
              "does not contain a single root".format(a, b))
        return None
    if np.abs(fa) < eps:
        return a
    if np.abs(fb) < eps:
        return b

    for iteration in range(Nmax):
        x = (a + b)/2
        fx = f(x)
        if np.abs(fx) < eps:
            break
        if f(a) * fx > 0:
            # root is not between a and x
            a = x
        else:
            b = x
    else:
        print("bisect: no root found after {0} iterations (eps={1}); "
              "best guess is {2} with error {3}".format(Nmax, eps, x, fx))
        x = None
    return x
```

### Finding the range with the bisection algorithm

Define the trial function `f`.

Note that our `y_lindrag()` function depends on `x` **and** `v` but `bisect()` only accepts functions `f` that depend on a *single variable*, $f(x)$. We therefore have to wrap `y_lindrag(x, v)` into a function `f(x)` that sets `v` already to a value *outside* the function: [Python's scoping rules](https://stackoverflow.com/questions/291978/short-description-of-the-scoping-rules#292502) say that inside the function `f(x)`, the variable `x` has the value assigned to the argument of `f(x)` but any other variables such as `v` or `b1`, which were *not defined inside `f`*, will get the value that they had *outside `f`* in the *enclosing code*.

```python
def f(x):
    v0 = initial_v(100, 30)
    b1 = 1.
    return y_lindrag(x, v0, b1=b1)
```

The initial bracket $[a_\text{initial}, b_\text{initial}]$ is a little bit difficult for this function: choose the right bracket near the point where the argument of the logarithm becomes 0 (which is actually the maximum $x$ value $\lim_{t\rightarrow +\infty} x(t) = \frac{v_{0x}}{b}$):

$$
b_\text{initial} = \frac{v_{0x}}{b} - \epsilon'
$$

where $\epsilon'$ is a small number.

```python
v = initial_v(100, 30)

b1 = 1.
m = 0.5
b = b1/m
bisection(f, 0.1, v[0]/b - 1e-12, eps=1e-6)
```

    43.30067423347077

Note that this solution is *not* the maximum value $\lim_{t\rightarrow +\infty} x(t) = \frac{v_{0x}}{b}$:

```python
v[0]/b
```

    43.30127018922194

### Find the range as a function of the initial angle

```python
b1 = 1.
m = 0.5
b = b1/m
v0 = 100
u = []
for theta in np.arange(1, 90):
    v = initial_v(v0, theta)
    def f(x):
        return y_lindrag(x, v, b1=b1)
    R = bisection(f, 0.1, v[0]/b - 1e-16, eps=1e-5)
    if R is not None:
        u.append((theta, R))
u = np.array(u)
```

    /var/folders/sm/37rm_wm16tq9qsf4n5md98ph0000gp/T/ipykernel_28716/482518341.py:4: RuntimeWarning: divide by zero encountered in log
      return x/v0x * (v0y + g/b) + g/(b*b) * np.log(1 - b*x/v0x)

```python
plt.plot(u[:, 0], u[:, 1])
plt.xlabel(r"launch angle $\theta$ ($^\circ$)")
plt.ylabel(r"range $R$ (m)");
```

Write a function `find_range()` to calculate the range for a given initial velocity $v_0$ and plot $R(\theta)$ for $10\,\text{m/s} ≤ v_0 ≤ 100\,\text{m/s}$.

```python
def find_range(v0, b1=1, m=0.5):
    b = b1/m
    u = []
    for theta in np.linspace(1, 90, 90):
        v = initial_v(v0, theta)
        def f(x):
            return y_lindrag(x, v, b1=b1)
        R = bisection(f, 0.1, v[0]/b - 1e-16, eps=1e-5)
        if R is not None:
            u.append((theta, R))
    return np.array(u)
```

```python
for v0 in (10, 25, 50, 75, 100):
    u = find_range(v0)
    plt.plot(u[:, 0], u[:, 1], label="{} m/s".format(v0))
plt.legend(loc="best")
plt.xlabel(r"$\theta$ (degrees)")
plt.ylabel(r"range $R$ (m)");
```

As a bonus, find the dependence of the *optimum launch angle* on the initial velocity, i.e., that angle that leads to the largest range:

```python
np.argmax(u[:, 1])
```

    10

```python
velocities = np.linspace(5, 100, 100)
results = []  # (v0, theta_opt)
for v0 in velocities:
    u = find_range(v0)
    thetas, ranges = u.transpose()
    # find index for the largest range and pull corresponding theta
    theta_opt = thetas[np.argmax(ranges)]
    results.append((v0, theta_opt))
results = np.array(results)

plt.plot(results[:, 0], results[:, 1])
plt.xlabel(r"velocity $v_0$ (m/s)")
plt.ylabel(r"$\theta_\mathrm{best}$ (degrees)");
```

The launch angle decreases with the velocity.

The steps in the graph are an artifact of choosing to only calculate the trajectories for integer angles (see `for theta in np.linspace(1, 90. 90)` in `find_range()`).

## Newton-Raphson algorithm
(see derivation in class and in the second part of [12_rootfinding.pdf (PDF)](12_rootfinding.pdf)
 or [Newton's Method](http://mathworld.wolfram.com/NewtonsMethod.html) on MathWorld)

### Activity: Implement Newton-Raphson
1. Implement the Newton-Raphson algorithm

   `while` $|f(x)| > \epsilon$

   $\Delta x = -\frac{f(x))}{f'(x)}$

   $x \leftarrow x + \Delta x$

2. Test with $g(x)$.

   $$
   g(x) = 2 \cos x - x
   $$

3. Bonus: test performance of `newton_raphson()` against `bisection()`.

```python
def g(x):
    return 2*np.cos(x) - x
```

```python
xvals = np.linspace(0, 7, 30)
plt.plot(xvals, np.zeros_like(xvals), 'k--')
plt.plot(xvals, g(xvals))
```

```python
def newton_raphson(f, x, h=1e-3, Nmax=100, eps=1e-14):
    """Find root x0 so that f(x0)=0 with the Newton-Raphson algorithm"""
    for iteration in range(Nmax):
        fx = f(x)
        if np.abs(fx) < eps:
            break
        df = (f(x + h/2) - f(x - h/2))/h
        Delta_x = -fx/df
        x += Delta_x
    else:
        print("Newton-Raphson: no root found after {0} iterations (eps={1}); "
              "best guess is {2} with error {3}".format(Nmax, eps, x, fx))
        x = None
    return x
```

```python
x0 = newton_raphson(g, 2)
print(x0)
```

    1.0298665293222586

```python
g(x0)
```

    6.661338147750939e-16

But note that the algorithm only converges well near the root. With other values it might not converge at all:

```python
newton_raphson(g, 3)
```

    Newton-Raphson: no root found after 100 iterations (eps=1e-14); best guess is -9322690.425062027 with error 16607175.892541457

```python
newton_raphson(g, 10)
```

    1.0298665293222589

```python
newton_raphson(g, 15)
```

    Newton-Raphson: no root found after 100 iterations (eps=1e-14); best guess is 52.96931452309739 with error -117.5878129311007

Let's look how Newton-Raphson iterates: also return all intermediate $x$ values:

```python
def newton_raphson_with_history(f, x, h=1e-3, Nmax=100, eps=1e-14):
    xvals = []
    for iteration in range(Nmax):
        fx = f(x)
        if np.abs(fx) < eps:
            break
        df = (f(x + h/2) - f(x - h/2))/h
        Delta_x = -fx/df
        x += Delta_x
        xvals.append(x)
    else:
        print("Newton-Raphson: no root found after {0} iterations (eps={1}); "
              "best guess is {2} with error {3}".format(Nmax, eps, x, fx))
        x = None
    return x, np.array(xvals)
```

```python
x = {}

x0, xvals = newton_raphson_with_history(g, 1.5)
x[1.5] = xvals
print("root x0 = {} after {} iterations".format(x0, len(xvals)))

x0, xvals = newton_raphson_with_history(g, 5)
x[5] = xvals
print("root x0 = {} after {} iterations".format(x0, len(xvals)))

x0, xvals = newton_raphson_with_history(g, 10)
x[10] = xvals
print("root x0 = {} after {} iterations".format(x0, len(xvals)))
```

    root x0 = 1.0298665293222589 after 4 iterations
    root x0 = 1.0298665293222589 after 58 iterations
    root x0 = 1.0298665293222589 after 21 iterations

```python
for xstart in sorted(x.keys()):
    plt.semilogx(x[xstart], label=str(xstart))
plt.legend(loc="best")
plt.xlabel("iteration")
plt.ylabel("current guess for root $x_0$");
```


# An Introduction to Bayesian Statistical Analysis

Before we jump in to model-building and using MCMC to do wonderful things, it is useful to understand a few of the theoretical underpinnings of the Bayesian statistical paradigm. A little theory (and I do mean a *little*) goes a long way towards being able to apply the methods correctly and effectively.

There are several introductory references to Bayesian statistics that go well beyond what we will cover here.

## What *is* Bayesian Statistical Analysis?

Though many of you will have taken a statistics course or two during your undergraduate (or graduate) education, most of those who have will likely not have had a course in *Bayesian* statistics. Most introductory courses, particularly for non-statisticians, still do not cover Bayesian methods at all, except perhaps to derive Bayes' formula as a trivial rearrangement of the definition of conditional probability. Even today, Bayesian courses are typically tacked onto the curriculum, rather than being integrated into the program.

In fact, Bayesian statistics is not just a particular method, or even a class of methods; it is an entirely different paradigm for doing statistical analysis.

> Practical methods for making inferences from data using probability models for quantities we observe and about which we wish to learn.
*-- Gelman et al. 2013*

A Bayesian model is described by parameters, uncertainty in those parameters is described using probability distributions.

All conclusions from Bayesian statistical procedures are stated in terms of *probability statements*

This confers several benefits to the analyst, including:

- ease of interpretation, summarization of uncertainty
- can incorporate uncertainty in parent parameters
- easy to calculate summary statistics

## Bayesian vs Frequentist Statistics: What's the difference?

Any statistical paradigm, Bayesian or otherwise, involves at least the following:

1. Some **unknown quantities** about which we are interested in learning or testing. We call these *parameters*.
2. Some **data** which have been observed, and hopefully contain information about (1).
3. One or more **models** that relate the data to the parameters, and is the instrument that is used to learn.

### The Frequentist World View

- The data that have been observed are considered **random**, because they are realizations of random processes, and hence will vary each time one goes to observe the system.
- Model parameters are considered **fixed**. The parameters' values are unknown, but they are fixed, and so we *condition* on them.

In mathematical notation, this implies a (very) general model of the following form:

\\[f(y | \theta)\\]

Here, the model \\(f\\) accepts data values \\(y\\) as an argument, conditional on particular values of \\(\theta\\).

Frequentist inference typically involves deriving **estimators** for the unknown parameters. Estimators are formulae that return estimates for particular estimands, as a function of data. They are selected based on some chosen optimality criterion, such as *unbiasedness*, *variance minimization*, or *efficiency*.

> For example, lets say that we have collected some data on the prevalence of autism spectrum disorder (ASD) in some defined population. Our sample includes \\(n\\) sampled children, \\(y\\) of them having been diagnosed with autism. A frequentist estimator of the prevalence \\(p\\) is:

>

> \\[\hat{p} = \frac{y}{n}\\]
>

> Why this particular function? Because it can be shown to be unbiased and minimum-variance.

It is important to note that new estimators need to be derived for every estimand that is introduced.

### The Bayesian World View

- Data are considered **fixed**. They used to be random, but once they were written into your lab notebook/spreadsheet/IPython notebook they do not change.
- Model parameters themselves may not be random, but Bayesians use probability distribtutions to describe their uncertainty in parameter values, and are therefore treated as **random**. In some cases, it is useful to consider parameters as having been sampled from probability distributions.

This implies the following form:

\\[p(\theta | y)\\]

This formulation used to be referred to as ***inverse probability***, because it infers from observations to parameters, or from effects to causes.

Bayesians do not seek new estimators for every estimation problem they encounter. There is only one estimator for Bayesian inference: **Bayes' Formula**.

## Bayesian Inference, in 3 Easy Steps

Gelman et al. (2013) describe the process of conducting Bayesian statistical analysis in 3 steps.

### Step 1: Specify a probability model

As was noted above, Bayesian statistics involves using probability models to solve problems. So, the first task is to *completely specify* the model in terms of probability distributions. This includes everything: unknown parameters, data, covariates, missing data, predictions. All must be assigned some probability density.

This step involves making choices.

- what is the form of the sampling distribution of the data?
- what form best describes our uncertainty in the unknown parameters?

### Step 2: Calculate a posterior distribution

The mathematical form \\(p(\theta | y)\\) that we associated with the Bayesian approach is referred to as a **posterior distribution**.

> posterior /pos·ter·i·or/ (pos-tēr´e-er) later in time; subsequent.

Why posterior? Because it tells us what we know about the unknown \\(\theta\\) *after* having observed \\(y\\).

This posterior distribution is formulated as a function of the probability model that was specified in Step 1. Usually, we can write it down but we cannot calculate it analytically. In fact, the difficulty inherent in calculating the posterior distribution for most models of interest is perhaps the major contributing factor for the lack of widespread adoption of Bayesian methods for data analysis. Various strategies for doing so comprise this tutorial.

**But**, once the posterior distribution is calculated, you get a lot for free:

- point estimates
- credible intervals
- quantiles
- predictions

### Step 3: Check your model

Though frequently ignored in practice, it is critical that the model and its outputs be assessed before using the outputs for inference. Models are specified based on assumptions that are largely unverifiable, so the least we can do is examine the output in detail, relative to the specified model and the data that were used to fit the model.

Specifically, we must ask:

- does the model fit data?
- are the conclusions reasonable?
- are the outputs sensitive to changes in model structure?

## Why be Bayesian?

At this point, it is worth addressing the question of why one might consider an alternative statistical paradigm to the classical/frequentist statistical approach. After all, it is not always easy to specify a full probabilistic model, nor to obtain output from the model once it is specified. So, why bother?

> ... the Bayesian approach is attractive because it is useful. Its usefulness derives in large measure from its simplicity. Its simplicity allows the investigation of far more complex models than can be handled by the tools in the classical toolbox.
*-- Link and Barker 2010*

We already noted that there is just one estimator in Bayesian inference, which lends to its ***simplicity***. Moreover, Bayes affords a conceptually simple way of coping with multiple parameters; the use of probabilistic models allows very complex models to be assembled in a modular fashion, by factoring a large joint model into the product of several conditional probabilities.

Bayesian statistics is also attractive for its ***coherence***. All unknown quantities for a particular problem are treated as random variables, to be estimated in the same way. Existing knowledge is given precise mathematical expression, allowing it to be integrated with information from the study dataset, and there is formal mechanism for incorporating new information into an existing analysis.

Finally, Bayesian statistics confers an advantage in the ***iterpretability*** of analytic outputs. Because models are expressed probabilistically, results can be interpreted probabilistically. Probabilities are easy for users (particularly non-technical users) to understand and apply.

### Example: confidence vs. credible intervals

A commonly-used measure of uncertainty for a statistical point estimate in classical statistics is the ***confidence interval***. Most scientists were introduced to the confidence interval during their introductory statistics course(s) in college. Yet, a large number of users mis-interpret the confidence interval.

Here is the mathematical definition of a 95% confidence interval for some unknown scalar quantity that we will here call \\(\theta\\):

\\[Pr(a(Y) < \theta < b(Y) | \theta) = 0.95\\]

how the endpoints of this interval are calculated varies according to the sampling distribution of \\(Y\\), but for as an example, the confidence interval for the population mean when \\(Y\\) is normally distributed is calculated by:

\\[Pr(\bar{Y} - 1.96\frac{\sigma}{\sqrt{n}}< \theta < \bar{Y} + 1.96\frac{\sigma}{\sqrt{n}}) = 0.95\\]

It would be tempting to use this definition to conclude that there is a 95% chance \\(\theta\\) is between \\(a(Y)\\) and \\(b(Y)\\), but that would be a mistake.

Recall that for frequentists, unknown parameters are **fixed**, which means there is no probability associated with them being any value except what they are fixed to. Here, the interval itself, and not \\(\theta\\) is the random variable. The actual interval calculated from the data is just one possible realization of a random process, and it must be strictly interpreted only in relation to an infinite sequence of identical trials that might be (but never are) conducted in practice.

A valid interpretation of the above would be:

> If the experiment were repeated an infinite number of times, 95% of the calculated intervals would contain \\(\theta\\).

This is what the statistical notion of "confidence" entails, and this sets it apart from probability intervals.

Since they regard unknown parameters as random variables, Bayesians can and do use probability intervals to describe what is known about the value of an unknown quantity. These intervals are commonly known as ***credible intervals***.

The definition of a 95% credible interval is:

\\[Pr(a(y) < \theta  There is a 95% chance \\(\theta\\) is between \\(a\\) and \\(b\\).

Hence, the credible interval is a statement of what we know about the value of \\(\theta\\) based on the observed data.

## Probability

> *Misunderstanding of probability may be the greatest of all impediments to scientific literacy.*
> — Stephen Jay Gould

Because of its reliance on probabilty models, its worth talking a little bit about probability. There are three different ways to define probability, depending on how it is being used.

### 1. Classical probability

\\[Pr(X=x) = \frac{\text{# x outcomes}}{\text{# possible outcomes}}\\]

Classical probability is an assessment of **possible** outcomes of elementary events. Elementary events are assumed to be equally likely.

### 2. Frequentist probability

\\[Pr(X=x) = \lim_{n \rightarrow \infty} \frac{\text{# times x has occurred}}{\text{# independent and identical trials}}\\]

Unlike classical probability, frequentist probability is an EMPIRICAL definition. It is an objective statement desribing events that have occurred.

### 3. Subjective probability

\\[Pr(X=x)\\]

Subjective probability is a measure of one's uncertainty in the value of \\(X\\). It characterizes the state of knowledge regarding some unknown quantity using probability.

It is not associated with long-term frequencies nor with equal-probability events.

For example:

- X = the true prevalence of diabetes in Perth is < 15%
- X = the blood type of the person sitting next to you is type A
- X = the England will win this summer's European Championship
- X = it is raining in Nashville

## Probability distributions

Bayesian inference uses probability distributions as building blocks for specifying models. They are used to assign uncertainty to unknown parameters, or sampling distributions to data.

Probability distributions assign probabilities to each possible outcome or value of a particular variable. Probabilities must be **positive**, and the sum (or integral) of the probabilities of all possible values must be 1.

There are two distinct classes of distributions, determined by whether the variable being described is **discrete** or **continuous**.

### Discrete Probability Distributions

$$X = \{0,1\}$$

$$Y = \{\ldots,-2,-1,0,1,2,\ldots\}$$

**Probability Mass Function**:

For discrete $X$,

$$Pr(X=x) = f(x|\theta)$$

***e.g. Poisson distribution***

The Poisson distribution models unbounded counts:

$$Pr(X=x)=\frac{e^{-\lambda}\lambda^x}{x!}$$

* $X=\{0,1,2,\ldots\}$
* $\lambda > 0$

$$E(X) = \text{Var}(X) = \lambda$$

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

theta = 3
x = np.random.poisson(theta, size=1000)
x.mean(), x.var()
```

```python
_ = plt.hist(x, bins=30)
```

```python
y = stats.poisson.pmf(range(10), theta)
plt.plot(y, 'ro')
```

### Continuous Random Variables

$$X \in [0,1]$$

$$Y \in (-\infty, \infty)$$

**Probability Density Function**:

For continuous $X$,

$$Pr(x \le X \le x + dx) = f(x|\theta)dx \, \text{ as } \, dx \rightarrow 0$$

***e.g. normal distribution***

$$f(x) = \frac{1}{\sqrt{2\pi\sigma^2}}\exp\left[-\frac{(x-\mu)^2}{2\sigma^2}\right]$$

* $X \in \mathbf{R}$
* $\mu \in \mathbf{R}$
* $\sigma>0$

$$\begin{align}E(X) &= \mu \cr
\text{Var}(X) &= \sigma^2 \end{align}$$

```python
mu, sig = 10, 3

x = np.random.normal(mu, sig, 1000)
x.mean(), x.std()
```

```python
_ = plt.hist(x, bins=30)
```

```python
xvals = np.linspace(-10, 30)
y = stats.norm.pdf(xvals, mu, sig)
plt.plot(xvals, y, 'ro')
```

## Bayes' Formula

Now that we have some probability under our belt, we turn to Bayes' formula. This, as you recall, is the engine that allows us to obtain estimates of unknown quantities that we care about, using information retained by the data we observe. It turns out to be quite simple to derive Bayes' formula directly from the definition of conditional probability.

Again, the goal in Bayesian inference is to calculate the **posterior distribution** of our unknowns:

\\[Pr(\theta|Y=y)\\]

This expression is a **conditional probability**. It is the probability of \\(\theta\\) *given* the observed values of \\(Y=y\\).

In general, the conditional probability of A given B is defined as follows:

\\[Pr(B|A) = \frac{Pr(A \cap B)}{Pr(A)}\\]

To gain an intuition for this, it is helpful to use a Venn diagram:

Notice from this diagram that the following conditional probability is also true:

\\[Pr(A|B) = \frac{Pr(A \cap B)}{Pr(B)}\\]

These can both be rearranged to be expressions of the joint probability of A and B. Setting these equal to one another:

\\[Pr(B|A)Pr(A) = Pr(A|B)Pr(B)\\]

Then rearranging:

\\[Pr(B|A) = \frac{Pr(A|B)Pr(B)}{Pr(A)}\\]

This is Bayes' formula. Replacing the generic A and B with things we care about reveals why Bayes' formula is so important:

The equation expresses how our belief about the value of \\(\theta\\), as expressed by the **prior distribution** \\(P(\theta)\\) is reallocated following the observation of the data \\(y\\), as expressed by the posterior distribution the posterior distribution.

The innocuous denominator \\(P(y)\\) cannot be calculated directly, and is actually the expression in the numerator, integrated over all \\(\theta\\):

\\[Pr(\theta|y) = \frac{Pr(y|\theta)Pr(\theta)}{\int Pr(y|\theta)Pr(\theta) d\theta}\\]

The intractability of this integral is one of the factors that has contributed to the under-utilization of Bayesian methods by statisticians.

### Priors

Once considered a controversial aspect of Bayesian analysis, the prior distribution characterizes what is known about an unknown quantity before observing the data from the present study. Thus, it represents the information state of that parameter. It can be used to reflect the information obtained in previous studies, to constrain the parameter to plausible values, or to represent the population of possible parameter values, of which the current study's parameter value can be considered a sample.

### Likelihood functions

The likelihood represents the information in the observed data, and is used to update prior distributions to posterior distributions. This updating of belief is justified becuase of the **likelihood principle**, which states:

> Following observation of \\(y\\), the likelihood \\(L(\theta|y)\\) contains all experimental information from \\(y\\) about the unknown \\(\theta\\).

Bayesian analysis satisfies the likelihood principle because the posterior distribution's dependence on the data is only through the likelihood. In comparison, most frequentist inference procedures violate the likelihood principle, because inference will depend on the design of the trial or experiment.

What is a likelihood function? It is closely related to the probability density (or mass) function. Taking a common example, consider some data that are binomially distributed (that is, they describe the outcomes of \\(n\\) binary events). Here is the binomial sampling distribution:

\\[p(Y|\theta) = \frac{n!}{y! (n-y)!} \theta^{y} (1-\theta)^{n-y}\\]

We can code this easily in Python:

```python
from scipy.misc import comb

pbinom = lambda y, n, p: comb(n, y) * p**y * (1-p)**(n-y)
```

This function returns the probability of observing \\(y\\) events from \\(n\\) trials, where events occur independently with probability \\(p\\).

```python
pbinom(3, 10, 0.5)
```

```python
pbinom(1, 25, 0.5)
```

```python
yvals = range(10+1)
plt.plot(yvals, [pbinom(y, 10, 0.5) for y in yvals], 'ro')
```

What about the likelihood function?

The likelihood function is the exact same form as the sampling distribution, except that we are now interested in varying the parameter for a given dataset.

```python
pvals = np.linspace(0, 1)
y = 4
plt.plot(pvals, [pbinom(y, 10, p) for p in pvals])
```

So, though we are dealing with the same equation, these are entirely different functions; the distribution is discrete, while the likelihood is continuous; the distribtion's range is from 0 to 10, while the likelihood's is 0 to 1; the distribution integrates (sums) to one, while the likelhood does not.

## Example: Genetic probabilities

Let's put Bayesian inference into action using a very simple example. I've chosen this example because it is one of the rare occasions where the posterior can be calculated by hand. We will show how data can be used to update our belief in competing hypotheses.

Hemophilia is a rare genetic disorder that impairs the ability for the body's clotting factors to coagualate the blood in response to broken blood vessels. The disease is an **x-linked recessive** trait, meaning that there is only one copy of the gene in males but two in females, and the trait can be masked by the dominant allele of the gene.

This implies that males with 1 gene are *affected*, while females with 1 gene are unaffected, but *carriers* of the disease. Having 2 copies of the disease is fatal, so this genotype does not exist in the population.

In this example, consider a woman whose mother is a carrier (because her brother is affected) and who marries an unaffected man. Let's now observe some data: the woman has two consecutive (non-twin) sons who are unaffected. We are interested in determining **if the woman is a carrier**.

To set up this problem, we need to set up our probability model. The unknown quantity of interest is simply an indicator variable \\(W\\) that equals 1 if the woman is affected, and zero if she is not. We are interested in the probability that the variable equals one, given what we have observed:

\\[Pr(W=1 | s_1=0, s_2=0)\\]

Our prior information is based on what we know about the woman's ancestry: her mother was a carrier. Hence, the prior is \\(Pr(W=1) = 0.5\\). Another way of expressing this is in terms of the **prior odds**, or:

\\[O(W=1) = \frac{Pr(W=1)}{Pr(W=0)} = 1\\]

Now for the likelihood: The form of this function is:

\\[L(W | s_1=0, s_2=0)\\]

This can be calculated as the probability of observing the data for any passed value for the parameter. For this simple problem, the likelihood takes only two possible values:

\\[\begin{aligned}
L(W=1 &| s_1=0, s_2=0) = (0.5)(0.5) = 0.25 \cr
L(W=0 &| s_1=0, s_2=0) = (1)(1) = 1
\end{aligned}\\]

With all the pieces in place, we can now apply Bayes' formula to calculate the posterior probability that the woman is a carrier:

\\[\begin{aligned}
Pr(W=1 | s_1=0, s_2=0) &= \frac{L(W=1 | s_1=0, s_2=0) Pr(W=1)}{L(W=1 | s_1=0, s_2=0) Pr(W=1) + L(W=0 | s_1=0, s_2=0) Pr(W=0)} \cr
 &= \frac{(0.25)(0.5)}{(0.25)(0.5) + (1)(0.5)} \cr
 &= 0.2
\end{aligned}\\]

Hence, there is a 0.2 probability of the woman being a carrier.

Its a bit trivial, but we can code this in Python:

```python
prior = 0.5
p = 0.5

L = lambda w, s: np.prod([(1-i, p**i * (1-p)**(1-i))[w] for i in s])
```

```python
s = [0,0]

post = L(1, s) * prior / (L(1, s) * prior + L(0, s) * (1 - prior))
post
```

Now, what happens if the woman has a third unaffected child? What is our estimate of her probability of being a carrier then?

Bayes' formula makes it easy to update analyses with new information, in a sequential fashion. We simply assign the posterior from the previous analysis to be the prior for the new analysis, and proceed as before:

```python
L(1, [0])
```

```python
s = [0]
prior = post

L(1, s) * prior / (L(1, s) * prior + L(0, s) * (1 - prior))
```

Thus, observing a third unaffected child has further reduced our belief that the mother is a carrier.

## References

Gelman A, Carlin JB, Stern HS, Dunson DB, Vehtari A, Rubin DB. Bayesian Data Analysis, Third Edition. CRC Press; 2013.


# ***Introduction to Radar Using Python and MATLAB***
## Andy Harrison - Copyright (C) 2019 Artech House
<br/>

# LFM Pulse Ambiguity Function
***

Referring to Section 8.6.2, the complex envelope of an LFM pulse is written as (Equation 8.65)

\begin{equation}\label{eq:single_lfm_pulse}
    s(t) = \text{rect}\bigg[ \frac{t}{\tau_p} \bigg]\, e^{j \pi \beta t^2}.
\end{equation}

Using (Equation 8.65) and (Equation.53), the ambiguity function is written as (Equation 8.66)

\begin{equation}
    \big| \chi(\tau, f) \big|^2  = \Bigg| \int\limits_{-\infty}^{\infty} \text{rect}\bigg[ \frac{t}{\tau_p}\bigg] \, \text{rect}\bigg[ \frac{t - \tau}{\tau_p} \bigg] e^{j \pi \beta t^2}\, e^{-j \pi \beta (t - \tau)^2}\, e^{j 2 \pi f t}\, dt\Bigg|^2.
\end{equation}

Performing the integration leads to (Equation 8.67)

\begin{equation}
    \big| \chi(\tau, f) \big|^2  = \Big| (\tau_p - |\tau|) \, \text{sinc}\big[ \tau_p (f + \beta \tau) (\tau_p - |\tau|) \big] \Big|^2 \quad \text{for  } 0 \le |\tau| \le \tau_p.
\end{equation}
***

Begin by getting the library path

```python
import lib_path
```

Set the pulsewidth (s) and the bandwidth (Hz)

```python
pulsewidth = 1e-3

bandwidth = 2e3
```

Generate the time delay (s) using the `linspace` routine from `scipy`

```python
from numpy import linspace

# Set the time delay

time_delay = linspace(-pulsewidth, pulsewidth, 5000)
```

Calculate the ambiguity function for the LFM pulse

```python
from Libs.ambiguity.ambiguity_function import lfm_pulse

from numpy import finfo

ambiguity = lfm_pulse(time_delay, finfo(float).eps, pulsewidth, bandwidth)
```

Plot the zero-Doppler cut using the `matplotlib` routines

```python
from matplotlib import pyplot as plt

# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Plot the ambiguity function

plt.plot(time_delay, ambiguity, '')

# Set the x and y axis labels

plt.xlabel("Time (s)", size=12)

plt.ylabel("Relative Amplitude", size=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)

# Set the plot title and labels

plt.title('LFM Pulse Ambiguity Function', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)
```

Set the Doppler mismatch frequencies using the `linspace` routine

```python
doppler_frequency = linspace(-bandwidth, bandwidth, 5000)
```

Calculate the ambiguity function for the LFM pulse

```python
ambiguity = lfm_pulse(finfo(float).eps, doppler_frequency, pulsewidth, bandwidth)
```

Display the zero-range cut for the LFM pulse

```python
plt.plot(doppler_frequency, ambiguity, '')

# Set the x and y axis labels

plt.xlabel("Doppler (Hz)", size=12)

plt.ylabel("Relative Amplitude", size=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)

# Set the plot title and labels

plt.title('LFM Pulse Ambiguity Function', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)
```

Set the time delay and Doppler mismatch frequency and create the two-dimensional grid using the `meshgrid` routine from `scipy`

```python
from numpy import meshgrid

# Set the time delay

time_delay = linspace(-pulsewidth, pulsewidth, 500)

# Set the Doppler mismatch

doppler_frequency = linspace(-bandwidth, bandwidth, 500)

# Create the grid

t, f = meshgrid(time_delay, doppler_frequency)
```

Calculate the ambiguity function for the LFM pulse

```python
ambiguity = lfm_pulse(t, f, pulsewidth, bandwidth)
```

Display the two-dimensional contour plot for the LFM pulse ambiguity function

```python
# Plot the ambiguity function

plt.contour(t, f, ambiguity, 20, cmap='jet', vmin=-0.2, vmax=1.0)

# Set the x and y axis labels

plt.xlabel("Time (s)", size=12)

plt.ylabel("Doppler (Hz)", size=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)

# Set the plot title and labels

plt.title('LFM Pulse Ambiguity Function', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)
```

```python

```


# Hydrogen atom

\begin{equation}
\label{eq1}
-\frac{\hbar^2}{2 \mu} \left[ \frac{1}{r^2} \frac{\partial }{\partial r} \left( r^2 \frac{ \partial \psi}{\partial r}\right) + \frac{1}{r^2 \sin \theta} \frac{\partial }{\partial \theta} \left( \sin \theta \frac{\partial \psi}{\partial \theta}\right) + \frac{1}{r^2 \sin^2 \theta} \frac{\partial^2 \psi}{\partial \phi^2} \right] - \frac{e^2}{ 4 \pi \epsilon_0 r} \psi= E \psi
\end{equation}

\begin{equation}
\label{eqsol1}
\psi_{n\ell m}(r,\vartheta,\varphi) = \sqrt {{\left (  \frac{2}{n a^*_0} \right )}^3 \frac{(n-\ell-1)!}{2n(n+\ell)!}} e^{- \rho / 2} \rho^{\ell} L_{n-\ell-1}^{2\ell+1}(\rho) Y_{\ell}^{m}(\vartheta, \varphi )
\end{equation}

```python
import k3d
from ipywidgets import interact, FloatSlider

import numpy as np
import scipy.special
import scipy.misc

r = lambda x,y,z: np.sqrt(x**2+y**2+z**2)
theta = lambda x,y,z: np.arccos(z/r(x,y,z))
phi = lambda x,y,z: np.arctan2(y,x)

a0 = 1.
R = lambda r,n,l: (2*r/(n*a0))**l * np.exp(-r/n/a0) * scipy.special.genlaguerre(n-l-1,2*l+1)(2*r/n/a0)
WF = lambda r,theta,phi,n,l,m: R(r,n,l) * scipy.special.sph_harm(m,l,phi,theta)
absWF = lambda r,theta,phi,n,l,m: abs(WF(r,theta,phi,n,l,m)).astype(np.float32)**2
N = 50j
a = 30.0
x,y,z = np.ogrid[-a:a:N,-a:a:N,-a:a:N]
x = x.astype(np.float32)
y = y.astype(np.float32)
z = z.astype(np.float32)
```

```python
orbital = WF(r(x,y,z),theta(x,y,z),phi(x,y,z),4,1,0).real.astype(np.float32) # 4p
```

```python
plt_vol = k3d.volume(orbital)
plt_label = k3d.text2d(r'n=1\; l=0\; m=0',(0.,0.))
plot = k3d.plot()
plot += plt_vol
plot += plt_label

plt_vol.opacity_function  = [0.        , 0.        , 0.21327923, 0.98025   , 0.32439035,
       0.        , 0.5       , 0.        , 0.67560965, 0.        ,
       0.74537706, 0.9915    , 1.        , 0.        ]

plt_vol.color_map = k3d.colormaps.paraview_color_maps.Cool_to_Warm_Extended
plt_vol.color_range = (-0.5,0.5)

plot.display()
```

## animation
### single wave function is sent at a time

```python
E = 4
for l in range(E):
    for m in range(-l,l+1):
        psi2 = WF(r(x,y,z),theta(x,y,z),phi(x,y,z),E,l,m).real.astype(np.float32)
        plt_vol.volume =  psi2/np.max(psi2)
        plt_label.text = 'n=%d \quad l=%d \quad m=%d'%(E,l,m)

```

### using time series

 - series of volumetric data are sent to k3d,
 - player interpolates between

```python
E = 4
psi_t = {}
t = 0.0
for l in range(E):
    for m in range(-l,l+1):

        psi2 = WF(r(x,y,z),theta(x,y,z),phi(x,y,z),E,l,m)
        psi_t[str(t)] = (psi2.real/np.max(np.abs(psi2))).astype(np.float32)
        t += 0.3
plt_vol.volume = psi_t

```

```python

```

```python

```


# Intrinsic and Extrinsic Calibration from 2D-3D Correspondences

Given a set of 2D-3D correspondences $\langle x_i, X_i \rangle \; (i=1,\dots,N)$ and an initial guess of the intrinsic parameter $K_0$, we can find $R$, $t$, and $K$ satisfying
\begin{equation}
x \sim K (R X + t)\,.
\end{equation}

```python
# Synthetic data

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import cv2

T = 20

def lookat(pos, eye):
    up = np.array([0, 0, 1])
    ez = eye - pos
    ez = ez / np.linalg.norm(ez)
    ex = np.cross(ez, up)
    ex = ex / np.linalg.norm(ex)
    ey = np.cross(ez, ex)
    ey = ey / np.linalg.norm(ey)
    R_c2w = np.array([ex, ey, ez])
    return R_c2w.T, pos

def plot_cam(ax, R_c2w, t_c2w, name=None, scale=0.5):
    ex = R_c2w[:,0] * scale + t_c2w
    ey = R_c2w[:,1] * scale + t_c2w
    ez = R_c2w[:,2] * scale + t_c2w
    p = np.array([ex, t_c2w, ey, t_c2w, ez, t_c2w])
    ax.plot(p[:,0], p[:,1], p[:,2])
    ax.text(p[0,0], p[0,1], p[0,2], 'x')
    ax.text(p[2,0], p[2,1], p[2,2], 'y')
    ax.text(p[4,0], p[4,1], p[4,2], 'z')
    if name is not None:
        ax.text(p[1,0], p[1,1], p[1,2], name)

# GT cameras in C2W
R1_gt, t1_gt = lookat(np.array([2, 0, 2]), np.array([0, 0, 0]))
R2_gt, t2_gt = lookat(np.array([1, 1, 2]), np.array([0, 0, 0]))
R3_gt, t3_gt = lookat(np.array([0, 2, 2]), np.array([0, 0, 0]))

# GT positions (T x 3 matrix)
X_gt = np.random.rand(T, 3) - 0.5

# GT intrinsics
K1_gt = np.array([[ 640, 0, 320], [0,  640, 240], [0, 0, 1]], dtype=np.float)
K2_gt = np.array([[1280, 0, 640], [0, 1280, 360], [0, 0, 1]], dtype=np.float)
K3_gt = np.array([[1920, 0, 960], [0, 1920, 540], [0, 0, 1]], dtype=np.float)

# GT position observations (T x 3 matrix)
x1_gt = (X_gt @ R1_gt - R1_gt.T @ t1_gt) @ K1_gt.T
x2_gt = (X_gt @ R2_gt - R2_gt.T @ t2_gt) @ K2_gt.T
x3_gt = (X_gt @ R3_gt - R3_gt.T @ t3_gt) @ K3_gt.T

# verify z>0
assert np.sum(x1_gt[:,2] < 0) == 0
assert np.sum(x2_gt[:,2] < 0) == 0
assert np.sum(x3_gt[:,2] < 0) == 0

x1_gt = x1_gt / x1_gt[:,2][:,None]
x2_gt = x2_gt / x2_gt[:,2][:,None]
x3_gt = x3_gt / x3_gt[:,2][:,None]

# Visualize
def visualize(R_list, t_list, v, title):
    fig = plt.figure()
    ax = fig.gca(projection='3d')

    for i, (R, t) in enumerate(zip(R_list, t_list)):
        plot_cam(ax, R, t, name=f'C{i}')

    ax.scatter3D(v[:,0], v[:,1], v[:,2])
    ax.set_xlim3d(-2, 2)
    ax.set_ylim3d(-2, 2)
    ax.set_zlim3d(-2, 2)
    ax.set_title(title)
    plt.show()

visualize([R1_gt, R2_gt, R3_gt], [t1_gt, t2_gt, t3_gt], X_gt, 'GT')

```

# Case 1: Extrinsic calibration only == PnP

```python
def calibRt(X, x, K, distCoeffs):
    X = np.copy(X)
    x = np.copy(x)
    ret, rvec, tvec = cv2.solvePnP(X, x, K, distCoeffs)
    rmat = cv2.Rodrigues(rvec)[0]
    return rmat, tvec, ret

# estimate
R1, t1, ret = calibRt(X_gt, x1_gt[:,:2], K1_gt, np.zeros((5,1)))

# eval
Err_R = np.linalg.norm(R1.T - R1_gt)
Err_t = np.linalg.norm(-R1.T@t1 - t1_gt.reshape((3,1)))
print(f'Error: R={Err_R}, t={Err_t}')
```

    Error: R=2.482534153247273e-16, t=6.930488906978537e-16

# Case 2: Intirinsic and extrinsic calibration (w/o distortion)

```python
def calibKRt(X, x, image_size, K0, distCoeffs0):
    X = np.copy(X.astype(np.float32))
    x = np.copy(x.astype(np.float32))
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera([X], [x], image_size, K0, distCoeffs0, flags=cv2.CALIB_USE_INTRINSIC_GUESS+cv2.CALIB_FIX_ASPECT_RATIO+cv2.CALIB_ZERO_TANGENT_DIST+cv2.CALIB_FIX_K1+cv2.CALIB_FIX_K2+cv2.CALIB_FIX_K3+cv2.CALIB_FIX_K4+cv2.CALIB_FIX_K5+cv2.CALIB_FIX_K6)
    rvec = rvecs[0]
    tvec = tvecs[0]
    rmat = cv2.Rodrigues(rvec)[0]
    return rmat, tvec

# estimate
K0 = np.array([[1024, 0, 512], [0, 1024, 384], [0, 0, 1]], dtype=np.float)
R1, t1 = calibKRt(X_gt, x1_gt[:,:2], (1920, 1080), K0, np.zeros(5))

# eval
Err_R = np.linalg.norm(R1.T - R1_gt)
Err_t = np.linalg.norm(-R1.T@t1 - t1_gt.reshape((3,1)))
print(f'Error: R={Err_R}, t={Err_t}')
```

    Error: R=2.397298986104679e-07, t=4.449654014188124e-07

# Case 3: Intrinsic and extrinsic calibration (with distortion)

```python
def calibKdRt(X, x, image_size, K0, distCoeffs0):
    X = np.copy(X.astype(np.float32))
    x = np.copy(x.astype(np.float32))
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera([X], [x], image_size, K0, distCoeffs0, flags=cv2.CALIB_USE_INTRINSIC_GUESS+cv2.CALIB_FIX_ASPECT_RATIO)
    rvec = rvecs[0]
    tvec = tvecs[0]
    rmat = cv2.Rodrigues(rvec)[0]
    return rmat, tvec, dist

# estimate
K0 = np.array([[1024, 0, 512], [0, 1024, 384], [0, 0, 1]], dtype=np.float)
R1, t1, distCoeffs = calibKdRt(X_gt, x1_gt[:,:2], (1920, 1080), K0, np.zeros(5))

# eval
Err_R = np.linalg.norm(R1.T - R1_gt)
Err_t = np.linalg.norm(-R1.T@t1 - t1_gt.reshape((3,1)))
print(f'Error: R={Err_R}, t={Err_t}')
```

    Error: R=1.8013973527361309e-06, t=2.400157487600344e-07


```python
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib nbagg
plt.ioff()    # this stops the graphs from overwriting each other

from sympy import *
a, s, t, u, v, w, x, y, z = symbols("a, s, t, u, v, w, x, y, z")
k, m, n = symbols("k, m, n", integer=True)
f, g, h = symbols("f, g, h", cls=Function)
init_printing()

# Enable viewing of variables and expressions without print statements
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
```

```python
mvec_df = pd.read_csv('mvec.csv')
mvec_df.describe()
```

      AggPercEnd
      AggPercSix
      LagSec
      Slope
      AUC
      PercWBC
      PercRBC
      PercHCT
      PercPLT

      count
      36.000000
      36.000000
      36.000000
      36.000000
      36.000000
      36.000000
      36.000000
      36.000000
      36.000000

      mean
      0.766667
      0.324306
      229.138889
      23.916667
      201.111111
      -0.194734
      -0.171356
      -0.176014
      0.127047

      std
      0.173617
      0.174897
      77.745061
      9.345358
      63.326337
      0.292090
      0.315597
      0.315857
      0.416223

      min
      0.500000
      0.000000
      138.000000
      9.000000
      110.800000
      -0.700730
      -0.759615
      -0.762749
      -0.470000

      25%
      0.643750
      0.225000
      178.250000
      19.500000
      162.500000
      -0.307284
      -0.385859
      -0.392710
      -0.175833

      50%
      0.725000
      0.337500
      214.000000
      23.000000
      183.500000
      -0.203846
      -0.118924
      -0.116919
      0.101313

      75%
      0.875000
      0.431250
      275.000000
      27.500000
      234.200000
      -0.041898
      0.048740
      0.049328
      0.424242

      max
      1.050000
      0.650000
      468.000000
      43.000000
      331.900000
      0.275547
      0.250000
      0.241685
      0.788732

```python
timemvec_df = mvec_df[mvec_df['Adjustment']=='Time']
# wt_only = timemvec_df[timemvec_df['Genotype']=='WT']
lag_plt = sns.boxplot(x="Genotype",y="LagSec", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

```python
plt.figure()
sns.boxplot(x="Genotype",y="AggPercEnd", data=timemvec_df)
plt.show()
plt.figure()
sns.swarmplot(x="Genotype",y="AggPercEnd", data=timemvec_df)
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b61f2f60>

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b6267dd8>

```python
plt.figure()
sns.boxplot(x="Genotype",y="AggPercSix", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b62ab7b8>

```python
plt.figure()
sns.boxplot(x="Genotype",y="Slope", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b6321860>

```python
plt.figure()
sns.boxplot(x="Genotype",y="AUC", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b631f6a0>

```python
plt.figure()
sns.boxplot(x="Genotype",y="PercWBC", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b640b320>

```python
plt.figure()
sns.boxplot(x="Genotype",y="PercRBC", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b6476fd0>

```python
plt.figure()
sns.boxplot(x="Genotype",y="PercHCT", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b64ef400>

```python
plt.figure()
sns.boxplot(x="Genotype",y="PercPLT", data=timemvec_df)
plt.show()
```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x1f6b6561eb8>


###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT license. (c)2014 Lorena A. Barba, Olivier Mesnard. Thanks: NSF for support via CAREER award #1149784.

##### Version 0.4 -- April 2015

# Source panel method

We are now getting close to the finish line with *AeroPython*! Our first few lessons introduced the fundamental flow solutions of potential flow, and we quickly learned that using our superposition powers we could get some useful results in aerodynamics.

The superposition of a [doublet](http://nbviewer.ipython.org/urls/github.com/barbagroup/AeroPython/blob/master/lessons/03_Lesson03_doublet.ipynb) and a free stream gave the flow around a circular cylinder, and we learned about the *D'Alembert paradox*: the result of zero drag for potential flow around a cylinder. Adding a [vortex](http://nbviewer.ipython.org/urls/github.com/barbagroup/AeroPython/blob/master/lessons/06_Lesson06_vortexLift.ipynb) at the center of the cylinder, we learned about lift and the *Kutta-Joukowski theorem* stating that lift is proporional to circulation: $L=\rho U \Gamma$. A most important result!

Adding together fundamental solutions of potential flow and seeing what we get when interpreting a dividing streamline as a solid body is often called an *indirect method*. This method goes all the way back to Rankine in 1871! But its applicability is limited because we can't stipulate a geometry and find the flow associated to it.

In [Lesson 9](http://nbviewer.ipython.org/urls/github.com/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb), we learned that it is possible to stipulate first the geometry, and then solve for the source strengths on a panel discretization of the body that makes the flow tangent at the boundary. This is called a *direct method* and it took off in the 1960s with the work of Hess and Smith at Douglas Aircraft Company.

A set of panels (line segments in 2D) can represent the surface of any solid body immersed in a potential flow by making the source-sheet strengths such that the normal velocity at each panel is equal to zero. This is a very powerful idea! But you should realize that all the panel strengths are coupled to each other, which is why we end up with a linear system of equations.

For an arbitrary geometry, we need to build a set of panels according to some points that define  the geometry. In this lesson, we will read from a file a geometry definition corresponding to a **NACA0012 airfoil**, create a set of panels, and solve for the source-sheet strengths to get flow around the airfoil.

*Make sure you have studied [Lesson 9](http://nbviewer.ipython.org/github/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb) carefully before proceeding!* We will not repeat the full mathematical formulation in this notebook, so refer back as needed.

First, load our favorite Python libraries, and the `integrate` module from SciPy:

```python
import math
import numpy
from scipy import integrate
from matplotlib import pyplot
```

Next, we read the body geometry from a file using the NumPy function [`loadtxt()`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html). The file comes from the [Airfoil Tools](http://airfoiltools.com/airfoil/details?airfoil=n0012-il) website and it contains a set of coordinates for the standard NACA0012 symmetric profile. We saved the file in the `resources` folder and load it from our local copy.

The geometry points get loaded into one NumPy array, so we separate the data into two arrays: `x,y` (for better code readability). The subsequent code will plot the geometry of the airfoil.

```python
# reads of the geometry from a data file
with open ('./resources/naca0012.dat') as file_name:
    x, y = numpy.loadtxt(file_name, dtype=float, delimiter='\t', unpack=True)

# plots the geometry
%matplotlib inline

val_x, val_y = 0.1, 0.2
x_min, x_max = x.min(), x.max()
y_min, y_max = y.min(), y.max()
x_start, x_end = x_min-val_x*(x_max-x_min), x_max+val_x*(x_max-x_min)
y_start, y_end = y_min-val_y*(y_max-y_min), y_max+val_y*(y_max-y_min)

size = 10
pyplot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.grid(True)
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.plot(x, y, color='k', linestyle='-', linewidth=2);
```

## Discretization into panels

Like in [Lesson 9](http://nbviewer.ipython.org/urls/github.com/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb), we will create a discretization of the body geometry into panels (line segments in 2D). A panel's attributes are: its starting point, end point and mid-point, its length and its orientation. See the following figure for the nomenclature used in the code and equations below.

We can modify the `Panel` class from our previous notebook slightly, to work better for our study of flow over an airfoil. The only difference is that we identify points on the top or bottom surfaces with the words `upper` and `lower`, which is only used later for plotting results with different colors for the top and bottom surfaces of the profile.

```python
class Panel:
    """Contains information related to a panel."""
    def __init__(self, xa, ya, xb, yb):
        """Creates a panel.

        Arguments
        ---------
        xa, ya -- Cartesian coordinates of the first end-point.
        xb, yb -- Cartesian coordinates of the second end-point.
        """
        self.xa, self.ya = xa, ya
        self.xb, self.yb = xb, yb

        self.xc, self.yc = (xa+xb)/2, (ya+yb)/2       # control-point (center-point)
        self.length = math.sqrt((xb-xa)**2+(yb-ya)**2)     # length of the panel

        # orientation of the panel (angle between x-axis and panel's normal)
        if xb-xa <= 0.:
            self.beta = math.acos((yb-ya)/self.length)
        elif xb-xa > 0.:
            self.beta = math.pi + math.acos(-(yb-ya)/self.length)

        # location of the panel
        if self.beta <= math.pi:
            self.loc = 'upper'
        else:
            self.loc = 'lower'

        self.sigma = 0.                             # source strength
        self.vt = 0.                                # tangential velocity
        self.cp = 0.                                # pressure coefficient
```

For the circular cylinder, the discretization into panels was really easy. This is the part that gets more complicated when you want to compute the flow around a general geometry, while the solution part is effectively the same as in [Lesson 9](http://nbviewer.ipython.org/urls/github.com/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb).

The function below will create the panels from the geometry data that was read from a file. It is better to have small panels near the leading-edge and the trailing edge, where the curvature is large. One method to get a non uniform distribution around the airfoil is to first discretize a circle with diameter equal to the airfoil's chord, with the leading edge and trailing edge touching the circle at a node, as shown in the following sketch.

Then, we store the $x$-coordinates of the circle points, `x_circle`, which will also be the $x$-coordinates of the panel nodes, `x`, and project the $y$-coordinates of the circle points onto the airfoil by interpolation. We end up with a node distribution on the airfoil that is refined near the leading edge and the trailing edge. It will look like this:

With the discretization method just described, the function `definePanels()` returns an array of objects, each an instance of the class `Panel` and containing all information about a panel, given the desired number of panels and the set of body coordinates.

A few remarks about the implementation of the function `definePanels()`:

* we just need to compute the $x$-coordinates of the circle (`x_circle`) since the $y$-coordinates of the panel nodes will be computed by interpolation;
* we create a circle with `N+1` points, but the first and last points coincide;
* we extend our NumPy arrays by adding an extra value that is equal to the first one; thus we don't have to do anything special with the value `x[i+1]` in the different loops;
* the *while*-loop is used to find two consecutive points, (`x[I]`,`y[I]`) and (`x[I+1]`,`y[I+1]`), on the foil such that the interval [`x[I]`,`x[I+1]`] contains the value `x_ends[i]`; we use the keyword `break` to get out of the loop;
* once the two points have been identified, the value `y_ends[i]` is computed by interpolation.

```python
def define_panels(x, y, N=40):
    """Discretizes the geometry into panels using the 'cosine' method.

    Arguments
    ---------
    x, y -- Cartesian coordinates of the geometry (1D arrays).
    N - number of panels (default 40).

    Returns
    -------
    panels -- Numpy array of panels.
    """
    R = (x.max()-x.min())/2                                    # radius of the circle
    x_center = (x.max()+x.min())/2                             # x-coord of the center
    x_circle = x_center + R*numpy.cos(numpy.linspace(0, 2*math.pi, N+1))  # x-coord of the circle points

    x_ends = numpy.copy(x_circle)      # projection of the x-coord on the surface
    y_ends = numpy.empty_like(x_ends)  # initialization of the y-coord Numpy array

    x, y = numpy.append(x, x[0]), numpy.append(y, y[0])    # extend arrays using numpy.append

    # computes the y-coordinate of end-points
    I = 0
    for i in range(N):
        while I < len(x)-1:
            if (x[I] <= x_ends[i] <= x[I+1]) or (x[I+1] <= x_ends[i] <= x[I]):
                break
            else:
                I += 1
        a = (y[I+1]-y[I])/(x[I+1]-x[I])
        b = y[I+1] - a*x[I+1]
        y_ends[i] = a*x_ends[i] + b
    y_ends[N] = y_ends[0]

    panels = numpy.empty(N, dtype=object)
    for i in range(N):
        panels[i] = Panel(x_ends[i], y_ends[i], x_ends[i+1], y_ends[i+1])

    return panels
```

Now we can use this function, calling it with a desired number of panels whenever we execute the cell below. We also plot the resulting geometry.

```python
N = 40                            # number of panels
panels = define_panels(x, y, N)   # discretizes of the geometry into panels

# plots the geometry and the panels
val_x, val_y = 0.1, 0.2
x_min, x_max = min(panel.xa for panel in panels), max(panel.xa for panel in panels)
y_min, y_max = min(panel.ya for panel in panels), max(panel.ya for panel in panels)
x_start, x_end = x_min-val_x*(x_max-x_min), x_max+val_x*(x_max-x_min)
y_start, y_end = y_min-val_y*(y_max-y_min), y_max+val_y*(y_max-y_min)

size = 10
pyplot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.grid(True)
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.plot(x, y, color='k', linestyle='-', linewidth=2)
pyplot.plot(numpy.append([panel.xa for panel in panels], panels[0].xa),
         numpy.append([panel.ya for panel in panels], panels[0].ya),
         linestyle='-', linewidth=1, marker='o', markersize=6, color='#CD2305');
```

## Freestream conditions

The NACA0012 airfoil will be immersed in a uniform flow with velocity $U_\infty$ and an angle of attack $\alpha=0$. Even though it may seem like overkill to create a class for the freestream, we'll do it anyway. When creating a class, one is expecting to also create several intances of its objects. Here, we just have one freestream, so why define a class? Well, it makes the code more readable and does not block the programer from using the variable names `u_inf` and `alpha` for something else outside of the class.
Also, every time we need the freestream condition as input to a function, we will just have to pass the object as an argument and not all the attributes of the freestream.

```python
class Freestream:
    """Freestream conditions."""
    def __init__(self, u_inf=1.0, alpha=0.0):
        """Sets the freestream conditions.

        Arguments
        ---------
        u_inf -- Farfield speed (default 1.0).
        alpha -- Angle of attack in degrees (default 0.0).
        """
        self.u_inf = u_inf
        self.alpha = alpha*math.pi/180          # degrees --> radians
```

```python
# defines and creates the object freestream
u_inf = 1.0                                # freestream spee
alpha = 0.0                                # angle of attack (in degrees)
freestream = Freestream(u_inf, alpha)      # instantiation of the object freestream
```

## Flow tangency boundary condition

Enforcing the flow-tangency condition on each *control point* approximately makes the body geometry correspond to a dividing streamline (and the approximation improves if we represented the body with more and more panels). So, for each panel $i$, we make $u_n=0$ at $(x_{c_i},y_{c_i})$, which leads to the equation derived in the previous lesson:

\begin{equation}
u_{n_i} = \frac{\partial}{\partial n_i}\left\lbrace \phi\left(x_{c_i},y_{c_i}\right) \right\rbrace = 0
\end{equation}

i.e.

\begin{equation}
0 = U_\infty \cos\beta_i + \frac{\sigma_i}{2} + \sum_{j=1,j\neq i}^{N_p} \frac{\sigma_j}{2\pi} \int \frac{
\left(x_{c_i}-x_j(s_j)\right) \cos\beta_i
+ \left(y_{c_i}-y_j(s_j)\right) \sin\beta_i
}
{\left(x_{c_i}-x_j(s)\right)^2 + \left(y_{c_i}-y_j(s)\right)^2} {\rm d}s_j
\end{equation}

In the equation above, we calculate the derivative of the potential in the normal direction to enforce the flow tangency condition on each panel. But later, we will have to calculate the derivative in the tangential direction to compute the surface pressure coefficient. And, when we are interested in plotting the velocity field onto a mesh, we will have to calculate the derivative in the $x$- and $y$-direction.

Therefore the function below is similar to the one implemented in [Lesson 9](http://nbviewer.ipython.org/github/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb) to obtain the integrals along each panel, but we've generalized it to adapt to the direction of derivation (by means of two new arguments, `dxdz` and `dydz`, which respectively represent the value of $\frac{\partial x_{c_i}}{\partial z_i}$ and $\frac{\partial y_{c_i}}{\partial z_i}$, $z_i$ being the desired direction).

Moreover, the function is also more general in the sense of allowing any evaluation point, not just a control point on a panel (the argument `p_i` has been replaced by the coordinates `x` and `y` of the control-point, and `p_j` has been replaced with `panel`).

```python
def integral(x, y, panel, dxdz, dydz):
    """Evaluates the contribution of a panel at one point.

    Arguments
    ---------
    x, y -- Cartesian coordinates of the point.
    panel -- panel which contribution is evaluated.
    dxdz -- derivative of x in the z-direction.
    dydz -- derivative of y in the z-direction.

    Returns
    -------
    Integral over the panel of the influence at one point.
    """
    def func(s):
        return ( ((x - (panel.xa - math.sin(panel.beta)*s))*dxdz
                  +(y - (panel.ya + math.cos(panel.beta)*s))*dydz)
                / ((x - (panel.xa - math.sin(panel.beta)*s))**2
                   +(y - (panel.ya + math.cos(panel.beta)*s))**2) )
    return integrate.quad(lambda s:func(s), 0., panel.length)[0]
```

## Building the linear system

Here, we build and solve the linear system of equations of the form

\begin{equation}[A][\sigma] = [b].\end{equation}

In building the matrix, below, we call the `integral()` function with the correct values for the last parameters: $\cos \beta_i$ and $\sin\beta_i$, corresponding to a derivative in the normal direction.

Finally, we use `linalg.solve()` from NumPy to solve the system and find the strength of each panel.

```python
def build_matrix(panels):
    """Builds the source matrix.

    Arguments
    ---------
    panels -- array of panels.

    Returns
    -------
    A -- NxN matrix (N is the number of panels).
    """
    N = len(panels)
    A = numpy.empty((N, N), dtype=float)
    numpy.fill_diagonal(A, 0.5)

    for i, p_i in enumerate(panels):
        for j, p_j in enumerate(panels):
            if i != j:
                A[i,j] = 0.5/math.pi*integral(p_i.xc, p_i.yc, p_j, math.cos(p_i.beta), math.sin(p_i.beta))

    return A

def build_rhs(panels, freestream):
    """Builds the RHS of the linear system.

    Arguments
    ---------
    panels -- array of panels.
    freestream -- farfield conditions.

    Returns
    -------
    b -- 1D array ((N+1)x1, N is the number of panels).
    """
    b = numpy.empty(len(panels), dtype=float)

    for i, panel in enumerate(panels):
        b[i] = -freestream.u_inf * math.cos(freestream.alpha - panel.beta)

    return b
```

```python
A = build_matrix(panels)                    # computes the singularity matrix
b = build_rhs(panels, freestream)           # computes the freestream RHS
```

```python
# solves the linear system
sigma = numpy.linalg.solve(A, b)

for i, panel in enumerate(panels):
    panel.sigma = sigma[i]
```

## Surface pressure coefficient

From Bernoulli's equation, the pressure coefficient on the $i$-th panel is

\begin{equation}C_{p_i} = 1-\left(\frac{u_{t_i}}{U_\infty}\right)^2
\end{equation}

where $u_{t_i}$ is the tangential component of the velocity at the center point of the $i$-th panel,

\begin{equation}
u_{t_i} = -U_\infty \sin\beta_i + \sum_{j=1}^{N_p} \frac{\sigma_j}{2\pi} \int \frac{
\left(x_{c_i}-x_j(s_j)\right) \frac{\partial x_{c_i}}{\partial t_i}
+ \left(y_{c_i}-y_j(s_j)\right) \frac{\partial y_{c_i}}{\partial t_i}
}
{\left(x_{c_i}-x_j(s)\right)^2 + \left(y_{c_i}-y_j(s)\right)^2} {\rm d}s_j
\end{equation}

with

\begin{equation}
\frac{\partial x_{c_i}}{\partial t_i} = -\sin\beta_i \quad\text{and} \quad \frac{\partial y_{c_i}}{\partial t_i} = \cos\beta_i
\end{equation}

Notice that below we call the function `integral()` with different arguments: $-\sin\beta_i$ and $\cos\beta_i$ to get the derivation in the tangential direction.

```python
def get_tangential_velocity(panels, freestream):
    """Computes the tangential velocity on the surface.

    Arguments
    ---------
    panels -- array of panels.
    freestream -- farfield conditions.
    """
    N = len(panels)
    A = numpy.empty((N, N), dtype=float)
    numpy.fill_diagonal(A, 0.0)

    for i, p_i in enumerate(panels):
        for j, p_j in enumerate(panels):
            if i != j:
                A[i,j] = 0.5/math.pi*integral(p_i.xc, p_i.yc, p_j, -math.sin(p_i.beta), math.cos(p_i.beta))

    b = freestream.u_inf * numpy.sin([freestream.alpha - panel.beta for panel in panels])

    sigma = numpy.array([panel.sigma for panel in panels])

    vt = numpy.dot(A, sigma) + b

    for i, panel in enumerate(panels):
        panel.vt = vt[i]
```

```python
# computes the tangential velocity at the center-point of each panel
get_tangential_velocity(panels, freestream)
```

```python
def get_pressure_coefficient(panels, freestream):
    """Computes the surface pressure coefficients.

    Arguments
    ---------
    panels -- array of panels.
    freestream -- farfield conditions.
    """
    for panel in panels:
        panel.cp = 1.0 - (panel.vt/freestream.u_inf)**2
```

```python
# computes the surface pressure coefficients
get_pressure_coefficient(panels, freestream)
```

### Theoretical solution

There is a classical method to obtain the theoretical characteristics of airfoils, known as *Theodorsen's method*. It uses the Joukowski transformation but is able to deal with any airfoil by an additional transformation between a "near circle" and a circle. The method is hairy indeed! But the resulting values of pressure coefficient are provided for some airfoils in table form in the 1945 [NACA Report No.824](http://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/19930090976.pdf), available from the NASA web server (see p. 71).

The values of $(u/U_{\infty})^2$ are given for several stations along the chord length. We transcribed them here, saving them into an array:

```python
voverVsquared=numpy.array([0, 0.64, 1.01, 1.241, 1.378, 1.402, 1.411, 1.411, 1.399, 1.378, 1.35, 1.288, 1.228, 1.166, 1.109, 1.044, 0.956, 0.906, 0])
print(voverVsquared)
```

    [ 0.     0.64   1.01   1.241  1.378  1.402  1.411  1.411  1.399  1.378
      1.35   1.288  1.228  1.166  1.109  1.044  0.956  0.906  0.   ]

```python
xtheo=numpy.array([0, 0.5, 1.25, 2.5, 5.0, 7.5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 95, 100])
xtheo = xtheo/100
print(xtheo)
```

    [ 0.      0.005   0.0125  0.025   0.05    0.075   0.1     0.15    0.2     0.25
      0.3     0.4     0.5     0.6     0.7     0.8     0.9     0.95    1.    ]

### And plot the result!

We will use the values from the NACA report (also given in the book by Abbot and von Doenhoff, ["Theory of Wing Sections,"](http://books.google.com/books/about/Theory_of_Wing_Sections_Including_a_Summ.html?id=DPZYUGNyuboC) 1949) to visually compare the pressure distribution with the result of our source panel method. Let's see how it looks!

```python
# plots the surface pressure coefficient
val_x, val_y = 0.1, 0.2
x_min, x_max = min( panel.xa for panel in panels ), max( panel.xa for panel in panels )
cp_min, cp_max = min( panel.cp for panel in panels ), max( panel.cp for panel in panels )
x_start, x_end = x_min-val_x*(x_max-x_min), x_max+val_x*(x_max-x_min)
y_start, y_end = cp_min-val_y*(cp_max-cp_min), cp_max+val_y*(cp_max-cp_min)

pyplot.figure(figsize=(10, 6))
pyplot.grid(True)
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('$C_p$', fontsize=16)
pyplot.plot([panel.xc for panel in panels if panel.loc == 'upper'],
         [panel.cp for panel in panels if panel.loc == 'upper'],
         color='r', linewidth=1, marker='x', markersize=8)
pyplot.plot([panel.xc for panel in panels if panel.loc == 'lower'],
         [panel.cp for panel in panels if panel.loc == 'lower'],
         color='b', linewidth=0, marker='d', markersize=6)
pyplot.plot(xtheo, 1-voverVsquared, color='k', linestyle='--',linewidth=2)
pyplot.legend(['upper', 'lower'], loc='best', prop={'size':14})
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.gca().invert_yaxis()
pyplot.title('Number of panels : %d' % N);
```

That looks pretty good! The only place where the panel method doesn't quite match the tabulated data from Theordorsen's method is at the trailing edge. But note that the flow-tangency boundary condition in the panel method is applied at the control point of the panel (not at the endpoints), so this discrepancy is not surprising.

##### Accuracy check

For a closed body, the sum of all the source strengths must be zero. If not, it means the body would be adding or absorbing mass from the flow! Therfore, we should have

$$\sum_{j=1}^{N} \sigma_j l_j = 0$$

where $l_j$ is the length of the $j^{\text{th}}$ panel.

With this, we can get a get an idea of the accuracy of the source panel method.

```python
# calculates the accuracy
accuracy = sum([panel.sigma*panel.length for panel in panels])
print('--> sum of source/sink strengths:', accuracy)
```

    --> sum of source/sink strengths: 0.00461703117528

## Streamlines onto a mesh grid

To get a streamline plot, we have to create a mesh (like we've done in all *AeroPython* lessons!) and compute the velocity field onto it. Knowing the strength of every panel, we find the $x$-component of the velocity by taking derivative of the velocity potential in the $x$-direction, and the $y$-component by taking derivative in the $y$-direction:

$$u\left(x,y\right) = \frac{\partial}{\partial x}\left\lbrace \phi\left(x,y\right) \right\rbrace$$

$$v\left(x,y\right) = \frac{\partial}{\partial y}\left\lbrace \phi\left(x,y\right) \right\rbrace$$

Notice that here we call the funcion `integral()` with $1,0$ as the final arguments when calculating the derivatives in the $x$-direction, and $0,1$ for the derivatives in th $y$-direction.

```python
def get_velocity_field(panels, freestream, X, Y):
    """Returns the velocity field.

    Arguments
    ---------
    panels -- array of panels.
    freestream -- farfield conditions.
    X, Y -- mesh grid.
    """
    Nx, Ny = X.shape
    u, v = numpy.empty((Nx, Ny), dtype=float), numpy.empty((Nx, Ny), dtype=float)

    for i in range(Nx):
        for j in range(Ny):
            u[i,j] = freestream.u_inf*math.cos(freestream.alpha)\
                     + 0.5/math.pi*sum([p.sigma*integral(X[i,j], Y[i,j], p, 1, 0) for p in panels])
            v[i,j] = freestream.u_inf*math.sin(freestream.alpha)\
                     + 0.5/math.pi*sum([p.sigma*integral(X[i,j], Y[i,j], p, 0, 1) for p in panels])

    return u, v
```

```python
# defines a mesh grid
Nx, Ny = 20, 20                  # number of points in the x and y directions
val_x, val_y = 1.0, 2.0
x_min, x_max = min( panel.xa for panel in panels ), max( panel.xa for panel in panels )
y_min, y_max = min( panel.ya for panel in panels ), max( panel.ya for panel in panels )
x_start, x_end = x_min-val_x*(x_max-x_min), x_max+val_x*(x_max-x_min)
y_start, y_end = y_min-val_y*(y_max-y_min), y_max+val_y*(y_max-y_min)

X, Y = numpy.meshgrid(numpy.linspace(x_start, x_end, Nx), numpy.linspace(y_start, y_end, Ny))

# computes the velicity field on the mesh grid
u, v = get_velocity_field(panels, freestream, X, Y)
```

```python
# plots the velocity field
size=10
pyplot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
pyplot.streamplot(X, Y, u, v, density=1, linewidth=1, arrowsize=1, arrowstyle='->')
pyplot.fill([panel.xc for panel in panels],
         [panel.yc for panel in panels],
         color='k', linestyle='solid', linewidth=2, zorder=2)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.title('Streamlines around a NACA 0012 airfoil, AoA = %.1f' % alpha);
```

We can now calculate the pressure coefficient. In Lesson 9, we computed the pressure coefficient on the surface of the circular cylinder. That was useful because we have an analytical solution for the surface pressure on a cylinder in potential flow. For an airfoil, we are interested to see how the pressure looks all around it, and we make a contour plot in the flow domain.

```python
# computes the pressure field
cp = 1.0 - (u**2+v**2)/freestream.u_inf**2

# plots the pressure field
size=12
pyplot.figure(figsize=(1.1*size, (y_end-y_start)/(x_end-x_start)*size))
pyplot.xlabel('x', fontsize=16)
pyplot.ylabel('y', fontsize=16)
contf = pyplot.contourf(X, Y, cp, levels=numpy.linspace(-2.0, 1.0, 100), extend='both')
cbar = pyplot.colorbar(contf)
cbar.set_label('$C_p$', fontsize=16)
cbar.set_ticks([-2.0, -1.0, 0.0, 1.0])
pyplot.fill([panel.xc for panel in panels],
         [panel.yc for panel in panels],
         color='k', linestyle='solid', linewidth=2, zorder=2)
pyplot.xlim(x_start, x_end)
pyplot.ylim(y_start, y_end)
pyplot.title('Contour of pressure field');
```

### Final words

We've learned to use a source-sheet to represent any solid body: first a [circular cylinder](http://nbviewer.ipython.org/github/barbagroup/AeroPython/blob/master/lessons/09_Lesson09_flowOverCylinder.ipynb) (which we knew we could get by superposing a doublet and a freestream), and now an airfoil.

But what is the feature of airfoils that makes them interesting? Well, the fact that we can use them to generate lift and make things that fly, of course! But what do we need to generate lift? Think, think ... what is it?

## References

1. [Airfoil Tools](http://airfoiltools.com/index), website providing airfoil data.
1. Ira Herbert Abbott, Albert Edward Von Doenhoff and Louis S. Stivers, Jr. (1945), "Summary of Airfoil Data," NACA Report No.824, [PDF on the NASA web server](http://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/19930090976.pdf) (see p. 71)
1. Ira Herbert Abbott, Albert Edward Von Doenhoff, "Theory of Wing Sections, Including a Summary of Airfoil Data" (1949), Dover Press.

A further reference on Theodorsen's method is:

* Roland Schinzinger, Patricio A. A. Laura (1991), "Conformal Mapping: Methods and Applications." Dover edition in 2003. [Read on Google Books](https://books.google.com/books?id=qe-7AQAAQBAJ&lpg=PA128&ots=wbg0jLlqq5&dq=method%20theodorsen&pg=PA128#v=onepage&q=%22method%20of%20theodorsen%20and%20garrick%22&f=false)

---

###### Please ignore the cell below. It just loads our style for the notebook.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('../styles/custom.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Alegreya Sans', sans-serif;
    font-style:regular;
    font-weight: 200;
    font-size: 50pt;
    line-height: 100%;
    color:#CD2305;
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Fenix', serif;
    font-size: 22pt;
    line-height: 100%;
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Fenix', serif;
    margin-top:12px;
	font-size: 16pt;
    margin-bottom: 3px;
    font-style: regular;
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Fenix', serif;
    font-size: 2pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Alegreya Sans', sans-serif;
    font-weight: 300;
    font-size: 16pt;
    color: #CD2305;
    font-style: italic;
    margin-bottom: .5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'Source Code Pro', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

    .CodeMirror{
            font-family: "Source Code Pro";
			font-size: 90%;
    }
/*    .prompt{
        display: None;
    }*/

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>


# SymPyとチャート式で復習する高校数学I - PyLadies Tokyo Meetup #6 LT

## お前だれよ？

- [@iktakahiro](https://twitter.com/iktakahiro)
- blog: https://librabuch.jp
- [PyData.Tokyo](https://pydata.tokyo) オーガナイザー
- Python 2014 チュートリアル PyData 担当
- Pythonエンジニア養成読本 第四章PyData入門 執筆

## 本日のテキスト

## 数学1の範囲

1. 式の計算
1. 実数, 一次不等式
1. 集合と命題
1. 2次関数
1. 2次方程式と2次不等式
1. 三角比
1. 三角形への応用
1. データの分析

## 本日の復習範囲

- 式の計算
- 一次不等式
- 2次関数
- 2次方程式と2次不等式

## SymPyとは

[SymPy](http://www.sympy.org/en/index.html)とは、数式処理（symbolic mathematis）を行うためのパッケージです。

pipインストール可能です。

```sh
pip install sympy
```

次のような計算が数式処理の最もベーシックなものです。

```python
import sympy

# 記号の定義
x, y = sympy.symbols('x y')

# 式の定義
expr = 2 * x + y
print('定義された式:\n', expr)

# x, y に数値を代入
a1 = expr.subs([(x, 4), (y, 3)])
print('\nx=4, Y=3の場合:\n', a1)

a2 = expr - y
print('\nexpr から y をマイナス:\n', a2)
```

    定義された式:
     2*x + y

    x=4, Y=3の場合:
     11

    expr から y をマイナス:
     2*x

## 式の計算

次の計算をせよ。（13頁, 基礎例題5）

(1)　$(5x^3+3x-2x^2-4)+(3x^3-3x^2+5)$

```python
l = 5 * x ** 3 + 3 * x - 2 * x ** 2 - 4
r = 3 * x ** 3 - 3 * x ** 2 + 5
l + r
```

    8*x**3 - 5*x**2 + 3*x + 1

答え:　$8x^3-5x^2+3x+1$

## 式の展開

次の式を展開せよ。（20頁, 基礎例題10）

(1)　$(x-2y+1)(x-2y-2)$

(2)　$(a+b+c)^2$

```python
sympy.expand((x-2*y+1)*(x-2*y-2))
```

    x**2 - 4*x*y - x + 4*y**2 + 2*y - 2

答え:　$x^2-4xy-x+4y^2+2y-2$

```python
a, b, c = sympy.symbols('a b c')

sympy.expand((a+b+c)**2)
```

    a**2 + 2*a*b + 2*a*c + b**2 + 2*b*c + c**2

答え:　$a^2+b^2+c^2+2ab+2bc+2ca$

## 因数分解

次の式を因数分解せよ（26頁, 基礎例題14）

(1)　$x^2+8x+15$

```python
sympy.factor(x**2+8*x+15)
```

    (x + 3)*(x + 5)

答え:　$(x+3)(x+5)$

## 不等式

次の不等式を解け。（58頁, 基礎例題35）

(1)　$4x+5>2x-3$

```python
from sympy.solvers.inequalities import reduce_rational_inequalities

reduce_rational_inequalities([[4 * x + 5 > 2 * x -3]], x)
```

    And(-4 < re(x), im(x) == 0, re(x) < oo)

答え: $x > -4$

## 二次関数

次の二次関数のグラフをかけ。（109頁, 基礎例題66）

(1)　$y = 2x^2-4x-1$

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

x = np.arange(-1.8, 4 , 0.2)
y = 2 * x ** 2 - 4 * x - 1

plt.style.use('ggplot')

plt.plot(x,y)
plt.axhline(y=0, color='gray')
plt.axvline(x=0, color='gray')
plt.xlabel("x")
plt.ylabel("y")

plt.show()
```

（SymPyは出てきませんでした..）

## 2次方程式

次の2次方程式を解け。（141頁, 基礎例題85）

（1） $x^2-x-20$

（2） $x^2-12x-36$

```python
import sympy
from sympy.solvers import solve

x = sympy.symbols('x')

expr = x**2 - x - 20
solve(expr, x)
```

    [-4, 5]

答え:　$x = -4, 5$

```python
expr = x**2 - 12 * x + 36
solve(expr, x)
```

    [6]

答え:　$x = 6$