# Fourier Series of Curves: Background
This notebook introduces Fourier series of (closed) curves and shows how to compute them.

The goal is take a continuous line drawing (i.e. something drawn with a single line) and to recreate the drawing process using circles rotating around each others. An example can be seen below:

Start: A Line Drawing | Goal: Circles drawing the line
- | -

```python
%matplotlib inline

# Initial imports
import numpy as np
import matplotlib.pyplot as plt
```

Enabling equation numbering

```javascript
%%javascript
MathJax.Hub.Config({
    TeX: { equationNumbers: { autoNumber: "AMS" } }
});

MathJax.Hub.Queue(
  ["resetEquationNumbers", MathJax.InputJax.TeX],
  ["PreProcess", MathJax.Hub],
  ["Reprocess", MathJax.Hub]
);
```

    <IPython.core.display.Javascript object>

## Prerequisites
For the following, you should be familiar with:
- __Complex numbers.__ This notebook represents points as complex numbers and uses also a complex form of Fourier Series. For this reason, an understanding of complex numbers is required.

## Representing Line Drawings
Starting with an image as shown above is not sufficient enough to recreate the drawing process. We need the information at which point in time which part of the image was drawn. For this reason, we represent the line drawing as a closed curve.

Such a curve is given by a continuous function $f:[a,b]\to\mathbb{C}$ over the time-interval $[a,b]$ with $f(a) = f(b)$. Often, we will just set $a=0$ and $b=1$, but we will also allow for more general definitions.

### Concatenating Lines
It is possible to concatenate curves to get a longer, piecewise defined curve. Examples for such piecewise defined curves are polygons (as concatenations of straight lines), splines or paths in an svg-image.

