# Convolution of Rectangles

Examining the effects of geometric operations on Rectangles
by convolution and how this can be expressed programatically in Python.

```python
%matplotlib inline
```

```python
import numpy as np
import sympy as sym
import scipy as sp
from scipy import signal

import matplotlib.pyplot as plt
```

# Numerical Convoltion with SciPy

Information on the included convolution function with `scipy` can be
found in the
[documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.convolve.html).

## 1D Convolution of 2 Rectangles

In the following, $2$ rectangles, $R_{1}$ and $R_{2}$, are represented by
_rectangular pulse_ signals describing the rectangle that is being moved,
and the constraint the other rectangle imposes.
In terms of signal processing, $R_{1}$ is the _signal_ and $R_{2}$ is the
_filter_.

### Overlap

$R_{1}$ (`R1`) in this example is the rectangle that
gets _moved_ from left to right. With $R_{1,l} = 100$ and $R_{1,r} = 200$.
$R_{2}$ (`R2`) is any rectangle, imposing an overlap constraint on $R_{1}$.
In this case, $R_{2}$ is defined by $R_{2,l} = 120$ and $R_{2,r} = 180$.
Where the result of the convolution (`res`) of these signals is $0$ whenever
this constaint is not violated.

```python
R1 = lambda x: 0. if x <= 100 or x >= 200 else 1.
R2 = lambda x: 0. if x <= 120 or x >= 180 else 1.

x = np.arange(301)                          # vector from 0 to 300 (discrete)
R1_eval = [R1(x_) for x_ in x]              # Evaluates R1 for each value in t
R2_eval = [R2(x_) for x_ in x]              # Evaluates R2 for each value in t
#R2_eval = signal.unit_impulse(300, 150)    # For a rectangle with minimal length (dirac)

# Divide by sum over filter, to scale back y-values of convolution back to [0 ... 1]
res = signal.convolve(R1_eval, R2_eval, mode='same') / sum(R2_eval)
```

#### Plot the Result

```python
fig, (ax_R1, ax_R2, ax_res) = plt.subplots(3, 1, sharex=True)

ax_R1.plot(R1_eval)
ax_R1.set_title('$R_{1}$: Moving')
ax_R1.margins(0, 0.1)
ax_R2.plot(R2_eval)
ax_R2.set_title('$R_{2}$: Constraining')
ax_R2.margins(0, 0.1)
ax_res.plot(res)
ax_res.set_title('Legal Placement')
ax_res.margins(0, 0.1)
fig.tight_layout()
#fig.show()
```

### Minimal Edge Distance
$R_{1}$ is again the rectangle, which is moved along the $x$-axis,
with $R_{1,l} = 140$ and $R_{1,r} = 160$.
$R_{2}$ keeps the same dimensions as above, however this time
it is described by the minium legal edge dsitance to any other shape.
In this example it is sepcified as $2$ arbitrary units in either direction
of its edge.

**Note**: The functions from above can also be defined by multiplying
the _boolean_ return value of `(x < y)` by a floating point number, denoted by
the `.` after the numeral.
A boolean expression returns `True` or `False` which may be interpreted as
`1` and `0` respectively.
If multiplied by `1.`, the datatype is converted to `float`
and the value may be used further in calculations.

$$
R_{1}(x) = 1 \cdot
\begin{cases}
    1, & \text{if } x \lt 160 \\
    0, & \text{otherwise}
\end{cases} - 1 \cdot
\begin{cases}
    1, & \text{if } x \lt 140 \\
    0, & \text{otherwise}
\end{cases}
$$

```python
R1 = lambda x: (1. * (x < 160)) - (1. * (x < 140))
R2 = lambda x: ((1. * (x < 122)) - (1. * (x < 118)))\
             + ((1. * (x < 182)) - (1. * (x < 178)))   # '\' denotes a new line

x = np.linspace(0,300,num=3000)     # vector from 0 to 300 (pseudo continuous)
R1_eval = [R1(x_) for x_ in x]      # Evaluates R1 for 3000 steps between 0 and 300
R2_eval = [R2(x_) for x_ in x]      # Evaluates R2 for 3000 steps between 0 and 300

# 1/2 * Σ(R2_eval) because the filter now has 2 pulses
res = signal.convolve(R1_eval, R2_eval, mode='same') / (.5 * sum(R2_eval))
```

#### Plot the Results

```python
fig, (ax_R1, ax_R2, ax_res) = plt.subplots(3, 1, sharex=True)

ax_R1.plot(R1_eval)
ax_R1.set_title('$R_{1}$: Moving')
ax_R1.margins(0, 0.1)
ax_R2.plot(R2_eval)
ax_R2.set_title('$R_{2}$: Constraining')
ax_R2.margins(0, 0.1)
ax_res.plot(res)
ax_res.set_title('Legal Placement')
ax_res.margins(0, 0.1)
fig.tight_layout()
#fig.show()
```

As seen above, this smaller $R_{1}$ fits _inside_ $R_{2}$.
Restoring $R_{2}$'s orignial size from the first scenario,
with $R_{1,l} = 100$ and $R_{1,r} = 200$,
making it larger than the legal _gap_ inside $R_{2}$, shows that it can
now only be placed outside of $R_{2}$

```python
R1 = lambda x: (1. * (x < 200)) - (1. * (x < 100))
R2 = lambda x: ((1. * (x < 122)) - (1. * (x < 118)))\
             + ((1. * (x < 182)) - (1. * (x < 178)))

t = np.arange(301)
R1_eval = [R1(x_) for x_ in x]
R2_eval = [R2(x_) for x_ in x]

res = signal.convolve(R1_eval, R2_eval, mode='same') / (.5 * sum(R2_eval))
```

#### Plot the Result

```python
fig, (ax_R1, ax_R2, ax_res) = plt.subplots(3, 1, sharex=True)

ax_R1.plot(R1_eval)
ax_R1.set_title('$R_{1}$: Moving')
ax_R1.margins(0, 0.1)
ax_R2.plot(R2_eval)
ax_R2.set_title('$R_{2}$: Constraining')
ax_R2.margins(0, 0.1)
ax_res.plot(res)
ax_res.set_title('Legal Placement')
ax_res.margins(0, 0.1)
fig.tight_layout()
#fig.show()
```

### Interpreting the Results

This plot also shows, what happens if an operation violates
more than one constraint.
The value of `res` denotes the amount
of constraints that are being violated at a given $x$.

## 2D Convolution of 2 Rectangles

`scipy` comes with the appropriate function to do 2D convolution much the same way.

### Overlap

Here $R_{1}$ is the rectangle that is moved across $2$D space, called the
_filter_ or _kernel_. $R_{2}$ is constraining this operation, by only allowing
$R_{1}$ to be placed where it doesn't overap.
This particular $2$D space is defined as a $15 \times 15$ grid.
For simplification, $0$s denote empty / _legal_ grid space,
and $1$ denotes the presence of a shape which may not be overlapped.

```python
w,h = 15,15                             # Grid dimensions

# The filter has to be centered within the grid.
R1 = np.pad( np.ones([3, 3])            # 3x3 square matrix with 1 at each index describing the kernel
           , (6, 6)                     # padding for 6 indices in each derection, for both axis
           , 'constant'                 # padding with constant values
           , constant_values=(0, 0) )   # these constant values are 0 for both axis

R2 = np.zeros([w, h])                   # Empty base grid (0s)
R2[6:9, 3:7] = 1                        # Place random rectangle

res = signal.convolve2d(R1, R2, boundary='symm', mode='same')
```

#### Plot the Results

```python
fig, (ax_R1, ax_R2, ax_res) = plt.subplots(1, 3)

ax_R1.set_aspect('equal')
ax_R1.imshow(R1, interpolation='nearest')
ax_R1.set_title('$R_{1}$: Moving')

ax_R2.set_aspect('equal')
ax_R2.imshow(R2, interpolation='nearest')
ax_R2.set_title('$R_{2}$: Constraining')
ax_res.set_aspect('equal')
ax_res.imshow(res, interpolation='nearest')
ax_res.set_title('Legal Placement for $R_{1}$')
fig.tight_layout()
#fig.show()
```

# Symbolic Convolution with Sympy

