## Quantum Fourier Transform

## Classical discrete Fourier transform (DFT)
Given a vector $(x_0,\ldots,x_{N-1})\in\mathbb{C}^N$, the discrete Fourier transform computes the vector $(y_0,\ldots,y_{N-1})$ for which
$$ y_j = \frac{1}{\sqrt{N}}\sum_{k=0}^{N-1} x_k e^{\frac{2\pi i}{N}jk}.$$

## Quantum Fourier Transformation (QFT)
In close analogy to the classical Fourier transform, the QFT takes an arbitrary input state $\left|\mathrm{input}\right> = \sum x_{j}\left|j\right>$ (with the orthonormal basis states $\left|0\right>$, $\left|1\right>$, ..., $\left|N-1\right>$), and maps it to the output state
$$\left|\mathrm{output}\right> = \sum_{k} y_{k}\left|k\right>,$$
with coefficients
$$y_{k}=\frac{1}{\sqrt{N}}\sum_{n}x_{n}e^{2\pi i\cdot n\cdot k/N}.$$

## Quantum binary representation
We take $N=2^{n}$, with $n$ being the number of qubits. In the binary representation, the states $\{\left|x\right>\}$ read explicitly

$$\left|x\right> = \left|x_{1}x_{2}\dots x_{n}\right>,$$
where
$$x = x_{1}2^{n-1}+x_{2}2^{n-2}+\dots+x_{n}2^{0}.$$

For example, in a four-qubit system $\left|7\right> = \left|0111\right>$ and $\left|8\right> = \left|1000\right>$.
Similarly, we use the following notation to represent binary fractions:

$$[0.x_{l}x_{l+1}\dots x_{m}] = \frac{x_{l}}{2^{1}} + \frac{x_{l+1}}{2^{2}} + \frac{x_{m}}{2^{m-l+1}}.$$

## Product representation of QFT
The product representation of the QFT is important. Accordingly, the QFT can be expressed (up to normalization) as
$$\left|x_{1}x_{2}\dots x_{n}\right> \longrightarrow \frac{(\left|0\right>+e^{2\pi i[0.x_{n}]}\left|1\right>)\otimes (\left|0\right>+e^{2\pi i[0.x_{n-1}x_{n}]}\left|1\right>)\otimes \dots \otimes (\left|0\right>+e^{2\pi i[0.x_{1}x_{2}\dots x_{n}]}\left|1\right>)}{2^{n/2}}.$$
For example, the QFT on three qubits can be written as

$$\mathrm{QFT}\left|x_{1}x_{2}x_{3}\right> = \frac{1}{2^{3/2}}\left(\left|0\right>+e^{2\pi i[0.x_{3}]}\left|1\right>\right) \otimes \left(\left|0\right>+e^{2\pi i[0.x_{2}x_{3}]}\left|1\right>\right) \otimes \left(\left|0\right>+e^{2\pi i[0.x_{1}x_{2}x_{3}]}\left|1\right>\right).$$

Thus, for the simple input state $\left|0,0,0\right>$ with $x_{i}=0$, it is easy to see that the QFT produces a uniform superposition of all computational basis vectors (since only the Hadamard gates act non-trivially in this case).  Explicitly:
$$
\begin{split}
\mathrm{QFT}\left|0,0,0\right> & = \frac{1}{\sqrt{2^{3}}}(\left|0\right>+\left|1\right>) \otimes (\left|0\right>+\left|1\right>) \otimes (\left|0\right>+\left|1\right>) \\
 & = \frac{1}{\sqrt{2^{3}}} \bigotimes_{i} \left|+\right>_{i}
\end{split}
$$

## Inverse QFT
Below we will also implement the inverse QFT algorithm (denoted by $\mathrm{QFT}^{\dagger}$, or sometimes $\mathrm{QFT}^{-1}$), which fulfills $\mathrm{QFT}\cdot\mathrm{QFT}^{\dagger}=\mathbb{1}$, where $\mathbb{1}$ is the identity. For example, with the inverse QFT we can invert the above three qubit QFT as

$$\mathrm{QFT}^\dagger\mathrm{QFT}\left|x_{1}x_{2}x_{3}\right> = \mathrm{QFT}^{\dagger}(\left|0\right>+e^{2\pi i[0.x_{3}]}\left|1\right>) \otimes (\left|0\right>+e^{2\pi i[0.x_{2}x_{3}]}\left|1\right>) \otimes (\left|0\right>+e^{2\pi i[0.x_{1}x_{2}x_{3}]}\left|1\right>)/2^{3/2}=\left|x_{1}x_{2}x_{3}\right>.$$

__Example for inverse QFT:__ For further illustration, we will run a concrete example of this kind below.
Specifically, consider the following example with $x_{1}=0$, $x_{2}=1$, and $x_{3}=0$ encoding the number 2 in binary representation.
Accordingly, we prepare the initial state (up to normalization)

$$\left|\Psi_\mathrm{in}\right>=(\left|0\right>+e^{2\pi i[0.0]}\left|1\right>) \otimes (\left|0\right>+e^{2\pi i[0.10]}\left|1\right>) \otimes (\left|0\right>+e^{2\pi i[0.010]}\left|1\right>),$$
which is equivalent to
$$\left|\Psi_\mathrm{in}\right>=(\left|0\right>+\left|1\right>) \otimes (\left|0\right>+e^{2\pi i\frac{1}{2}}\left|1\right>) \otimes (\left|0\right>+e^{2\pi i\frac{1}{4}}\left|1\right>).$$

This state can be prepared using the single-qubits gates $H$ and $R_{z}$ as follows:

$$\left|\Psi_\mathrm{in}\right>=H\left|0\right> \otimes R_{z}(\pi)H\left|0\right> \otimes R_{z}(\pi/2)H\left|0\right>.$$

Based on the analysis above we then expect $\mathrm{QFT}^{\dagger} \left|\Psi_\mathrm{in}\right> = \left|0,1,0\right>$.
We can generalize the above example to multiple qubits with the following state preparation code:
```python
circ = Circuit()
circ.h(range(num_qubits))
for ii in range(num_qubits - 1):
    circ.rz(ii+1, math.pi/(2**ii))
```

## Implementation of QFT

The QFT circuit can be implemented using Hadamard gates, controlled phase gates, and SWAP gates. The Hadamard gate $H$, the phase gate ${\displaystyle R_{m}}$, and the SWAP gate are given by
$$H = \frac{1}{\sqrt{2}}
\begin{pmatrix}
1 & 1\\
1 & -1\\
\end{pmatrix} \qquad \qquad
R_{m} =
\begin{pmatrix}
1 & 0\\
0 & e^{\frac{2\pi i}{2^{m}}}\\
\end{pmatrix} \qquad \qquad
\mathrm{SWAP} =
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & 0 & 1 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 1\\
\end{pmatrix}.$$

## Braket Implementation

```python
# General imports
import boto3
import numpy as np
import math
import matplotlib.pyplot as plt
# magic word for producing visualizations in notebook
%matplotlib inline
import string
import time
from datetime import datetime
# import logging
import logging

# AWS imports: Import Braket SDK modules
from braket.circuits import Circuit, Gate, Instruction, circuit, Observable
from braket.aws import AwsDevice, AwsQuantumTask
from braket.devices import LocalSimulator
```

```python
# S3 storage
my_bucket = f"amazon-braket-xxx" #"amazon-braket-xxx" # the name of the bucket
my_prefix = "qft-qpe-output" # the name of the folder in the bucket
s3_folder = (my_bucket, my_prefix)

# set up the device to be the managed simulator
device = AwsDevice("arn:aws:braket:::device/quantum-simulator/amazon/sv1")
# device = LocalSimulator()
```

```python
## QFT function
def qft(qubits):
    """
    Construct a circuit object corresponding to the Quantum Fourier Transform (QFT)
    algorithm, applied to the argument qubits.  Does not use recursion to generate the QFT.

    Args:
        qubits (int): The list of qubits on which to apply the QFT
    """
    qftcirc = Circuit()

    # get number of qubits
    num_qubits = len(qubits)

    for k in range(num_qubits):
        # First add a Hadamard gate
        qftcirc.h(qubits[k])

        # Then apply the controlled rotations, with weights (angles) defined by the distance to the control qubit.
        # Start on the qubit after qubit k, and iterate until the end.  When num_qubits==1, this loop does not run.
        for j in range(1,num_qubits - k):
            angle = 2*math.pi/(2**(j+1))
            qftcirc.cphaseshift(qubits[k+j],qubits[k], angle)

    # Then add SWAP gates to reverse the order of the qubits:
    for i in range(math.floor(num_qubits/2)):
        qftcirc.swap(qubits[i], qubits[-i-1])

    return qftcirc

## Inverse QFT function
def inverse_qft(qubits):
    """
    Construct a circuit object corresponding to the inverse Quantum Fourier Transform (QFT)
    algorithm, applied to the argument qubits.  Does not use recursion to generate the circuit.

    Args:
        qubits (int): The list of qubits on which to apply the inverse QFT
    """
    # instantiate circuit object
    qftcirc = Circuit()

    # get number of qubits
    num_qubits = len(qubits)

    # First add SWAP gates to reverse the order of the qubits:
    for i in range(math.floor(num_qubits/2)):
        qftcirc.swap(qubits[i], qubits[-i-1])

    # Start on the last qubit and work to the first.
    for k in reversed(range(num_qubits)):

        # Apply the controlled rotations, with weights (angles) defined by the distance to the control qubit.
        # These angles are the negative of the angle used in the QFT.
        # Start on the last qubit and iterate until the qubit after k.
        # When num_qubits==1, this loop does not run.
        for j in reversed(range(1, num_qubits - k)):
            angle = -2*math.pi/(2**(j+1))
            qftcirc.cphaseshift(qubits[k+j],qubits[k], angle)

        # Then add a Hadamard gate
        qftcirc.h(qubits[k])

    return qftcirc
```

## Examples of QFT circuits

```python
# show inverse QFT example circuit
num_qubits = 2
qubits=range(num_qubits)
my_qft_circ = qft(qubits)
print('QFT CIRCUIT:')
print(my_qft_circ)

# show inverse QFT example circuit
print('')
print('INVERSE-QFT CIRCUIT:')
my_iqft_circ = inverse_qft(qubits)
print(my_iqft_circ)
```

    QFT CIRCUIT:
    T  : |0|     1     |2| 3  |

    q0 : -H-PHASE(1.57)---SWAP-
            |             |
    q1 : ---C-----------H-SWAP-

    T  : |0|     1     |2| 3  |

    INVERSE-QFT CIRCUIT:
    T  : | 0  |1|     2      |3|

    q0 : -SWAP---PHASE(-1.57)-H-
          |      |
    q1 : -SWAP-H-C--------------

    T  : | 0  |1|     2      |3|

## Quantum Phase Estimation

**Background**