Formally, a piecewise defined curve $f:[a,b]\to\mathbb{C}$ is given by a sequence $a=t_0 < t_1 < \ldots < t_n=b$ of border-points of the pieces and a corresponding number of curves $f_i:[t_i,t_{i+1}]\to\mathbb{C}$ ($i=0,\ldots,n-1$) with $f(t) = f_i(t)$ for $t\in[t_i,t_{i+1}[$ and $f(b) = f_{n-1}(b)$.

In our case, we also want $f$ to fulfill the following conditions:
- __Continuity__ of $f$: all $f_i$ need to be continuous and connected, i.e. $f_i(t_{i+1}) = f_{i+1}(t_{i+1})$
- A __Closed Curve__ $f$, i.e. $f(a) = f(b)$

## Representing Circles
Looking at the target animation, we see a center circle and a second circle rotating on the first circle. A third circle rotates on the second one and so on. This is depicted in the following figure:

Mathematically, each circle $k$ ($k=1,\ldots n$) is given by
- a center point $p_k$ and
- a radius $r_k$.

The position of a point $q_k$ rotating on the circle can be determined by
- a starting angle $\alpha_k$ and
- the speed of this point $\beta_k$.

On a non-moving circle $k$, the coordinates of $q_k$ at time $t$ are then given by
$$
\begin{equation}
    q_k(t) = p_k + r_k\left(\cos(\alpha_k+t\cdot\beta_k)\atop \sin(\alpha_k+t\cdot\beta_k)\right)
    \label{eqn:circle_vec}
\end{equation}
$$

Using complex numbers (\ref{eqn:circle_vec}) can be written as:
$$
\begin{equation}
    q_k(t) = p_k + r_ke^{i(\alpha_k+t\cdot\beta_k)} = p_k + \underbrace{r_ke^{i\alpha_k}}_{=:c_k}e^{i\beta_kt}
    \label{eqn:circle_complex}
\end{equation}
$$

Since the centers of our circles are rotating on other circles, we get for $k > 1$ a time-dependent center of $p_k(t) = q_{k-1}(t)$. This allows us to rewrite (\ref{eqn:circle_complex}) as:

$$
\begin{equation}
    q_k(t) = p_k(t) + c_ke^{i\beta_kt}
           = p_{k-1}(t) + c_{k-1}e^{i\beta_{k-1}t} + c_ke^{i\beta_kt} = \ldots = p_1 + \sum_{j=1}^kc_je^{i\beta_jt}
    \label{eqn:circle_as_sum}
\end{equation}
$$

### Drawing closed curves
When looking at the introductory example, we see that the point $q_n$ in the last (i.e. the $n$-th) circle draws the line.
for each interval $[a,b]$, the function $f:[a,b]\to\mathbb{C}$ with $f(t) := q_n(t)$ defines a curve.

This way of formulating the rotating circles, and especially the last, line-drawing dot $q_{n}$ strongly resembles the complex formulation of a Fourier series, which we will briefly review in the following.

## Fourier series of a closed curve
Fourier series and Fourier transformation are a well-known concept to approximate periodic functions.
Given a integrable T-periodic $f:\mathbb{R}\to\mathbb{R}$ or $f:\mathbb{R}\to\mathbb{C}$, with $f(t+T) = f(t)$, the Fourier series of $f$ is defined by its finite subsequences:

$$
\begin{equation}
    \hat{f}_n(t) = \sum_{k=-n}^n c_ke^{i\frac{2\pi kt}{T}}
    \label{eqn:fourier_series}
\end{equation}
$$

with Fourier coefficients

$$
\begin{equation}
    c_k := \frac{1}{T}\int_0^{T}f(t)e^{-i\frac{2\pi kt}{T}}dt
    \label{eqn:fourier_coefficients}
\end{equation}
$$

#### Redrawing closed curves
A closed curve $f$ over the interval $[a,b]$ can easily be extended to an $(b-a)$-periodic function over $\mathbb{R}$, which iterates over and over again over the original curve. Thus, we can compute the Fourier coefficients using (\ref{eqn:fourier_coefficients}). In the following, we assume that $f$ is defined over an interval $[0,T]$.

The Fourier approximation (\ref{eqn:fourier_series}) of the curve $f$ resembles equation (\ref{eqn:circle_as_sum}) with a different range of indices, $p_1 = c_0$ and $\beta_k = \frac{2\pi k}{T}$. This means, that the Fourier series contains all required parameters for the circles that can redraw $f$.

#### Convergence
There is not guarantee, that $\hat{f}_n$ converges in the general case and if it converges, it does not necessarily converge against $f$.
Luckily, (pointwise) convergence is guaranteed for continuous almost always differentiable functions. Since our main focus is on continuous piecewise curves with differential pieces, this condition is met.

### Piecewise defined functions
As mentioned above, a curve can be given as a concatenation of smaller curves. In that case, the interval $[0,T]$ is split into $0=t_0 < t_1 < \ldots < t_n=T$ with $f(t) = f_j(t)$ for $t\in[t_j,t_{j+1}[$ and $f(T) = f_{n-1}(T)$.

This means, that we can rewrite (\ref{eqn:fourier_coefficients}) as:

$$
\begin{equation}
    c_k := \frac{1}{T}\int_0^{T}f(t)e^{-i\frac{2\pi kt}{T}}dt = \frac{1}{T}\sum_{j=0}^{n-1}\int_{t_j}^{t_{j+1}}f_j(t)e^{-i\frac{2\pi kt}{T}}dt
    \label{eqn:fourier_piecewise_coefficients}
\end{equation}
$$

Thus, if we can compute for some classes of curves the arbitrary integral
$$
\begin{equation}
    \int_{a}^{b}g(t)e^{-i\lambda t}dt
    \label{eqn:fourier_piece_integral}
\end{equation}
$$

for all instances $g$ of these curves, we can also compute the Fourier series of all concatenations of such curves.

#### Examples
Examples for the computation of integral (\ref{eqn:fourier_piece_integral}) can be found in extra notebooks. These are:
- [Straight Lines](Fourier-Series-of-Curves-Example-1-Polygones.ipynb)
- [Ellipse Arcs](Fourier-Series-of-Curves-Example-2-Arcs.ipynb)
- [Bézier Curves](Fourier-Series-of-Curves-Example-3-Bezier.ipynb)


```python
from collections import OrderedDict
import warnings
from IPython.display import display
import sympy as sp
import matplotlib.pyplot as plt
from chempy.chemistry import Equilibrium, ReactionSystem, Substance
from chempy.kinetics.integrated import binary_rev
from chempy.kinetics.ode import get_odesys
%matplotlib inline
sp.init_printing()
```

```python
t, kf, kb, X, Y, Z = sp.symbols('t k_f k_b X Y Z')
binary_rev(t, kf, kb, Y, Z, X, sp)
```

```python
eq = Equilibrium.from_string('Fe+3 + SCN- = FeSCN+2; 10**2.065')
rsys = ReactionSystem(eq.as_reactions(kf=900))
rsys
```

```python
odesys, extra = get_odesys(rsys)
```

```python
def plot_against_analytic(odsy, c0, tend=1, integrator='cvode', atol=1e-12, rtol=1e-10):
    xout, yout, info = odsy.integrate(tend, c0, integrator=integrator, atol=atol, rtol=rtol)
    ref = binary_rev(xout[1:], rsys.rxns[0].param, rsys.rxns[1].param, c0['FeSCN+2'], c0['Fe+3'], c0['SCN-'])
    {k: v for k, v in info.items() if not k.startswith('internal')}
    plt.figure(figsize=(14, 4))
    plt.subplot(1, 2, 1)
    plt.plot(xout[1:], ref, alpha=0.3, linewidth=3)
    _ = odsy.plot_result()
    plt.subplot(1, 2, 2)
    plt.plot(xout[1:], yout[1:, odsy.names.index('FeSCN+2')] - ref)
    ref[:3], ref[-3:]
    return {k: v for k, v in info.items() if not k.startswith('internal')}
```

```python
conc = {'Fe+3': 10e-3, 'SCN-': 2e-3, 'FeSCN+2': 2e-4}
plot_against_analytic(odesys, conc)
```

Below are the derivations for ``binary_rev``:

```python
x, a, b, c, U, V, S = sp.symbols('x a b c U V S')
u, v = 2*a*x + b, sp.sqrt(b**2 - 4*a*c)  # b**2 > 4*a*c
Prim = sp.log((U-V)/(U+V))/V  # primitive recip. 2nd order polynomial
prim = sp.log((u-v)/(u+v))/v  # primitive recip. 2nd order polynomial
prim.diff(x).simplify()
```

```python
y = Y + X - x
z = Z + X - x
```

```python
dxdt = kf*y*z - kb*x
dxdt
```

```python
dxdt.expand()
```

```python
expr2 = dxdt.expand().collect(x)
expr2
```

```python
coeffs = expr2.as_poly(x).coeffs()
coeffs
```

```python
eq = (sp.exp(Prim) - sp.exp(t + S))
eq
```

```python
sp.Eq(Prim*V, (t+S)*V)
```

```python
eq = sp.exp(Prim*V) - sp.exp((t+S)*V)
eq
```

```python
eq2 = eq.subs({U: u})
eq2
```

```python
sol, = sp.solve(eq2, x)
sol
```

```python
s, = sp.solve(sol.subs(t, 0) - X, S)
s
```

```python
sol2 = sol.subs(S, s).simplify()
sol2
```

```python
R, W = sp.symbols('R W')
r = b + 2*X*a
w = sp.exp(-V*t)
sol3 = sol2.subs({w: W, r: R}).simplify().collect(W)
sol3
```

```python
exprs = [
    sp.Eq(x, sol3),
    sp.Eq(R, r),
    sp.Eq(W, w),
    sp.Eq(V, v),
    sp.Eq(a, coeffs[0]),
    sp.Eq(b, coeffs[1]),
    sp.Eq(c, coeffs[2]),
]
for expr in exprs:
    display(expr)

```

```python
master = exprs[0]
for e in exprs[1:]:
    master = master.subs(e.lhs, e.rhs)
master
```

```python
cses, (reform,) = sp.cse(master)
reform
```

```python
cses
```

```python

```

```python
ret = 'return ' + str(reform.rhs).replace('k_f', 'kf').replace('k_b', 'kb')
for key, val in cses:
    print('%s = %s' % (key, str(val).replace('k_f', 'kf').replace('k_b', 'kb').replace('exp', 'be.exp').replace('sqrt', 'be.sqrt')))
print(ret)
```

```python
with warnings.catch_warnings():
    warnings.filterwarnings('error')
    binary_rev(1.0, 1e10, 1e-4, 0, 3e-7, 2e-7)  # check that our formulation avoids overflow
```

Below we will test ``get_native``:

```python
from chempy.kinetics._native import get_native
```

```python
print({k: v.composition for k, v in rsys.substances.items()})
rsys.substances = OrderedDict([(k, Substance.from_formula(v.name)) for k, v in rsys.substances.items()])
print({k: v.composition for k, v in rsys.substances.items()})
```

```python
nsys = get_native(rsys, odesys, 'gsl')
```

```python
plot_against_analytic(nsys, conc, integrator='gsl', atol=1e-16, rtol=1e-16)
```

```python

```


```python
# import linnaeus and Algorithm class
import linnaeus as lin
from linnaeus import Algorithm
```

### Example 1 (oracle equivalence)

### Algorithm 1

$\text{Repeat}$

$x_3 \gets 2x_1 - x_2$

$x_2 \gets x_1$

$x_1 \gets x_3 - \frac{1}{10} \nabla f(x_3)$

```python
# define algorithm "algo1"
algo1 = Algorithm("algo1")

# add oracle gradient of f to algo1
gradf = algo1.add_oracle("gradf")

# add variables x1, x2, and x3 to algo1
x1, x2, x3 = algo1.add_var("x1", "x2", "x3")

# add update equations
# x3 <- 2x1 - x2
algo1.add_update(x3, 2*x1 - x2)
# x2 <- x1
algo1.add_update(x2, x1)
# x1 <- x3 - 1/10*gradf(x3)
algo1.add_update(x1, x3 - 1/10*gradf(x3))

# parse algo1, translate it into canonical form
algo1.parse()
```

    --------------------------------------------------------------
    Parse algo1.
    State-space realization:

\begin{align} \left[\begin{matrix}x^{+}_{1}\\x^{+}_{2}\\x^{+}_{3}\end{matrix}\right] & = \left[\begin{matrix}2.0 & -1 & 0\\1 & 0 & 0\\2 & -1 & 0\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}-0.1\\0\\0\end{matrix}\right] \left[\begin{matrix}\operatorname{gradf}{\left(y_{0} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\end{matrix}\right] & = \left[\begin{matrix}2 & -1 & 0\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}0\end{matrix}\right] \left[\begin{matrix}\operatorname{gradf}{\left(y_{0} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

### Algorithm 2

$\text{Repeat}$

$\xi_3 \gets \xi_1$

$\xi_1 \gets  \xi_1 - \xi_2 - \frac{1}{5} \nabla f(\xi_3)$

$\xi_2 \gets \xi_2 + \frac{1}{10} \nabla f(\xi_3)$

```python
algo2 = Algorithm("algo2")
xi1, xi2, xi3 = algo2.add_var("xi1", "xi2", "xi3")
gradf = algo2.add_oracle("gradf")

# xi3 <- xi1
algo2.add_update(xi3, xi1)
# xi1 <- xi1 - xi2 - 1/5*gradf(xi1)
algo2.add_update(xi1, xi1 - xi2 - 1/5*gradf(xi3))
# xi2 <- xi2 + 1/10*gradf(xi3)
algo2.add_update(xi2, xi2 + 1/10*gradf(xi3))

algo2.parse()
```

    --------------------------------------------------------------
    Parse algo2.
    State-space realization:

\begin{align} \left[\begin{matrix}\xi^{+}_{1}\\\xi^{+}_{2}\\\xi^{+}_{3}\end{matrix}\right] & = \left[\begin{matrix}1 & -1 & 0\\0 & 1 & 0\\1 & 0 & 0\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\\\xi_{3}\end{matrix}\right]+\left[\begin{matrix}-0.2\\0.1\\0\end{matrix}\right] \left[\begin{matrix}\operatorname{gradf}{\left(y_{0} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\end{matrix}\right] & = \left[\begin{matrix}1 & 0 & 0\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\\\xi_{3}\end{matrix}\right]+\left[\begin{matrix}0\end{matrix}\right] \left[\begin{matrix}\operatorname{gradf}{\left(y_{0} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

```python
# check oracle equivalence
lin.is_equivalent(algo1, algo2, words = True)
```

    --------------------------------------------------------------
    algo1 is equivalent to algo2.
    --------------------------------------------------------------

    True

### Example 2 (shift equivalence)

### Algorithm 3

$\text{Repeat}$

$x_1 \gets \text{prox}_{f}(x_3)$

$x_2 \gets \text{prox}_{g}(2x_1 - x_3)$

$x_3 \gets x_3 + x_2 - x_1$

```python
algo3 = Algorithm("algo3")
x1, x2, x3 = algo3.add_var("x1", "x2", "x3")
proxf, proxg = algo3.add_oracle("proxf", "proxg")

# x1 <- proxf(x3)
algo3.add_update(x1, proxf(x3))
# x2 <- proxg(2x1 - x3)
algo3.add_update(x2, proxg(2*x1 - x3))
# x3 <- x3 + x2 - x1
algo3.add_update(x3, x3 + x2 - x1)

algo3.parse()
```

    --------------------------------------------------------------
    Parse algo3.
    State-space realization:

\begin{align} \left[\begin{matrix}x^{+}_{1}\\x^{+}_{2}\\x^{+}_{3}\end{matrix}\right] & = \left[\begin{matrix}0 & 0 & 0\\0 & 0 & 0\\0 & 0 & 1\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}1 & 0\\0 & 1\\-1 & 1\end{matrix}\right] \left[\begin{matrix}\operatorname{proxf}{\left(y_{0} \right)}\\\operatorname{proxg}{\left(y_{1} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\\y_{1}\end{matrix}\right] & = \left[\begin{matrix}0 & 0 & 1\\0 & 0 & -1\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}0 & 0\\2 & 0\end{matrix}\right] \left[\begin{matrix}\operatorname{proxf}{\left(y_{0} \right)}\\\operatorname{proxg}{\left(y_{1} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

### Algorithm 4

$\text{Repeat}$

$\xi_1 \gets \text{prox}_{g}(- \xi_1 + 2\xi_2) + \xi_1 - \xi_2$

$\xi_2 \gets \text{prox}_{f}(\xi_1)$

```python
algo4 = Algorithm("algo4")
xi1, xi2 = algo4.add_var("xi1", "xi2")
proxf, proxg = algo4.add_oracle("proxf", "proxg")

# xi1 <- proxg(-xi1 + 2xi2) + xi1 - xi2
algo4.add_update(xi1, proxg(-xi1 + 2*xi2) + xi1 - xi2)
# xi2 <- proxf(xi1)
algo4.add_update(xi2, proxf(xi1))

algo4.parse()
```

    --------------------------------------------------------------
    Parse algo4.
    State-space realization:

\begin{align} \left[\begin{matrix}\xi^{+}_{1}\\\xi^{+}_{2}\end{matrix}\right] & = \left[\begin{matrix}1 & -1\\0 & 0\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\end{matrix}\right]+\left[\begin{matrix}1 & 0\\0 & 1\end{matrix}\right] \left[\begin{matrix}\operatorname{proxg}{\left(y_{0} \right)}\\\operatorname{proxf}{\left(y_{1} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\\y_{1}\end{matrix}\right] & = \left[\begin{matrix}-1 & 2\\1 & -1\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\end{matrix}\right]+\left[\begin{matrix}0 & 0\\1 & 0\end{matrix}\right] \left[\begin{matrix}\operatorname{proxg}{\left(y_{0} \right)}\\\operatorname{proxf}{\left(y_{1} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

```python
# check cyclic permutation (shift equivalence)
lin.is_permutation(algo3, algo4, words = True)
```

    --------------------------------------------------------------
    algo3 is a permutation of algo4.
    --------------------------------------------------------------

    True

### Example 3 (permutation and conjugation)

$\text{For problem, }$

$\text{minimize} \; f(x) + g(x) $

### Douglas-Rachford splitting

$\text{Repeat}$

$x_1 \gets \text{prox}_{tf}(x_3)$

$x_2 \gets \text{prox}_{tg}(2x_1 - x_3)$

$x_3 \gets x_3 + x_2 - x_1$

```python
DR = Algorithm("Douglas-Rachford splitting")
x1, x2, x3 = DR.add_var("x1", "x2", "x3")
f, g = DR.add_function("f", "g")
t = DR.add_parameter("t")

# x1 <- prox_tf(x3)
DR.add_update(x1, lin.prox(f, t)(x3))
# x2 <- prox_tg(2x1 - x3)
DR.add_update(x2, lin.prox(g, t)(2*x1 - x3))
# x3 <- x3 + x2 - x1
DR.add_update(x3, x3 + x2 - x1)

DR.parse()
```

    --------------------------------------------------------------
    Parse Douglas-Rachford splitting.
    State-space realization:

\begin{align} \left[\begin{matrix}x^{+}_{1}\\x^{+}_{2}\\x^{+}_{3}\end{matrix}\right] & = \left[\begin{matrix}0 & 0 & 1\\0 & 0 & 1\\0 & 0 & 1\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}- t & 0\\- 2 t & - t\\- t & - t\end{matrix}\right] \left[\begin{matrix}\frac{d}{d y_{0}} f{\left(y_{0} \right)}\\\frac{d}{d y_{1}} g{\left(y_{1} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\\y_{1}\end{matrix}\right] & = \left[\begin{matrix}0 & 0 & 1\\0 & 0 & 1\end{matrix}\right] \left[\begin{matrix}x_{1}\\x_{2}\\x_{3}\end{matrix}\right]+\left[\begin{matrix}- t & 0\\- 2 t & - t\end{matrix}\right] \left[\begin{matrix}\frac{d}{d y_{0}} f{\left(y_{0} \right)}\\\frac{d}{d y_{1}} g{\left(y_{1} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

### ADMM

$\text{Repeat}$

$\xi_1 \gets \text{argmin}_{\xi_1}\{g^\star(\xi_1)+ \frac{\rho}{2}
				\left \| \xi_1 + \xi_2 + \xi_3 \right \|^2  \} $

$\xi_2 \gets \text{argmin}_{\xi_2}\{f^\star(\xi_2)+ \frac{\rho}{2}
				\left \| \xi_1 + \xi_2 + \xi_3 \right \|^2  \} $

$\xi_3 \gets \xi_3 + \xi_1 + \xi_2 $

```python
ADMM = Algorithm("ADMM")
f, g = ADMM.add_function("f", "g")
rho = ADMM.add_parameter("rho")
xi1, xi2, xi3 = ADMM.add_var("xi1", "xi2", "xi3")

# xi1 <- argmin(x1, g^*(xi1) + 1/2*rho*||xi1 + xi2 + xi3||^2)
ADMM.add_update(xi1, lin.argmin(xi1, g(xi1) + 1/2*rho*lin.norm_square(xi1 + xi2 + xi3)))
# xi2 <- argmin(x2, f^*(xi2) + 1/2*rho*||xi1 + xi2 + xi3||^2)
ADMM.add_update(xi2, lin.argmin(xi2, f(xi2) + 1/2*rho*lin.norm_square(xi1 + xi2 + xi3)))
# xi3 <- xi3 + xi1 + xi2
ADMM.add_update(xi3, xi3 + xi1 + xi2)

ADMM.parse()
```

    --------------------------------------------------------------
    Parse ADMM.
    State-space realization:

\begin{align} \left[\begin{matrix}\xi^{+}_{1}\\\xi^{+}_{2}\\\xi^{+}_{3}\end{matrix}\right] & = \left[\begin{matrix}0 & -1 & -1\\0 & 1 & 0\\0 & 0 & 0\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\\\xi_{3}\end{matrix}\right]+\left[\begin{matrix}- \frac{1}{\rho} & 0\\\frac{1}{\rho} & - \frac{1}{\rho}\\0 & - \frac{1}{\rho}\end{matrix}\right] \left[\begin{matrix}\frac{d}{d y_{0}} g{\left(y_{0} \right)}\\\frac{d}{d y_{1}} f{\left(y_{1} \right)}\end{matrix}\right] \\ \left[\begin{matrix}y_{0}\\y_{1}\end{matrix}\right] & = \left[\begin{matrix}0 & -1 & -1\\0 & 1 & 0\end{matrix}\right] \left[\begin{matrix}\xi_{1}\\\xi_{2}\\\xi_{3}\end{matrix}\right]+\left[\begin{matrix}- \frac{1}{\rho} & 0\\\frac{1}{\rho} & - \frac{1}{\rho}\end{matrix}\right] \left[\begin{matrix}\frac{d}{d y_{0}} g{\left(y_{0} \right)}\\\frac{d}{d y_{1}} f{\left(y_{1} \right)}\end{matrix}\right] \end{align}

    --------------------------------------------------------------

```python
# check conjugation and permutation
# between permutation of DR and ADMM
lin.test_conjugate_permutation(DR, ADMM)
```

    --------------------------------------------------------------
    ==============================================================
    Parameters of Douglas-Rachford splitting:

$$t$$

    Parameters of ADMM:

$$\rho$$

    Douglas-Rachford splitting is a conjugate permutation of ADMM, if the parameters satisfy:

$$\rho=t$$

    ==============================================================
    --------------------------------------------------------------


#Физика двигателя EV3
Друзья, мы с вами уже получили из основных фундаментальных законов физики уравнение для описания двигателя постоянного тока: $$\dot\omega+\frac{k_m k_e}{JR}\omega=\frac{k_m}{JR}U. \tag{1}$$ Это дифференциальное уравнение при известных начальных условиях имеет однозначное решение. Давайте попробуем решить его при помощи инструментов языка Python. Но для начала стоит сделать замену констант, чтобы упростить вычисления и сократить выражение. Введем электромеханическую константу: $$T_m=\frac{JR}{k_e k_m}.\tag{2}$$ Далее введем все необходимые параметры в рабочее пространство.

```python
import sympy                                  # импорт модуля символьных вычислений sympy
U, T_m, t = sympy.symbols("U, T_m","t")       # обозначение символов переменных напряжения, постоянной времени и самого времени
k_e = sympy.symbols("k_e",positive=True)      # конструктивная постоянная, должна быть положительной для однозначного решения уравнения
omega = sympy.Function("omega")               # обозначение функции угловой скорости
```

Мы импортировали необходимую нам библиотеку SymPy для выполнения символьных вычислений и описали все переменные, которые используются в уравнении 1 после подстановки замены констант 2. Теперь мы можем описать дифференциальное уравнение, указав зависимость скорости от времени: $$\dot\omega(t)=\frac{d}{dt}\omega=\frac{U}{T_m k_e}-\frac{\omega(t)}{T_m}. \tag{3}$$

```python
ode = sympy.Eq( omega(t).diff(t), U/(k_e*T_m) - omega(t)/T_m)   # задание дифференциального уравнения двигателя
ode
```

$\displaystyle \frac{d}{d t} \omega{\left(t \right)} = \frac{U}{T_{m} k_{e}} - \frac{\omega{\left(t \right)}}{T_{m}}$

Также мы точно знаем, что в начальный момент времени двигатель не вращается, и угол поворота равен нулю. Начальным условием для двигателя в мемент времени $t=0[sec]$ будет угловая скорость $\omega=0[rad/sec]$.

```python
ode_sol = sympy.dsolve(ode, omega(t), ics={omega(0):0})         # решение дифференциального уравнения с нулевыми начальными условиями
ode_sol
```

$\displaystyle \omega{\left(t \right)} = \frac{U - U e^{- \frac{t}{T_{m}}}}{k_{e}}$

Мы получили аналитическое решение дифференциального уравнения при заданных начальных условиях. Также можно разбить полученную дробь на две части, для наглядности постоянной части уравнения и её экспоненциальной части.

```python
ode_sol = sympy.expand(ode_sol)                                 # упрощение результата решения
ode_sol
```

$\displaystyle \omega{\left(t \right)} = \frac{U}{k_{e}} - \frac{U e^{- \frac{t}{T_{m}}}}{k_{e}}$

Теперь у нас есть полноценное уравнение угловой скорости вращения двигателя постоянного тока, и мы можем изобразить его график. Я подставлю приблизительные показания постоянных двигателя Lego EV3, $T_m=0.08[sec]$,  $\ k_e=0.5[V*sec/rad]$,  $\ U=8[V]$.

```python
ode_num = ode_sol.subs({k_e : 0.5, T_m : 0.08, U : 8})          # подстановка числовых значений в уравнение
ode_num
```

$\displaystyle \omega{\left(t \right)} = 16.0 - 16.0 e^{- 12.5 t}$

Теперь без проблем можно изобразить кривую разгона двигателя.

```python
from sympy.plotting import plot                                 # импорт функции изображения графиков
plot(ode_num.rhs,(t, 0, 0.5), line_color='red',
     title='Разгон двинателя',xlabel = 'time,[sec]',
     ylabel = 'speed, [rad/sec]')                               # изображение графика с различными настройками
```

Двигатель Lego EV3 имеет датчик угла поворота двигателя, оптический энкодер. Поэтому для работы с моделью двигателя нам понадобится угол поворота, а не угловая скорость. И мы можем получить ее, взяв интеграл от угловой скорости по времени в промежутке от 0 до текущего времени t.

```python
theta_eq = sympy.integrate(ode_sol.rhs,(t,0,t))                 # интегрирование правой части уравнения угловой скорости
theta_eq
```

$\displaystyle - \frac{T_{m} U}{k_{e}} + \frac{T_{m} U e^{- \frac{t}{T_{m}}}}{k_{e}} + \frac{U t}{k_{e}}$

Также уравнение угла поворота двигателя можно получить, решив дифференциальное уравнение относительно угла поворота $\omega=\dot\theta$, то есть $$\ddot\theta=\frac{U}{T_m k_e}-\frac{\dot\theta}{T_m}.  \tag{4}$$ При этом начальные условия также сохраняются, в момент времени $t = 0[sec]$ угол поворота двигателя равен $\theta = 0[rad]$ и угловая скорость $\dot\theta = 0[rad]$. Ниже проведем проверку результата интегрирования на предыдущем шаге, обратите внимание, как задается начальное условие $\dot\theta(0)=0$:

```python
theta = sympy.Function("theta")                                                 # задание функции угла поворота
ode_theta = sympy.Eq( theta(t).diff(t,2), - theta(t).diff(t)/T_m + U/(k_e*T_m)) # задание дифференциального уравнения
ode_sol_theta = sympy.dsolve(ode_theta, theta(t),
                             ics={theta(0):0, theta(t).diff(t).subs(t,0): 0})   # решение дифференциального уравнения
ode_sol_theta
```

$\displaystyle \theta{\left(t \right)} = - \frac{T_{m} U}{k_{e}} + \frac{T_{m} U e^{- \frac{t}{T_{m}}}}{k_{e}} + \frac{U t}{k_{e}}$

Последним шагом изобразим график угла поворота двигателя, чтобы в дальнейшем сравнить его с реальными показаниями.

```python
theta_num = ode_sol_theta.subs({k_e : 0.5, T_m : 0.08, U : 8})                  # подстановка численных значений
plot(theta_num.rhs,(t, 0, 0.5), line_color='red',
     title='Угол поворота двигателя',
     xlabel = 'time,[sec]', ylabel = 'angle, [rad]')                            # изображение графика
```


# Symbolic Mathematics in Python

There are times when you need to solve a difficult problem symbollically or analytically. If you have ever used Wolfram Alpha, then you have already done this. Sympy is a python library that allows you to do symbolic mathematics in python.

```python
import sympy as sym
```

## 1. Introduction

### Example 1.1

If you try to write the follwing in python by itself, you will get an error telling you x is undefined:
$$x-x$$

```python
x-x
```

(The error above is on purpose). Variables in python need to be defined before you can say something specific about them

```python
x=102
x-x
```

    0

If you are trying to show that $x-x=0$ is true for any $x$, the above answer would not be valid. Instead you can use a symbolic expression to show that it is true

**First we define the variable as a symmbolic expression**

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

**Now we can use the variable in a symbolic expression**

```python
x-x
```

$\displaystyle 0$

### Example 1.2

Sympy can be used to perform algebreic operations (among other things). Consider the following expression: $$(3a-4b)^3$$

We can use symppy to expand the expression algebraically.

**First we need to define the variables as symbolic expressions**

```python
a,b = sym.symbols('a,b')
```

**Side note** Notice that the left hand side of the epression has two variables being defined. Python can define more than one variable at a time:

```python
x1,y1 =10,20
print(x1)
print(y1)
```

    10
    20

**Back to the expression** We can define an expression using the variables $a$ and $b$.

```python
expr = (3*a-4*b)**3
print(expr)
```

    (3*a - 4*b)**3

We can also make it look nicer in our notebook. This doesn't affect the math, but it makes our notebook more readable.

```python
sym.init_printing()
```

```python
expr
```

**Now we expand the function algebreically**

```python
expr.expand()
```

Sympy can also factor the equation

```python
sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3)
```

If you want to copy and paste a result, you print the result.

```python
print(sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3))
```

    2*(a - 2*b)*(13*a**2 - 28*a*b + 16*b**2)

You can also chain together functions

```python
expr.expand().factor()
```

### Exercise 1.1

Show that the following two expressions are true.
$$(2w-3z)(2w+3z)=4w^2-9z^2$$
$$(2w-3z)^2\ne4w^2-9z^2$$

```python
# Your code here
w,z=sym.symbols('w,z')
expr=(2*w-3*z)*(2*w+3*z)
print(expr,'=',expr.expand())
print("this shows that the first equation is true")
expr2=(2*w-3*z)**2
print(expr2,'=',expr2.expand())
print("this shows that the second equation is true, because (2*w-3*z)**2 is not equal to (4*w**2 - 9*z**2)")
```

    (2*w - 3*z)*(2*w + 3*z) = 4*w**2 - 9*z**2
    this shows that the first equation is true
    (2*w - 3*z)**2 = 4*w**2 - 12*w*z + 9*z**2
    this shows that the second equation is true, because (2*w-3*z)**2 is not equal to (4*w**2 - 9*z**2)

## 2. Solving Equations

Sympy can be used to symbolilically solve equations. As before, you need to define which variables are symbols

### Example 2.1

Use sympy to solve the following equation
$$ax^3+bx^2+cx+d=0$$

```python
# Define the variables
a,b,c,d,x = sym.symbols('a,b,c,d,x')
```

```python
# Define the expression
expr=a*x**3+b*x**2+c*x+d
expr
```

We can use the `solvset` function to solve this equation

```python
solutions=sym.solveset(expr,x)
```

```python
print(solutions)
```

    {-(-3*c/a + b**2/a**2)/(3*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a)}

```python
solutions
```

What if I need help. You can do this with any python function. `function?`

```python
# Run this command to see a help box
sym.solveset?
```

### Exercise 2.1

Use the `solveset` function to solve the following chemical problem.

Phosgene gas, $\text{COCl}_2$, dissociates at high temperatures according to the following equilibrium:

$$ \text{COCl}_2 \rightleftharpoons  \text{CO} + \text{Cl}_2 $$

At $\text{400 C}$, the equilibrium constant $K_c=8.05$.

If you start with a  $\text{0.250 M}$ phosgene sample at $\text{400 C}$, determine the concentrations of all species at equilibrium.

```python
# Your code here
#kc=(products/reactants)
#8.05=(x^2/(0.250-x)). Use ICE table to solve for this
#0=(x^2/(0.250-x)-8.05)
sym.solveset?
x = sym.symbols('x')
sym.solveset((((x**2)/(0.250-x))-8.05),x)
```

Why did you pick your answer?

The concentration should be positive, and so the answer is that each species is ~0.24M.

## 3. Calculus

We can use also Sympy to differentiate and integrate. Let us experiment with differentiating the following expression:

$$x ^ 2 - \cos(x)$$

```python
sym.diff(x ** 2 - sym.cos(x), x)
```

Similarly we can integrate:

```python
sym.integrate(x ** 2 - sym.cos(x), x)
```

We can also carry out definite integrals:

```python
sym.integrate(x ** 2 - sym.cos(x), (x, 0, 5))
```

### Exercise 3.1

Use Sympy to calculate the following:

1. $\frac{d(x ^2 + xy - \ln(y))}{dy}$
1. $\int_0^5 e^{2x}\;dx$

```python
x,y=sym.symbols('x,y')
sym.diff(x**2+x*y-sym.ln(y),y)
```

```python
from sympy import *
sym.integrate(exp(2*x), (x, 0, 5))
```

### Exercise 3.2
Solve the following definite integral
$$\int\limits_{ - \infty }^\infty  {\frac{1}{{\sigma \sqrt {2\pi } }}{e^{ - \frac{1}{2}{{\left( {\frac{{x - \mu }}{\sigma }} \right)}^2}}}}$$

Hint, the sympy symbol for infinity is `oo`

```python
# Your code here
from sympy import *
σ,μ,x=sym.symbols('σ,μ,x')
sym.integrate((1/(σ*sqrt(2*sym.pi)))*exp(-(1/2)*((x-μ)/σ)**2), (x, -oo, oo))
```

$\displaystyle \begin{cases} 0.353553390593274 \sqrt{2} \left(2 - \operatorname{erfc}{\left(\frac{0.707106781186548 μ}{σ} \right)}\right) + 0.353553390593274 \sqrt{2} \operatorname{erfc}{\left(\frac{0.707106781186548 μ}{σ} \right)} & \text{for}\: \left(\left(2 \left|{\arg{\left(σ \right)}}\right| \leq \frac{\pi}{2} \wedge \left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)}}\right| < \pi\right) \vee \left(\left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)}}\right| \leq \pi \wedge 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \vee \left(\left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)}}\right| < \pi \wedge 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \vee 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \wedge \left(\left(2 \left|{\arg{\left(σ \right)}}\right| \leq \frac{\pi}{2} \wedge \left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)} + 2 \pi}\right| < \pi\right) \vee \left(\left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)} + 2 \pi}\right| \leq \pi \wedge 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \vee \left(\left|{2 \arg{\left(μ \right)} - 4 \arg{\left(σ \right)} + 2 \pi}\right| < \pi \wedge 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \vee 2 \left|{\arg{\left(σ \right)}}\right| < \frac{\pi}{2}\right) \\\int\limits_{-\infty}^{\infty} \frac{\sqrt{2} e^{- \frac{0.5 \left(x - μ\right)^{2}}{σ^{2}}}}{2 \sqrt{\pi} σ}\, dx & \text{otherwise} \end{cases}$

Lookup Gaussian functions: https://en.wikipedia.org/wiki/Gaussian_function
Does your answer maake sense?

## 4. Plotting with Sympy

Finally Sympy can be used to plot functions. Note that this makes use of [matplotlib](http://matplotlib.org/).

Let us plot $x^2$:

```python
expr = x ** 2
p = sym.plot(expr)
```

    <Figure size 640x480 with 1 Axes>

### Exercise 4.1 Plot the following function:

1. $y=x + cos(x)$
1. ${\frac{1}{{ \sqrt {2\pi } }}{e^{ - \frac{x^2}{2}}}}$

```python
# Your code here
expr = x + cos(x)
p = sym.plot(expr)
```

```python
expr = (1/(sqrt(2*sym.pi)))*exp(-(x**2)/2)
p = sym.plot(expr)
```

# Lecture

## L1. Hydrogen Atom

Sympy has built in modules for the eigenfunctions of the hydrogen atom.

```python
import sympy.physics.hydrogen
import numpy as np
```

You can caluclate the eigenvalues ($E$) in Hartrees

`sym.physics.hydrogen.E_nl(n,Z)`

```python
sym.physics.hydrogen.E_nl(1,1)
```

We can use a loop to print out many energies

```python
for n in range(1,5):
    print(sym.physics.hydrogen.E_nl(n,1))
```

    -1/2
    -1/8
    -1/18
    -1/32

We can plot the hydrogen radial wavefunction (1s orbital)

```python
r = sympy.symbols('r')
sympy.physics.hydrogen.R_nl(1, 0, r, 1)
#principle number, angular momentum, variable, Z (charge)
```

```python
sym.plot(sympy.physics.hydrogen.R_nl(1, 0, r, 1),(r,0,10.50))
```

And the probablity distribution function

```python
sympy.symbols('r')
prob_1s=sympy.physics.hydrogen.R_nl(1, 0, r, 1)*sympy.physics.hydrogen.R_nl(1, 0, r, 1)
prob_1s
```

```python
sym.plot(prob_1s,(r,0,10))
```

Plot a 2s orbital

```python
sympy.symbols('r')
prob_2s=sympy.physics.hydrogen.R_nl(2, 0, r, 1)*sympy.physics.hydrogen.R_nl(2, 0, r, 1)
prob_2s
```

```python
sym.plot(prob_2s,(r,0,10))
```

We can change the range to see the node better.

```python
sym.plot(prob_2s,(r,1,8))
```

Notice the node!

### Exercise L1.1

Plot the radial distriubution function for a 2p, 3s, 4s, and 3d orbital.

```python
r = sympy.symbols('r')
sympy.physics.hydrogen.R_nl(2, 1, r, 1)
#principle number, angular momentum, variable, Z (charge)
```

```python
sym.plot(sympy.physics.hydrogen.R_nl(2, 1, r, 1),(r,0,15))
```

```python
sympy.symbols('r')
prob_2p=sympy.physics.hydrogen.R_nl(2, 1, r, 1)*sympy.physics.hydrogen.R_nl(2, 1, r, 1)
prob_2p
```

```python
sym.plot(prob_2p,(r,0,10))
```

```python
r = sympy.symbols('r')
sympy.physics.hydrogen.R_nl(3, 0, r, 1)
#principle number, angular momentum, variable, Z (charge)
```

```python
sym.plot(sympy.physics.hydrogen.R_nl(3, 0, r, 1),(r,0,20))
```

```python
sympy.symbols('r')
prob_3s=sympy.physics.hydrogen.R_nl(3, 0, r, 1)*sympy.physics.hydrogen.R_nl(3, 0, r, 1)
prob_3s
```

```python
sym.plot(prob_3s,(r,1,20))
```

```python
r = sympy.symbols('r')
sympy.physics.hydrogen.R_nl(4, 0, r, 1)
#principle number, angular momentum, variable, Z (charge)
```

```python
sym.plot(sympy.physics.hydrogen.R_nl(4, 0, r, 1),(r,0,25))
```

```python
sympy.symbols('r')
prob_4s=sympy.physics.hydrogen.R_nl(4, 0, r, 1)*sympy.physics.hydrogen.R_nl(4, 0, r, 1)
prob_4s
```

```python
sym.plot(prob_4s,(r,1,25))
```

```python
r = sympy.symbols('r')
sympy.physics.hydrogen.R_nl(3, 2, r, 1)
#principle number, angular momentum, variable, Z (charge)
```

```python
sym.plot(sympy.physics.hydrogen.R_nl(3, 2, r, 1),(r,0,25))
```

```python
sympy.symbols('r')
prob_3d=sympy.physics.hydrogen.R_nl(3, 2, r, 1)*sympy.physics.hydrogen.R_nl(3, 2, r, 1)
prob_3d
```

$\displaystyle \frac{8 r^{4} e^{- \frac{2 r}{3}}}{98415}$

```python
sym.plot(prob_3d,(r,0,25))
```

```python

```


This notebook is my solution for Design Problem #3 of the Structural Mechanics course of MIT Open Course Work.

Paul Lagace. 16.20 Structural Mechanics. Fall 2002. Massachusetts Institute of Technology: MIT OpenCourseWare, https://ocw.mit.edu. License: Creative Commons BY-NC-SA.

```python
import numpy as np
import sympy as sp
from sympy import init_printing
init_printing(use_latex=True)
from sympy.utilities.lambdify import lambdify
```

```python
# All the material properties for base and piezoelectric materials
al_7075_T6 = {'E_L':70e9, 'E_T':70e9, 'nu':0.3, 'a_L':12.9*1.8e-6, 'a_T':12.9*1.8e-6,
              'rho':2.8e3, 'price':60*2.204, 'name':'AL 7075-T6'}
Ti_6Al_4V = {'E_L':107e9, 'E_T':107e9, 'nu':0.3, 'a_L':5*1.8e-6, 'a_T':5*1.8e-6,
             'rho':4.43e3, 'price':85*2.204, 'name':'Ti-6Al-4V'}
AS4_3501_6 = {'E_L':80e9, 'E_T':30e9, 'nu':0.43, 'a_L':4.2*1.8e-6, 'a_T':7.3*1.8e-6,
              'rho':1.54e3, 'price':140*2.204, 'name':'Graphite/Epoxy AS4/3501-6'}
P70_5501_6 = {'E_L':170e9, 'E_T':25e9, 'nu':0.45, 'a_L':2.1*1.8e-6, 'a_T':6.3*1.8e-6,
              'rho':1.6e3, 'price':300*2.204, 'name':'Graphite/Epoxy P70/3501-6'}
base_materials = [al_7075_T6, Ti_6Al_4V, AS4_3501_6, P70_5501_6]

pzt_4 = {'E':64e9, 'nu':0.31, 'a':4.5e-6, 'd_113':-0.123e3, 'd_333':0.289e3,
         'Ek':10e5, 'rho':7.5e3, 'price':5600*2.204, 'name':'PZT 4'}
pzt_5H = {'E':48e9, 'nu':0.31, 'a':4.5e-6, 'd_113':-0.274e3, 'd_333':0.593e3,
         'Ek':4e5, 'rho':7.5e3, 'price':5600*2.204, 'name':'PZT 5H'}
pzt_5A = {'E':53e9, 'nu':0.31, 'a':4.5e-6, 'd_113':-0.171e3, 'd_333':0.374e3,
         'Ek':7e5, 'rho':7.5e3, 'price':5600*2.204, 'name':'PZT 5A'}
piezo_materials = [pzt_4, pzt_5H, pzt_5A]
```

```python
T_min = -75  # deg C
T_max = 100  # deg C
Length = 29  # m
displacement = 0.01  # m
```

```python
s_b1, E_bl, a_bl, dT = sp.symbols('sigma_b1, E_bl, alpha_bl dT')
s_p1, E_p, a_p, d_113, E_k = sp.symbols('sigma_p1, E_p, alpha_p, d_113, E_k')
t_b, t_p, L, L_d, x, du = sp.symbols('t_b, t_p, L, L_d, x, du')
```

```python
# Equation for strain of the base material
e_m_b1 = s_b1/E_bl
e_t_b1 = a_bl*dT
e_b1 = e_m_b1 + e_t_b1
print("Base material strain equation:")
e_b1
```

```python
# strain of the piezoelectric material
e_m_p1 = s_p1/E_p
e_t_p1 = a_p*dT
e_p_p1 = d_113*E_k
e_p1 = e_m_p1 + e_t_p1 + e_p_p1
print("Piezoelectric material strain equation:")
e_p1
```

```python
# equation for equilibrating stress in the cross section
# the composite structure must be in equilibrium with no
# net forces and stresses that cancel out based on the
# stress in each material and their cross-section area
equilibrating_stress = sp.integrate(s_b1, (x, -t_b-t_p/2, -t_p/2))\
    + sp.integrate(s_p1, (x, -t_p/2, t_p/2))\
    + sp.integrate(s_b1, (x, t_p/2, t_p/2 + t_b))
print("Relationship of self-equilibrating stresses:")
sp.simplify(equilibrating_stress)
```

```python
# solve for the base material stress based on displacement integrated from the
# strain equation over the length of the structure
u_b = sp.integrate(e_b1, (x, 0, L_d))
stress_b1 = sp.solve(u_b - du, s_b1)[0]
stress_b1_func = lambdify((E_bl, a_bl, dT, L_d, du), stress_b1)
print("Base material stress equation:")
sp.simplify(stress_b1)
```

```python
# piezo material stress equation, solve just like base material above
u_p = sp.integrate(e_p1, (x, 0, L_d))
stress_p1 = sp.solve(u_p - du, s_p1)[0]
stress_p1_func = lambdify((E_p, a_p, d_113, E_k, dT, L_d, du), stress_p1)
print("Piezo material stress equation:")
sp.simplify(stress_p1)
```

```python
# solve for the electric field variable from the piezo material displacement
# equation
Efield_p1 = sp.solve(u_p - du, E_k)[0]
Efield_p1_func = lambdify((E_p, a_p, d_113, s_p1, dT, L_d, du), Efield_p1)
print("Piezo material electric field equation:")
sp.simplify(Efield_p1)
```

```python
def thermal_strain(a, dT):
    return a*dT

def mech_strain(stress, E):
    return stress/E

def piezo_strain(d, Ek):
    return d*Ek
```

```python
for base in base_materials:
    # calculate min and max stresses to keep the base material at the
    # design displacement limit
    s_b_min = stress_b1_func(base['E_L'], base['a_L'], T_min, Length, -displacement)
    s_b_max = stress_b1_func(base['E_L'], base['a_L'], T_max, Length, displacement)

    print(f"For base material: {base['name']}:")
    print(f"    min temp stress = {s_b_min*1e-6:.2f} MPa")
    print(f"    max temp stress = {s_b_max*1e-6:.2f} MPa")
    for piezo in piezo_materials:
        # calculate stress for min displacement of the piezo material
        # this should be negative to compress material to design displacement
        s_p_min = stress_p1_func(piezo['E'], piezo['a'], piezo['d_113'],
                                 0, T_min, Length, -displacement)
        # calculate ratio of thicknesses based on stresses to reach design
        # displacement at -75 deg C
        t_ratio = -s_p_min/(2*s_b_min)
        if t_ratio < 0:
            s_p_max = stress_p1_func(piezo['E'], piezo['a'], piezo['d_113'],
                                     piezo['Ek']/5e11, T_max, Length, displacement)
            t_ratio = -s_p_max/(2*s_b_max)
            delta_T = 100
            delta_u = 0.01
            s_p_min = -t_ratio*2*s_b_min
        else:
            s_p_max = -t_ratio*2*s_b_max
            delta_T = 100
            delta_u = 0.01

        # calculate electric field needed to have piezo strain match max displacement
        # this uses max piezo stress based on the stress and thickness ratio of
        # base material at 100 deg C
        Efield = Efield_p1_func(piezo['E'], piezo['a'], piezo['d_113'], s_p_max,
                                delta_T, Length, delta_u)

        # check the max and min displacements of the piezo to confirm strain
        # meets design requirements
        du_max = thermal_strain(piezo['a'], T_max)*Length\
            + mech_strain(s_p_max, piezo['E'])*Length\
            + piezo_strain(piezo['d_113'], Efield)*Length

        du_min = thermal_strain(piezo['a'], T_min)*Length\
            + mech_strain(s_p_min, piezo['E'])*Length

        print(f"    with {piezo['name']}:")
        print(f"        min temp stress = {s_p_min*1e-6:.2f} MPa")
        print(f"        max temp stress = {s_p_max*1e-6:.2f} MPa")
        print(f"        tb/tp = {t_ratio:.5f}")
        print(f"        Ek = {Efield:.3e} V/m")
        print(f"        V/tb = {Efield/t_ratio:.3e} V")
        if Efield > piezo['Ek']/2:
            print(" "*12 + f"required electric field will depole the piezo")
        if du_max > displacement*1.01:
            print(" "*8 + f"max displacement too great.")
            print(f"        du = {du_max:.3f} m")
        if du_min < -displacement*1.01:
            print(" "*8 + f"min displacement too great.")
            print(f"        du = {du_min:.3f} m")

        # cost estimate - price for piezo / price for base
        cost_ratio = t_ratio*(base['rho']/piezo['rho'])*(base['price']/piezo['price'])
        print(" "*8 + f"cost ratio is {1/cost_ratio:.2f}")
    print("")
```

    For base material: AL 7075-T6:
        min temp stress = 97.77 MPa
        max temp stress = -138.40 MPa
        with PZT 4:
            min temp stress = -0.47 MPa
            max temp stress = 0.66 MPa
            tb/tp = 0.00240
            Ek = 9.394e-07 V/m
            V/tb = 3.917e-04 V
            cost ratio is 104236.95
        with PZT 5H:
            min temp stress = -0.35 MPa
            max temp stress = 0.50 MPa
            tb/tp = 0.00180
            Ek = 4.217e-07 V/m
            V/tb = 2.344e-04 V
            cost ratio is 138982.60
        with PZT 5A:
            min temp stress = -0.39 MPa
            max temp stress = 0.55 MPa
            tb/tp = 0.00199
            Ek = 6.757e-07 V/m
            V/tb = 3.402e-04 V
            cost ratio is 125871.03

    For base material: Ti-6Al-4V:
        min temp stress = 35.33 MPa
        max temp stress = -59.40 MPa
        with PZT 4:
            min temp stress = -0.47 MPa
            max temp stress = 0.79 MPa
            tb/tp = 0.00664
            Ek = 9.552e-07 V/m
            V/tb = 1.439e-04 V
            cost ratio is 16805.07
        with PZT 5H:
            min temp stress = -0.35 MPa
            max temp stress = 0.59 MPa
            tb/tp = 0.00498
            Ek = 4.288e-07 V/m
            V/tb = 8.614e-05 V
            cost ratio is 22406.76
        with PZT 5A:
            min temp stress = -0.39 MPa
            max temp stress = 0.65 MPa
            tb/tp = 0.00550
            Ek = 6.871e-07 V/m
            V/tb = 1.250e-04 V
            cost ratio is 20292.91

    For base material: Graphite/Epoxy AS4/3501-6:
        min temp stress = 17.77 MPa
        max temp stress = -32.89 MPa
        with PZT 4:
            min temp stress = -0.47 MPa
            max temp stress = 0.87 MPa
            tb/tp = 0.01319
            Ek = 9.653e-07 V/m
            V/tb = 7.317e-05 V
            cost ratio is 14766.23
        with PZT 5H:
            min temp stress = -0.35 MPa
            max temp stress = 0.65 MPa
            tb/tp = 0.00989
            Ek = 4.333e-07 V/m
            V/tb = 4.380e-05 V
            cost ratio is 19688.31
        with PZT 5A:
            min temp stress = -0.39 MPa
            max temp stress = 0.72 MPa
            tb/tp = 0.01093
            Ek = 6.943e-07 V/m
            V/tb = 6.356e-05 V
            cost ratio is 17830.92

    For base material: Graphite/Epoxy P70/3501-6:
        min temp stress = -10.43 MPa
        max temp stress = -5.64 MPa
        with PZT 4:
            min temp stress = 16.66 MPa
            max temp stress = 9.01 MPa
            tb/tp = 0.79912
            Ek = 2.000e-06 V/m
            V/tb = 2.503e-06 V
            cost ratio is 109.50
        with PZT 5H:
            min temp stress = 10.12 MPa
            max temp stress = 5.47 MPa
            tb/tp = 0.48528
            Ek = 8.000e-07 V/m
            V/tb = 1.649e-06 V
            cost ratio is 180.31
        with PZT 5A:
            min temp stress = 13.15 MPa
            max temp stress = 7.11 MPa
            tb/tp = 0.63076
            Ek = 1.400e-06 V/m
            V/tb = 2.220e-06 V
            cost ratio is 138.72

```python
# equation for electric field given a temperature change great enough to
# displace the structure beyond the limit

# piezo stress in terms of the base material and thickness ratio
stress_peizo = -2*stress_b1*(t_b/t_p)
# substitute this into the electric field equation
print("Equation for the electric field as a function of temperature change,"
      " material properties, and the max design displacement:")
sp.expand(Efield_p1_func(E_p, a_p, d_113, stress_peizo, dT, L_d, du))
```

The choice is really between the two different graphite/epoxy composites, given that their thermal coefficients are low and close to the piezoelectric. This means less piezo can be used and there is a smaller displacement to counteract at the extreme temperature ends.

Because the thermal coefficient for the P70/3501-6 composite is less than the piezo electric, there is no concern of displacement at the lower temperature end. The composite contracts 0.0082m and the piezoelectric contracts 0.0098 m; and both are within the limit of 0.0100 m. The tradeoff is a need for a greater electric at the high end of the temperature range.

However, we can calculate a thickness ratio between base and piezoelectric to meet a desired electric field, and this combination will require far less piezoelectric than the others. This is because the base material has more favorable thermal properties and we want that to dominate the overall properties of the structure. With far less piezoelectric, the higher electric field does not equate to greater voltage because a thinner dieletric will have a greater field strength at the same voltage.

On the required voltage, all structure combinations only require an electric field when the structure is in the sun and power can be supplied via solar panels. Given this, reducing the initial cost should be prioritized. With the P70/3501-6 base, the ratio of base material to piezo is so great that the normalized cost calculated above is essentially zero.

It is recommended to use P70/3501-6 base material with PZT 5H piezoelectric material for this structure.

```python

```


# Monte Carlo Ferromagnet

Christina C Lee

Physics Prerequisites: Statistics Mechanics/ Phase Transitions

Numerics Prerequisite: Monte Carlo Markov Chain

## The physics

What process turns liquid water into a solid?  As temperature lowers, materials transition from a regime where the desire to maximize entropy dominates to desiring to minimize energy.   In transitions like liquid-solid, the energy arises from atom-atom forces.  This problem falls into a category called first order phase transitions, which are difficult to study.

Instead of looking at the liquid-solid problem to understand phase transitions, we will look at a magnetic material undergoing a second order phase transition. In our approximation of the problem, the atoms exist at fixed positions on a lattice, and interact with their neighbor according to the following Hamiltonian,
\begin{equation}
    {\cal H} = -J \sum_{} S^z_i S_j^z
\end{equation}
, which is basically just the energy.  This Ising Model has nearest neighbors interacting, and each spin variablesolely points in the $\pm z$ direction.

At a given temperature $T$ (inverse temperature $\beta=1/T$, $k_b=1$), the occupancy of a given configuration $c_i$ follows the Maxwell-Boltzmann Probability Distribution,
\begin{equation}
P(c_i)=\frac{\mathrm{e}^{-\beta E(c_i)}}{\sum\limits_j \mathrm{e}^{-\beta E(c_j)}}
\end{equation}
We need to determine observables given this probability distribution.

## Tunable Parameters

I will say a little bit about the tunable parameters here, but Monte Carlo simulations are as much an art as a science, so I leave it up to you to play with the numbers and build intuition for what works.

### Temperature

On the left I am performing a low temperature search of a couch store.  I found a minimum, and even though I got stuck there, I thoroughly got to know what that minimum had to offer. On the right though, I'm performing a high temperature search of the Alexander Hills, Death Valley, California in pursuit of Neoproterzoic carbonates and dimictites.  I needed to be able to explore a lot more of the environment.

While in a physical system like a magnet, temperature has a physical meaning, we can create other types of situations, like optimization or approximating a probability distribution, where we use temperature to describe how stuck we are to a particular minimum.  This intuition also plays a role in interpreting the results of of our simulation.

Temperature can also provide other complications when we are studying critical phenomena, phase transitions.  Near the critical temperature, computations get much more difficult.  Elaboration in later post.

### Size of Lattice

* Larger lattice gives more precise results.
* Larger lattice takes more memory and time.
* Finite Size effects (to be disscussed later) display some interesting physics.

### Coupling Constant J

Usually normalized out anyway...

### Number of time steps

Each Monte Carlo time step is one complete sweep of the lattice ($N$ random flip attempts).  The more time steps, the more accurate the results, but the longer you have to wait.

### When to Measure

Though a time step does include $N$ spin flips, that doesn't mean we have actually moved to a truly new configuration.  If we want to randomly sample our configuration space, we need to wait a bit longer to sample.  Too short a time, and our measurements are coupled and not accurate.  Too long, and we are wasting computations.  In a later post, I will look at the autocorrelation time, which is a good way to figure our if your measurement time is good.

### Other potential Options

Some less important things: periodic versus open boundary conditions, shape and symmetries of simulation cell.

## The Numerics

In the post on the Markov Chain, each state was a location on our grid.  Now our state is one configuration of all our $N$ spins.  That means for an Ising spin we have $2^N$ different configurations.  Yeah... We aren't going to be exploring all of those.

So how do we get from one state to the next?

First, we choose a spin that we could flip.  This potential spin is chosen randomly for convenience's sake.

Then we use the Metropolis-Hastings Algorithm to decide whether or no to flip the spin and generate a new configuration.

We split the solution into two cases based on $\alpha = \frac{\pi_i}{\pi_j}= \frac{P(c_i)}{P(c_j)}$:

* If $\alpha > 1$ then always accept the transition, $p_{i \to j} = 1$
* If $\alpha < 1$, accept the transition with probability $\alpha = p_{i \to j}$

Does this obey detailed balance?

From the Monte Carlo Markov Chain post, we take our equation for detailed balance

\begin{equation}
\frac{p_{i \to j}}{ p_{j \to i}} = \frac{\pi_i}{\pi_j}
\end{equation}

How about ergodicity? We can reach any configuration by flipping spins.

For the Ising Ferromagnet, our $\alpha$ is
\begin{equation}
\alpha= \frac{Z}{Z} \frac{\mathrm{e}^{-\beta E_i}}{\mathrm{e}^{-\beta E_j}}
= \mathrm{e}^{\beta \left(E_j - E_i \right)}
\end{equation}
which is simply a function of difference in energy between two states.  Therefore we don't need to know the absolute energy at each point in time, just how the spin flip changes its local environment.

## Tunable Parameters

I will say a little bit about the tunable parameters here, but Monte Carlo simulations are as much an art as a science, so I leave it up to you to play with the numbers and build intuition for what works.

### Temperature

On the left I am performing a low temperature search of a couch store.  I found a minimum, and even though I got stuck there, I thoroughly got to know what that minimum had to offer. On the right though, I'm performing a high temperature search of the Alexander Hills, Death Valley, California in pursuit of Neoproterzoic carbonates and dimictites.  I needed to be able to explore a lot more of the environment.

While in a physical system like a magnet, temperature has a physical meaning, we can create other types of situations, like optimization or approximating a probability distribution, where we use temperature to describe how stuck we are to a particular minimum.  This intuition also plays a role in interpreting the results of of our simulation.

Temperature can also provide other complications when we are studying critical phenomena, phase transitions.  Near the critical temperature, computations get much more difficult.  Elaboration in later post.

### Size of Lattice

* Larger lattice gives more precise results.
* Larger lattice takes more memory and time.
* Finite Size effects (to be disscussed later) display some interesting physics.

### Coupling Constant J

Usually normalized out anyway...

### Number of time steps

Each Monte Carlo time step is one complete sweep of the lattice ($N$ random flip attempts).  The more time steps, the more accurate the results, but the longer you have to wait.

### When to Measure

Though a time step does include $N$ spin flips, that doesn't mean we have actually moved to a truly new configuration.  If we want to randomly sample our configuration space, we need to wait a bit longer to sample.  Too short a time, and our measurements are coupled and not accurate.  Too long, and we are wasting computations.  In a later post, I will look at the autocorrelation time, which is a good way to figure our if your measurement time is good.

### Other potential Options

Some less important things: periodic versus open boundary conditions, shape and symmetries of simulation cell.

```julia
using Statistics;
using Plots;
gr()

push!(LOAD_PATH,"../Packages")
using Lattices;
```

    ┌ Info: Recompiling stale cache file /home/shaula/.julia/compiled/v1.0/Lattices.ji for Lattices [top-level]
    └ @ Base loading.jl:1187

Instead of going into calculating all the lattice parameters again, we will use a class I define in the file Lattices.jl .  This class contains

Lattice Types
* Chain
* Square
* Honeycomb
You can edit the file to make your own types.

Once a lattice is created, it contains Members of Type:
* `name`, a string
* `l`, length in number of unit cells
* `dim`, dimension of lattice
* `a`, array containing the basis vectors by which positions are generated
* `unit`, array of positions inside a single unit
* `N`, number of total sites
* `X`, array of positions
* `nnei`, number of nearest neighbors
* `neigh`, Array of nearest neighbors [i][j], where i is site and j is 1:nnei

Today, I will just look at the square lattice, since that indicates much of the standard phase transition properties.  Some of the lattices I have shown (kagome, triangular, ...) are special frustrated lattices, and thus will behave very wierdly in this situation.

```julia
## Define l here
l=50;

lt=MakeLattice("Square",l);
S=ones(Int8,l,l);  #Our spins
dt=1/(lt.N);
```

```julia
# The energy contribution of just one site
function dE(i::Int)
    Eii=0;
    for j in 1:lt.nnei
        Eii+=S[lt.neigh[i,j]];
    end
    Eii*=-J*S[i];  # we are computing J sz_i sz_j for one i
    return Eii;
end
# The energy of the entire lattice
function E()
    Evar=0;
    for k in 1:lt.N
        Evar+=.5*dE(k);
    end
    return Evar;
end
# The magnetization of the entire lattice
function M()
    Mvar=0;
    for k in 1:lt.N
        Mvar+=S[k];
    end
    return Mvar;
end
"defined functions"
```

    "defined functions"

# Adjustable Parameters

I have set up the simulation so that you can perform two different things.  For one, you can set `video=true` and `t` to a small variable.  Then in a new window you see what the configuration looks like each time you measure.

Or you can set `video=false` and `t` to a large variable, and actually measure the statistics for the system over a bunch of configurations.

```julia
beta=.2;
J=1;
t=10000;
nskip=10;   # don't measure every sweep= better decorrelation
"Parameters set"
```

    "Parameters set"

```julia
nmeas=floor(Int,t/nskip); # how many times we will measure
Ma=Array{Int32}(undef,nmeas); # our magnetization measurements
Ea=Array{Int32}(undef,nmeas); # our energy measurements
"done"
```

    "done"

```julia
tm=1; #Our measurement time step
for ti in 1:t
    for j in 1:lt.N
        ii = rand(1:lt.N); #Choosing a random site
        de=dE(ii);
        if(de>0 || rand()<exp(2*beta*de) )
            S[ii]=-S[ii]; #Switch the sign
        end
    end

    if isapprox(mod(ti,nskip),0)
        Ma[tm]=M();
        Ea[tm]=E();
        tm+=1;

    end
end
Mave=mean(Ma/lt.N);
Mstd=std(Ma/lt.N);
Eave=mean(Ea/lt.N);
Estd=std(Ea/lt.N);
Mave, Mstd
```

    (0.0008727999999999999, 0.03443763305935428)

```julia
heatmap(S)
```

```julia
plot(1:nskip:5000,Ma[1:500]/lt.N)
hline!([Mave])
plot!(xlabel="Monte Carlo Step"
    ,ylabel="Magetization"
    ,title="Magnetization versus Time, Beta=$beta",legend=false)
```

```julia
histogram(Ma/lt.N,normed=true,label="Samples Normed")

x=collect(Mave-Mstd*3:.001:Mave+Mstd*3)
gaussian=1/(Mstd*sqrt(2*pi))*exp.(-(x.-Mave).^2/(2*Mstd^2));

plot!(x,gaussian,label="Gaussian Fit")
plot!(xlabel="Magnetization",
    ylabel="Normed Counts",
    title="Magnetization Sample Distribution beta=$beta")
```

```julia
plot(1:nskip:5000,Ea[1:500]/lt.N)
hline!([Eave])
plot!(xlabel="Monte Carlo Steps",
    ylabel="Energy",
    title="Energy versus Time, Beta=$beta",legend=false)
```

```julia
histogram(Ea/lt.N,normed=true,label="Samples Normed")

x=collect(Eave-3*Estd:.001:Eave+3*Estd)
gaussian=1/(Estd*sqrt(2*pi))*exp.(-(x.-Eave).^2/(2*Estd^2));
plot!(x,gaussian,label="Gaussian Fit")

plot!(xlabel="Energy",
    ylabel="Normed Counts",
    title="Energy Historam, beta=$beta")
```

# Example Results
So here are some example results I got.

Note: These pictures came from an old version using Julia 0.4.8 and PyPlot.

Paramagnet Gif

Paramagnet Magnetization Samples.  A properly tuned simulations should be showing white noise without correlations or getting stuck in particular areas.

Histogram for Magnetization, $\beta=0.3$

Histogram for Energy $\beta=0.3$

Magnet at $\beta=0.7$ Gif

Histogram for Magnetization

Histogram for Energy

# To be covered later:

This is an extremely rich problem.  Since this post seems long enough for me already, I'll leave these to exercises for you right now, or you can wait until I hold your hand through them.

* Plot magnetization and energy as a function of temperature
* What's the transition temperature?
* How does the dispersion change as a function of temperature? Use that to calculate specific heat and susceptibility
* How do results change with system size?
* Change dimension?
* Put it on a different lattice.  BE CAREFUL of lattices like triangular, checkerboard, pyrochlore, ...
* Ferromagnetic versus Antiferromagnetic coupling
* Autocorrelation function
* Structure Factor (Fourier transform)

```julia

```


#### _Speech Processing Labs 2021: SIGNALS 2: FIR Filters_

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

import cmath
from math import floor
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
plt.style.use('ggplot')

from dspMisc import *
```

# Filtering the Source: Finite Impulse Response Filters

### Learning Outcomes
* Be able to describe what an FIR filter is
* Be able to explain what the impulse response of an FIR filter is
* See how an FIR filter can be used as a lowpass frequency filter.
* See how changing the coefficients of an FIR filter can change it's frequency response

### Need to know
* Topic Videos: Spectral Envelope, Filter, Impulse Train
* [Interpreting the DFT](../signals-lab-1/signals-1-1-interpreting-the-discrete-fourier-transform.ipynb)
* [Building the source: impulse trains](./signals-2-1-impulse-as-source.ipynb)

Equation alert: If you're viewing this on github, please note that the equation rendering is not always perfect. You should view the notebooks through a jupyter notebook server for an accurate view.

## Filters

We've seen in the past notebooks that sometimes our input signal isn't exactly what we want. There is a vast literature in signal processing about designing filters to transform one signal into another. In speech processing, our signals often includes some sort of noise that we'd like to get rid of.  However, we can also use filters to shape a simple input, like an impulse train, into something much more complicated, like a speech waveform.

In class you've seen two types of filters:

* Finite Impulse Response (FIR)
* Infinite Impulse Response (IIR)

Both perform a transform on an input sequence $x[n]$ to give us some desired output sequence $y[n]$.  The difference between the two types of filters is basically whether we only use the inputs to derive each output $y[n]$ (FIR), or whether we also use previous outputs (IIR).

In the following we'll illustrate some of the properties of FIR filters.

```python

```

## 1  Finite Impulse Response Filters

Finite Impulse Response (FIR) filters have the following form:

$$
\begin{align}
y[n] &= b[0]x[n] + b[1]x[n-1] + \dots + b[K]x[n-K] \\
 &= \sum_{k=0}^K b(k) x[n-k]
\end{align}
$$

Here, we have:
* an input sequence $x[n]$ of length $N$
* a set of $K$ filter coefficients.

We can read the equation as saying that $n$th ouput of the filter, $y[n]$, is a weighted sum of the previous K inputs $x[n],...,x[n-K]$.

### Example
Let's plot the $b[k]\cdot x[n-k]$ terms where $x$ is sinusoid of 4 Hz and the filter is `b=[0.2, 0.2, 0.2, 0.2, 0.2]`

```python
## Plotting a filter input window

## Set the number of samples N, sampling rate f_s
N=64
f_s = 64
t_s = 1/f_s
print("sampling rate: f_s = %f\nsampling time: t_s: %f" % (f_s, t_s))

x, time_steps = gen_sinusoid(frequency=4, phase=0, amplitude=1, sample_rate=f_s, seq_length=N, gen_function=np.cos)

## Plot the sinusoid we've just created:
fig, timedom = plt.subplots(figsize=(16, 4))
timedom.plot(time_steps, x, 'o', color='grey')
timedom.set_xlabel('Time (s)')
timedom.set_ylabel('Amplitude')

## Filter coefficients (you could try changing these)
b = [0.2, 0.2, 0.2, 0.2, 0.2]

## K is the number of filter coefficients
K=len(b)
print("number of filter coefficients: %d" % K)

## Let's see what happens when n=19 (or try changing this variable!)
n=19
print("filter using b at n=%d, K=%d\n" % (n, K))

## Plot the values that are input to the filter
## +1's because python indexing/slicing doesn't include the end point
timedom.plot(time_steps[n+1-K:n+1], x[n+1-K:n+1], 'o', color='red')

## Calculate the b[k]*x[n-k] terms and add them to a list
filtered_n = []
for k in range(len(b)):
    ## print out the variables here
    print("%d:, b[%d]=%f, x[%d-%d]=%f, b[%d]*x[%d-%d]=%f" % (n-k, k, b[k], n, k,  x[n-k], k, n, k, b[k]*x[n-k]))
    filtered_n.append(b[k]*x[n-k])

## reverse the list so that they're in time order
filtered_n.reverse()

## Plot the b[k]*x[n-k] terms
timedom.plot(time_steps[n+1-K:n+1], filtered_n, 'o', color='blue')

## Calculate the filter output (add up the product terms)
print("\ny[%d] = %f" % (n, sum(filtered_n)))
```

In the plot above, you should see:
* A cosine wave with frequency 4 Hz in grey.
* The inputs the the filter x[19],...,x[15] in red.
    * 5 input values for 5 filter coefficients
* The product $b[k]*x[n-k]$ for $n=19$, and $k=0,...,4$ in blue

So with `b=[0.2, 0.2, 0.2, 0.2, 0.2]` all the input values get scaled down and the overall curve is less peaky.

## 2 An FIR moving average filter

The example above is actually a useful special case of an FIR filter is where each of the filter coefficients is just $1/K$. In this case our FIR equation looks like this:

 $$
 \begin{align}
     y[n] &= \sum_{k=0}^{K-1} \frac{1}{K} x[n-k] \\
          &= \frac{1}{K} \sum_{k=0}^{K-1}  x[n-k]
 \end{align}
 $$

This equation says that when we apply the filter, we step through the input. At each step, we output the average of the previous $K$ inputs. You might know this by another more intuitive name: a _moving average_.  You might also have seen this as method to 'smooth' an input.

Let's play around with this idea a bit and see how it relates to our notion of frequency response.

### Example:

Let's look at this 5-point moving average filter.  In this case all the filter coefficients $b[k] = 1/5$ for $k=0,..,K-1=4$

$$
\begin{align}
y[n] &= \frac{1}{5} \sum_{k=0}^4 x[n-k] \\
&= \frac{1}{5}x[n] + \frac{1}{5}x[n-1] + \frac{1}{5}x[n-2] + \frac{1}{5}x[n-3] + \frac{1}{5}x[n-3]
\end{align}
$$

Now, let's code this specific filter up and apply it to some sequences!

```python
# Apply a moving average filter of size K to input sequence x
def moving_average(x, K=5):
    ## There are quicker ways to do this in numpy, but let's just do it this way for now for transparency

    ## We know that we'll have as many outputs as inputs, so we can initialize y to all zeros
    N = len(x)
    y = np.zeros(N)

    ## Go through the input one step at a time
    for n in range(N):

        ## Add up the last K inputs (including the current one)
        for k in range(K):

            ## Exercise: why do we have to put this conditional in here?
            if n-k >= 0:
                y[n] = y[n] + x[n-k]

        ## Divide by the size of the input window to get an average
        y[n] = (1/K) * y[n]

    return y

```

### Generate a 'noisy' sinusoid

Let's generate a compound sinusoid with one low frequency and one high(er) frequency component.  We can take the higher frequency component as representing some sort of periodic noise in the signal.

```python
## Set the number of samples N, sampling rate f_s (hence sampling time t_s)
N=64
f_s = 64
t_s = 1/f_s

print("sampling rate: f_s = %f\nsampling time: t_s: %f" % (f_s, t_s))
```

```python
## make some sinusoids:
## Since the sample rate and sequence length is the same, the generated time steps will match for
## x1 and x2
x1, time_steps = gen_sinusoid(frequency=4, phase=0, amplitude=1, sample_rate=f_s, seq_length=N, gen_function=np.cos)
x2, time_steps = gen_sinusoid(frequency=24, phase=0, amplitude=1, sample_rate=f_s, seq_length=N, gen_function=np.cos)

# add them up!
x_noisy = x1 + x2
```

```python
## Plot the compound sinusoid we've just created:
fig, timedom = plt.subplots(figsize=(16, 4))
timedom.plot(time_steps, x_noisy, color='magenta')
timedom.set_xlabel('Time (s)')
timedom.set_ylabel('Amplitude')
```

You should be able to see that the 4Hz cosine wave is perturbed by a 24 Hz cosine. So, this could represent a situation where there is high frequency noise in our signal that we'd like to get rid of.  Let's see if we can use our filter to smooth out this high frequency noise.

### Apply the moving average filter

```python
## Apply our moving average filter
K=5
y_mov = moving_average(x_noisy,K=K)
```

```python
## Plot the results
fig, timedom = plt.subplots(figsize=(16, 4))
timedom.set_xlabel('Time (s)')
timedom.set_ylabel('Amplitude')

## The input signal
timedom.plot(time_steps, x_noisy, color='magenta', label='Noisy input')

## The underlying 4Hz signal
timedom.plot(time_steps, x1, color='grey', label='low freq input component')

## After the moving average has been applied
timedom.plot(time_steps, y_mov, color='blue', label='filter output')

timedom.set_title('%d-point moving average filter applied to a noisy sinsuoid' % K)
timedom.legend()
```

You should see:
* the original signal in magenta
* the low frequency cosine component in grey
* the output of the filter in blue

### Exercise:

* From the plot, does it appear that the moving average recovered the original 4Hz signal in terms of:
    * frequency?
    * amplitude?
    * phase?
* Are there still high frequency components in the filter output?

Can you see some issues with applying this filter? What happens at the end points? Are they valid representations of the lower frequency signal?

### Notes

```python

```

### Get the DFT of the filtered signal

We can apply the DFT to our output to check our observations from the time-domain output.

```python
## DFT of the original input
mags_x, phases_x = get_dft_mag_phase(x_noisy, N)

## DFT of the filter output
mags_y, phases_y = get_dft_mag_phase(y_mov, N)
```

```python
dft_freqs = get_dft_freqs_all(sample_rate=f_s, seq_len=N)

fig, mags = plt.subplots(figsize=(16, 4))
mags.set(xlim=(-1, N/2), ylim=(-1, N))
mags.scatter(dft_freqs, mags_x, color='magenta', label='noisy input')
mags.scatter(dft_freqs, mags_y, color='blue', label='filtered output')
mags.set_xlabel("Frequency (Hz)")
mags.set_ylabel("Magnitude")
mags.set_title("DFT Magnitude before and after moving average filter")
mags.legend()

#Let's not worry about phase right now, but feel free to uncomment and have a look!
#fig, phases = plt.subplots(figsize=(16, 4))
#phases.set(xlim=(-1, N/2), ylim=(-10, 10))
#phases.scatter(dft_freqs, phases_x, color='magenta', label='noisy input')
#phases.scatter(dft_freqs, phases_y, color='blue', label='filtered output')
#phases.set_xlabel("Frequency (Hz)")
#phases.set_ylabel("Phase (rad)")
#phases.set_title("DFT Phase before and after moving average filter")
#phases.legend()

```

### Exercise

* Based on the magnitude spectrum:
    * Did the filter get rid of the 24Hz component?
    * Do you see any signs of leakage?

* What happens if you change the frequency of the second sinusoid to something lower (e.g. 6Hz)?

### Notes

```python

```

## 3 FIR as convolution

An FIR Filter that takes $K$ previous elements of $x[n]$ as input has the following general form:

$$ y[n] = \sum_{k=0}^{K-1} b[k] x[n-k] $$

You might recognize this as a **convolution** of the two sequences $b$ and $x$  (i.e. $b * x$).

So, we can theoretically set our filter to coefficients to whatever we want.  Here's a function that generalizes our moving average filter to allow for this:

```python
def fir_filter(x, filter_coeffs):
    N = len(x)
    K = len(filter_coeffs)

    y = np.zeros(N)
    for n in range(N):
        for k in range(K):
            if n-k >= 0:
                #print("y[%d]=%f, b[%d]=%f, x[%d]=%f" % (n, y[n], k, filter_coeffs[k], n-k, x[n-k]))
                y[n] = y[n] + (filter_coeffs[k]*x[n-k])
        #print("y[%d]=%f" % (n, y[n]))
    return y

```

### Changing the filter coefficients

Let's try it out with different coefficient values, comparing our unweighted average `h_avg` filter with a weighted average `h_wavg` filter.

```python

## The 5-point moving average from before
h_avg = np.array([1/5, 1/5, 1/5, 1/5, 1/5])
y_avg = fir_filter(x_noisy, h_avg)

## A 5-point symmetrically weighted average
h_wavg = np.array([1/5, 1/3, 1, 1/3, 1/5])
y_wavg = fir_filter(x_noisy, h_wavg)
```

### Filter effects in the time domain

```python
## Plot the filter outputs
fig, timedom = plt.subplots(figsize=(16, 4))

## The original "noisy" input
timedom.plot(time_steps, x_noisy, color='magenta', label='input x_noisy')
timedom.scatter(time_steps, x_noisy, color='magenta')

## The 5-point moving average
timedom.plot(time_steps, y_avg, color='blue', label='unweighted average: y_avg')
timedom.scatter(time_steps, y_avg, color='blue')

## The 5-point weighted average
timedom.plot(time_steps, y_wavg, color='orange', label='weighted average: y_wavg')
timedom.scatter(time_steps, y_wavg, color='orange')
timedom.legend()
timedom.set_xlabel('Time (s)')
timedom.set_ylabel('Amplitude')
```

In this time vs amplitude graph, you should see:
* the 'noisy input' in magenta
* the output of the unweighted average filter in blue (`y_avg`)
* the output of the weighted average filter in orange (`y_wavg`)

### Exercise

Q: Why is the output of `y_wavg` more spikey than that of `y_avg`?

### Notes

```python

```

## 4 FIR Filters in the Frequency Domain

### The DFT of the filtered outputs

We can look at the effect of the two FIR filters defined above in the frequency domain by performing a DFT on the filter outputs.

```python
## DFT of the original input
mags_x, phases_x = get_dft_mag_phase(x_noisy, N)

## DFT after weighted average filter: h_wavg = np.array([1/5, 1/3, 1, 1/3, 1/5])
mags_wavg, phases_wavg = get_dft_mag_phase(y_wavg, N)

## DFT after unweighted average filter: h_avg = np.array([1/5, 1/5, 1/5, 1/5, 1/5])
mags_avg, phases_avg = get_dft_mag_phase(y_avg, N)

dft_freqs = get_dft_freqs_all(sample_rate=f_s, seq_len=N)

## Plot magnitude spectrums
fig, mags = plt.subplots(figsize=(16, 4))
mags.set(xlim=(-1, N/2), ylim=(-1, N))
mags.scatter(dft_freqs, mags_x, color='magenta', label='input')
mags.scatter(dft_freqs, mags_avg, color='blue', label='unweighted average')
mags.scatter(dft_freqs, mags_wavg, color='orange', label='weighted average')
mags.legend()

## Plot phase spectrums
fig, phases = plt.subplots(figsize=(16, 4))
phases.set(xlim=(-1, N/2), ylim=(-10, 10))
phases.scatter(dft_freqs, phases_x, color='magenta', label='input')
phases.scatter(dft_freqs, phases_avg, color='blue', label='unweighted average')
phases.scatter(dft_freqs, phases_wavg, color='orange', label='weighted average')
phases.legend()

```

### Exercise:

* Describe the difference between the different FIR filters based on the frequency magnitude and phases responses plotted above.

* Does the weighted average filter do as good a job at filtering out the higher frequency signals?

### Notes

```python

```

## 5 Convolution in Time,  Multiplication in Frequency

Now we get to the really cool bit.  We know that the DFT allows us to go from the time domain to the frequency domain (and the Inverse DFT let's us go back).  But it also has this very important property:

$$ h[k] * x[n] \mapsto \text{ DFT} \mapsto H(m) \cdot X(m) $$

That is, convolving an input sequence $x$ with a set of filter coefficients, $h$ in the time domain  ($h*x$) is the same as (pointwise) multiplication of the DFT of $h$ with the DFT of $x$.  So, if we know what type of frequency response $h$ has, we can treat this as apply a sort of mask to the DFT of $x$.  This property is known as the **convolution theorem**.

Another way to think about it is that if the DFT outputs of $h$ has zero magnitude, then applying that filter to some input will also zero out those frequencies in the filter output!

We'll see some visualizations of this shortly, but first, we can also note that you can go back the other way using the Inverse DFT:

$$ H(m) \cdot X(m) \mapsto \text{ IDFT} \mapsto h[k] * x[n] $$

And also it works for multiplication in time domain too:

$$ h[k] \cdot x[n] \mapsto \text{DFT} \mapsto H(m) * X(m) $$

This is useful for understanding why the leakage graph around each DFT bin looks the way it does (though we won't go into it here!).

In fact, we can use this to not only show that the moving average acts as a low pass filter, but also to be able to calculate exactly the type of filter it will be.

###  Plotting different FIR filter frequency responses

In order to multiply together the filter and the input frequency responses, we need to make sure the filter and the input have the same number of samples.  We can do this by just padding out the filter with zeros (you can see why in the optional extra material at the end of this notebook).  We can then look how changing the 'shape' of a filter changes it's frequency response. The following exercise shows some examples.

### Exercise
* Run the function plot_filter_freq_responses (defined in the next cell) to plot the frequency responses of filters with different shapes (e.g. `h_plat`, `h_tri`, `h_rect` in the cell below the next).
    * What's the difference in frequency response of the triangular filter and the rectangular filter?
    * What's the difference between the moving average of size 5 and one of size 9 (h_rect9)?
    * Try some other FIR filters!

```python
# Given a list of filters, the sample rate and a specific sequence length
# plot the DFT frequency response of each filter.
# Each filter should be defined as a list of coeffients (b[k])
def plot_filter_freq_responses(filters, sample_rate, seq_length):

    ## Get the set of DFT output frequencies given the sample rate and desired sequence length
    dft_freqs_filter = (sample_rate/seq_length) * np.arange(seq_length)

    ## Calculate the time steps for each filter value given the sample rate and sequence length
    time_steps = (1/sample_rate) * np.arange(seq_length)

    ## Set up some plots:
    # the filter itself (time v amplitude)
    fig_time, sinusoid = plt.subplots(figsize=(16, 4))

    # the frequency response (freq v magnitude)
    fig_freq, fresponse = plt.subplots(figsize=(16, 4))

    x_filters = {}
    ## For each filter:
    for i, h in enumerate(filters):

        # pad the filter coefficients with zeros until we get the desired sequence length
        x_zeros = np.zeros(seq_length - len(h))
        x = np.concatenate([h, x_zeros])

        # Get the DFT outputs
        mags, phases = get_dft_mag_phase(x, seq_length)

        # Plot the filter
        sinusoid.scatter(time_steps, x)
        sinusoid.plot(time_steps, x, label=repr(h))

        # plot the magnitude response
        fresponse.scatter(dft_freqs_filter, mags)
        fresponse.plot(dft_freqs_filter, mags, label=repr(h))
        fresponse.set(xlim=(-1,seq_length/2))

        # return the filters and the DFT responses just in case
        x_filters = {'x':x, 'mags':mags, 'phases':phases, 'coeffs':h}

    sinusoid.set_xlabel('Time(s)')
    sinusoid.set_ylabel('Amplitude')
    sinusoid.set_title('Zero padded filters of different shapes')
    sinusoid.legend()

    fresponse.set_xlabel('Frequency (Hz)')
    fresponse.set_ylabel('Magnitude')
    fresponse.set_title('DFT magnitude response of ero padded filters of different shapes')
    fresponse.legend()

    return x_filters

```

```python
h_plat = np.array([0.1, 0.2, 0.2, 0.2, 0.1])
h_tri = np.array([0.04, 0.12, 0.15, 0.12, 0.01])
h_rect = np.array([1/5, 1/5, 1/5, 1/5, 1/5])
h_rect9 = np.array([1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9, 1/9])
## Try some others if you like!

N=64
f_s=64

filter_dict = plot_filter_freq_responses(filters=[h_tri, h_plat, h_rect, h_rect9], sample_rate=f_s, seq_length=N)
```

### Notes

```python

```

## 7 Applying an FIR filter to an impulse train

Now, finally we can look at the effect of applying an FIR filter to an impulse train.  Remember, we're using impulse trains to represent a sound source (i.e. vocal pulses at a specific frequency).  Eventually, we want to be able to define filters that capture the effects of the vocal tract.

First, here's a function that produces a sequence of pulses at a given frequency and sample rate. We'll then apply a triangular filter and look at what the frequency response is of the output is.

```python
## Let's make an impulse train
N=200
f_s = 8000
t_s = 1/f_s

frequency = 200

x_imp, time_steps = make_impulse_train(sample_rate=f_s, frequency=frequency, n_samples=N)

```

```python
## Plot the impulse train made in the cell before
fig, td = plt.subplots(figsize=(16, 4))
td.scatter(time_steps, x_imp)
td.plot(time_steps, x_imp)
td.set_xlabel('Time (s)')
td.set_ylabel('Amplitude')
td.set_title('Impulse train')
```

You should see an impulse train, with an impulse frequency of 200 Hz.  Let's see what happens when we apply the filter.

```python
## define our FIR filter coefficients
h_mov = np.array([0.1, 0.3, 0.5, 0.3, 0.1])

#try this later!
#h_mov = np.array([1])

## Apply the filter to our impulse train
y_mov = fir_filter(x_imp, h_mov)

## plot the impulse train and the filtered version of it
fig, td = plt.subplots(figsize=(16, 4))
td.scatter(time_steps, x_imp)
td.plot(time_steps, x_imp, label='impulse train')
td.plot(time_steps, y_mov, label='filtered impulse train')
td.set_xlabel('Time (s)')
td.set_ylabel('Amplitude')
td.legend()
td.set_title('Impulse train and filtered impulse train')
```

Here you should see our impulse train (red) and the filtered impulse train (blue).  The filtered version has smaller amplitude and a broader peak (more samples have non-zero value).  Also, the filtered peaks are slightly after the original impulses.

Now let's look at the DFT output:

```python
## Get the DFT the filter output
mags, phases = get_dft_mag_phase(y_mov, N)

## Plot the DFT frequencies rather than DFT output indices
dft_freqs = get_dft_freqs_all(f_s, N)

## Plot the magnitude spectrum
fig, ax = plt.subplots(figsize=(16, 4))
ax.scatter(dft_freqs[0:int(N/2)],mags[0:int(N/2)])
ax.plot(dft_freqs[0:int(N/2)], mags[0:int(N/2)])

```

### Exercises

* What do the spikes in the DFT outputs represent?
* What does the first spike after the 0 frequency one represent?
* What does this filter appear to do?
    * Remember applying the FIR filter in the time domain (via convolution) is the same as multiplying the DFT of the filter to the DFT of the input signal

### Notes

```python

```

### Exercises

* What happens when the impulse train frequency doesn't fall on one of the bins? e.g. `frequency = 130`, for 200 samples, with sampling rate 8000 samples/second
* Does the magnitude spectrum have the harmonic structure you'd expect?

### Notes

```python

```

* Next: [Infinite Impluse Response filters](./signals-2-3-iir-filters.ipynb)
* Optional extension materials: [The moving average as a rectangular filter](./signals-2-4-rectangular-filters-extension.ipynb)

```python

```


#
$\textbf{Superposition}$

 It’s only when you look at the tiniest quantum particles like atoms, electrons, photons, and the like — that you see intriguing phenomena like $\textbf{superposition and entanglement}$.

$\textbf{Superposition}$ refers to the quantum phenomenon where a quantum system can exist in multiple states or places at the exact same time. In other words, something can be “here” and “there,” or “up” and “down” at the same time.

```python
%matplotlib inline
# Importing standard Qiskit libraries and configuring account
from qiskit import QuantumCircuit, execute, Aer
from qiskit.visualization import plot_histogram
from qiskit.visualization import plot_state_qsphere
from math import pi
from qiskit.quantum_info import Statevector
from IPython.core.display import Image, display
import numpy as np
```

```python
qch = QuantumCircuit(1,1)
qch.h(0)
qch.measure(0,0)
qch.draw('mpl')
```

```python
backend = Aer.get_backend('qasm_simulator')
job_sim = execute(qch, backend, shots=1024)
sim_result = job_sim.result()

measurement_result = sim_result.get_counts(qch)
print(measurement_result)
plot_histogram(measurement_result)
```

#
$\textbf{Random Circuit}$

Generate random circuit of arbitrary size and form

```python
from qiskit.circuit.random import random_circuit

circ = random_circuit(3, 2, measure=True)
circ.draw(output='mpl')

```

#
$\textbf{Quantum Entanglement}$

 $\textbf{Quantum entanglement}$ is a quantum mechanical phenomenon in which the quantum states of two or more objects have to be described with reference to each other, even though the individual objects may be spatially separated.

For example, it is possible to prepare two particles in a single quantum state such that when one is observed to be spin-up, the other one will always be observed to be spin-down and vice versa, this despite the fact that it is impossible to predict, according to quantum mechanics, which set of measurements will be observed.

### Quantum entanglement has applications in the emerging technologies of quantum computing and quantum cryptography, and has been used to realize quantum teleportation experimentally.

#
$\textbf{Bell State or Entanglement circuit}$

The Bell states are four specific maximally entangled quantum states of two qubits. They are in a superposition of 0 and 1--that is, a linear combination of the two states. Their entanglement means the following:

The qubit held by Alice (subscript "A") can be 0 as well as 1. If Alice measured her qubit in the standard basis, the outcome would be perfectly random, either possibility 0 or 1 having probability 1/2. But if Bob (subscript "B") then measured his qubit, the outcome would be the same as the one Alice got. So, if Bob measured, he would also get a random outcome on first sight, but if Alice and Bob communicated, they would find out that, although their outcomes seemed random, they are perfectly correlated.
    \begin{equation}\begin{aligned}
\textbf{For initial state {00},}\left|\Psi^{+}\right\rangle &=\frac{1}{\sqrt{2}}\left(|0\rangle_{A} \otimes|0\rangle_{B}+|1\rangle_{A} \otimes|1\rangle_{B}\right)(1) \\
\textbf{For initial state {10},}\left|\Psi^{-}\right\rangle &=\frac{1}{\sqrt{2}}\left(|0\rangle_{A} \otimes|0\rangle_{B}-|1\rangle_{A} \otimes|1\rangle_{B}\right)(2) \\
\textbf{For initial state {01},}\left|\Phi^{+}\right\rangle &=\frac{1}{\sqrt{2}}\left(|0\rangle_{A} \otimes|1\rangle_{B}+|1\rangle_{A} \otimes|0\rangle_{B}\right)(3) \\
\textbf{For initial state {11},}\left|\Phi^{-}\right\rangle &=\frac{1}{\sqrt{2}}\left(|0\rangle_{A} \otimes|1\rangle_{B}-|1\rangle_{A} \otimes|0\rangle_{B}\right)(4)
\end{aligned}\end{equation}

```python

```

```python
entanglement1 = QuantumCircuit(2,2)
entanglement1.h(0)
entanglement1.cx(0,1)
entanglement1.draw('mpl')
```

```python
sv = Statevector.from_label('00')
# plot_state_qsphere(sv.data)
new_sv = sv.evolve(entanglement1)
#print(new_sv)
plot_state_qsphere(new_sv.data)
```

```python
backend = Aer.get_backend('statevector_simulator')
final_state = execute(entanglement1,backend).result().get_statevector()
from qiskit_textbook.tools import array_to_latex
array_to_latex(final_state, pretext="\\text{Statevector} = ")
```

$\displaystyle
\text{Statevector} = \begin{bmatrix}
\tfrac{1}{\sqrt{2}} \\
0 \\
0 \\
\tfrac{1}{\sqrt{2}}
\end{bmatrix}
$

```python
entanglement2 = entanglement1.copy()
sv = Statevector.from_label("01")# Here 0 is the second qubit and 1 is the first qubit
new_sv = sv.evolve(entanglement2)
plot_state_qsphere(new_sv.data)
```

```python
# Let's see the result
final_state = execute(entanglement1,backend).result().get_statevector()
from qiskit_textbook.tools import array_to_latex
array_to_latex(final_state, pretext="\\text{Statevector} = ")
```

$\displaystyle
\text{Statevector} = \begin{bmatrix}
\tfrac{1}{\sqrt{2}} \\
0 \\
0 \\
\tfrac{1}{\sqrt{2}}
\end{bmatrix}
$