General information on symbolic integration with `sympy` can be found in the
[documentation](https://docs.sympy.org/latest/modules/integrals/integrals.html?highlight=integrate#sympy.integrals.integrals.integrate).

## 1D Continuos Convolution

$1$ Dimensional time continuous convolution is defined as

$$ f(t) * g(t) = \int^{\infty}_{\infty} f(\tau) \cdot g(t - \tau) d\tau $$

Which can be expressed in Programatically as follows:

```python
def convolve_1D(f, g, t, lower=-sym.oo, upper=sym.oo):
    tau = sym.symbols('tau')
    convolution = sym.integrate( f.subs(t, tau) * g.subs(t, t - tau)
                               , (tau, lower, upper))
    return convolution
```

Or shorter, however this implies that symbols `t` and `tau` are defined
outside the function.
Additionaly, this may cause problems when evaluating,
because no integral limits are specified.

```python
tau = sym.symbols('tau')
t = sym.symbols('t')
conv_1D = lambda f,g,t: sym.integrate(f.subs(t, tau) * g.subs(t, (t - tau)), tau)
```

Additionally, if $\mathcal{F}$ denotes the _Fourier Transform Operator_,
the convolution theorem states that:
$$ f * g = \mathcal{F}^{-1} \{ \mathcal{F}\{f\} \cdot \mathcal{F}\{g\}\}$$
Which can be expressed Programatically as follows:

```python
def convolve_1D_ft (f, g, t):
    tau = sym.symbols('tau')
    F = sym.fourier_transform(f, t, tau)
    G = sym.fourier_transform(g, t, tau)
    FG = sym.simplify(F * G)
    return sym.inverse_fourier_transform(FG, tau, t)
```

### Overlap

When pretending that a Rectangle moving in space is a
signal moving in time, the same scenario as in the previous section,
can no be described symbolicaly with `sympy`.

Unfortunately because of
[a bug](https://github.com/sympy/sympy/issues/18636)
sympy is currently unable to resolve the `Heaviside` name to
the corresponding `numpy` function.
So substitution has to be done manually instead of _lambdifying_.

```python
t = sym.symbols('t')

R1 = sym.Heaviside(200. - t) - sym.Heaviside(100. - t)
R2 = sym.Heaviside(150. - t) - sym.Heaviside(140. - t)

res = convolve_1D(R1, R2, t, lower=0., upper=300.)
res = sym.simplify(res)

# This doesn't work because of issue #18636
# see github: https://github.com/sympy/sympy/issues/18636
# This bug may be fixed in future versions of sympy
#R1_lamb = sym.lambdify(t, R1, modules=['numpy'])
#R2_lamb = sym.lambdify(t, R2, modules=['numpy'])

# So it has to be done this way ¯\_(ツ)_/¯
t_vals = np.linspace(0,300, num=300)
R1_eval = [R1.subs(t,t_) for t_ in t_vals]
R2_eval = [R2.subs(t,t_) for t_ in t_vals]

# +150 is necessary because the base signal R1 wasn't 0 centered and
# convolve_1D has no "mode='same'" option (yet).

res_eval = [res.subs(t,t_+150) for t_ in t_vals]
```

#### Plot the Results

```python
fig, (ax_R1, ax_R2, ax_res) = plt.subplots(3, 1, sharex=True)

ax_R1.plot(R1_eval)
ax_R1.set_title('$R_{1}$: Moving')
ax_R1.margins(0, 0.1)
ax_R2.plot(R2_eval)
ax_R2.set_title('$R_{2}$: Constraining')
ax_R2.margins(0, 0.1)
ax_res.plot(res_eval)
ax_res.set_title('Legal Placement')
ax_res.margins(0, 0.1)
fig.tight_layout()
#fig.show()
```

#### Examin the Result

The result does not have to be evaluated, it can
be examined symbolicaly as well.

```python
sym.pretty_print(res)
```

    -(t - 350.0)⋅θ(350.0 - t)⋅θ(450.0 - t) + (t - 340.0)⋅θ(340.0 - t)⋅θ(440.0 - t)
     + (t - 250.0)⋅θ(250.0 - t)⋅θ(450.0 - t) - (t - 240.0)⋅θ(240.0 - t)⋅θ(440.0 -
    t) + ((t - 350.0)⋅θ(350.0 - t) + 200.0)⋅θ(150.0 - t) - ((t - 340.0)⋅θ(340.0 -
    t) + 200.0)⋅θ(140.0 - t) - ((t - 250.0)⋅θ(250.0 - t) + 100.0)⋅θ(150.0 - t) + (
    (t - 240.0)⋅θ(240.0 - t) + 100.0)⋅θ(140.0 - t)

### Symbolic Fourier Transform

Using scipy's `FFT` is much faster for defined intervalls. A symbolic Fourier Transform
is onl

# Feature Checklist

- [X] 1D Numeric Overlap
- [X] 1D Numeric Edge Distance
- [ ] 1D Numeric Stretch Operation
- [X] 2D Numeric Overlap
- [ ] 2D Numeric Edge Distance
- [ ] 2D Numeric Stretch Operation
- [X] 1D Symbolic with integration
- [X] 1D Symbolic with fourier transform
- [ ] Fix recursion error when calling $\mathcal{F}^{-1}$
- [X] 1D Symbolic Overlap
- [ ] 1D Symbolic Edge Distance
- [ ] 1D Symbolic Stretch Operation
- [X] Mention `sympy` `lambdify` bug for `Heaviside`
- [ ] Implement 'same' option for `conv_1D` function
- [X] Mention boolean value multiplication behaviour when evaluating `sympy` functions
- [ ] 2D Symbolic with integration
- [ ] 2D Symbolic with fourier transform
- [ ] 2D Symbolic Overlap
- [ ] 2D Symbolic Edge Distance
- [ ] 2D Symbolic Stretch Operation

```python

```


```python
from IPython.display import Image
from IPython.core.display import HTML
from sympy import *
Image(url= "https://i.imgur.com/7iKqGvi.png")
```

```python
G = Function("G")
t,x = symbols("t x")
G = lambda x: tan(t)
e0 = Integral(G(x),(t,1,x))
e0
```

$\displaystyle \int\limits_{1}^{x} \tan{\left(t \right)}\, dt$

```python
Derivative(e0)
```

$\displaystyle \frac{d}{d x} \int\limits_{1}^{x} \tan{\left(t \right)}\, dt$

```python
Derivative(e0).doit()
```

$\displaystyle \frac{\sin{\left(x \right)}}{\cos{\left(x \right)}}$

```python
print((Derivative(e0).doit().subs(x,pi/5)))
```

    sqrt(5/8 - sqrt(5)/8)/(1/4 + sqrt(5)/4)

```python
Image(url= "https://i.imgur.com/0zlmqZx.png")
```

```python
Image(url= "https://i.imgur.com/ShD4hGp.png")
```

```python
G = Function("G")
g = Function("g")

t,x = symbols("t x")
G = lambda x: (1+sqrt(t))
e1 = Integral(G(x),(t,8,x))
Eq(g(x),e1)
```

$\displaystyle g{\left(x \right)} = \int\limits_{8}^{x} \left(\sqrt{t} + 1\right)\, dt$

```python
e2 = Derivative(e1)
Eq(Derivative(g(x)),e2)
```

$\displaystyle \frac{d}{d x} g{\left(x \right)} = \frac{d}{d x} \int\limits_{8}^{x} \left(\sqrt{t} + 1\right)\, dt$

```python
e3 = Eq(e2,e2.doit())
e3
```

$\displaystyle \frac{d}{d x} \int\limits_{8}^{x} \left(\sqrt{t} + 1\right)\, dt = \sqrt{x} + 1$

```python
print(e3.rhs)
```

    sqrt(x) + 1

```python
Image(url= "https://i.imgur.com/7LsE1Dw.png")
```

```python
Image(url= "https://i.imgur.com/t1lTIAX.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda t: ((x**2+11*x+30)/(1+cos(x)**2))
e4 = Integral(F(t),(x,0,t))
Eq(f(t),e4)
```

$\displaystyle f{\left(t \right)} = \int\limits_{0}^{t} \frac{x^{2} + 11 x + 30}{\cos^{2}{\left(x \right)} + 1}\, dx$

```python
e5 = e4.doit()
e5
```

$\displaystyle \int\limits_{0}^{t} \frac{\left(x + 5\right) \left(x + 6\right)}{\cos^{2}{\left(x \right)} + 1}\, dx$

```python
Image(url= "https://i.imgur.com/NxLeX4E.png")

```

```python
Image(url= "https://i.imgur.com/MTPdAgx.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: (t**3+6*t**2+2)
e6 = Integral(F(t),(t,0,x))
Eq(f(t),e6)
```

$\displaystyle f{\left(t \right)} = \int\limits_{0}^{x} \left(t^{3} + 6 t^{2} + 2\right)\, dt$

```python
e7 = e6.doit()
e8 = Eq(e6,e7)
e9 = e8.rhs
e8
```

$\displaystyle \int\limits_{0}^{x} \left(t^{3} + 6 t^{2} + 2\right)\, dt = \frac{x^{4}}{4} + 2 x^{3} + 2 x$

```python
e10 = diff(e9,x,2)
e10
```

$\displaystyle 3 x \left(x + 4\right)$

```python
print(e10)
```

    3*x*(x + 4)

```python
Image(url= "https://i.imgur.com/CAdGget.png")
```

```python
Image(url= "https://i.imgur.com/IyekLnu.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: sin(t**4)
e11 = Integral(F(t),(t,x,1))
e12 = Eq(f(x),e11)
e12
```

$\displaystyle f{\left(x \right)} = \int\limits_{x}^{1} \sin{\left(t^{4} \right)}\, dt$

```python
e13 = e12.rhs
diff(e13)
```

$\displaystyle - \sin{\left(x^{4} \right)}$

```python
print(diff(e13))
```

    -sin(x**4)

```python
Image(url= "https://i.imgur.com/2G8LUnx.png")
```

```python
Image(url= "https://i.imgur.com/PQ8nOPj.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: tan(t**4)
e14 = Integral(F(x),(t,x,19))
Eq(f(x),e14)
```

$\displaystyle f{\left(x \right)} = \int\limits_{x}^{19} \tan{\left(t^{4} \right)}\, dt$

```python
e15 = diff(e14)
e15
```

$\displaystyle - \tan{\left(x^{4} \right)}$

```python
print(e15)
```

    -tan(x**4)

```python
Image(url= "https://i.imgur.com/7OZnZgw.png")
```

```python
Image(url= "https://i.imgur.com/76QKwRr.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: cos(t)/t**7
e16 = Integral(F(x),(t,-8,sqrt(x)))
Eq(f(x),e16)
```

$\displaystyle f{\left(x \right)} = \int\limits_{-8}^{\sqrt{x}} \frac{\cos{\left(t \right)}}{t^{7}}\, dt$

```python
diff(e16)
```

$\displaystyle \frac{\cos{\left(\sqrt{x} \right)}}{2 x^{4}}$

```python
print(diff(e16))
```

    cos(sqrt(x))/(2*x**4)

```python
Image(url= "https://i.imgur.com/8ZNAHU1.png")
```

```python
Image(url= "https://i.imgur.com/PKtOwYp.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: 1/(3+t+3*t**2)
e17 = Integral(F(x),(t,1,x))
Eq(f(x),e17)
```

$\displaystyle f{\left(x \right)} = \int\limits_{1}^{x} \frac{1}{3 t^{2} + t + 3}\, dt$

```python
plot(e17)
```

```python
e18 = e17.doit()
e18
```

$\displaystyle \frac{2 \sqrt{35} \operatorname{atan}{\left(\frac{6 \sqrt{35} x}{35} + \frac{\sqrt{35}}{35} \right)}}{35} - \frac{2 \sqrt{35} \operatorname{atan}{\left(\frac{\sqrt{35}}{5} \right)}}{35}$

```python
# source https://www.numerade.com/ask/question/find-the-interval-on-which-the-curve-v-6t4t-dt-is-concave-upward-note-when-using-interval-notation-in-webwork-you-use-for-i-for-0and-u-for-the-union-symbol-interval-37414/
solve(diff(e18,x,2))
```

    [-1/6]

```python
plot(diff(e18,x,1))
```

```python
diff(e18).subs(x,0)
```

$\displaystyle \frac{1}{3}$

```python
Image(url= "https://i.imgur.com/5tdxJ27.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: (t**2 - 9)/(1+cos(t)**2)
e19 = Integral(F(x),(t,0,x))
Eq(f(x),e19)
```

$\displaystyle f{\left(x \right)} = \int\limits_{0}^{x} \frac{t^{2} - 9}{\cos^{2}{\left(t \right)} + 1}\, dt$

```python
e19.doit()
```

$\displaystyle \int\limits_{0}^{x} \frac{\left(t - 3\right) \left(t + 3\right)}{\cos^{2}{\left(t \right)} + 1}\, dt$

```python
Image(url= "https://i.imgur.com/kvmNL0r.png")
```

```python
Image(url= "https://i.imgur.com/1KWdpIf.png")
```

```python
F = Function("F")
f = Function("f")

t,x = symbols("t x")
F = lambda x: 2*sqrt(x)
e20 = Integral(F(x),(x,4,26))
Eq(f(x),e20)
```

$\displaystyle f{\left(x \right)} = \int\limits_{4}^{26} 2 \sqrt{x}\, dx$

```python
e20.doit()
```

$\displaystyle - \frac{32}{3} + \frac{104 \sqrt{26}}{3}$

```python
print(e20.doit())
```

    -32/3 + 104*sqrt(26)/3

```python
Image(url= "https://i.imgur.com/w4Obpx3.png")
```

```python
Image(url= "https://i.imgur.com/5OBJTxf.png")
```

```python
F = Function("F");f = Function("f"); t,x = symbols("t x")
F = lambda x: 32*x**4
e21 = Integral(F(x),(x,0,2))
Eq(f(x),e21)
```

$\displaystyle f{\left(x \right)} = \int\limits_{0}^{2} 32 x^{4}\, dx$

```python
print(e21.doit().evalf())
```

    204.800000000000

```python
Image(url= "https://i.imgur.com/GrRSMUT.png")
```

```python
Image(url= "https://i.imgur.com/sCUjhRD.png")
```

```python
F = Function("F");f = Function("f"); t,x,r = symbols("t x r")
F = lambda x: (cbrt(r)+(1/cbrt(r)))
e22 = Integral(F(x),(r,0,2))
Eq(f(x),e22)
```

$\displaystyle f{\left(x \right)} = \int\limits_{0}^{2} \left(\sqrt[3]{r} + \frac{1}{\sqrt[3]{r}}\right)\, dr$

```python
print(e22.doit().evalf())
```

    4.27098315279461

```python
Image(url= "https://i.imgur.com/VncNNIi.png")
```

```python
Image(url= "https://i.imgur.com/RlDkGh4.png")
```

```python
F = Function("F");f = Function("f"); t,x,r,u = symbols("t x r u")
F = lambda x: u*(sqrt(u)+u**(Rational(1,5)))
e23 = Integral(F(x),(u,0,1))
Eq(f(x),e23)
```

$\displaystyle f{\left(x \right)} = \int\limits_{0}^{1} u \left(\sqrt[5]{u} + \sqrt{u}\right)\, du$

```python
e23.doit()
```

$\displaystyle \frac{47}{55}$

```python
Image(url= "https://i.imgur.com/swHKv0O.png")
```

```python
Image(url= "https://i.imgur.com/7AWdfU7.png")
```

```python
F = Function("F");f = Function("f"); t,x,r,u = symbols("t x r u")
F = lambda x: (x-1)*(8*x+3)
e24 = Integral(F(x),(x,2,-2))
Eq(f(x),e24)
```

$\displaystyle f{\left(x \right)} = \int\limits_{2}^{-2} \left(x - 1\right) \left(8 x + 3\right)\, dx$

```python
e24.doit()
```

$\displaystyle - \frac{92}{3}$

```python
Image(url= "https://i.imgur.com/5Ax16FE.png")
```

```python
Image(url= "https://i.imgur.com/Tkau0B8.png")
```

```python
F = Function("F");f = Function("f"); t,x,r,u = symbols("t x r u")
F = lambda x: abs(13*x**2-x**3-40*x)
e25 = Integral(F(x),(x,-1,7))
Eq(f(x),e25)
```

$\displaystyle f{\left(x \right)} = \int\limits_{-1}^{7} \left|{x^{3} - 13 x^{2} + 40 x}\right|\, dx$

```python
e25.doit()
```

$\displaystyle \frac{959}{6}$

```python
Image(url= "https://i.imgur.com/WYpzw18.png")
```


# Hydrogen Wave Function

```python
#Import libraries
from numpy import *
import matplotlib.pyplot as plt
from sympy.physics.hydrogen import Psi_nlm
```

## Analytical Equation
$$
\psi_{n \ell m}(r, \theta, \varphi)=\sqrt{\left(\frac{2}{n a_{0}^{*}}\right)^{3} \frac{(n-\ell-1) !}{2 n(n+\ell) !}} e^{-\rho / 2} \rho^{\ell} L_{n-\ell-1}^{2 \ell+1}(\rho) Y_{\ell}^{m}(\theta, \varphi)
$$

```python
#Symbolic representation
from sympy import Symbol
```

```python
#Symbolic variables
R=Symbol("R", real=True, positive=True)
Φ=Symbol("Φ", real=True)
Θ=Symbol("Θ", real=True)
Z=Symbol("Z", positive=True, integer=True, nonzero=True)
```

```python
#Analytical wave function
Psi_nlm(1,0,0,R,Φ,Θ,Z) #First three entries are n,l and m
```

$\displaystyle \frac{Z^{\frac{3}{2}} e^{- R Z}}{\sqrt{\pi}}$

```python
#Numerical hydrogen wave function
#Points array contains r and θ for fixed ϕ=0
def wavefunc(point,n,l,m,Z=1):
    r=point[0]
    θ=point[1]
    ϕ=point[2]
    return abs(Psi_nlm(n,l,m,r,ϕ,θ,Z))
```

```python
#Transform polar coordinates into cartesian to plot
def polar2cart(r, theta):
    return (
         r * cos(theta),
         r * sin(theta)
    )
```

```python
%%timeit
wavefunc([1,.1,.2],4,3,2,Z=1)
```

    72.8 µs ± 276 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

```python
import sympy
```

```python
n, l, m, r, phi, theta = sympy.symbols("n, l, m, r, phi, theta")

lam_psi = sympy.lambdify([n, l, m, r, phi, theta], Psi_nlm(n, l, m, r, phi, theta, Z=1))
```

```python
%%timeit
lam_psi(4,3,2,1,.1,.2)
```

    22.6 µs ± 70.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

```python
#2D polar variables
# 0.01 < r < 5  &  0 < θ < 2π
r = linspace(0.01,5,10)
ϕ = linspace(0,pi,20)
θ = linspace(0,2*pi,20)
```

```python
rm , tm, pm = meshgrid(r,θ,ϕ)
```

```python
%%timeit
out = lam_psi(4,3,2,rm,pm,tm)
```

    419 µs ± 788 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)

```python
#Create polar grid
grid = []
for i in r:
    for j in θ:
            grid.append([i,j])
grid = array(grid)
```

```python
#Cartesian grid
x, y = array((polar2cart(grid[:,0],grid[:,1])))
```

```python
#Import parallel libraries
from joblib import Parallel, delayed
import multiprocessing
num_cores = multiprocessing.cpu_count()
```

```python
#Call wavefunc in parallel
n, l, m = 1 ,0 ,0
results = Parallel(n_jobs=num_cores)(delayed(wavefunc)(i,n,l,m) for i in grid)
```

```python
#Plot figure
fig = plt.figure()
plt.scatter(x, y,c=results,cmap="magma")
plt.colorbar()
plt.axis("equal")
plt.show()

```

```python

```


Construir o gráfico e encontrar o foco e uma equação da diretriz.

1) $x^2 = -4y$

 $2p = -4$, logo