__Quantum Phase Estimation Algorithm__:
The QPE algorithm takes a unitary $U$ as input. For the sake of simplicity (we will generalize the discussion below), suppose that the algorithm also takes as input an eigenstate $|\psi \rangle$ fulfilling

$$U|\psi \rangle = \lambda |\psi \rangle,$$

with $\lambda = \exp(2\pi i\varphi)$.

QPE uses two registers of qubits: we will refer to the first register as *precision* qubits (as the number of qubits $n$ in the first register sets the achievable precision of our results) and the second register as *query* qubits (as the second register hosts the eigenstate $|\psi \rangle$).
Suppose we have prepared this second register in $|\psi \rangle$.  We then prepare a uniform superposition of all basis vectors in the first register using a series of Hadamard gates.

Next, we apply a series of controlled-unitaries $C-U^{2^{k}}$ for different powers of $k=0,1,\dots, n-1$ (as illustrated in the circuit diagram below).
For example, for $k=1$ we get
\begin{equation}
\begin{split}
(|0 \rangle + |1 \rangle) |\psi \rangle  & \rightarrow |0 \rangle |\psi \rangle + |1 \rangle U|\psi \rangle \\
& = (|0 \rangle + e^{2\pi i \varphi}|1 \rangle) |\psi \rangle.
\end{split}
\end{equation}

Note that the second register remains unaffected as it stays in the eigenstate $|\psi \rangle$.
However, we managed to transfer information about the phase of the eigenvalue of $U$ (i.e., $\varphi$) into the first *precision* register by encoding it as a relative phase in the state of the qubits in the first register.

Similarly, for $k=2$ we obtain
\begin{equation}
\begin{split}
(|0 \rangle + |1 \rangle) |\psi \rangle  & \rightarrow |0 \rangle |\psi \rangle + |1 \rangle U^{2}|\psi \rangle \\
& = (|0 \rangle + e^{2\pi i 2\varphi}|1 \rangle) |\psi \rangle,
\end{split}
\end{equation}

where this time we wrote $2\varphi$ into the precision register. The process is similar for all $k>2$.

Introducing the following notation for binary fractions
$$[0. \varphi_{l}\varphi_{l+1}\dots \varphi_{m}] = \frac{\varphi_{l}}{2^{1}} + \frac{\varphi_{l+1}}{2^{2}} + \frac{\varphi_{m}}{2^{m-l+1}},$$

one can easily show that the application of a controlled unitary $C-U^{2^{k}}$ leads to the following transformation

\begin{equation}
\begin{split}
(|0 \rangle + |1 \rangle) |\psi \rangle  & \rightarrow |0 \rangle |\psi \rangle + |1 \rangle U^{2^{k}}|\psi \rangle \\
& = (|0 \rangle + e^{2\pi i 2^{k}\varphi}|1 \rangle) |\psi \rangle \\
& = (|0 \rangle + e^{2\pi i [0.\varphi_{k+1}\dots \varphi_{n}]}|1 \rangle) |\psi \rangle,
\end{split}
\end{equation}

where the first $k$ bits of precision in the binary expansion (i.e., those bits to the left of the decimal) can be dropped, since $e^{2\pi i \theta} = 1$ for any whole number $\theta$.

The QPE algorithm implements a series of these transformations for $k=0, 1, \dots, n-1$, using $n$ qubits in the precision register.
In its entirety, this sequence of controlled unitaries leads to the transformation

$$ |0, \dots, 0 \rangle \otimes |\psi \rangle \longrightarrow
(|0 \rangle + e^{2\pi i [0.\varphi_{n}]}|1 \rangle)
\otimes (|0 \rangle + e^{2\pi i [0.\varphi_{n-1}\varphi_{n}]}|1 \rangle)
\otimes \dots
\otimes (|0 \rangle + e^{2\pi i [0.\varphi_{1}\dots\varphi_{n}]}|1 \rangle)
\otimes |\psi \rangle.
$$

By inspection, one can see that the state of the register qubits above corresponds to a quantum Fourier transform of the state $|\varphi_1,\dots,\varphi_n\rangle$. Thus, the final step of the QPE algorithm is to run the *inverse* Quantum Fourier Transform (QFT) algorithm on the precision register to extract the phase information from this state. The resulting state is
$$|\varphi_{1}, \varphi_{2}, \dots, \varphi_{n}  \rangle \otimes |\psi\rangle.$$

Measuring the precision qubits in the computational basis then gives the classical bitstring $\varphi_{1}, \varphi_{2}, \dots, \varphi_{n}$, from which we can readily infer the phase estimate $\tilde{\varphi} = 0.\varphi_{1} \dots \varphi_{n}$ with the corresponding eigenvalue $\tilde{\lambda} = \exp(2\pi i \tilde{\varphi})$.

__Simple example for illustration__: For concreteness, let us consider a simple example with the unitary given by the Pauli $X$ gate, $U=X$, for which $|\Psi \rangle = |+\rangle = (|0 \rangle + |1 \rangle)/\sqrt{2}$ is an eigenstate with eigenvalue $\lambda = 1$, i.e., $\varphi=0$.
This state can be prepared simply with a Hadamard gate as $|\Psi \rangle = H|0 \rangle$.
We take a precision register consisting of just two qubits ($n=2$).

Thus, after the first layer of Hadamard gates, the quantum state is
$$|0,0,0 \rangle \rightarrow |+,+,+\rangle.$$

Next, the applications of the controlled-$U$ gates (equal to $C-X$ operations, or CNOT gates in this example) leave this state untouched, since $|+\rangle$ is an eigenstate of $X$ with eigenvalue $+1$.
Finally, applying the inverse QFT leads to

$$\mathrm{QFT}^{\dagger}|+++\rangle=\mathrm{QFT}^\dagger\frac{|00\rangle + |01\rangle + |10\rangle + |11\rangle}{4}\otimes |+\rangle = |00\rangle \otimes |+\rangle,$$

from which we deduce $\varphi = [0.00]=0$ and therefore $\lambda=1$, as expected.
Here, in the last step we have used $|00\rangle + |01\rangle + |10\rangle + |11\rangle = (|0\rangle + e^{2\pi i[0.0]}|1\rangle)(|0\rangle + e^{2\pi i[0.00]}|1\rangle)$, which makes the effect of the inverse QFT more apparent.

__Initial state of query register__: So far, we have assumed that the query register is prepared in an eigenstate $|\Psi\rangle$ of $U$. What happens if this is not the case? Let's reconsider the simple example above.

Suppose now that the query register is instead prepared in the state $|\Psi\rangle = |1\rangle$.
We can always express this state in the eigenbasis of $U$, i.e., $|1\rangle = \frac{1}{\sqrt{2}}(|+\rangle - |-\rangle)$.
By linearity, application of the QPE algorithm then gives (up to normalization)

\begin{equation}
\begin{split}
\mathrm{QPE}(|0,0,\dots\rangle \otimes |1\rangle) & = \mathrm{QPE}(|0,0,\dots\rangle \otimes |+\rangle)
- \mathrm{QPE}(|0,0,\dots\rangle \otimes |-\rangle) \\
& =  |\varphi_{+}\rangle \otimes |+\rangle - |\varphi_{-}\rangle \otimes |-\rangle. \\
\end{split}
\end{equation}

When we measure the precision qubits in this state, 50% of the time we will observe the eigenphase $\varphi_{+}$ and 50% of the time we will measure $\varphi_{-}$. We illustrate this example numerically below.

This example motivates the general case: we can pass a state that is not an eigenstate of $U$ to the QPE algorithm, but we may need to repeat our measurements several times in order to obtain an estimate of the desired phase.

## QPE Circuit

The QPE circuit can be implemented using Hadamard gates, controlled-$U$ unitaries, and the inverse QFT (denoted as $\mathrm{QFT}^{-1}$).
Following the discussion above, the circuit that implements the QPE algorithm reads as below, where m is the size of lower query register and n is the size of upper precision register.

## QPE Example

Let us determine the eigenstates of Pauli $X = \begin{pmatrix}
0 & 1\\
1 & 0\\
\end{pmatrix}$ operator using QPE with the two-qubit query register.

```python
# Controlled Unitary circuit
def cU(cInd, tInd):
    return Circuit().cnot(cInd,tInd)

def str2circ(s, indQ0):
    circ = Circuit()
    for ind in range(len(s)):
        if s[ind]=='1':
            circ.x(ind+indQ0)
    return circ

```

```python
# QPE circuit for |+> eigenstate of X
circ_QPE = Circuit().h(0).h(1).h(2)
circ_QPE = circ_QPE.add_circuit(cU(1,2))
circ_QPE = circ_QPE.add_circuit(cU(0,2))
circ_QPE = circ_QPE.add_circuit(cU(0,2))
circ_QPE = circ_QPE.add_circuit(inverse_qft([0,1]))
circ_QPE = circ_QPE.add_circuit(Circuit().h(2))

print (circ_QPE)

task = device.run(circ_QPE, s3_folder, shots=1000)
result = task.result()

## Get measurement counts
counts = result.measurement_counts

# print counts
print(counts)

# plot using Counter
plt.bar(counts.keys(), counts.values());
plt.xlabel('bitstrings');
plt.ylabel('counts');

```


```python
%matplotlib inline
from sympy import *
init_session()
```

    IPython console for SymPy 1.8 (Python 3.9.7-64-bit) (ground types: python)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at https://docs.sympy.org/1.8/

# Quadratic finite impulse response predictor

```python
var("a, b, c, x", real=True)
```

Samples -- from $s(0)$ to $s(n - 1)$ these are stored in the history buffer, $s(n)$ is the new predicted sample

```python
s = Function("s", real=True)(x)
s
```

Function that we use for predicting

```python
eq = Equality(s, a * x**2 + b * x + c)
eq
```

## Least squares
Number of samples in the history buffer:

```python
n = 4
```

```python
i = symbols("i")
error_eqs = Sum((eq.rhs - eq.lhs).subs(x, i)**2, (i, 0, n - 1))
error_eqs
```

```python
params = solve([error_eqs.diff(sym).doit().simplify() for sym in (a, b, c)], (a, b, c))
params
```

```python
prediction_lstsq = eq.subs(params).simplify()
prediction_lstsq
```

```python
prediction_lstsq.subs(x, n)
```

```python

```

```python

```


```python
import sympy as sp
sp.init_printing()
```

```python
Uo,x,s=sp.symbols('Uo x s', real=True)
```

```python
U=Uo*((s/x)**12-(s/x)**6)
U
```

```python
Up=U.diff(x)
Up
```

```python
roots = sp.solve(Up,x)
roots
```

```python
x0=roots[1]
print(x0)
x0
```

```python
Um=U.subs(x,x0)
print(Um)
Um
```

```python
Upp=Up.diff(x)
Upp
```

```python
k=Upp.subs(x,x0)
print(k)
print(sp.latex(k))
k
```

```python
m=sp.symbols("m")
```