$P = -2$

Calculando o foco

$F = \frac{P}{2}$

$F = -\frac{2}{2}$

$F = 1$, $F(0,-1)$

Calculando a diretriz

$d = -\frac{p}{2}$

$d = -\frac{2}{2}$

$d : y = - 1$

$V(0,0)$

$F(0,-1)$

Gráfico da parábola

```python
from sympy import *
from sympy.plotting import plot_implicit
x = symbols("x")
plot(x**2,(x,-3,3),
title=u'Gráfico da parábola', xlabel='x', ylabel='y');
```

Problemas Propostos. Pag. 172 - 175/01.ipynb


- - - -
# Mechpy Tutorials
a mechanical engineering toolbox

source code - https://github.com/nagordon/mechpy
documentation - https://nagordon.github.io/mechpy/web/

- - - -

Neal Gordon
2017-02-20

- - - -

## Composite Plate Mechanics with Python

reference: hyer page 584. 617

The motivation behind this talk is to explore the capability of python as a scientific computation tool as well as solve a typical calcuation that could either be done by hand, or coded. I find coding to be a convient way to learn challenging mathmatics because I can easily replicate my work in the future when I can't remember the details of the calcuation or, if there are any errors, they can typically be easily fixed and the other calcuations re-ran without a lot of duplcation of effort.

Composite mechanics can be very iterative by nature and is easiest to employ linear algebra to find displacements, strains and stresses of composites. Coding solutions is also handy when visualizations are required.

For this example, we are interested in calcuating the stress critical ply in a simple asymteric composite plate with a pressure load applied. We can chooose a variety of boundary conditions of our plate, but this solution is limited to 2 dimensional displacements, x and z. If we are interested in 3 dimensional displacements, the problem becomes much more challenging as partial differentiation of the governing equations gives us a PDE, which is more challenging to solve.

The steps to solving are
- Identify governing and equilibrium equations
- import python required libraries
- declare symbolic variables
- declare numeric variables, including material properties, plate dimensions, and plate pressure
- solve 4th order differntial equation with 7 constants
- apply plate boundary conditions and acquire u(x) and w(x) displacement functions
- acquire strain equations from displacement
- acquire stress equations from strain
- determine critical ply from highest ply stress ratio

```python
# Import Python modules and
import numpy as np
from sympy import *
from pprint import pprint

# printing and plotting settings
init_printing(use_latex='mathjax')
get_ipython().magic('matplotlib inline') # inline plotting

x,y,q = symbols('x,y,q')
```

As mentioned before, if we want to perform a 3 dimensional displacement model of the composite plate, we would have 6 reaction forces that are a function of x and y. Those 6 reaction forces are related by 3 equalibrium equations

```python
# # hyer page 584
# # Equations of equilibrium
# Nxf = Function('N_x')(x,y)
# Nyf = Function('N_y')(x,y)
# Nxyf = Function('N_xy')(x,y)
# Mxf = Function('M_x')(x,y)
# Myf = Function('M_y')(x,y)
# Mxyf = Function('M_xy')(x,y)

# symbols for force and moments
Nx,Ny,Nxy,Mx,My,Mxy = symbols('N_x,N_y,N_xy,M_x,M_y,M_xy')
Nxf,Nyf,Nxyf,Mxf,Myf,Mxyf = symbols('Nxf,Nyf,Nxyf,Mxf,Myf,Mxyf')
```

```python
Eq(0,diff(Nx(x,y), x)+diff(Nxy(x,y),y))
```

$$0 = \frac{\partial}{\partial x} \operatorname{N_{x}}{\left (x,y \right )} + \frac{\partial}{\partial y} \operatorname{N_{xy}}{\left (x,y \right )}$$

```python
Eq(0,diff(Nxy(x,y), x)+diff(Ny(x,y),y))
```

$$0 = \frac{\partial}{\partial x} \operatorname{N_{xy}}{\left (x,y \right )} + \frac{\partial}{\partial y} \operatorname{N_{y}}{\left (x,y \right )}$$

```python
Eq(0,  diff(Mx(x,y),x,2) + 2*diff(Mxy(x,y),x,y) + diff(My(x,y) ,y,2)+ q  )
```

$$0 = q + \frac{\partial^{2}}{\partial x^{2}}  \operatorname{M_{x}}{\left (x,y \right )} + 2 \frac{\partial^{2}}{\partial x\partial y}  \operatorname{M_{xy}}{\left (x,y \right )} + \frac{\partial^{2}}{\partial y^{2}}  \operatorname{M_{y}}{\left (x,y \right )}$$

What makes composite plates special is the fact that they typically not isotropic. This is handled by the 6x6 ABD matrix that defines the composites properties axially, in bending, and the coupling between the two.

```python
# composite properties
A11,A22,A66,A12,A16,A26,A66 = symbols('A11,A22,A66,A12,A16,A26,A66')
B11,B22,B66,B12,B16,B26,B66 = symbols('B11,B22,B66,B12,B16,B26,B66')
D11,D22,D66,D12,D16,D26,D66 = symbols('D11,D22,D66,D12,D16,D26,D66')

## constants of integration when solving differential equation
C1,C2,C3,C4,C5,C6 = symbols('C1,C2,C3,C4,C5,C6')

# plate and composite parameters
th,a,b = symbols('th,a,b')

# displacement functions
u0 = Function('u0')(x,y)
v0 = Function('v0')(x,y)
w0 = Function('w0')(x,y)
```

Let's compute our 6 displacement conditions which is where our PDE's show up

```python
Nxf = A11*diff(u0,x) + A12*diff(v0,y) + A16*(diff(u0,y) + diff(v0,x)) - B11*diff(w0,x,2) - B12*diff(w0,y,2) - 2*B16*diff(w0,x,y)
Eq(Nx, Nxf)
```

$$N_{x} = A_{11} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + A_{12} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + A_{16} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - B_{11} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{12} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{16} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
Nyf = A12*diff(u0,x) + A22*diff(v0,y) + A26*(diff(u0,y) + diff(v0,x)) - B12*diff(w0,x,2) - B22*diff(w0,y,2) - 2*B26*diff(w0,x,y)
Eq(Ny,Nyf)
```

$$N_{y} = A_{12} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + A_{22} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + A_{26} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - B_{12} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{22} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{26} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
Nxyf = A16*diff(u0,x) + A26*diff(v0,y) + A66*(diff(u0,y) + diff(v0,x)) - B16*diff(w0,x,2) - B26*diff(w0,y,2) - 2*B66*diff(w0,x,y)
Eq(Nxy,Nxyf)
```

$$N_{xy} = A_{16} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + A_{26} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + A_{66} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - B_{16} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{26} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{66} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
Mxf = B11*diff(u0,x) + B12*diff(v0,y) + B16*(diff(u0,y) + diff(v0,x)) - D11*diff(w0,x,2) - D12*diff(w0,y,2) - 2*D16*diff(w0,x,y)
Eq(Mx,Mxf)
```

$$M_{x} = B_{11} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + B_{12} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + B_{16} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - D_{11} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - D_{12} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 D_{16} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
Myf = B12*diff(u0,x) + B22*diff(v0,y) + B26*(diff(u0,y) + diff(v0,x)) - D12*diff(w0,x,2) - D22*diff(w0,y,2) - 2*D26*diff(w0,x,y)
Eq(My,Myf)
```