```python
omega=sp.sqrt(k/m)
omega
```

```python

```


dS/dt=-bS+gI, dI/dt=bS-gI (uso b para beta y g para gamma)

```python
from sympy import *
from sympy.abc import S,I,t,b,g
```

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
import pylab as pl
```

```python
#puntos criticos
P=-b*S+g*I
Q=b*S-g*I
#establecer P(S,I)=0 y Q(S,I)=0
Peqn=Eq(P,0)
Qeqn=Eq(Q,0)
print(solve((Peqn,Qeqn),S,I))
#Eigenvalores y eigenvectores
M=Matrix([[-b,g],[b,-g]])
print(M.eigenvals())
pprint(M.eigenvects())
```

    {S: I*g/b}
    {-b - g: 1, 0: 1}
    ⎡⎛      ⎡⎡g⎤⎤⎞                     ⎤
    ⎢⎜      ⎢⎢─⎥⎥⎟  ⎛           ⎡⎡-1⎤⎤⎞⎥
    ⎢⎜0, 1, ⎢⎢b⎥⎥⎟, ⎜-b - g, 1, ⎢⎢  ⎥⎥⎟⎥
    ⎢⎜      ⎢⎢ ⎥⎥⎟  ⎝           ⎣⎣1 ⎦⎦⎠⎥
    ⎣⎝      ⎣⎣1⎦⎦⎠                     ⎦

```python
b=1
g=1
def dx_dt(x,t):
    return [ -b*x[0]+g*x[1] , b*x[0]-g*x[1] ]