$$M_{y} = B_{12} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + B_{22} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + B_{26} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - D_{12} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - D_{22} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 D_{26} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
Mxyf = B16*diff(u0,x) + B26*diff(v0,y) + B66*(diff(u0,y) + diff(v0,x)) - D16*diff(w0,x,2) - D26*diff(w0,y,2) - 2*D66*diff(w0,x,y)
Eq(Mxy,Mxyf)
```

$$M_{xy} = B_{16} \frac{\partial}{\partial x} \operatorname{u_{0}}{\left (x,y \right )} + B_{26} \frac{\partial}{\partial y} \operatorname{v_{0}}{\left (x,y \right )} + B_{66} \left(\frac{\partial}{\partial y} \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial}{\partial x} \operatorname{v_{0}}{\left (x,y \right )}\right) - D_{16} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - D_{26} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 D_{66} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

Now, combine our 6 displacement conditions with our 3 equalibrium equations to get three goverening equations

```python
eq1 = diff(Nxf,x) + diff(Nxf,y)
eq1
```

$$A_{11} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + A_{11} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + A_{12} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{v_{0}}{\left (x,y \right )} + A_{12} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{v_{0}}{\left (x,y \right )} + A_{16} \left(\frac{\partial^{2}}{\partial x\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{2}}{\partial x^{2}}  \operatorname{v_{0}}{\left (x,y \right )}\right) + A_{16} \left(\frac{\partial^{2}}{\partial y^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{2}}{\partial x\partial y}  \operatorname{v_{0}}{\left (x,y \right )}\right) - B_{11} \frac{\partial^{3}}{\partial x^{3}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{11} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{w_{0}}{\left (x,y \right )} - B_{12} \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{12} \frac{\partial^{3}}{\partial y^{3}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{16} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{16} \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
eq2 = diff(Nxyf,x) + diff(Nyf,y)
eq2
```

$$A_{12} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + A_{16} \frac{\partial^{2}}{\partial x^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + A_{22} \frac{\partial^{2}}{\partial y^{2}}  \operatorname{v_{0}}{\left (x,y \right )} + A_{26} \left(\frac{\partial^{2}}{\partial y^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{2}}{\partial x\partial y}  \operatorname{v_{0}}{\left (x,y \right )}\right) + A_{26} \frac{\partial^{2}}{\partial x\partial y}  \operatorname{v_{0}}{\left (x,y \right )} + A_{66} \left(\frac{\partial^{2}}{\partial x\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{2}}{\partial x^{2}}  \operatorname{v_{0}}{\left (x,y \right )}\right) - B_{12} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{w_{0}}{\left (x,y \right )} - B_{16} \frac{\partial^{3}}{\partial x^{3}}  \operatorname{w_{0}}{\left (x,y \right )} - B_{22} \frac{\partial^{3}}{\partial y^{3}}  \operatorname{w_{0}}{\left (x,y \right )} - 3 B_{26} \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 B_{66} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{w_{0}}{\left (x,y \right )}$$

```python
eq3 = diff(Mxf,x,2) + 2*diff(Mxyf,x,y) + diff(Myf,y,2) + q
eq3
```

$$B_{11} \frac{\partial^{3}}{\partial x^{3}}  \operatorname{u_{0}}{\left (x,y \right )} + B_{12} \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + B_{12} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{v_{0}}{\left (x,y \right )} + B_{16} \left(\frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{3}}{\partial x^{3}}  \operatorname{v_{0}}{\left (x,y \right )}\right) + 2 B_{16} \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{u_{0}}{\left (x,y \right )} + B_{22} \frac{\partial^{3}}{\partial y^{3}}  \operatorname{v_{0}}{\left (x,y \right )} + B_{26} \left(\frac{\partial^{3}}{\partial y^{3}}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{v_{0}}{\left (x,y \right )}\right) + 2 B_{26} \frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{v_{0}}{\left (x,y \right )} + 2 B_{66} \left(\frac{\partial^{3}}{\partial x\partial y^{2}}  \operatorname{u_{0}}{\left (x,y \right )} + \frac{\partial^{3}}{\partial x^{2}\partial y}  \operatorname{v_{0}}{\left (x,y \right )}\right) - D_{11} \frac{\partial^{4}}{\partial x^{4}}  \operatorname{w_{0}}{\left (x,y \right )} - 2 D_{12} \frac{\partial^{4}}{\partial x^{2}\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} - 4 D_{16} \frac{\partial^{4}}{\partial x^{3}\partial y}  \operatorname{w_{0}}{\left (x,y \right )} - D_{22} \frac{\partial^{4}}{\partial y^{4}}  \operatorname{w_{0}}{\left (x,y \right )} - 4 D_{26} \frac{\partial^{4}}{\partial x\partial y^{3}}  \operatorname{w_{0}}{\left (x,y \right )} - 4 D_{66} \frac{\partial^{4}}{\partial x^{2}\partial y^{2}}  \operatorname{w_{0}}{\left (x,y \right )} + q$$

Yikes, I do not want to solve that (at least right now). If we make the assumption that the plate has equal displacement of y in the x and y direction, then we can simply things ALOT! These simplifications are valid for cross ply unsymmetric laminates plate, Hyer pg 616. This is applied by setting some of our material properties to zero. $ A16=A26=D16=D26=B16=B26=B12=B66=0 $
Almost like magic, we now have some equations that aren't so scary.

```python
u0 = Function('u0')(x)
v0 = Function('v0')(x)
w0 = Function('w0')(x)
```

```python
Nxf = A11*diff(u0,x) + A12*diff(v0,y) - B11*diff(w0,x,2)
Eq(Nx, Nxf)
```

$$N_{x} = A_{11} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )} - B_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )}$$

```python
Nyf = A12*diff(u0,x) + A22*diff(v0,y) - B22*diff(w0,y,2)
Eq(Ny,Nyf)
```

$$N_{y} = A_{12} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )}$$

```python
Nxyf = A66*(diff(u0,y) + diff(v0,x))
Eq(Nxy,Nxyf)
```

$$N_{xy} = A_{66} \frac{d}{d x} \operatorname{v_{0}}{\left (x \right )}$$

```python
Mxf = B11*diff(u0,x) - D11*diff(w0,x,2) - D12*diff(w0,y,2)
Eq(Mx,Mxf)
```

$$M_{x} = B_{11} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )} - D_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )}$$

```python
Myf = B22*diff(v0,y) - D12*diff(w0,x,2) - D22*diff(w0,y,2)
Eq(My,Myf)
```

$$M_{y} = - D_{12} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )}$$

```python
Mxyf = 0
Eq(Mxy,Mxyf)
```

$$M_{xy} = 0$$

Now we are getting somewhere. Finally we can solve the differential equations

```python
dsolve(diff(Nx(x)))
```

$$\operatorname{N_{x}}{\left (x \right )} = C_{1}$$

```python
dsolve(diff(Mx(x),x,2)+q)
```

$$\operatorname{M_{x}}{\left (x \right )} = C_{1} + C_{2} x - \frac{q x^{2}}{2}$$

Now solve for u0 and w0 with some pixie dust

```python
eq4 = (Nxf-C1)
eq4
```

$$A_{11} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )} - B_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )} - C_{1}$$

```python
eq5 = Mxf -( -q*x**2 + C2*x + C3 )
eq5
```

$$B_{11} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )} - C_{2} x - C_{3} - D_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )} + q x^{2}$$

```python
eq6 = Eq(solve(eq4,diff(u0,x))[0] , solve(eq5, diff(u0,x))[0])
eq6
```

$$\frac{1}{A_{11}} \left(B_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )} + C_{1}\right) = \frac{1}{B_{11}} \left(C_{2} x + C_{3} + D_{11} \frac{d^{2}}{d x^{2}}  \operatorname{w_{0}}{\left (x \right )} - q x^{2}\right)$$

```python
w0f = dsolve(eq6, w0)
w0f
```

$$\operatorname{w_{0}}{\left (x \right )} = - \frac{A_{11} C_{2} x^{3}}{6 A_{11} D_{11} - 6 B_{11}^{2}} + \frac{A_{11} q x^{4}}{12 A_{11} D_{11} - 12 B_{11}^{2}} + C_{1} + C_{5} x + \frac{x^{2} \left(- A_{11} C_{3} + B_{11} C_{4}\right)}{2 A_{11} D_{11} - 2 B_{11}^{2}}$$

```python
eq7 = Eq(solve(eq6, diff(w0,x,2))[0] , solve(eq4,diff(w0,x,2))[0])
eq7
```

$$\frac{1}{A_{11} D_{11} - B_{11}^{2}} \left(- A_{11} C_{2} x - A_{11} C_{3} + A_{11} q x^{2} + B_{11} C_{1}\right) = \frac{1}{B_{11}} \left(A_{11} \frac{d}{d x} \operatorname{u_{0}}{\left (x \right )} - C_{1}\right)$$

```python
u0f = dsolve(eq7)
u0f
```

$$\operatorname{u_{0}}{\left (x \right )} = \frac{1}{A_{11} D_{11} - B_{11}^{2}} \left(- \frac{B_{11} C_{1}}{2} x^{2} - B_{11} C_{3} x - B_{11} C_{4} D_{11} + \frac{B_{11} q}{3} x^{3} + C_{2} D_{11} x + \frac{B_{11}^{3} C_{4}}{A_{11}}\right)$$

- - - -


# 1 Dimentional Ordinary Differential Equations (ODE)

## Exponential Model

\begin{equation}
\begin{split}
\frac{dx}{dt} =&  r\cdot x
\end{split}
\end{equation}

Initial value problem $x(0) = X_0$ (inital conditions). Solution is given by

$$x(t)=X_0\exp\left(r\cdot t\right)$$

```python
import numpy as np
import pandas as pd
from functools import partial
from scipy.integrate import odeint

def exponential_growth(x, t, r):
    """
        Exponential Growth model.
    """
    dxdt = r*x

    return dxdt

```

```python
initial_conditions = [10, 100, 300, 500]

import matplotlib.pyplot as plt

t_span = np.linspace(0, 5, num=100)
r_values =[ -2,  0,  2]

fig, ax = plt.subplots(1,1, figsize=(15.5, 7))
for ic in initial_conditions:
    y =  odeint(exponential_growth, y0=ic, t=t_span, args=(r_values[0],) )
    ax.plot(t_span, y, label='x(0)={}'.format(ic))
    ax.set_xlabel('Time')
    ax.set_ylabel('x(t)')
ax.set_title('r={}'.format(r_values[0]))
ax.legend()

##############################################

fig, ax = plt.subplots(1,2, figsize=(15.5, 7))
for ic in initial_conditions:
    y =  odeint(exponential_growth, y0=ic, t=t_span, args=(r_values[0],) )
    ax[0].plot(t_span, y, label='x(0)={}'.format(ic))
    ax[0].set_xlabel('Time')
    ax[0].set_ylabel('x(t)')
    ax[0].set_title('r={}'.format(r_values[0]))

    y =  odeint(exponential_growth, y0=ic, t=t_span, args=(r_values[-1],) )
    ax[1].plot(t_span, y, label='x(0)={}'.format(ic))
    ax[1].set_xlabel('Time')
    ax[1].set_ylabel('x(t)')
    ax[1].set_title('r={}'.format(r_values[-1]))
    ax[1].set_ylim([0, 1000])
```

## Logistic Model

\begin{equation}
\begin{split}
\frac{dx}{dt} =&  r\cdot x\cdot \left(1-\frac{x}{K}\right)
\end{split}
\end{equation}

```python
xlimit = (-1.3, 1.3)
ylimit = (-0.5, 0.5)

def logistic_growth(x, t, r, K):
    """
        Logistic Growth model.
    """
    dxdt = r*x*(1-x/K)

    return dxdt

initial_conditions = [10, 100, 300, 400]

import matplotlib.pyplot as plt

t_span = np.linspace(0, 5, num=100)
r_values =[ -2,  0,  2]
K = 500

fig, ax = plt.subplots(1,1, figsize=(15.5, 7))
for ic in initial_conditions:
    y =  odeint(logistic_growth, y0=ic, t=t_span, args=(r_values[0],500) )
    ax.plot(t_span, y, label='x(0)={}'.format(ic))
    ax.set_xlabel('Time')
    ax.set_ylabel('x(t)')
ax.set_title('r={}'.format(r_values[0]))
ax.legend()

##############################################

fig, ax = plt.subplots(1,1, figsize=(15.5, 7))
for ic in initial_conditions:
    y =  odeint(logistic_growth, y0=ic, t=t_span, args=(r_values[0],500) )
    ax.plot(t_span, y, label='x(0)={}'.format(ic))
    ax.set_xlabel('Time')
    ax.set_ylabel('x(t)')
    ax.set_title('r={}'.format(r_values[0]))

    y =  odeint(logistic_growth, y0=ic, t=t_span, args=(r_values[-1],500) )
    ax.plot(t_span, y, label='x(0)={}'.format(ic), linestyle='--')
    ax.set_xlabel('Time')
    ax.set_ylabel('x(t)')
ax.set_title('continious line (-) r=-2 | non-continuos line (--): r=2')

```

```python

```


Consider a sky-diver falling towards the surface of Earth. The diver brought with them a temperature sensor that took multiple measurements along the descent. The diver is interested in determining their terminal velocity given these measurements.

Problem: Given a temperature measurement, can one estimate the velocity as a function of time?

```julia
using LinearAlgebra: diagm
using Random
using CairoMakie
using ForwardDiff

CairoMakie.activate!()

include("kalmanflt.jl");
include("extendedkalmanflt.jl");
```

The system is a particle falling due to gravity WITH air resistance. For this simulation, the falling object will acheive terminal velocity. The velocity as a function of time is modeled as

$$
\begin{align}
v(t) &= v_{\infty} \tanh\left(\frac{g t}{v_{\infty}}\right)
\end{align}
$$

where $v_{\infty}$ is the terminal velocity as time approaches infinity and $g$ is the acceleration due to gravity.

What we need now is a mapping between temperature and altitude.
I will use the data from https://www.engineeringtoolbox.com/air-altitude-temperature-d_461.html and linearize the temperature model. For the terminal velocity, I make-up a weak, linear relationship between temperature and terminal velocity.

$$
\begin{align}
h_{n} &= h_{n}(t) = h_{n-1} + v_{n-1} \Delta t \\
v_{n} &= v_{n}(t,\Delta t) = v_{n-1} + \frac{\partial}{\partial t}\left[v_{\infty}\tanh\left(\frac{g t}{v_{\infty}}\right)\right] \Delta t = v_{n-1} +  g \Delta t \text{sech}^2\left(\frac{g t}{v_{\infty}}\right)\\
v_{\infty} &\rightarrow v_{\infty}(T) = m_v T + b_v \\
T &\rightarrow T(h) = m_T h + b_T
\end{align}
$$

```julia
time_steps = 150

par_values_dict = Dict(
    "time_delta_s"=>0.1,
    "VInfS_m__Ks"=>-0.1481,
    "VInfI_m__s"=>94.44,
    "TS_K__m"=>-0.006_835,
    "TI_K"=>288.706,
    "accel_m__s2"=>-9.8,
)

@enum StateIndex begin
    ALT=1
    VEL=2
    TIM=3
end
length_state_vector = length(instances(StateIndex))

@enum MeasurementIndex begin
    TEMP=1
end
length_measurement_vector = length(instances(MeasurementIndex));
```

```julia
FloatOrDual = Union{Float64, ForwardDiff.Dual}

@inline function linear_fcn(
        m::FloatOrDual,
        b::FloatOrDual,
        x::FloatOrDual
)::FloatOrDual
    return m * x + b
end;
```

```julia
function time_step_s(
        time_delta_s::FloatOrDual,
        current_time_s::FloatOrDual
)::FloatOrDual
    return time_delta_s + current_time_s
end

function time_step_s(
        time_delta_s::FloatOrDual,
        state::Vector
)::FloatOrDual
    return time_step_s(time_delta_s, state[Int(TIM)])
end;
```

```julia
function fTemperature_K(
        TS_K__m::FloatOrDual,
        TI_K::FloatOrDual,
        altitude_m::FloatOrDual
)::FloatOrDual
    return linear_fcn(TS_K__m, TI_K, altitude_m)
end

function fTemperature_K(
        TS_K__m::FloatOrDual,
        TI_K::FloatOrDual,
        state::Vector
)::FloatOrDual
    return fTemperature_K(TS_K__m, TI_K, state[Int(ALT)])
end;

function measurement_function_temperature_K(
        state::Vector
)::FloatOrDual
    return fTemperature_K(
        par_values_dict["TS_K__m"],
        par_values_dict["TI_K"],
        state
    )
end;

measurement_function! = (vector_fcn::Vector, state::Vector) ->
begin
    vector_fcn[Int(TEMP)] = measurement_function_temperature_K(state)
    return nothing
end

measurement_function = state -> (
    vector_fcn = fill(zero(promote_type(eltype(state), Float64)), length_measurement_vector);
    measurement_function!(vector_fcn, state);
    return vector_fcn
);
```

```julia
function fVInf_m__s(
        VInfS_m__Ks::FloatOrDual,
        VInfI_m__s::FloatOrDual,
        T_K::FloatOrDual
)::FloatOrDual
    return linear_fcn(VInfS_m__Ks, VInfI_m__s, T_K)
end

function fVInf_m__s(
        VInfS_m__Ks::FloatOrDual,
        VInfI_m__s::FloatOrDual,
        TS_K__m::FloatOrDual,
        TI_K::FloatOrDual,
        state::Vector
)::FloatOrDual
    return fVInf_m__s(VInfS_m__Ks, VInfI_m__s, fTemperature_K(TS_K__m, TI_K, state))
end;
```

```julia
function hn_m(
        time_delta_s::FloatOrDual,
        hnm1_m::FloatOrDual,
        vnm1_m__s::FloatOrDual
)::FloatOrDual
    return linear_fcn(time_delta_s, hnm1_m, vnm1_m__s)
end

function hn_m(
        time_delta_s::FloatOrDual,
        state::Vector
)::FloatOrDual
    return hn_m(time_delta_s, state[Int(ALT)], state[Int(VEL)])
end;
```