#trayectorias en tiempo hacia adelante
ts=np.linspace(0,10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#trayectorias en tiempo hacia atras
ts=np.linspace(0,-10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#etiquetas de ejes y estilo de letra
plt.xlabel('S',fontsize=20)
plt.ylabel('I',fontsize=20)
plt.tick_params(labelsize=12)
plt.ticklabel_format(style="sci", scilimits=(0,0))
plt.xlim(0,100000)
plt.ylim(0,100000)
#campo vectorial
X,Y=np.mgrid[0:100000:15j,0:100000:15j]
u=-b*X+g*Y
v=b*X-g*Y
pl.quiver(X,Y,u,v,color='dimgray')
plt.savefig("SIS.pdf",bbox_inches='tight')
plt.show()
```

```python
b=1
g=3
def dx_dt(x,t):
    return [ -b*x[0]+g*x[1] , b*x[0]-g*x[1] ]
#trayectorias en tiempo hacia adelante
ts=np.linspace(0,10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#trayectorias en tiempo hacia atras
ts=np.linspace(0,-10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#etiquetas de ejes y estilo de letra
plt.xlabel('S',fontsize=20)
plt.ylabel('I',fontsize=20)
plt.tick_params(labelsize=12)
plt.ticklabel_format(style="sci", scilimits=(0,0))
plt.xlim(0,100000)
plt.ylim(0,100000)
#campo vectorial
X,Y=np.mgrid[0:100000:15j,0:100000:15j]
u=-b*X+g*Y
v=b*X-g*Y
pl.quiver(X,Y,u,v,color='dimgray')
plt.show()
```

```python
b=3
g=1
def dx_dt(x,t):
    return [ -b*x[0]+g*x[1] , b*x[0]-g*x[1] ]
#trayectorias en tiempo hacia adelante
ts=np.linspace(0,10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#trayectorias en tiempo hacia atras
ts=np.linspace(0,-10,500)
ic=np.linspace(20000,100000,3)
for r in ic:
    for s in ic:
        x0=[r,s]
        xs=odeint(dx_dt,x0,ts)
        plt.plot(xs[:,0],xs[:,1],"-", color="orangered", lw=1.5)
#etiquetas de ejes y estilo de letra
plt.xlabel('S',fontsize=20)
plt.ylabel('I',fontsize=20)
plt.tick_params(labelsize=12)
plt.ticklabel_format(style="sci", scilimits=(0,0))
plt.xlim(0,100000)
plt.ylim(0,100000)
#campo vectorial
X,Y=np.mgrid[0:100000:15j,0:100000:15j]
u=-b*X+g*Y
v=b*X-g*Y
pl.quiver(X,Y,u,v,color='dimgray')
plt.show()
```

```python

```


# Lecture 8. PDEs and sparse matrices

## Todays lecture
- Where the sparse matrices come from
- A brief reminder: finite difference, finite elements, finite volume methods
- Basic packages

## PDEs

Partial differential equations (PDEs) are ubiquitous in mathematical modelling

From simple linear PDEs to more complicated nonlinear cases:

- diffusion
- heat conducation
- fluid dynamics
- reaction-diffusion
- quantum chemistry computations
- etc.

## Sparse matrices

After any convenient **local discretization**,  i.e.

- Finite difference method (FDM)
- Finite element method (FEM)
- Finite volume method (FVM)

We get matrices with **a lot of zeros**, typically called **sparse matrices**.

## Toy problem

Consider Poisson equation with Dirichlet boundary conditions
$$
\begin{align}
& \mathrm{div} (k(x) \nabla u) = f(x), \quad x \in \Omega \\
& \quad u|_{\partial \Omega} = 0,
\end{align}
$$
where $k(x) > 0, \; x \in \Omega$ is given coefficients.
Let us discretize it with FDM, FEM and FVM.

### 1D FDM
$$
\left(\frac{\partial}{\partial x} k \frac{\partial u} {\partial x}\right)_i \approx \frac{k_{i+\frac{1}{2}}\frac{\partial u}{\partial x}_{i + \frac{1}{2}} - k_{i-\frac{1}{2}}\frac{\partial u}{\partial x}_{i - \frac{1}{2}}}{h} + \mathcal{O}(h^2)
$$

which leads to the final discretization

$$
\left(\frac{\partial}{\partial x} k \frac{\partial u} {\partial x}\right)_i \
\approx \frac{k_{i+\frac{1}{2}}\left(u_{i+1} - u_{i}\right) - k_{i-\frac{1}{2}}\left(u_{i} - u_{i-1}\right)}{h^2} + \mathcal{O}(h^2), \quad i = 1, \ldots, n-1, \quad u_0 = u_n = 0.
$$

- This discretization leads to the symmetric, tridiagonal, positive-definite matrix for unknown vector $u_h = [u_1, \dots, u_{n-1}]^{\top}$.
- This matrix have $-k_{i-\frac{1}{2}}$ on subdiagonals, and $\left(k_{i-\frac{1}{2}} + k_{i + \frac{1}{2}}\right)$ on the diagonal.

### 2D FDM

In two dimensions, $k(x, y) \in \mathbb{R}^{2 \times 2}$ for every point $(x, y) \in \Omega$  is **diffusion tensor**.

If $k(x, y)$ is a diagonal matrix
$$
k(x,y) =
\begin{bmatrix}
K_1(x, y) & 0\\
0 & K_2(x, y)
\end{bmatrix}
$$
we have

$$
\mathrm{div}(k(x,y) \nabla u) = \frac{\partial}{\partial x} K_1 (x, y) \frac{\partial u}{\partial x} + \frac{\partial}{\partial y} K_2 (x, y) \frac{\partial u}{\partial y},
$$

and we discretize each term and get **block tridiagonal matrix with tridiagonal blocks.**

For the simplest case $K_1 = K_2 = I$ we get **2D Poisson problem**, and the matrix can be written

$$\Delta_{2D} = \Delta_{1D} \otimes I + I \otimes \Delta_{1D},$$

where $\Delta_{1D} = \frac{1}{h^2}\mathrm{tridiag}(-1, 2, -1)$ is a **one-dimensional** Laplace operator.

### FEM

- Create mesh in the domain $\Omega$. Most often case is triangulation, but others are possible

- For each common node $i$ build basis function $\psi_i$ which is linear on adjoint triangles and $0$ on others triangles

- Approximate solution $u$ using this basis:
$$
u(x) \approx u_N(x) = \sum_{i=1}^N c_i \psi_i(x)
$$
- Projectional approach: substitute approximate solution $u_N(x)$ and enforce orthogonlity of residuals to this basis:

$$
\mathrm{div}( k \nabla u_N) - f \perp \mathrm{Span}\{\psi_1, \dots, \psi_N\}
$$
$$
(\psi_i, \mathrm{div}( k \nabla u_N) - f) = 0, \; i = 1,\dots, N
$$
$$
\sum_{i=1}^N c_i (\nabla \psi_i, k\nabla \psi_j) = (\psi_i, f)
$$
This is a linear system
$$
Ac = b
$$
with unknown vector $c = [c_1, \dots,c_n]^{\top}$, matrix
$$
A = [a_{ij}], \; a_{ij} = (\nabla \psi_i, k\nabla \psi_j)
$$
and right-hand side
$$
b = [b_i], \; b_i = (\psi_i, f)
$$

Is matrix $A$ symmetric and positive-definite?

### FVM 1D

- Consider integral over $i$-th element of mesh
$$
\int_{x_{i - 1/2}}^{x_{i + 1/2}} \left(\left( k(x) u'(x) \right)' - f(x)\right) dx = 0
$$
- Now get equation on flows through the $i$-th point of the mesh
$$
k(x_{i + 1/2})u'(x_{i+1/2}) - k(x_{i-1/2})u'(x_{i-1/2}) = \int_{x_{i-1/2}}^{x_{i + 1/2}} f(x)dx
$$
- Approximate flows
$$
k(x_{i + 1/2})\frac{u_{i+1} - u_i}{h} - k(x_{i-1/2})\frac{u_{i} - u_{i-1}}{h} = \int_{x_{i-1/2}}^{x_{i + 1/2}} f(x)dx
$$
- We get linear system on vector $u$ with matrix, which is the same as for FDM and FEM, but with different right-hand side

### Pros & Cons

What method in what cases should you use?

## Sparse vs. Dense matrices

- A sparse matrix is a matrix with enough zeros that is worth taking advantage of them [Wilkinson]
- A **structured matrix** has enough structure that is worthwhile using it (i.e., Toeplitz + FFT)
- A dense matrix is neither sparse nor structured

## Design of sparse matrix data structure

- Most operations should give the same result for dense and sparse
- Storage should be $\mathcal{O}(\mathrm{nonzeros})$.
- Time for a sparse matrix operations should be $\mathcal{O}(N)$ flops

The last requirement basically means fewer cache misses.

## Storing a sparse matrix

That we already discussed in detail in a separate lecture in the NLA course. However, let us repeat a little bit.

- Coordinate storage, $(i, j)$ array.
- Compressed sparse row formata, $(ia, ja, sa)$ format.
- There is also compressed row storage

What is good for what?

## Matrix-by-vector product

The matrix-by-vector product is very easy to be implemented in the compressed sparse row format:

```python
for i in range(n):
    for k in range(ia[i]:ia[i+1]):
        y[i] += sa[k] * x[ja[k]]
```

## Summary of CSR

- CSR is good for matrix-by-vector product
- Insertion  of new elements is very expensive

## Efficiency of sparse matrix operations

Sparse matrix operations are mostly about **memory access**, not about operations, thus the efficiency in flops is typically very low.

Thus, efficiency of order 10-15% is considered **high**.

Let us test it.

```python
import numpy as np
import time
n = 4000
a = np.random.randn(n, n)
v = np.random.randn(n)
t = time.time()
np.dot(a, v)
t = time.time() - t
print('Time: {0: 3.1e}, Efficiency: {1: 3.1e} Gflops'.\
      format(t,  ((2 * n ** 2)/t) / 10 ** 9))

```

    Time:  1.3e-02, Efficiency:  2.5e+00 Gflops

```python
import scipy as sp
import scipy.sparse
n = 4000
r = 100
ex = np.ones(n);
a = sp.sparse.spdiags(np.vstack((ex, -2*ex, ex)), [-1, 0, 1], n, n, 'csr');
rhs = np.random.randn(n, r)
t = time.time()
a.dot(rhs)
t = time.time() - t
print('Time: {0: 3.1e}, Efficiency: {1: 3.1e} Gflops'.\
      format(t,  (3 * n * r) / t / 10 ** 9))

```

    Time:  7.9e-03, Efficiency:  1.5e-01 Gflops

## Morale
For sparse data representations
- the computational time is smaller
- the computatational efficiency is also smaller!

## Possible solutions

- Use blocking
- Use block matrix-by-vector product (multiply at once)

## What are FastPDE methods about?

- They are typically methods for large sparse linear systems
- These systems have **certain additional structure**, i.e. it is not a random **sparse matrix** (for example, not an adjacency matrix of a Facebook graph, although some algorithms can be reused)

Next lecture considers methods to solve large sparse linear systems

## FEniCS demo

```python
%matplotlib inline
from __future__ import print_function
import fenics
import matplotlib.pyplot as plt
```

```python
import dolfin
import mshr
import math

domain_vertices = [dolfin.Point(0.0, 0.0),
                   dolfin.Point(10.0, 0.0),
                   dolfin.Point(10.0, 2.0),
                   dolfin.Point(8.0, 2.0),
                   dolfin.Point(7.5, 1.0),
                   dolfin.Point(2.5, 1.0),
                   dolfin.Point(2.0, 4.0),
                   dolfin.Point(0.0, 4.0),
                   dolfin.Point(0.0, 0.0)]

p = mshr.Polygon(domain_vertices);
rect_mesh = mshr.generate_mesh(p, 20)
fenics.plot(rect_mesh)
```

```python
V = fenics.FunctionSpace(rect_mesh, 'P', 1)
u_D = fenics.Expression('1 + x[0]*x[0] + 2*x[1]*x[1]', degree=2)

def boundary(x, on_boundary):
    return on_boundary

bc = fenics.DirichletBC(V, u_D, boundary)

u = fenics.TrialFunction(V)
v = fenics.TestFunction(V)
f = fenics.Constant(-6.0)   # Or f = Expression(’-6’, degree=0)
# Left-hand side
a = fenics.dot(fenics.grad(u), fenics.grad(v))*fenics.dx
# Right-hand side
L = f*v*fenics.dx

u = fenics.Function(V)
fenics.solve(a == L, u, bc)

fenics.plot(u)
```

```python
error_L2 = fenics.errornorm(u_D, u, 'L2')
print("Error in L2 norm = {}".format(error_L2))
error_H1 = fenics.errornorm(u_D, u, 'H1')
print("Error in H1 norm = {}".format(error_H1))
```

    Error in L2 norm = 0.17411792877577373
    Error in H1 norm = 1.4224251909002845

## FVM demo using FiPy
To install it run for Python 2
```
conda create --name <MYFIPYENV> --channel guyer --channel conda-forge fipy nomkl
```

```python
%matplotlib inline
import fipy

cellSize = 0.05
radius = 1.

mesh = fipy.Gmsh2D('''
               cellSize = %(cellSize)g;
               radius = %(radius)g;
               Point(1) = {0, 0, 0, cellSize};
               Point(2) = {-radius, 0, 0, cellSize};
               Point(3) = {0, radius, 0, cellSize};
               Point(4) = {radius, 0, 0, cellSize};
               Point(5) = {0, -radius, 0, cellSize};
               Circle(6) = {2, 1, 3};
               Circle(7) = {3, 1, 4};
               Circle(8) = {4, 1, 5};
               Circle(9) = {5, 1, 2};
               Line Loop(10) = {6, 7, 8, 9};
               Plane Surface(11) = {10};
               ''' % locals())

```

```python
phi = fipy.CellVariable(name = "solution variable",
                    mesh = mesh,
                    value = 0.)
```

```python
# viewer = fipy.Viewer(vars=phi, datamin=-1, datamax=1.)
```

```python
D = 1.
eq = fipy.TransientTerm() == fipy.DiffusionTerm(coeff=D)
```

```python
X, Y = mesh.faceCenters
phi.constrain(X, mesh.exteriorFaces)
timeStepDuration = 10 * 0.9 * cellSize**2 / (2 * D)
steps = 10
for step in range(steps):
    eq.solve(var=phi, dt=timeStepDuration)
#     if viewer is not None:
#         viewer
```

```python
viewer
```

## Summary

- Discretization methods: FDM, FEM and FVM
- Sparse matrices are important
- Package demos


```python
from sympy import integrate, init_printing
from sympy.abc import x
init_printing(use_latex="mathjax")
```

```python
f = x**2 - 3*x + 2
integrate(f)

```

$\displaystyle \frac{x^{3}}{3} - \frac{3 x^{2}}{2} + 2 x$

```python
from sympy.abc import a,b,c
f = a*x**2+b*x+c
integrate(f, x)

```

$\displaystyle \frac{a x^{3}}{3} + \frac{b x^{2}}{2} + c x$

```python
from sympy import cos,pi

integrate(cos(x), (x,0,pi/2.0)) # from 0 to pi/2
```

$\displaystyle 1$

```python
integrate(x, (x,0,5))

```

$\displaystyle \frac{25}{2}$

```python
from sympy.abc import x,y,z,a,b,c,d
from sympy import simplify

```

```python
I1 = integrate(1, (y,c,d))
simplify( integrate(I1, (x,a,b) ) )

```

$\displaystyle \left(a - b\right) \left(c - d\right)$

[Referencia](https://numython.github.io/posts/integrales-con-sympy/)

```python
from __future__ import division
from sympy import *
x, y, z, t = symbols('x y z t')
k, m, n = symbols('k m n', integer=True)
f, g, h = symbols('f g h', cls=Function)

integrate(x**2 * exp(x) * cos(x), x)
```

$\displaystyle \frac{x^{2} e^{x} \sin{\left(x \right)}}{2} + \frac{x^{2} e^{x} \cos{\left(x \right)}}{2} - x e^{x} \sin{\left(x \right)} + \frac{e^{x} \sin{\left(x \right)}}{2} - \frac{e^{x} \cos{\left(x \right)}}{2}$

```python
integrate(5/(1+x**2), (x,-oo,oo))
```

$\displaystyle 5 \pi$

[Referencia](https://docs.sympy.org/latest/modules/integrals/integrals.html) [Comprobacion](https://www.youtube.com/watch?v=6uIeKpA2dHw)

```python
f = sin(k*x)*cos(m*x)
integrate(f, x)
```

$\displaystyle \begin{cases} 0 & \text{for}\: k = 0 \wedge m = 0 \\\frac{\cos^{2}{\left(m x \right)}}{2 m} & \text{for}\: k = - m \\- \frac{\cos^{2}{\left(m x \right)}}{2 m} & \text{for}\: k = m \\- \frac{k \cos{\left(k x \right)} \cos{\left(m x \right)}}{k^{2} - m^{2}} - \frac{m \sin{\left(k x \right)} \sin{\left(m x \right)}}{k^{2} - m^{2}} & \text{otherwise} \end{cases}$

```python
f = sin(2*x)*cos(4*x)
integrate(f)
```

$\displaystyle \frac{\sin{\left(2 x \right)} \sin{\left(4 x \right)}}{3} + \frac{\cos{\left(2 x \right)} \cos{\left(4 x \right)}}{6}$

```python
limit(((x - 1)/(x + 1))**x, x, oo)
```

$\displaystyle e^{-2}$

```python
exp(-2)
```

$\displaystyle e^{-2}$

```python
limit(sin(x)/x, x, 2)
```

$\displaystyle \frac{\sin{\left(2 \right)}}{2}$

```python
limit((1 - cos(x))/x**2, x, 0)
```

$\displaystyle \frac{1}{2}$

```python
 S.Half

```

$\displaystyle \frac{1}{2}$

```python
limit((1 + k/x)**x, x, oo)

```

$\displaystyle e^{k}$

```python
 limit((x + 1)*(x + 2)*(x + 3)/x**3, x, oo)
```

$\displaystyle 1$

[Referencia](https://github.com/sympy/sympy/blob/master/sympy/series/tests/test_demidovich.py)

```python
diff( cos(x) * (1 + x))
```

$\displaystyle - \left(x + 1\right) \sin{\left(x \right)} + \cos{\left(x \right)}$

```python
diff( cos(x) * (1 + x),x)
```

$\displaystyle - \left(x + 1\right) \sin{\left(x \right)} + \cos{\left(x \right)}$

```python
diff( cos(x) * (1 + x),x,x)
```

$\displaystyle - (\left(x + 1\right) \cos{\left(x \right)} + 2 \sin{\left(x \right)})$

```python
diff(log(x * y), y)
```

$\displaystyle \frac{1}{y}$

[Referencia](https://pybonacci.org/2012/04/30/como-calcular-limites-derivadas-series-e-integrales-en-python-con-sympy/)


# Complex Numbers

```python
import sympy as sym
sym.init_printing()
import sympy.vector as sv

x, y, z, t = sym.symbols('x,y,z,t')
R= sv.CoordSys3D('R') # cf ReferenceFrame of sympy.physics.vector

def v(x,y,z): # vector field as a function of scalar variables x,y,z
    return x*y*R.i + 2*y*z*R.j + 3*x*z*R.k

def voft(l): # vector field along path l as a function of t
    x,y,z = (l.dot(R.i),l.dot(R.j),l.dot(R.k)) # x,y,z as functions of t
    return v(x,y,z)

def li(l,v): # dl/dt
    dl = sym.diff(l,t)
    return sym.integrate(voft(l).dot(dl),(t,0,1))

l1 = 2*t*R.j
l2 = 2*(1-t)*R.j + 2*t*R.k
l3 = 2*(1-t)*R.k
```

```python

```


# Python for Engineers
 # Introduction to Python Variables and Operators:
Newton's Equations of Motion

## What is covered?
1. Definition of a variable in python
2. The assignment operator
3. Definition of a statement in python
3. A variable's scope
4. Kinds of number variables
5. String variables
6. Printing a variable's value
7. Python Arithmetic operators

## Equations Review
Assuming constant acceleration

\begin{equation}
v_f = v_i + a t
\end{equation}

\begin{equation}
s = v_i + \frac{1}{2}at^2
\end{equation}

\begin{equation}
v_f^2 = v_i^2 + 2as
\end{equation}

where:
$v_f$: final velocity in $m/s$
$v_i$: initial velocity in $m/s$
$a$: acceleration in $m/s^2$
$t$: time in seconds
$s$: distance in meters

## 1 [Variables in Python and the Assignment Operator](https://www.tutorialspoint.com/python/python_variable_types.htm)
Variables are nothing but reserved memory locations to store values. This means that when you create a variable you reserve some space in memory1.

Based on the data type of a variable, the interpreter allocates memory and decides what can be stored in the reserved memory. Therefore, by assigning different data types to variables, you can store integers, decimals or characters in these variables1.

```python
# This code creates a variable called vi_mps and assigns it the value of 3.2
# Assignment operator: =
vi_mps = 3.2
```

```python
# To see what variables are in scope use the built in function dir()
# We call the part of a program where a variable is accessible its scope
dir()
```

### What is a [Statement](https://docs.python.org/3/reference/simple_stmts.html) in python?
A python statement is a set of logical instructions that the interpreter can read and execute. The code in the above cell is an example of an __assignment statement__, it assigns (modifies) the value of the variable `vi_mps`.
The other kind of statement in python is an __expression statement__ (or just expression) which can be defined as a set of logical operations on number or strings (or anything) to compute a value.

```python
# to see the variable value use print()
print(vi_mps)

# we can also use a string variable to make the print statement more friendly
vi_string = 'This is the initial velocity in m/s: '
print(vi_string, vi_mps)
```

```python
# vi_mps is a float number, we can also define an integer or a complex number in python
# here is an integer
fourier_series_terms = 5
print('the number of terms in the series is: ', fourier_series_terms)

# here is a complex number
root_1 = 4.82j
print('the root is: ', root_1)
```

## [Python Arithmetic operators](https://www.tutorialspoint.com/python/arithmetic_operators_example.htm)
__Example 1)__ Assume a vehicle is has a constant acceleration of 2.3 $m/s^2$, calculate its final speed after 4 seconds if it starts from an initial speed of 10 $m/s^2$

```python
vi_mps = 10
a_mpss = 2.3
t_sec = 4

# addition operator: +
# multiplication operator: *
# the left hand side of the following assignment statement is an expression
vf_mps = vi_mps + a_mpss * t_sec

print('The final velcocity of the vehicle: ', vf_mps)
```

__Example 2)__ Calculate the distance traveled by the vehicle in example (1).

```python
# exponent operator: **
s_m = vi_mps + 0.5 * a_mpss * t_sec ** 2
print('The distance traveled by the vehicle is: ', s_m)
```

__Example 3)__ A runner travels a distance of 50 meters at an acceleration of 1 $m/s^2$ what was their initial speed if they had a speed of $12 m/s$ ?

```python
runner_a_mpss = 1
runner_s_m = 50
runner_vf_mps = 12

# subtraction operator: -
runner_vi_mps = (runner_vf_mps ** 2 - 2.0 * runner_a_mpss * runner_s_m) ** 0.5
print("The runner's intial speed is: ", vi_mps)
```


# Solving Differential Equations using Numerical Methods

## 1. Introduction

This notebook is an introductory class to how to solve differential equations using numerical simulations. It has been implemented using Julia running on  Jupyter labs. We start by defining our problem as an irreversible chemical reaction in the following form.

$$ a + b \rightarrow c \tag{1}$$

where `a,b,c` are chemical species, and the rate or _speed_ of the reaction is determined by the reaction rate constant `k`. This value can be identified as the probability that a collision between `a` and `b` succesfully produces a molecule `c`.

Using both the __Mass Action Law__ and the __Mass Conservation Law__, we can derive the differential equations corresponding to this reaction. The general formulation of the system of differential equations can be expressed as:

$$ u' = f(u,p,t) \tag{2}$$

where `f` is a general vector function that depends on the parameters `p` of the system, the time variable `t`. For our particular case, the equations of our system are :

$$\begin{align}
\frac{da}{dt} &= -k \cdot a \cdot b \tag{3}\\
\frac{db}{dt} &= -k \cdot a \cdot b \tag{4}\\
\frac{dc}{dt} &= k \cdot a \cdot b \tag{5}
\end{align}$$

We will write the variables `a,b,c`  in the form of a __state vector__, i.e., a column vector that contains all the variable of the system variables. In this particular problem:

$$u=\begin{bmatrix}
a\\
b\\
c\tag{6}
\end{bmatrix}$$

## 2. Euler Method
The Euler method is the simplest most straightforward explicit algorithm to solve numerically a set differential equations. Althgough it is very simple, the Euler method often serves as the basis to construct more complex methods to solve systems of equations nukerically. Here, a differential equation can be thought of as a formula by which the slope of the tangent line to the curve can be computed at any point on the curve, once the position of that point has been calculated. The idea is that while the curve is initially unknown, its starting point (i.e., its in initial condition, which we denote by `u₀`), is know. Then, from the differential equation, the slope to the curve at `u₀` can be computed, and so, the tangent line.

Take a small step along that tangent line up to a point `u1`. Along this small step, the slope does not change too much, so
the point `u1` will be close to the curve. We can then continue the same reasoning for `u2`,`u3` and so on.

Here we will learn how to implement this method on a computer using any programing language. This notebook has been designed using Julia.

We start by defining a set of initial conditions for the state vector:

```julia
u₀=[0.02,0.01,0];
```

Next, we define a vector `time_span`, that determines the time where the equations will be solved

```julia
tspan = LinRange(0,100,100)
```

    100-element LinRange{Float64}:
     0.0,1.0101,2.0202,3.0303,4.0404,…,95.9596,96.9697,97.9798,98.9899,100.0

The results will be saved in a matrix `U`. This matrix will be initialized with the same number of rows of the state vector `u`, and same number of columns equal to the time vector `tspan`.

```julia
U=zeros((size(u₀,1), size(tspan,1)));
```

The first row of the matrix `U` will be the state of the of the system at `t=0`, so it is equal to the vector of initial conditions `u0`:

```julia
U[:,1]=u₀;
```

Finally, we need to set a value for the kinetic rate constant $k$:

```julia
k=1;
```

We will use each row of the matrix to save the value of each variable `a,b,c` for each time point in the simulation `a=U[1], b=U[2], c=U[3]`. This way, the set of Eqs 2.3 will be solved iteratively, i.e., the value of `U(t-1)` will be used to calculate the value of `U(t)`. We will iterate as many times as the length of the time vector. We do this by using a `for` loop:

```julia
for i = 2:size(tspan,1)
           U[1,i]= U[1,i-1]-k*U[2,i-1]*U[1,i-1] # Eq 1
           U[2,i]= U[2,i-1]-k*U[2,i-1]*U[1,i-1] # Eq 2
           U[3,i]= U[3,i-1]+k*U[2,i-1]*U[1,i-1] # Eq 3
       end
```

Next, we rename each row as the corresponding variable and plot the results in a graph, with the x-axis as the time vector `tspan`, and the y-axis the concentration of each variable `a,b,c`:

```julia
a=U[1,:]
b=U[2,:]
c=U[3,:];
```

```julia
using Plots; gr(); # this is to initialize the plots package in Julia
```

```julia
plot(tspan,a,label="a",seriestype=:scatter)
plot!(tspan,b,label="b",seriestype=:scatter)
plot!(tspan,c,label="c",seriestype=:scatter)
title!("Euler in Julia")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

## 3. Euler with integration step

The number of times that the equation is solved is very important. If we solved it just a few times, the solution may not be accurate. If you solve the system many times, the numerical simulations can be very slow. To  define how much time passes between diferent solutions of the system, we define a `timestep`. Note that now the `tspan` vector and the number of columns of the state matrix `U` depends on the `timestep` selected.

```julia
timestep=4
tspan=LinRange(0, 100, floor(Int,100/timestep))
U=zeros((size(u₀,1), size(tspan,1)))
U[:,1]=u₀
for i = 2:size(tspan,1)
           U[1,i]= U[1,i-1]-timestep*k*U[2,i-1]*U[1,i-1] # Eq 1
           U[2,i]= U[2,i-1]-timestep*k*U[2,i-1]*U[1,i-1] # Eq 2
           U[3,i]= U[3,i-1]+timestep*k*U[2,i-1]*U[1,i-1]  # Eq 3
       end
a=U[1,:]
b=U[2,:]
c=U[3,:]
plot(tspan,a,label="a",seriestype=:scatter)
plot!(tspan,b,label="b",seriestype=:scatter)
plot!(tspan,c,label="c",seriestype=:scatter)
title!("Euler in Julia")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

To compare how close are we to the analytical solutions, we can solve it and add it to the plot

```julia
plot(tspan,(U[2,1]/U[1,1])*exp.(k.*tspan.*(U[2,1]-U[1,1])))
```

```julia
plot!(tspan,b./a,seriestype=:scatter,label="numerical solution")
title!("Coparison between analytical and numerical")
xlabel!("Time [s]")
ylabel!("a/b")
```

## 4. Differential equations solvers

Normally, each modern language has implemented solvers for dynamical systems in the form of differential equations. In Julia, we first need to initialize the package for differenetial equations,

```julia
using DifferentialEquations
```

Next, we have to define a function `f` as a set of equations that result from the mass action analysis. To define a system of differential equations in DifferentialEquations.jl, we define our `f` as a vector function with a state vector vector `u` and parameters vector `p`. Thus, for the vector `u = [x,y,z]'`, we have the derivative function:

```julia
function simpleODE1!(du,u,p,t)
    k = p
    du[1] = -k*u[1]*u[2]
    du[2] = -k*u[1]*u[2]
    du[3] = k*u[1]*u[2]
end
```

    simpleODE1! (generic function with 1 method)

Notice here we used the in-place format which writes the output to the preallocated vector `du`. For systems of equations the in-place format is faster.  For our model, the only parameter is the rate constant `k`, therefore we build the parameter collection `p` as:

```julia
k=1;
p=(k);
u₀=[0.02,0.01,0];
```

Next, we define the `tspan` vector. Note that now, the vector has only two components: the initial and final time point for the simulation

```julia
tspan = (0.0,100.0);
```

Now we define a `ODEProblem` type using the constructor call. This is done by specifying this function `simpleODE`, the initial condition `u0`, the time span `tspan` and the parameters  `p`:

```julia
prob1 = ODEProblem(simpleODE1!,u₀,tspan,p)
```

    ODEProblem with uType Array{Float64,1} and tType Float64. In-place: true
    timespan: (0.0, 100.0)
    u0: [0.02, 0.01, 0.0]

Now, we solve the problem:

```julia
sol1 = solve(prob1)
```

    retcode: Success
    Interpolation: Automatic order switching interpolation
    t: 9-element Array{Float64,1}:
       0.0
       0.15383356961677971
       1.6921692657845766
       8.271827791540357
      19.86176245334914
      35.92987335869888
      57.673911215070845
      85.79206764253748
     100.0
    u: 9-element Array{Array{Float64,1},1}:
     [0.02, 0.01, 0.0]
     [0.0199693, 0.0099693, 3.06959e-5]
     [0.01967, 0.00966995, 0.000330049]
     [0.018529, 0.00852899, 0.00147101]
     [0.0169472, 0.00694719, 0.00305281]
     [0.0153629, 0.00536295, 0.00463705]
     [0.0139056, 0.00390558, 0.00609442]
     [0.0126907, 0.00269073, 0.00730927]
     [0.012254, 0.00225402, 0.00774598]

`sol1.t` stores the time points and `sol1.u` is an array storing the solution at the corresponding time points.

However, when dealing with systems of equations, `sol1` also acts like an array. `sol1[i]` returns the solution at the `i`th time point.

```julia
sol1.t[3],sol1.u[3]
```

    (1.6921692657845766, [0.01967, 0.00966995, 0.000330049])

Additionally, the solution acts like a matrix where `sol[j,i]` is the value of the `j`th variable at time `i`:

```julia
sol1[:,3]
```

    3-element Array{Float64,1}:
     0.019669951491554746
     0.009669951491554744
     0.000330048508445255

```julia
plot(sol1,label=["a","b","c"])
title!("ODE solver in Julia")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

One interesting feature is that, by default, the solution is a continuous function. So you do not need to extrapolate to a curve

```julia
sol1(60.4)
```

    3-element Array{Float64,1}:
     0.013761040160814916
     0.0037610401608149106
     0.006238959839185087

We can see how the implemented ODE solver chooses the time variable depending of the stiffness of the solution of the ODE. This saves a lot of time and computer power. Quite often, each ODE solver handles the integration time diferently, and choosing one or the other is impontant and depends on the problem to solve. We can see that the stock solver in Julia only solves the following few points

```julia
sol1 = solve(prob1,dense=false)
plot(sol1,label=["a","b","c"],seriestype=:scatter)
```

### 5.1 A DSL for Parameterized Functions

In many cases you may be defining a lot of functions with parameters. There exists the domain-specific language (DSL) defined by the `@ode_def` macro for helping with this common problem. Using this feature in Julia, we can rewrite the `simpleODE!` function as:

```julia
simpleODE3! = @ode_def abetterway begin
  da = -k*a*b
  db = -k*a*b
  dc = k*a*b
    end k
```

    (::abetterway{getfield(Main, Symbol("##3#7")),getfield(Main, Symbol("##4#8")),getfield(Main, Symbol("##5#9")),Nothing,Nothing,getfield(Main, Symbol("##6#10")),Expr,Expr}) (generic function with 2 methods)

```julia
prob3 = ODEProblem(simpleODE3!,u₀,tspan,p)
sol3 = solve(prob3)
plot(sol3,label=["a","b","c"])
title!("ODE solver using DSL")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

## 5. Reversible reactions and equilibrium

Now we will use the same approach  to solve the reversible reaction

$$ a + b \leftrightarrow  c \tag{7}$$

with rate constants `k1`, and `k2` for the forward and reverse reaction. Based on mass action kinetics, the differential equations to solve now are:

$$ \frac{da}{dt} = -k_1 \cdot a \cdot b + k_2 \cdot c \tag{8}$$

$$ \frac{db}{dt} = -k_1 \cdot a \cdot b + k_2 \cdot \tag{9}c$$

$$ \frac{dc}{dt} = k_1 \cdot a \cdot b - k_2 \cdot \tag{10}c$$

Now, $p$ that holds the parameters required to solve the system is a vector of two components  $k_1$, and $k_2$. So

```julia
k1=1
k2=0.01
p = (k1,k2); # we could also make this an array, or any other type!
```

```julia
function simpleODErev!(du,u,p,t)
    k1,k2 = p
    du[1] = -k1*u[1]*u[2]+k2*u[3]
    du[2] = -k1*u[1]*u[2]+k2*u[3]
    du[3] = k1*u[1]*u[2]-k2*u[3]
end
```

    simpleODErev! (generic function with 1 method)

```julia
prob4 = ODEProblem(simpleODErev!,u₀,tspan,p)
sol4 = solve(prob4)
plot(sol4,label=["a","b","c"])
title!("ODE solver in Julia")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

or, using the DSL notation

```julia
simpleODE2_rev! = @ode_def abetterway begin
  da = -k1*a*b+k2*c
  db = -k1*a*b+k2*c
  dc = k1*a*b-k2*c
    end k1 k2
```

    (::abetterway{getfield(Main, Symbol("##11#15")),getfield(Main, Symbol("##12#16")),getfield(Main, Symbol("##13#17")),Nothing,Nothing,getfield(Main, Symbol("##14#18")),Expr,Expr}) (generic function with 2 methods)

```julia
tspan = (0.0,100.0)
prob = ODEProblem(simpleODE2_rev!,u₀,tspan,p)
sol = solve(prob)
plot(sol,label=["a","b","c"])
title!("reversible ODE solver using DSL")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

At equilibrium, when $ \frac{\mathrm{d} c_{eq}}{\mathrm{d} t}=0$, we have:

$$
\begin{align}
k_2 \cdot c_{eq}= k_1 \cdot a_{eq}\cdot b_{eq} \tag{11}\\
c_{eq}= \frac{k_1 \cdot a_{eq}\cdot b_{eq}}{k_2} \tag{12}
\end{align}
$$

In addition, since  $ \frac{\mathrm{d} c}{\mathrm{d} t}=-\frac{\mathrm{d} a}{\mathrm{d} t} $ at any time, the condition $[a]+[c]=[a_o]$ is true at all time points. thererefore we can rewrite the equilibrium equation as:
$

$$
\begin{align}
c_{eq}= \frac{k_1 \cdot (a_{0}-c_{eq})\cdot b_{eq}}{k_2} \tag{13}\\
c_{eq}= \frac{k_1 \cdot a_{0}\cdot b_{eq}}{k_2}-\frac{k_1 \cdot c_{eq}\cdot b_{eq}}{k_2}  \tag{14}\\
c_{eq}+\frac{k_1 \cdot c_{eq}\cdot b_{eq}}{k_2} = \frac{k_1 \cdot a_{0}\cdot b_{eq}}{k_2} \tag{15}\\
c_{eq}(1+\frac{k_1 \cdot b_{eq}}{k_2}) = \frac{k_1 \cdot a_{0}\cdot b_{eq}}{k_2} \tag{16}\\
c_{eq}(\frac{k_2+k_1 \cdot b_{eq}}{k_2}) = \frac{k_1 \cdot a_{0}\cdot b_{eq}}{k_2} \tag{17}\\
c_{eq}= \frac{k_1 \cdot a_{0}\cdot b_{eq}}{k_2+k_1 \cdot b_{eq}} \tag{18}\\
\end{align}
$$

so, divinding numerator and denoiminator by $k_1$
$$
\begin{align}
c_{eq}= \frac{a_{0}\cdot b_{eq}}{\frac{k_2}{k_1}+\cdot b_{eq}} \tag{19}\\
c_{eq}= \frac{a_{0}\cdot b_{eq}}{K_{eq}+\cdot b_{eq}} \tag{20}
\end{align}
$$

where $K_{eq}$ is the equilibirum constant, defined as:

\begin{equation}
K_{eq}=\frac{k_2}{k_1} \tag{21}
\end{equation}

```julia
B=sol4[2,:]
c_eq=B*u₀[1]./(B.+(k2/k1))
plot(sol4,label=["a","b","c"])
plot!(sol4.t,c_eq,label="C_eq")
title!("Reversible ODE solver using DSL and Equilibrium solution")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```

## 6. A practical case

Now we will solve the more complex reaction

$$ NaCO_3 + CaCl_2  \overset{k_1}{\underset{k_2}{\longleftrightarrow}}  CaCO_3 + 2 NaCl \tag{22}$$

with forward and reverse kinetic constants `k1` and `k2`, respectively. We decompose the reversible reaction into two reactions:
$$\begin{align}
 NaCO_3 + CaCl_2  &\overset{k_1}{\longrightarrow}  CaCO_3 + 2 NaCl \tag{23}\\
 CaCO_3 + 2 NaCl &\overset{k_2}{\longrightarrow} NaCO_3 + CaCl_2 \tag{24}
 \end{align}
$$

The system consists of four species $X_s=1…4$ and two reactions $k_j=1…2$. Renaming the reactants for simplicity ($a = NaCO_3$, $b  =CaCl_2$, $c  = CaCo_3$, $d = NaCl)$, we obtain

$$\begin{align}
 a + b  &\overset{k_1}{\longrightarrow}  c + 2 d \tag{25}\\
 c + 2 d &\overset{k_2}{\longrightarrow} a + b \tag{26}
 \end{align}
$$

For this system, the stoichiometrix matrices are:
$$
A=\begin{bmatrix}
 1& 1 & 0 & 0  \tag{27}\\
 0 & 0 &1   &2
\end{bmatrix} $$

$$
B=\begin{bmatrix}
0 & 0 &1   &2 \tag{28}\\
1 & 1 &0   &0
\end{bmatrix}
\\
$$
The forward reaction is of order 2, the backward reaction is of order 3. Therefore, the units of $k_1 = s^{-1} M^{-1}$, and the units of $k_2 = s^{-1} M^{-2}$

Based on Mass Action Kinetics, the system of differential equations consists in the following four coupled ODEs:

$$\begin{align}
            \frac{ da }{dt} &= - k_1 \cdot a \cdot b + k_2 \cdot c \cdot d^2  \tag{29}\\
            \frac{ db }{dt} &= - k_1 \cdot a \cdot b + k_2 \cdot c \cdot d^2   \tag{30}  \\
             \frac{ dc }{dt} &=  k_1 \cdot a \cdot b - k_2 \cdot c \cdot d^2  \tag{31}  \\
              \frac{ dd }{dt} &= 2 k_1 \cdot a  \cdot b - 2 k_2 \cdot c \cdot d^2   \tag{32}  \\
\end{align}
$$

Next, we solve the equations numerically:

```julia
simpleODE3! = @ode_def abetterway begin
  da = -k1 * a * b + k2 * c * d^2
  db = -k1 * a * b + k2 * c * d^2
  dc = k1 * a * b - k2 * c * d^2
  dd = 2 * k1 * a * b - 2 * k2 * c * d^2
    end k1 k2
```

    (::abetterway{getfield(Main, Symbol("##19#23")),getfield(Main, Symbol("##20#24")),getfield(Main, Symbol("##21#25")),Nothing,Nothing,getfield(Main, Symbol("##22#26")),Expr,Expr}) (generic function with 2 methods)

```julia
k1=2.3e0;  # units 1/(Ms)
k2=2.5e0;  # units 1/(M M s)

p = (k1,k2)

a₀=0.02; # units (M)
b₀=0.01; # units (M)
c₀=0; # units (M)
d₀=0; # units (M)

u₀=[a₀,b₀,c₀,d₀];
```

```julia
tspan = (0.0,100.0)
prob = ODEProblem(simpleODE3!,u₀,tspan,p)
sol = solve(prob)
plot(sol,label=["a","b","c","d"])
title!("reversible ODE solver using DSL")
xlabel!("Time [s]")
ylabel!("Concentration [M]")
```


```python
# Created by Dr. Sangamesh Deepak R to teach Robotics online during covid 19 outbreak

import sympy as sy
import numpy as np
sy.init_printing()
```

```python
# Link parameters
a1 = 1
a2 = 0
a3 = sy.sqrt(2)

alpha1 = 0
alpha2 = sy.pi/4
alpha3 = 0

```

```python
# Joint parameters

theta1 = sy.Symbol(r'\theta_1')
theta2 = sy.Symbol(r'\theta_2')
theta3 = sy.Symbol(r'\theta_3')
theta4 = sy.Symbol(r'\theta_4')

d1 = 0
d2 = 0
d3 = sy.sqrt(2)
d4 = 0

```

```python
temp1 = 34.567
temp2 = np.pi/4
temp3 = sy.Symbol(r'\mu_k')
temp4 = sy.pi/4
[temp1, temp2, temp3, temp4]
```

```python
temp5 = sy.expand((temp3+temp4)**2)
temp6 = sy.expand((temp3+temp2)**2)
temp7 = sy.expand((temp1+temp2)**2)
[temp5, temp6, temp7]
```

```python
# transformation from of i' frame with respect to i frame

def link_transform(a_i, alpha_i):
    Link_T = sy.Matrix([[1, 0, 0, a_i], [0, sy.cos(alpha_i), -sy.sin(alpha_i), 0], [0, sy.sin(alpha_i), sy.cos(alpha_i), 0], \
                   [0,0,0,1] ])
    return Link_T
```

```python
# transformation of i frame with respect to (i-1)' frame'
def joint_transform(d_i, theta_i):
    Joint_T = sy.Matrix([[sy.cos(theta_i),  -sy.sin(theta_i), 0, 0],
                        [sy.sin(theta_i),  sy.cos(theta_i), 0, 0],
                        [0, 0, 1, d_i],
                        [0,0,0,1] ])
    return Joint_T

```

```python
# Computation of transformation matricies of different link frames with respect to the ground frame
T_0 = sy.Identity(4)
T_0_1 = joint_transform(d1, theta1)

T_1_2 = sy.trigsimp( link_transform(a1, alpha1)*joint_transform(d2, theta2) )
T_0_2 = sy.trigsimp( T_0_1* T_1_2);

T_2_3 = sy.trigsimp(link_transform(a2, alpha2)*joint_transform(d3, theta3) )
T_0_3 = sy.trigsimp( T_0_2* T_2_3);

T_3_4 = sy.trigsimp(link_transform(a3, alpha3)*joint_transform(d4, theta4) )
T_0_4 = sy.trigsimp( T_0_3* T_3_4);

```

```python
T_0_1, T_0_2, T_0_3, T_0_4 # Transformation matricies of first, second, third and fourth bodies
```

```python
T_0_4[2,3]   # (3,4)th element of trnasformation matrix for 4 frame
```

```python
# Extraction of Rotation matrices
R_0_1= T_0_1[0:3,0:3]
R_1_2= T_1_2[0:3,0:3]
R_2_3= T_2_3[0:3,0:3]
R_3_4= T_3_4[0:3,0:3]

r_0_1=T_0_1[0:3,3]
r_1_2=T_1_2[0:3,3]
r_2_3=T_2_3[0:3,3]
r_3_4=T_3_4[0:3,3]

```

```python
 def cross_product(a,b):
        c=sy.Matrix([
            [a[1,0]*b[2,0]-a[2,0]*b[1,0]],
            [a[2,0]*b[0,0]-a[0,0]*b[2,0]],
            [a[0,0]*b[1,0]-a[1,0]*b[0,0]]
            ])
        return c
```

```python
m=sy.Matrix([[0],[0],[1]])
n=sy.Matrix([[1],[0],[0]])
p = cross_product(m,n)
p
```

```python
d_d1=0
d_d2=0
d_d3=0
d_d4=0

d_theta1 = sy.Symbol(r'\dot{\theta}_1')
d_theta2 = sy.Symbol(r'\dot{\theta}_2')
d_theta3 = sy.Symbol(r'\dot{\theta}_3')
d_theta4 = sy.Symbol(r'\dot{\theta}_4')

d_d1, d_d2, d_d3, d_d4, d_theta1, d_theta2, d_theta3, d_theta4
```

```python
omega_0_0 = sy.Matrix([[0],[0],[0]])
v_0_0 = sy.Matrix([[0],[0],[0]])

```

```python
omega_1_1= R_0_1.T*(omega_0_0)+sy.Matrix([[0],[0],[d_theta1] ])

v_1_1 = R_0_1.T*(v_0_0 + cross_product(omega_0_0,r_0_1))+sy.Matrix([[0],[0],[d_d1] ])

omega_1_1, v_1_1
```

```python
omega_2_2= R_1_2.T*(omega_1_1)+sy.Matrix([[0],[0],[d_theta2] ])

v_2_2 = R_1_2.T*(v_1_1 + cross_product(omega_1_1,r_1_2))+sy.Matrix([[0],[0],[d_d2] ])

omega_2_2, v_2_2
```

```python
omega_3_3= R_2_3.T*(omega_2_2)+sy.Matrix([[0],[0],[d_theta3] ])

v_3_3 = R_2_3.T*(v_2_2 + cross_product(omega_2_2,r_2_3))+sy.Matrix([[0],[0],[d_d3] ])

omega_3_3, v_3_3
```

```python
omega_4_4= R_3_4.T*(omega_3_3)+sy.Matrix([[0],[0],[d_theta4] ])

v_4_4 = R_3_4.T*(v_3_3 + cross_product(omega_3_3,r_3_4))+sy.Matrix([[0],[0],[d_d4] ])

omega_4_4, v_4_4
```

```python
R_0_4= T_0_4[0:3,0:3]
v_0_4=sy.trigsimp(R_0_4*v_4_4)
omega_0_4 = sy.trigsimp(R_0_4*omega_4_4)
```

```python
mu_0_4 = sy.Matrix([v_0_4, omega_0_4])
mu_0_4
```

```python

a1= mu_0_4.subs([(d_theta1, 1), (d_theta2,0), (d_theta3, 0), (d_theta4,0)])
a2= mu_0_4.subs([(d_theta1, 0), (d_theta2,1), (d_theta3, 0), (d_theta4,0)])
a3= mu_0_4.subs([(d_theta1, 0), (d_theta2,0), (d_theta3, 1), (d_theta4,0)])
a4= mu_0_4.subs([(d_theta1, 0), (d_theta2,0), (d_theta3, 0), (d_theta4,1)])
```

```python
a1
```

```python
J=a1
J=J.col_insert(1,a2)
J=J.col_insert(2,a3)
J=J.col_insert(3,a4)
J
```

```python
J_num_1 = J.subs([(theta1, 0), (theta2, sy.pi/2), (theta3, -sy.pi/2), (theta4,0)])
J_num_1
```


[ADS Entry](http://adsabs.harvard.edu/abs/1999MNRAS.303L...1B)

```python
import sympy
sympy.init_printing()
```

Equation 1

```python
mu = sympy.Symbol('mu') # Mass per unit radius
v = sympy.Symbol('v') # Radial velocity
t = sympy.Symbol('t') # Time
r = sympy.Symbol('r') # Radius
G = sympy.Symbol('G') # Torque
l = sympy.Symbol('l') # Specific angular momentum
eqn_1 = [sympy.Eq(mu(r,t).diff(t),(mu(r,t)*v(r,t)).diff(r)),
        sympy.Eq((mu(r,t)*l(r,t)).diff(t),(mu(r,t)*v(r,t)*l(r,t)).diff(r)-G(r,t).diff(r))]
eqn_1
```

Equation 2

```python
eqn_2 = [sympy.Eq(G(r,t).diff(r),mu(r,t)*v(r,t)*l(r,t)/(2*r)),
        sympy.Eq(mu(r,t).diff(t),2*(sympy.sqrt(r)*G(r,t).diff(r)).diff(r))]
eqn_2
```

Verification of equation 2

```python
_ = eqn_2[:]
_ = [itm.subs(sympy.solve(eqn_1,[mu(r,t).diff(t),G(r,t).diff(r)])) for itm in _]
_ = [itm.subs(l(r,t),sympy.sqrt(r)) for itm in _]
_ = [itm.simplify() for itm in _]
_ = [itm.doit() for itm in _]
assert(_[0])
assert(_[1].simplify())
```

Equation 3

```python
e = sympy.Symbol('e') # Energy per unit mass
Omega = sympy.Symbol('Omega') # Angular velocity
eqn_3 = sympy.Eq((mu(r,t)*e(r,t)).diff(t)+(Omega(r,t)*G(r,t)-mu(r,t)*v(r,t)*e(r,t)).diff(r),
                G(r,t)*Omega(r,t).diff(r)).subs(e(r,t),-Omega(r,t)*l(r,t)/2)
eqn_3
```

Verification of equation 3

```python
_ = eqn_3
_ = _.subs(sympy.solve(eqn_1,[mu(r,t).diff(t),G(r,t).diff(r)]))
_ = _.subs(Omega(r,t),1/r**sympy.Rational(3,2))
_ = _.subs(l(r,t),sympy.sqrt(r))
_ = _.doit()
_ = _.simplify()
assert(_)
```

Equation 4

```python
u = sympy.Symbol('u') # Thermal energy density
h = sympy.Symbol('h') # Specific enthalpy
T = sympy.Symbol('T') # Temperature
s = sympy.Symbol('s') # Specific entropy
sympy.Eq((mu(r,t)*(e(r,t)+u(r,t))).diff(t)+
        (Omega(r,t)*G(r,t)-mu(r,t)*v(r,t)*(e(r,t)+h(r,t))).diff(r),
        G*Omega(r,t).diff(r)+
        mu(r,t)*T(r,t)*s(r,t).diff(t))
```

Equation 6

```python
rho = sympy.Symbol('rho') # Mass density
p = sympy.Symbol('p') # Pressure
eqn_6 = sympy.Eq(sympy.Derivative(v,t)-v*sympy.Derivative(v,r)+Omega**2*r,
                1/r**2+sympy.Derivative(p,r)/rho)
eqn_6
```

Poitive $v$ is moving toward decreasing r!

Equation 7

```python
_ = eqn_6
_ = _.subs(v,0)
_ = _.subs(sympy.Derivative(p,r),-5*p/(2*r))
_ = _.subs(p,rho*a**2)
_ = _.doit()
_ = _.subs(a,a(r,t))
_ = _.subs(Omega,Omega(r,t))
eqn_7 = _
eqn_7
```

Equation 8

```python
dot_m = sympy.Symbol(r'\dot{m}') # Mass acrretion rate
eqn_8 = sympy.Eq(mu(r,t)*v(r,t),dot_m)
eqn_8
```

verification of equation 8

```python
_ = sympy.Eq(eqn_8.lhs.diff(r),
             eqn_8.rhs.diff(r))
_ = _.subs(sympy.solve(eqn_1[0].subs(mu(r,t).diff(t),0),mu(r,t).diff(r),dict=True)[0])
assert(_)
```

Equation 9

```python
F_l = sympy.Symbol('F_l') # Constant of integration
eqn_9 = sympy.Eq(dot_m*r**2*Omega(r,t)-G(r,t),F_l)
eqn_9
```

verification of equation 9

```python
_ = sympy.Eq(eqn_9.lhs.diff(r),
             eqn_9.rhs.diff(r))
_ = _.subs(sympy.solve(eqn_1[1],G(r,t).diff(r),dict=True)[0])
_ = _.subs(l(r,t),sympy.sqrt(r))
_ = _.subs(sympy.solve(eqn_8,mu(r,t),dict=True)[0])
_ = _.subs(Omega(r,t),r**sympy.Rational(-3,2))
_ = _.doit()
_ = _.subs(v(r,t).diff(t),0)
assert(_)
```

Equation 10

```python
gamma = sympy.Symbol('gamma') # Adiabatic index
a = sympy.Symbol('a') # Isothermal speed of sound
F_E = sympy.Symbol('F_E') # Integration constant
eqn_10 = sympy.Eq(G(r,t)*Omega(r,t)-dot_m*(Omega(r,t)**2*r**2/2-1/r+gamma*a(r,t)**2/(gamma-1)),
                 F_E)
eqn_10
```

This is simply the second term in the left hand side of equation 4, since the right hand side vanishes.

```python
_ = [eqn_7,eqn_9,eqn_10]
_ = [itm.subs(F_l,0) for itm in _]
_ = [itm.subs(F_E,0) for itm in _]
_ = [itm.subs(sympy.solve(_[1],G(r,t),dict=True)[0])
     for itm in _]
_ = [itm.subs(sympy.solve(_[0],Omega(r,t),dict=True)[0])
     for itm in _]
_ = sympy.solve(_[2],a(r,t)**2)[0].simplify()
eqn_11 = sympy.Eq(a(r,t)**2,_)
eqn_11
```


# Diagonizing matrices in python
This shows you a couple of ways to find eigenvalues and eigenvectors of matrices in python.  The first method is numerical and the second method is analytical.

## Numerical eigenvalues/eigenvectors
Using the `numpy` library.

```python
import numpy as np # import the numpy numerical calculation library
```

```python
#define our matrix below.
matrix = np.array([[1.,2.,3.],[4.,5.,6.],[7.,8.,9.]])
```

```python
print(matrix)
```

    [[1. 2. 3.]
     [4. 5. 6.]
     [7. 8. 9.]]

The numpy library function to calculate eigenvalues and eigenvectors is called `np.linalg.eig()`.

```python
# calculate the eigenvalues and eigenvectors of matrix
eigenvalues, eigenvectors = np.linalg.eig(matrix)
```

```python
print(eigenvalues)
```

    [ 1.61168440e+01 -1.11684397e+00 -9.75918483e-16]

```python
# note that each column is one eigenvector
print(eigenvectors)
```

    [[-0.23197069 -0.78583024  0.40824829]
     [-0.52532209 -0.08675134 -0.81649658]
     [-0.8186735   0.61232756  0.40824829]]

```python
help(np.linalg.eig)
```

    Help on function eig in module numpy.linalg.linalg:

    eig(a)
        Compute the eigenvalues and right eigenvectors of a square array.

        Parameters
        ----------
        a : (..., M, M) array
            Matrices for which the eigenvalues and right eigenvectors will
            be computed

        Returns
        -------
        w : (..., M) array
            The eigenvalues, each repeated according to its multiplicity.
            The eigenvalues are not necessarily ordered. The resulting
            array will be of complex type, unless the imaginary part is
            zero in which case it will be cast to a real type. When `a`
            is real the resulting eigenvalues will be real (0 imaginary
            part) or occur in conjugate pairs

        v : (..., M, M) array
            The normalized (unit "length") eigenvectors, such that the
            column ``v[:,i]`` is the eigenvector corresponding to the
            eigenvalue ``w[i]``.

        Raises
        ------
        LinAlgError
            If the eigenvalue computation does not converge.

        See Also
        --------
        eigvals : eigenvalues of a non-symmetric array.

        eigh : eigenvalues and eigenvectors of a symmetric or Hermitian
               (conjugate symmetric) array.

        eigvalsh : eigenvalues of a symmetric or Hermitian (conjugate symmetric)
                   array.

        Notes
        -----

        .. versionadded:: 1.8.0

        Broadcasting rules apply, see the `numpy.linalg` documentation for
        details.

        This is implemented using the _geev LAPACK routines which compute
        the eigenvalues and eigenvectors of general square arrays.

        The number `w` is an eigenvalue of `a` if there exists a vector
        `v` such that ``dot(a,v) = w * v``. Thus, the arrays `a`, `w`, and
        `v` satisfy the equations ``dot(a[:,:], v[:,i]) = w[i] * v[:,i]``
        for :math:`i \in \{0,...,M-1\}`.

        The array `v` of eigenvectors may not be of maximum rank, that is, some
        of the columns may be linearly dependent, although round-off error may
        obscure that fact. If the eigenvalues are all different, then theoretically
        the eigenvectors are linearly independent. Likewise, the (complex-valued)
        matrix of eigenvectors `v` is unitary if the matrix `a` is normal, i.e.,
        if ``dot(a, a.H) = dot(a.H, a)``, where `a.H` denotes the conjugate
        transpose of `a`.

        Finally, it is emphasized that `v` consists of the *right* (as in
        right-hand side) eigenvectors of `a`.  A vector `y` satisfying
        ``dot(y.T, a) = z * y.T`` for some number `z` is called a *left*
        eigenvector of `a`, and, in general, the left and right eigenvectors
        of a matrix are not necessarily the (perhaps conjugate) transposes
        of each other.

        References
        ----------
        G. Strang, *Linear Algebra and Its Applications*, 2nd Ed., Orlando, FL,
        Academic Press, Inc., 1980, Various pp.

        Examples
        --------
        >>> from numpy import linalg as LA

        (Almost) trivial example with real e-values and e-vectors.

        >>> w, v = LA.eig(np.diag((1, 2, 3)))
        >>> w; v
        array([ 1.,  2.,  3.])
        array([[ 1.,  0.,  0.],
               [ 0.,  1.,  0.],
               [ 0.,  0.,  1.]])

        Real matrix possessing complex e-values and e-vectors; note that the
        e-values are complex conjugates of each other.

        >>> w, v = LA.eig(np.array([[1, -1], [1, 1]]))
        >>> w; v
        array([ 1. + 1.j,  1. - 1.j])
        array([[ 0.70710678+0.j        ,  0.70710678+0.j        ],
               [ 0.00000000-0.70710678j,  0.00000000+0.70710678j]])

        Complex-valued matrix with real e-values (but complex-valued e-vectors);
        note that a.conj().T = a, i.e., a is Hermitian.

        >>> a = np.array([[1, 1j], [-1j, 1]])
        >>> w, v = LA.eig(a)
        >>> w; v
        array([  2.00000000e+00+0.j,   5.98651912e-36+0.j]) # i.e., {2, 0}
        array([[ 0.00000000+0.70710678j,  0.70710678+0.j        ],
               [ 0.70710678+0.j        ,  0.00000000+0.70710678j]])

        Be careful about round-off error!

        >>> a = np.array([[1 + 1e-9, 0], [0, 1 - 1e-9]])
        >>> # Theor. e-values are 1 +/- 1e-9
        >>> w, v = LA.eig(a)
        >>> w; v
        array([ 1.,  1.])
        array([[ 1.,  0.],
               [ 0.,  1.]])

If the matrix is symmetric, the function `np.linalg.eigh()` is faster and more robust.

```python
# This is a hermitian matrix for testing the eigh() function
matrix2 = np.array([[1.,2.,3.],[2.,4.,5.],[3.,5.,6.]])
```

```python
print(matrix2)
```

    [[1. 2. 3.]
     [2. 4. 5.]
     [3. 5. 6.]]

```python
eigenvalues2, eigenvectors2 = np.linalg.eigh(matrix2)
```

```python
print(eigenvalues2)
```

    [-0.51572947  0.17091519 11.34481428]

```python
print(eigenvectors2)
```

    [[-0.73697623  0.59100905 -0.32798528]
     [-0.32798528 -0.73697623 -0.59100905]
     [ 0.59100905  0.32798528 -0.73697623]]

## Analytic eigenvalues/eigenvectors
Using the `sympy` library

```python
import sympy
```

```python
sympy.init_printing() # turn on pretty printing of math expressions
```

With `sympy` we have to first declare any variables that we wish to use.

Note the form below.  The names of the new variables appear twice.

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

```python
matrixA = sympy.Matrix([[a,b],[b,c]])
```

```python
matrixA
```

```python
# This returns the eigenvalues, their degeneracy, and the eigenvectors corresponding to each eigenvalue
matrixA.eigenvects()
```

```python
help(matrixA.eigenvects)
```

    Help on method eigenvects in module sympy.matrices.matrices:

    eigenvects(error_when_incomplete=True, iszerofunc=<function _iszero at 0x000001F9BD792598>, **flags) method of sympy.matrices.dense.MutableDenseMatrix instance
        Return list of triples (eigenval, multiplicity, basis).

        The flag ``simplify`` has two effects:
            1) if bool(simplify) is True, as_content_primitive()
            will be used to tidy up normalization artifacts;
            2) if nullspace needs simplification to compute the
            basis, the simplify flag will be passed on to the
            nullspace routine which will interpret it there.

        Parameters
        ==========

        error_when_incomplete : bool
            Raise an error when not all eigenvalues are computed. This is
            caused by ``roots`` not returning a full list of eigenvalues.

        If the matrix contains any Floats, they will be changed to Rationals
        for computation purposes, but the answers will be returned after being
        evaluated with evalf. If it is desired to removed small imaginary
        portions during the evalf step, pass a value for the ``chop`` flag.

```python

```