```julia
function vn_m__s(
    time_delta_s::FloatOrDual,
    t_s::FloatOrDual,
    VInf_m__s::FloatOrDual,
    accel_m__s2::FloatOrDual,
    vmn1_m__s::FloatOrDual
)::FloatOrDual
    return (
        vmn1_m__s
        + time_delta_s
        * accel_m__s2 * sech(accel_m__s2 * t_s / VInf_m__s)^2
    )
end

function vn_m__s(
    time_delta_s::FloatOrDual,
    VInfS_m__Ks::FloatOrDual,
    VInfI_m__s::FloatOrDual,
    TS_K__m::FloatOrDual,
    TI_K::FloatOrDual,
    accel_m__s2::FloatOrDual,
    state::Vector,
)::FloatOrDual
    return vn_m__s(
        time_delta_s,
        state[Int(TIM)],
        fVInf_m__s(VInfS_m__Ks, VInfI_m__s, TS_K__m, TI_K, state),
        accel_m__s2,
        state[Int(VEL)]
    )
end;
```

```julia
state_function_altitude_m(state::Vector) = hn_m(
    par_values_dict["time_delta_s"],
    state
);

state_function_velocity_m__s(state::Vector) = vn_m__s(
    par_values_dict["time_delta_s"],
    par_values_dict["VInfS_m__Ks"],
    par_values_dict["VInfI_m__s"],
    par_values_dict["TS_K__m"],
    par_values_dict["TI_K"],
    par_values_dict["accel_m__s2"],
    state
);

state_function_time_s(state::Vector) = time_step_s(
    par_values_dict["time_delta_s"],
    state
);

state_function! = (vector_fcn::Vector, state::Vector) ->
begin
    vector_fcn[Int(ALT)] = state_function_altitude_m(state)
    vector_fcn[Int(VEL)] = state_function_velocity_m__s(state)
    vector_fcn[Int(TIM)] = state_function_time_s(state)
    return nothing
end

state_function = state -> (
    vector_fcn = fill(zero(promote_type(eltype(state), Float64)), length_state_vector);
    state_function!(vector_fcn, state);
    return vector_fcn
);
```

```julia
"""Temperature variations"""
artificial_noise_K = 0.1  # Kelvin
observation_cov = diagm([1.0^2])  # Kelvin^2

system_noise_cov = diagm([100.0^2, 1.00^2, 0.00]);
```

```julia
true_state_fcn_time = Array{Vector{Float64}}(undef, time_steps)
measurements_array = Array{Vector{Float64}}(undef, time_steps)
states_fcn_time = Array{KalmanState{Float64}}(undef, time_steps);
```

```julia
"""Define a system where a falling object is thought to be released at rest
at 3 km above the Earth's surface with wind resistence.
"""
true_state_vector = Vector{Float64}([2915.000, 0.0, 0.0])
initial_state_vector = Vector{Float64}([3000.0, 1.2, 0.0])
initial_state_cov = [1000.0^2 10.0   0.0;
                       10.0    1.0^2 0.0;
                        0.0     0.0  0.1]
initial_state = KalmanState{Float64}(initial_state_vector, initial_state_cov)
curr_state = KalmanState{Float64}(initial_state.state, initial_state.cov)

for step in 1:time_steps

#     println("step=$step")
    curr_step_time_s = (step) * par_values_dict["time_delta_s"]

    # Set the true parameters for this time step
    true_state_vector = state_function(true_state_vector)
    true_state_fcn_time[step] = true_state_vector
#     println("true_state_vector=$(true_state_vector)")

    # Predict the next state and save those predictions into the parameter dictionary
    pred_state::KalmanState = predict_state(
        state_function, curr_state, system_noise_cov
    )
#     println("pred_state=$(pred_state)")

    meas_vec = measurement_function(true_state_vector)
    meas_vec[Int(TEMP)] += artificial_noise_K * randn()
#     println("meas_vec=$(meas_vec)")
    measurements_array[step] = meas_vec

    # Get the corrected current state
    curr_state = correct_state(
        measurement_function,
        pred_state,
        KalmanObservation{Float64}(meas_vec, observation_cov)
    )
#     println("curr_state=$(curr_state)")
    states_fcn_time[step] = curr_state

end
```

```julia
time_axis_s = [(step * par_values_dict["time_delta_s"]) for step in 1:time_steps]
scatter(
    time_axis_s,
    [measurements_array[step][1] for step in 1:time_steps],
    axis = (aspect = 2, xlabel = "Time (second)", ylabel = "Temperature (Kelvin)"),
    label="Measured Temperature"
)
lines!(
    time_axis_s,
    [measurements_array[step][1] for step in 1:time_steps]
)
axislegend()
current_figure()
```

```julia
true_positions_m = [tr_st[1] for tr_st in true_state_fcn_time]
esti_positions_m = [s.state[1] for s in states_fcn_time]

scatter(
    time_axis_s,
    true_positions_m,
    axis = (aspect = 2, xlabel = "Time (second)", ylabel = "Position (meters)"),
    label="True position"
)

lines!(
    time_axis_s,
    true_positions_m
)

scatter!(
    time_axis_s,
    esti_positions_m,
    label="Estimated position"
)

lines!(
    time_axis_s,
    esti_positions_m
)

errorbars!(
    time_axis_s,
    esti_positions_m,
    [sqrt(s.cov[1, 1]) for s in states_fcn_time]
)

axislegend()
current_figure()
```

```julia
true_velocities_m__s = [tr_st[2] for tr_st in true_state_fcn_time]
esti_velocities_m__s = [s.state[2] for s in states_fcn_time]

scatter(
    time_axis_s,
    true_velocities_m__s,
    axis = (aspect = 2, xlabel = "Time (second)", ylabel = "Velocity (meters / sec)"),
    label="True velocity"
)

lines!(
    time_axis_s,
    true_velocities_m__s
)

scatter!(
    time_axis_s,
    esti_velocities_m__s,
    label="Estimated position"
)

lines!(
    time_axis_s,
    esti_velocities_m__s
)

errorbars!(
    time_axis_s,
    esti_velocities_m__s,
    [sqrt(s.cov[2, 2]) for s in states_fcn_time]
)

axislegend()
current_figure()
```


# Optical Crosstalk in SPAD arrays

# Theory

Optical crosstalk is a interaction between pixels on the same
array. When an avalanche is triggered on one pixel it emits
secondary photons that can trigger another pixel on the same chip.
This secondary photon emission is proportional to the avalanche current
whose duration is typically < 20ns in SPADs using AQCs. Hence, when a
crosstalk event occurs, the second pixels is triggered with a delay
< 20ns. We can estimate the crosstalk by "coincidence counting",
that is counting the number of photons $C$ arriving in two pixels
within a short time window $\Delta t$ (e.g. 25-50ns).
The number of coincident events due to uncorrelated dark counts
can be computed from Poisson statistics. Then, the number
of crosstalk events is simply the total coincidence counts minus
the coincidence due to uncorrelated counts. The crosstalk probability
is the number of coincidences divided by the total counts.

**Reference**:
- [Restelli JMO 2006](http://dx.doi.org/10.1080/09500340600790121).

## Poisson statistics

Given the random variable $X \sim {\rm Poiss}\{\Lambda\}$,
where $\Lambda = \lambda\Delta t$. We ask, what is the probability
of having at least one count in a time window $\delta t$?

$$P\{X \ge 1\} = 1 - P\{X = 0\} = 1 - e^\Lambda
\xrightarrow[\scriptstyle\Lambda\to0]{} \Lambda$$

Hence, for two random variables $X_A \sim {\rm Poiss}\{\Lambda_A\}$
and $X_B \sim {\rm Poiss}\{\Lambda_B\}$, the propbability of having
at least one count in each variable in a time $\Delta t$ is:

$$P\{X_A \ge 1\}P\{X_B \ge 1\} = (1 - e^{-\Lambda_A})(1 - e^{-\Lambda_B})
\approx (\lambda_A\Delta t)(\lambda_B\Delta t)
\quad {\rm for}\quad\Delta t \ll \lambda^{-1}$$

In a measurement of duration $T$, number of "coincidences" $C_u$ is the number
of times both variables have at least one count in a window $\Delta t$

$$C_u = P\{X_A \ge 1\}P\{X_B \ge 1\} \frac{T}{\Delta t}
\approx \lambda_A\,\lambda_B\, T\,\,\Delta t
\quad {\rm for}\quad\Delta t \ll \lambda^{-1}$$

## Crosstalk probability

Given a measurement of duration $T$, with total counts $N_A$ and $N_B$ in
pixels $A$ and $B$. If $C$ are the total number of coincidence in windows of
duration $\Delta t$, then the coincidence $C_c$ due to crosstalk are:

\begin{equation}
C_c = C - C_u \qquad\qquad (1)
\end{equation}

where $C_u$ are the number of coincidences due to Poisson statistics,
i.e. the coincidences we would have if the two pixels were uncorrelated.

\begin{equation}
C_u = \left[1 - \exp\left(-\frac{N_A - C_c}{T}\right)\right]
      \left[1 - \exp\left(-\frac{N_B - C_c}{T}\right)\right]
      \frac{T}{\Delta t}
\end{equation}

The expression of $C_u$ can be replaced eq. (1) and then solved for $C_c$
(a numerical iterative solution is straightforward).
For simplicity, we could assume $C_c \ll N_A,N_B$ obtaining:

\begin{equation}
C_u = \left[1 - \exp\left(-\frac{N_A}{T}\right)\right]
      \left[1 - \exp\left(-\frac{N_B}{T}\right)\right]
      \frac{T}{\Delta t}
\end{equation}

In either cases, the probability of crosstalk is:

\begin{equation}
P_c = \frac{C_c}{N_A + N_B - C_c}
\end{equation}

The counts $C_c$ are independetent events and thus are Poisson distributed.
The standard deviation of $C_c$ is then
${\rm Std}\{C_c\} = \sqrt{C_c}$, and the standard deviation of $P_c$ is:

\begin{equation}
{\rm Std}\{P_c\} = \frac{\sqrt{C_c}}{N_A + N_B - C_c}
\end{equation}

## Define functions

```python
def coincidence_py(timestamps1, timestamps2):
    """Pure python coincidence counting."""
    coinc = 0
    i1, i2 = 0, 0
    while (i1 < timestamps1.size) and (i2 < timestamps2.size):
        if timestamps1[i1] == timestamps2[i2]:
            coinc += 1
            i1 += 1
            i2 += 1
        elif timestamps1[i1] > timestamps2[i2]:
            i2 += 1
        elif timestamps1[i1] < timestamps2[i2]:
            i1 += 1
    return coinc
```

```python
%load_ext Cython
```

```cython
%%cython
cimport numpy as np

def coincidence(np.int64_t[:] timestamps1, np.int64_t[:] timestamps2):
    """Cython coincidence counting."""
    cdef np.int64_t coinc, i1, i2, size1, size2
    size1 = timestamps1.size
    size2 = timestamps2.size
    coinc = 0
    i1, i2 = 0, 0
    while i1 < size1 and i2 < size2:
        if timestamps1[i1] == timestamps2[i2]:
            coinc += 1
            i1 += 1
            i2 += 1
        elif timestamps1[i1] > timestamps2[i2]:
            i2 += 1
        elif timestamps1[i1] < timestamps2[i2]:
            i1 += 1
    return coinc
```

```python
def crosstalk_probability(t1, t2, tol=1e-6, max_iter=100):
    """Estimate crosstalk probability between two pixels in a SPAD array.

    Given two input arrays of timestamps `t1` and `t2`, estimate
    the crosstalk probability using Poisson statistics without
    approximations.

    Arguments:
        t1, t2 (array of ints): arrays of timestamps from DCR measurements
            for the two pixels to be measured. Timestamps need to be
            integers and coincidences are detected when values in the two
            arrays are equal. These arrays need to be rescaled, if
            coincidence need to be computed on a delta t larger than \
            a single timestamp unit.
        tol (float): tollerance for iterative equasion solution
        max_iter (int): max iterations used to solve the equation

    Returns:
        A 3-element tuple:
        - crosstalk probability
        - crosstalk probability standard deviation
        - number of iterations used for the estimation.
    """
    T = (max((t1.max(), t2.max())) - min((t1.min(), t2.min())))
    C = coincidence(t1, t2)

    # Find C_c by solving eq. (1) iteratively
    C_c, C_u_prev = 0, 0
    for i in range(max_iter):
        C_u = ((1 - np.exp(-(t1.size - C_c)/T)) *
               (1 - np.exp(-(t2.size - C_c)/T)) * T)
        C_c = C - C_u
        if np.abs(C_u - C_u_prev) < tol:
            break
        C_u_prev = C_u

    P_c = C_c / (t1.size + t2.size - C_c)
    sigma = np.sqrt(C_c) / (t1.size + t2.size - C_c)
    return P_c, sigma, i
```

# Simulation

## Poisson processes

Here we simulate two poisson processes to check the the coincidences
are, as predicted, equal to $C_u$.

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

Simulation parameters:

```python
λ_A = 1000       # cps
λ_B = 2000       # cps
T = 600          # s
delta_t = 50e-9  # s
P_c = 0.05
```

From theory, the number of coincidences are:

```python
C_u = λ_A * λ_B * T * delta_t
C_u
```

    60.0

```python
np.sqrt(C_u)
```

    7.745966692414834

Let's simulate timestamps for two uncorrelated Poisson processes:

```python
dt = np.random.exponential(scale=1/λ_A, size=10**6)
dt.mean(), dt.sum()
```

    (0.0010001265879312606, 1000.1265879312606)

```python
np.random.seed(1)

t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < 600]
t_A = (t_A / delta_t).astype('int64')

t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < 600]
t_B = (t_B / delta_t).astype('int64')
```

The empirical coincidences are:

```python
C_u_sim = coincidence(t_A, t_B)
C_u_sim
```

    62

```python
assert C_u - 3*np.sqrt(C_u) < C_u_sim < C_u + 3*np.sqrt(C_u)
```

Let's repeat the simulation $N$ times to be sure that the
estimated coincidence falls within the espected error range
all the times:

```python
N = 500
fail = 0
for _ in range(N):
    t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
    assert t_A.max() > T
    t_A = t_A[t_A < T]
    t_A = (t_A / delta_t).astype('int64')

    t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
    assert t_B.max() > T
    t_B = t_B[t_B < T]
    t_B = (t_B / delta_t).astype('int64')

    C_u_sim = coincidence(t_A, t_B)
    if C_u < C_u_sim - 3*np.sqrt(C_u_sim) or C_u > C_u_sim + 3*np.sqrt(C_u_sim):
        fail += 1

print('>>> In %d simulations, the number of coincincence was outside the error range %d times'
      % (N, fail/N))
```

    >>> In 500 simulations, the number of coincincence was outside the error range 0 times

For further information of confidence intervals for Poisson distribution
estimators see:

- Patil & Kulkarni. *Statistical Journal* 10(2) pp211–227(2012).
  COMPARISON OF CONFIDENCE INTERVALS FOR THE POISSON MEAN: SOME NEW ASPECTS.
  [[PDF]](https://www.ine.pt/revstat/pdf/rs120203.pdf)

## Optical crosstalk

Here we simulate two Poisson processes plus crosstalk, to check
that the estimated crosstalk is consistent with the ground truth.

```python
λ_A = 1000       # cps
λ_B = 1550       # cps
T = 1200         # s
delta_t = 50e-9  # s
P_c = 0.1
```

```python
np.random.seed(1)
```

```python
t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
assert t_A.max() > T
t_A = t_A[t_A < 600]
t_A = (t_A / delta_t).astype('int64')
t_ct_AB = t_A[np.random.rand(t_A.size) <= P_c]

t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
assert t_B.max() > T
t_B = t_B[t_B < 600]
t_B = (t_B / delta_t).astype('int64')
t_ct_BA = t_B[np.random.rand(t_B.size) <= P_c]

t_B = np.hstack([t_B, t_ct_AB])
t_B.sort()
t_A = np.hstack([t_A, t_ct_BA])
t_A.sort()
```

```python
P_c_est, P_c_err, i = crosstalk_probability(t_A, t_B, delta_t)
P_c_est*100, P_c_err*300, i
```

    (9.9967084810755544, 0.076659160092492767, 4)

Let's repeat the simulation $N$ times to obtain a distribution
of estimated crosstalk values:

```python
λ_A = 1000       # cps
λ_B = 300       # cps
T = 1200         # s
delta_t = 50e-9  # s
P_c = 0.05
```

```python
N = 100
P = []
I = 0
for _ in range(N):
    t_A = np.cumsum(np.random.exponential(scale=1/λ_A, size=λ_A * T * 2))
    assert t_A.max() > T
    t_A = t_A[t_A < T]
    t_A = (t_A / delta_t).astype('int64')
    t_ct_AB = t_A[np.random.rand(t_A.size) <= P_c]

    t_B = np.cumsum(np.random.exponential(scale=1/λ_B, size=λ_B * T * 2))
    assert t_B.max() > T
    t_B = t_B[t_B < T]
    t_B = (t_B / delta_t).astype('int64')
    t_ct_BA = t_B[np.random.rand(t_B.size) <= P_c]

    t_B = np.hstack([t_B, t_ct_AB])
    t_B.sort()
    t_A = np.hstack([t_A, t_ct_BA])
    t_A.sort()

    P_c_est, P_c_err, i = crosstalk_probability(t_A, t_B)
    I += i
    P.append(P_c_est*100)
I/N
```

    3.0

```python
plt.hist(P, range=(96*P_c, 104*P_c), bins=60, label='Estimator');
plt.axvline(P_c*100, ls='--', color='k', label='True value')
plt.xlabel('Crosstalk probability (%)')
plt.title('Simulation of crosstalk estimator accuracy');
```

> **NOTE** The crosstalk estimator is well centered around the true value.