# QuTiP Lecture: Particle emission from a photon cascade

D. Lukin, Stanford University

In this Jupyter notebook, we use QuTiP: The Quantum Toolbox in Python to study a cascaded three level system excited by a classical pulse. This model system captures the essense of the dynamics of a biexcitonic in a quantum dot [[1,2]](#refs).

For more information about QuTiP see the project web page: http://qutip.org/

## Introduction

The system consists of three states: ground $\left|g\right\rangle$, intermediate $\left|i\right\rangle$, and excited $\left|e\right\rangle$. The process we are modeling is the two-photon excitation of the system with a classical pulse driving the transition $\left|g\right\rangle \to  \left|e\right\rangle$. The system is coupled to two decay channels, via collapse operators
 $\sigma_1 = \sqrt{\gamma_1} \left|i\right\rangle \left\langle e\right| $ and $\sigma_2 = \sqrt{\gamma_2} \left|g\right\rangle \left\langle i\right| $. Here, $\gamma_1$ and $\gamma_2$ are the coupling strength of the system to channels 1 and 2. In all simulations, we are setting $2\gamma_2 = \gamma_1 = \gamma$.

In a frame rotating at the laser's frequency for the two-photon resonance, the system Hamiltonian is given by

$$H_{\mbox{sys}} = H_0 + H_E(t)$$

where $$H_0 = E_b/2 \left|i\right\rangle \left\langle i\right|$$

and $H_E$ represents two-photon excitation of the system by a classical pulse in the rotating wave appoximation. Further,

$$H_I(t) =  \frac{(\mu \cdot E(t))^2}{E_b} (\left|g\right\rangle \left\langle e\right| + \left|e\right\rangle \left\langle g\right|) .$$

Here, $E(t)$ is the time dependent electric field pulse driving the system, $\mu$ is the coupling strength, and $E_b$ is the binding energy of the biexciton. The driving strength depends on the square of $\mu \cdot E(t)$ because the coherent drive occurs via virtual excitation of the intermediate state.

The system-only evolution (tracing over the dynamics of the two channels into which the system spontaneously emits), is given by the Master Equation:
\begin{equation}
\partial_t \rho = \mathcal L\rho = -i[H_I(t), \rho] + \mathcal D[\sigma_1]\rho +  \mathcal D[ \sigma_2]\rho,
\end{equation}
where $\mathcal D[c]\rho = c\rho c^\dagger - \frac 12 (c^\dagger c \rho + \rho c^\dagger c)$ is the standard Lindblad dissipator.

## Setting Up Computation in QuTiP

```python
%matplotlib inline
```

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

### Setup of time discretization and pulse

```python
# time parameters
gamma = 1.0                         # decay rate, sets the overall timescale of the system
globaldt = 0.15/gamma               # default time step
minpointsperpulse = 5.0             # minimum time resolution of a pulse

# define function E(t), a gaussian.
def pulse_shape(t, args):
    width = args['width']
    norm = args['norm']

    pulse_t_offset = 3.5
    t_offset = width*pulse_t_offset
    return norm * np.exp(-(t - t_offset) ** 2.0 /
                                  (2.0 * width ** 2.0))

# pulse normalization function
def pulsenormconst(args):
    args['norm'] = 1.0
    tlist = tlistfunc(args)
    unnorm_pulse = [pulse_shape(t, args) for t in tlist]
    unnorm_pulse_area = np.trapz(unnorm_pulse, tlist)
    return 1/unnorm_pulse_area

# in order to speed up the simulation for shorter pulses, we take advantage of
# QuTiP's built in support for variable time steps. This function generates a
# the list of times for which system dynamics will be calculated.
def tlistfunc(args):
    width = args['width']
    norm = args['norm']
    tmin = args['tmin']
    tmax = args['tmax']

    tlist = []

    pulse_t_offset = 3.5

    # if the global time resolution satisfies the minimum number of points per
    # pulse, make time list a simple linearly-spaced set of points.
    if width/minpointsperpulse > globaldt:
        tlist = np.linspace(tmin, tmax, int((tmax - tmin)/globaldt))

    # otherwise, increase resolution inside the pulse only.
    else:
        tlist1 = np.linspace(tmin, width*pulse_t_offset*2.0, \
                             int(minpointsperpulse*(pulse_t_offset*2.0 - tmin)))[:-1]
        tlist2 = np.linspace(width*pulse_t_offset*2.0, tmax, \
                             int((tmax - width*pulse_t_offset*2.0)/globaldt))
        tlist = np.append(tlist1, tlist2)

    return tlist

def prepare_pulse(width):
    pulseargs = {'width':width * gamma, 'tmin':0.0, 'tmax':width*1.5 + 5.0/gamma} # define pulse properties
    pulseargs['norm'] = pulse_area * pulsenormconst(pulseargs)    # compute numerically the normalization constant
    tlist = tlistfunc(pulseargs)                                  # generate time list
    pulse = [pulse_shape(t, pulseargs) for t in tlist]            # generate pulse shape
    return tlist, pulse, pulseargs
```

### Setup of the physics

For this notebook, we consider the case of so-called $\pi$ area pulses, which for short pulses prepare the system in the excited state $|e\rangle$.

```python
C = 1 # includes dipole coupling strength and binding energy

pulse_area = np.pi/(C**2) # setting area of pulse to Pi.

#System state space
g = fock(3,0)
i = fock(3,1)
e = fock(3,2)

# Hamiltonian
H0 = qeye(3)                          # identity because we are in the interaction picture
H_I = C/2 * (e*g.dag() + g*e.dag())  # time dependent field
H = [H0, [H_I, pulse_shape]]          # complete system Hamiltonian

s1 = np.sqrt(2*gamma) * i*e.dag()     # lowering operator for channel 1
s2 = np.sqrt(gamma) * g*i.dag()       # lowering operator for channel 2
```

## Dynamics of System Expectation Values
We begin by checking that the system is set up correctly by computing the system dynamics given by
$$
\partial_t \rho = \mathcal L\rho = -i[H_I(t), \rho] + \mathcal D[\sqrt{\gamma_1} \sigma_1]\rho +  \mathcal D[\sqrt{\gamma_2} \sigma_2]\rho,
$$
and extracting the expectation values of projection operators $\Pi_i = \left|i\right\rangle \left\langle i\right|$ and $\Pi_e = \left|e\right\rangle \left\langle e\right|$.

```python
#prepare pulse (argument is pulse width)
tlist, pulse, pulseargs = prepare_pulse(0.001)

#compute system evolution and expectation values of projectors. Supply pulseargs as parameter for H(t).
result = mesolve(H, g, tlist, [s1, s2], [e*e.dag(), i*i.dag()], args=pulseargs)
e_expvals = result.expect[0]
i_expvals = result.expect[1]

#plot the data
fig,ax=plt.subplots(figsize=(8,5))
ax.plot(tlist,e_expvals, label="population E")
ax.plot(tlist,i_expvals, label="population I")
ax.legend()
ax.set_xlabel('Time');
ax.set_ylabel('Population');
ax.set_xlim(0, 5)
ax.set_title('System evolution');
```

We see that the system behaves correctly: The short pulse dynamics are captured, where the system briefly visits unity excitation of the excite state, indicating that time discretization is properly set up. Note also that the decay rate of $\left|e\right\rangle$ is twice as fast as the decay rate of $\left|i\right\rangle$.

It is straightforward to compute the expectation value for the total number of photons emitted into either channel, since the rate of photon emission is given by $\mbox{Tr}[\rho(t) \sigma_1^\dagger \sigma_1]$ or $\mbox{Tr}[\rho(t) \sigma_2^\dagger \sigma_2]$ and thus
$$\langle N\rangle = \int_0^\infty\mbox{Tr}[\rho(t) \ \sigma_1^\dagger \sigma_1] \ dt = \gamma_1 \int_0^\infty \langle \Pi_e \rangle \ dt$$

```python
Nexp = 2*gamma*np.trapz(result.expect[0], tlist); print(Nexp)
```

    0.9977906071836861

## Computing N-photon emission probabilities

Using the formalism of conditioned evolution, the $N$-photon emission probability is given by
$$P(N) = \int_0^\infty dt_1 \int_0^\infty dt_2 \dots \int_0^\infty dt_N \ p(t_1, t_2, \dots t_N),$$
where $p(t_1, t_2, \dots t_N)$ is the probability that the system emits photons at only times $\{t_1, t_2, \dots t_N\}$.

The number of photons emitted into channels 1 and 2 is always equal, because of the cascade nature of the system. We can thus choose to monitor (condition) the emission from either channel, while tracing over the state of the second channel. We choose, arbitrarily, to monitor channel 1.

We define the collapse superoperator for channel $1$ as $\mathcal S\rho = \sigma_1\rho \sigma_1^\dagger$, and the nonunitary evolution conditioned on no emision as $\mathcal K = \mathcal L - \mathcal S$. Then $p(t_1, t_2, \dots t_N)$ is given by
$$p(t_1, t_2, \dots t_N) = \mbox{Tr}[\mathcal K(\infty, t_N) \ \mathcal S \ \mathcal K(t_N, t_{N-1}) \ \mathcal S \ \dots \ \mathcal S \ \mathcal K(t_1, 0)\left| g \right\rangle \left \langle g \right |]$$

This expression for $p(t_1, t_2, \dots t_N)$ is derived in Carmichael, Chapter 7 [[3](#refs)]. Since the final state of the atom is always $\left | g\right \rangle$, the trace takes only a single entry of the density matrix $\left\langle g\right |\rho(t\rightarrow\infty)\left | g \right\rangle$.

Explicitly the superoperator $\mathcal K$ is
$$
\mathcal K\rho = -i[H_I(t), \rho] +\mathcal D[\sqrt{\gamma_2} \sigma_2]\rho - (\sigma_1^\dagger\sigma_1 \rho + \rho \sigma_1^\dagger\sigma_1)/2
$$
We set up $\mathcal K$ in QuTiP as follows:

```python
Ktimedep = -1.0j * C/2 * (spre(e*g.dag() + g*e.dag()) - spost(e*g.dag() + g*e.dag()))
Ktimeindep =  - 0.5 * (spre(s1.dag()*s1) + spost(s1.dag()*s1)) \
              + sprepost(s2, s2.dag()) - 0.5*(spre(s2.dag()*s2) + spost(s2.dag()*s2))

K =  [Ktimeindep, [Ktimedep, pulse_shape]]
```

To evaluate $p(t_1, t_2, \dots t_N)$, we initialize the system in the ground state $\rho(0) = \left|g\right\rangle \left\langle g \right |$, and evolve it with $\mathcal K$ from $t = 0$ to $t = t_1$. Then we act on the evolved $\rho$ with $\mathcal S$, then evolve with $\mathcal K$ until $t = t_2$, etc.

When computing the total $N$-photon probability $P(N)$, it is possible to re-use solutions from different trajectories to make the algorithm more time-efficient. A natural implementation of this is with recursion.

```python
#parameters
width = 0.05 #pulse width

#prepare pulse
tlist, pulse, pulseargs = prepare_pulse(width)

def conditionalIteration(rhoinit, totalphotons, iternumber, tinitindex):
    '''
    Recursive function that computes the probability of emission of N photons, P(N)
    rhoinit - initial state of the system
    totalphotons - N
    iternumber - current recursion level
    tinitindex - the index in tlist that determines the timestep from which to begin the recursive computataion
    '''
    curritertrajprobs = []
    curritertimes = []
    if iternumber == totalphotons:
        out = mesolve(K, rhoinit, tlist[tinitindex:], [], [], args=pulseargs)
        rho = out.states[-1]
        prob = expect(g*g.dag(), rho)
        if totalphotons == 0: #if calculating P(0), the innermost recursion step is also the
            return prob       #outermost one, so return probability, not probability density.
        else:                 #else return the probability density
            return tlist[tinitindex], prob
    else:
        conditionedrho = mesolve(K, rhoinit, tlist[tinitindex:], [], [], args=pulseargs)
        for i in range(tinitindex,len(tlist)):
            rho = conditionedrho.states[i - tinitindex]
            rhoprojected = s1 * rho * s1.dag()
            outtime,outtrajprob = conditionalIteration(rhoprojected, totalphotons, iternumber + 1, i)
            curritertrajprobs.append(outtrajprob)
            curritertimes.append(outtime)
        integral = abs(np.trapz(curritertrajprobs, curritertimes))
        if iternumber == 0:
            return integral                    #for the outermost loop, return final cummilative probability.
        else:
            return integral, tlist[tinitindex] #for inner loops return probability density term by term.

#Calculate N-photon probabilities by calling the recursive function
P0 = conditionalIteration(g*g.dag(), 0, 0, 0); print("P(0) = " + str(P0))
P1 = conditionalIteration(g*g.dag(), 1, 0, 0); print("P(1) = " + str(P1))
P2 = conditionalIteration(g*g.dag(), 2, 0, 0); print("P(2) = " + str(P2))
```

    P(0) = 0.002715148979783426
    P(1) = 0.9873106987545373
    P(2) = 0.0009142280636881267

Thus, we see that for a short pulse, the system acts as a good single photon source. This calculation may require higher time resolution for short pulses to achieve high accuracy.

## Computing the system+channel density matrix

The results in the section are covered in Ref. [[2]](#refs).

We take a step back and consider a representation of the three-level system coupled to two dissipative channels where no information about the state is discarded. In the coarse-grained time basis [[4]](#refs), the state can be written as

$$\left|\Psi\right\rangle = \sum_{\phi_j, \mathbf T_1, \mathbf T_2} c_{\phi_j, \mathbf T_1, \mathbf T_2} \left|\phi_j, \mathbf T_1, \mathbf T_2\right\rangle,$$
where $\phi_j \in \{g, j, e\}$, $\mathbf T_k = \{t_1, t_2, \cdots, t_N\}$ is the list of times when a photon was emitted into channel $k$.

We may now trace over the second channel, thus obtaining a mixed state:

$$\chi = \mbox{Tr}_{\mathbf T_2}[\left|\Psi\right\rangle \left\langle \Psi \right |] = \sum_{\mathbf T''_2} \sum_{\phi_j, \mathbf T_1, \mathbf T_2} \sum_{\phi_j', \mathbf T_1', \mathbf T_2'} c_{\phi_j, \mathbf T_1, \mathbf T_2} c^*_{\phi'_j, \mathbf T_1', \mathbf T_2'}\langle \mathbf T_2''  \left|\phi_j, \mathbf T_1, \mathbf T_2\right\rangle \langle \phi'_j, \mathbf T'_1, \mathbf T'_2  \left|\mathbf T''_2\right\rangle.$$

The modes of channel 2 are orthogonal, thus,

$$\chi = \sum_{\mathbf T_2} \sum_{\phi_j, \mathbf T_1} \sum_{\phi_j', \mathbf T_1'} c_{\phi_j, \mathbf T_1, \mathbf T_2} c^*_{\phi'_j, \mathbf T_1', \mathbf T_2}\langle \mathbf T_2  \left|\phi_j, \mathbf T_1, \mathbf T_2\right\rangle \langle \phi'_j, \mathbf T'_1, \mathbf T_2  \left|\mathbf T_2\right\rangle.$$

We can re-define the states in the Hilbert space of system and channel 1 only, and write:

$$\chi = \sum_{\phi_j, \mathbf T_1} \sum_{\phi_j', \mathbf T_1'} \Big(\sum_{\mathbf T_2} c_{\phi_j, \mathbf T_1, \mathbf T_2} c^*_{\phi'_j, \mathbf T_1', \mathbf T_2}\Big) \left|\phi_j, \mathbf T_1\right\rangle\left\langle\phi'_j, \mathbf T'_1\right|$$

We see that
$$\sum_{\mathbf T_2} c_{\phi_j, \mathbf T_1, \mathbf T_2} c^*_{\phi'_i, \mathbf T_1', \mathbf T_2} = \left\langle\phi'_i, \mathbf T'_1\right|\chi\left|\phi_j, \mathbf T_1\right\rangle$$ is the matrix element of the reduced density matrix of the system and channel 1 only. Note that since the initial state and final state of the system is $\left | g\right\rangle$, only elements $\phi_j = \phi_j' = g$ are nonzero. We can thus drop the system state labels and denote the matrix elements by $\left\langle\mathbf T'_1\right|\chi\left| \mathbf T_1\right\rangle $.

It turns out, in the process of obtaining $P(N)$ in the previous section, we already computed the diagonal elements of $\chi$.

Calculation of the off-diagonal elements was made possible with the technique in Ref. [[4]](#refs) and was derived in Ref. [[2]](#refs). We present the final result:

$$
\left\langle\mathbf{T'}_1\big|\ \chi \ \big|\mathbf{T}_1\right\rangle =  \text{Tr}_\text{sys}\big[\mathcal{K}(\infty, \tau_N)\mathcal S_{Q[\tau_N]}\mathcal{K}(\tau_N, \tau_{N-1})\mathcal S_{Q[\tau_{N-1}]}\cdots \mathcal{K}(\tau_2,\tau_1)\mathcal S_{Q[\tau_1]} \mathcal{K}(\tau_1,0)\left | g\right\rangle\left\langle g\right |\big]
$$

Here, ${\tau_i}$ is a time ordered set of times $\mathbf T_1 \cup \mathbf T_1'$, $\mathcal K$ is defined as before, and $Q[\tau_{i}]$ takes on the value 1 if $\tau_i \in \mathbf T_1$, and 0 if $\tau_i \in \mathbf T_1'$. The operators
$\mathcal S_0, \mathcal S_1$ are defined as $$\mathcal S_0\rho = \sigma_1 \rho$$ $$\mathcal S_1\rho = \rho\sigma_1$$

Note, this calculation is very similar to
$$p(t_1, t_2, \dots t_N) = \mbox{Tr}[\mathcal K(\infty, t_N) \ \mathcal S \ \mathcal K(t_N, t_{N-1}) \ \mathcal S \ \cdots \ \mathcal S \ \mathcal K(t_1, 0)\left| g \right\rangle \left \langle g \right |].$$

The difference is that instead of applying the collapse operator on both sides for a given emission time $t$, one has to apply them either on the right or the left, depending on whether $\tau$ comes from $\mathbf T_1$ or $\mathbf T_1'$.

Computing the full density matrix for a large number of photon emissions is computationally expensive. Here, we will only calculate the density matrix for 1-photon channel states:

```python
#parameters
width = 0.05 #pulse width

#prepare pulse
tlist, pulse, pulseargs = prepare_pulse(width)

#array to be populated with terms of the system-channel density matrix
rhototal = [[0 for x in tlist] for x in tlist]

result = mesolve(K, g*g.dag(), tlist, [], [], args=pulseargs)
rho_pre_emission = result.states

#since rhototal is Hermitian, it is sufficient to calculate only terms on the diagonal and above.
#tL (tR) represents the emission time on the left, a.k.a in T1 (right, a.k.a in T1').
for tL in range(len(tlist)):
    result = mesolve(K, s1 * rho_pre_emission[tL], tlist[tL:], [], [], args=pulseargs)
    rho_post_tL = result.states
    for tR in range(tL,len(tlist)):
        i = tR - tL
        result = mesolve(K, rho_post_tL[i] * s1.dag(), tlist[tR:], [], [], args=pulseargs)
        rhofinal = result.states[-1]
        matelement = expect(g*g.dag(), rhofinal)
        rhototal[tR][tL] = np.conjugate(matelement)
        rhototal[tL][tR] = matelement
```

Having obtained the entire system+channel density matrix, we can compute quantities that previously were not accessible to us. For instance, the trace purity of the single photon emission into channel 1, defined as
$$\mathbb P = \lim_{t\to\infty} \int_0^t \mbox{d}t_1  \int_0^t \mbox{d}t_1' \frac{\bigl|\left\langle t_1'\big| \ \chi \ \big| \ t_1 \right\rangle \bigr|^2}{P(1)^2} $$

So let's compute the one-photon-purity:

```python
diagonal = np.diagonal(rhototal)
P1 = np.trapz(diagonal, tlist)
Purity = np.trapz([np.trapz(np.square(rhototal[row]), tlist) for row in range(len(tlist))] , tlist)/(P1**2)

print("P(1) = "+ str(P1)) #note that this number should agree with P(1) from the previous section
print("Purity = "+ str(Purity))
```

    P(1) = 0.9873106987545373
    Purity = 0.6682330028475325

## References

[1] Quantum dot single photon sources with ultra-low multi-photon probability, Lukas Hanschke, Kevin A. Fischer, Stefan Appel, Daniil Lukin, Jakob Wierzbowski, Shuo Sun, Rahul Trivedi, Jelena Vučković, Jonathan J. Finley, Kai Müller. (2018) [[arXiv:1801.01672](https://arxiv.org/abs/1801.01672)]

[2] Particle emission from open-quantum systems, Kevin A. Fischer, Rahul Trivedi, Daniil Lukin. (2017) [[arXiv:1803.04648](https://arxiv.org/abs/1803.04648)]

[3] H. Carmichael, An Open Systems Approach to Quantum Optics, Lectures Presented at the Université Libre de Bruxelles, 1991

[4] Scattering of Coherent Pulses from Quantum-Optical Systems, Kevin A. Fischer, Rahul Trivedi, Vinay Ramasesh, Irfan Siddiqi, Jelena Vučković. (2017) [[arXiv:1710.02875](https://arxiv.org/abs/1710.02875)]

## Version

```python
from qutip.ipynbtools import version_table

version_table()
```

Software	Version

QuTiP	4.3.0.dev0+36fd841

Numpy	1.14.1

SciPy	0.18.1

matplotlib	1.4.3

Cython	0.24.1

Number of CPUs	8

BLAS Info	OPENBLAS

IPython	4.2.0

Python	3.4.3 (default, Nov 28 2017, 16:41:13)
[GCC 4.8.4]

OS	posix [linux]

Mon Apr 09 13:12:57 2018 PDT


# Linear Gaussian filtering and smoothing (discrete)

Provided is an example of discrete, linear state-space models on which one can perform Bayesian filtering and smoothing in order to obtain
a posterior distribution over a latent state trajectory based on noisy observations.
In order to understand the theory behind these methods in detail we refer to [1] and [2].

**References**:
> [1] Särkkä, Simo, and Solin, Arno. Applied Stochastic Differential Equations. Cambridge University Press, 2019.
>
> [2] Särkkä, Simo. Bayesian Filtering and Smoothing. Cambridge University Press, 2013.

```python
import numpy as np

import probnum as pn
from probnum import filtsmooth, randvars, randprocs
from probnum.problems import TimeSeriesRegressionProblem
```

```python
rng = np.random.default_rng(seed=123)
```

```python
# Make inline plots vector graphics instead of raster graphics
%matplotlib inline
from IPython.display import set_matplotlib_formats

set_matplotlib_formats("pdf", "svg")

# Plotting
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

plt.style.use("../../probnum.mplstyle")
```

    /tmp/ipykernel_125474/236124620.py:5: DeprecationWarning: `set_matplotlib_formats` is deprecated since IPython 7.23, directly use `matplotlib_inline.backend_inline.set_matplotlib_formats()`
      set_matplotlib_formats("pdf", "svg")

## **Linear Discrete** State-Space Model: Car Tracking

We showcase the arguably most simple case in which we consider the following state-space model. Consider matrices $A \in \mathbb{R}^{d \times d}$ and $H \in \mathbb{R}^{m \times d}$ where $d$ is the state dimension and $m$ is the dimension of the measurements. Then we define the dynamics and the measurement model as follows:

For $k = 1, \dots, K$ and $x_0 \sim \mathcal{N}(\mu_0, \Sigma_0)$:

$$
\begin{align}
    \boldsymbol{x}_k &\sim \mathcal{N}(\boldsymbol{A} \, \boldsymbol{x}_{k-1}, \boldsymbol{Q}) \\
    \boldsymbol{y}_k &\sim \mathcal{N}(\boldsymbol{H} \, \boldsymbol{x}_k, \boldsymbol{R})
\end{align}
$$

This defines a dynamics model that assumes a state $\boldsymbol{x}_k$ in a **discrete** sequence of states arising from a linear projection of the previous state $x_{k-1}$ corrupted with additive Gaussian noise under a **process noise** covariance matrix $Q$.
Similarly, the measurements $\boldsymbol{y}_k$ are assumed to be linear projections of the latent state under additive Gaussian noise according to a **measurement noise** covariance $R$.
In the following example we consider projections and covariances that are constant over the state and measurement trajectories (linear time invariant, or **LTI**). Note that this can be generalized to a linear time-varying state-space model, as well. Then $A$ is a function $A: \mathbb{T} \rightarrow \mathbb{R}^{d \times d}$ and $H$ is a function $H: \mathbb{T} \rightarrow \mathbb{R}^{m \times d}$ where $\mathbb{T}$ is the "time dimension".

In other words, here, every relationship is linear and every distribution is a Gaussian distribution.
Under these simplifying assumptions it is possible to obtain a filtering posterior distribution over the state trajectory $(\boldsymbol{x}_k)_{k=1}^{K}$ by using a
**Kalman Filter**. The example is taken from Example 3.6 in [2].

### Define State-Space Model

#### I. Discrete Dynamics Model: Linear, Time-Invariant, Gaussian Transitions

```python
state_dim = 4
observation_dim = 2
```

```python
delta_t = 0.2

# Define linear transition operator
dynamics_transition_matrix = np.eye(state_dim) + delta_t * np.diag(np.ones(2), 2)

# Define process noise (covariance) matrix
noise_matrix = (
    np.diag(np.array([delta_t ** 3 / 3, delta_t ** 3 / 3, delta_t, delta_t]))
    + np.diag(np.array([delta_t ** 2 / 2, delta_t ** 2 / 2]), 2)
    + np.diag(np.array([delta_t ** 2 / 2, delta_t ** 2 / 2]), -2)
)
```

To create a discrete, LTI Gaussian dynamics model, `probnum` provides the `LTIGaussian` class.

```python
# Create discrete, Linear Time-Invariant Gaussian dynamics model
noise = randvars.Normal(mean=np.zeros(state_dim), cov=noise_matrix)
dynamics_model = randprocs.markov.discrete.LTIGaussian(
    transition_matrix=dynamics_transition_matrix,
    noise=noise,
)
```

#### II. Discrete Measurement Model: Linear, Time-Invariant, Gaussian Measurements

```python
measurement_marginal_variance = 0.5
measurement_matrix = np.eye(observation_dim, state_dim)
measurement_noise_matrix = measurement_marginal_variance * np.eye(observation_dim)
```

```python
noise = randvars.Normal(mean=np.zeros(observation_dim), cov=measurement_noise_matrix)
measurement_model = randprocs.markov.discrete.LTIGaussian(
    transition_matrix=measurement_matrix,
    noise=noise,
)
```

#### III. Initial State Random Variable

```python
mu_0 = np.zeros(state_dim)
sigma_0 = 0.5 * measurement_marginal_variance * np.eye(state_dim)
initial_state_rv = randvars.Normal(mean=mu_0, cov=sigma_0)
```

```python
prior_process = randprocs.markov.MarkovSequence(
    transition=dynamics_model, initrv=initial_state_rv, initarg=0.0
)
```

### Generate Data for the State-Space Model

Next, sample both latent states and noisy observations from the specified state-space model.

```python
time_grid = np.arange(0.0, 10.0, step=delta_t)
```

```python
latent_states, observations = randprocs.markov.utils.generate_artificial_measurements(
    rng=rng,
    prior_process=prior_process,
    measmod=measurement_model,
    times=time_grid,
)
```

```python
regression_problem = TimeSeriesRegressionProblem(
    observations=observations,
    locations=time_grid,
    measurement_models=[measurement_model] * len(time_grid),
)
```

### Kalman Filtering

#### I. Kalman Filter

```python
kalman_filter = filtsmooth.gaussian.Kalman(prior_process)
```

#### II. Perform Kalman Filtering + Rauch-Tung-Striebel Smoothing

```python
state_posterior, _ = kalman_filter.filtsmooth(regression_problem)
```

The method `filtsmooth` returns a `KalmanPosterior` object which provides convenience functions for e.g. sampling and interpolation.
We can also extract the just computed posterior smoothing state variables.
This yields a list of Gaussian random variables from which we can extract the statistics in order to visualize them.

```python
grid = state_posterior.locations
posterior_state_rvs = (
    state_posterior.states
)  # List of <num_time_points> Normal Random Variables
posterior_state_means = posterior_state_rvs.mean  # Shape: (num_time_points, state_dim)
posterior_state_covs = (
    posterior_state_rvs.cov
)  # Shape: (num_time_points, state_dim, state_dim)
```

### Visualize Results

```python
state_fig = plt.figure()
state_fig_gs = gridspec.GridSpec(ncols=2, nrows=2, figure=state_fig)

ax_00 = state_fig.add_subplot(state_fig_gs[0, 0])
ax_01 = state_fig.add_subplot(state_fig_gs[0, 1])
ax_10 = state_fig.add_subplot(state_fig_gs[1, 0])
ax_11 = state_fig.add_subplot(state_fig_gs[1, 1])

# Plot means
mu_x_1, mu_x_2, mu_x_3, mu_x_4 = [posterior_state_means[:, i] for i in range(state_dim)]

ax_00.plot(grid, mu_x_1, label="posterior mean")
ax_01.plot(grid, mu_x_2)
ax_10.plot(grid, mu_x_3)
ax_11.plot(grid, mu_x_4)

# Plot marginal standard deviations
std_x_1, std_x_2, std_x_3, std_x_4 = [
    np.sqrt(posterior_state_covs[:, i, i]) for i in range(state_dim)
]

ax_00.fill_between(
    grid,
    mu_x_1 - 1.96 * std_x_1,
    mu_x_1 + 1.96 * std_x_1,
    alpha=0.2,
    label="1.96 marginal stddev",
)
ax_01.fill_between(grid, mu_x_2 - 1.96 * std_x_2, mu_x_2 + 1.96 * std_x_2, alpha=0.2)
ax_10.fill_between(grid, mu_x_3 - 1.96 * std_x_3, mu_x_3 + 1.96 * std_x_3, alpha=0.2)
ax_11.fill_between(grid, mu_x_4 - 1.96 * std_x_4, mu_x_4 + 1.96 * std_x_4, alpha=0.2)

# Plot groundtruth
obs_x_1, obs_x_2 = [observations[:, i] for i in range(observation_dim)]

ax_00.scatter(time_grid, obs_x_1, marker=".", label="measurements")
ax_01.scatter(time_grid, obs_x_2, marker=".")

# Add labels etc.
ax_00.set_xlabel("t")
ax_01.set_xlabel("t")
ax_10.set_xlabel("t")
ax_11.set_xlabel("t")

ax_00.set_title(r"$x_1$")
ax_01.set_title(r"$x_2$")
ax_10.set_title(r"$x_3$")
ax_11.set_title(r"$x_4$")
handles, labels = ax_00.get_legend_handles_labels()
state_fig.legend(handles, labels, loc="center left", bbox_to_anchor=(1, 0.5))

state_fig.tight_layout()
```


# Anti-windup systems

In this notebook we are going to review a simple implementation of anti-windup system for a PID controller based on the classical implementation from (Åstrom,Hägglund, 2005). For better understand the problem of anti-windup, it is useful to implement a regular first order model.

In this case we consider the system:

$$G_1(s) = \frac{1}{\tau s + 1}$$

Notice that when the system is excited with a step response, the classical answer is the solution of the ODE. In the following lines we consider a discretized version of the system, to analyze it's response.

Notice that the system:

$$G_n(s) = \frac{1}{(\tau s + 1)^n}$$

Is an approximation of a delayed time system of the form

$$G_d(s) = \frac{e^{-sd}}{(\tau s + 1)}$$

For algorithmic purposes we consider $G_d(s) \approx G_2(n)$

```python
import numpy as np
import matplotlib.pyplot as plt
from sympy import *
# import control as cnt
%matplotlib inline
```

Let's the fine the system constants such as sampling time `TS` and time constant `TAU`

```python
TS = 0.1 # Sampling time
TAU = 10
```

```python
class System:

    def __init__(self, K=1):
        self.x = [0]
        self.K = 1
        self.A = 10 # Constant time
        self.T = TS # Sampling time
        self.t = [0]

    def update(self, control):
        """
            Update x_[k+1] as a function of x[k]
        """

        #  Dynamics first order approximation

        x_k = self.x[-1]-\
              self.x[-1]*self.T/self.A + \
              self.K *control*self.T/self.A

        self.x.append(x_k) # State
        self.time_update()

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,control):
        """
            Use it like function
        """
        self.update(control)
        return self.x[-1]

```

The system evolution can be easily computed in a for loop, in this case we consider $n =4 $

```python
u = np.concatenate([np.zeros(10),np.ones(2000)])

g1,g2,g3,g4 = System(), System(),System(),System()

for ctr in u:
    g4(g3(g2(g1(ctr))))
```

```python
# Plot of step responses
tu = np.arange(0,(len(u))*TS,TS)
fig, ax = plt.subplots(figsize=(10,5))
plt.plot(tu,u);
ax.plot(g1.t,g1.x,label='s1');
ax.plot(g2.t,g2.x,label='s2');
ax.plot(g3.t,g3.x,label='s3');
ax.plot(g4.t,g4.x,label='s4');
ax.grid(True)
ax.legend();
```

## Implementation of derivative and integrals

The following are two simple implementations of a derivative and an integral. The computations are intended for testing purposes.

```python
# %%writefile -a pid.py

class Derivator:
    def __init__(self):
        self.x = [0]
        self.dx = [0]
        self.T = TS
        self.t = [0]

    def diff(self,val):
        """
            Compute (x_k - x_{k-1})/T and updates the memory
        """
        dif = (val - self.x[-1]) / self.T
        self.x.append(val) # memory
        self.dx.append(dif) # memory
        self.time_update()
        return self.dx[-1]

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,val):
        """ Call like diff(error) """
        return self.diff(val)
```

```python
d4 = Derivator()

for sg in g4.x:
    d4(sg)

# Plot and decorations
fig, ax = plt.subplots(figsize=(10,5))
pl=ax.plot(g4.t,g4.x,label='s4');
pl=ax.plot(d4.t,d4.dx,label='d4');
ax.grid(True)
ax.set_title("Derivative behavior")
ax.legend([r"$x$",r"$dx/dt$"])
ax.set_xlabel("t [s]");
```

```python
# %%writefile -a pid.py

class Integrator:
    def __init__(self):
        self.x = [0]
        self.ix = [0]
        self.T = TS
        self.t = [0]

    def integ(self,val):
        """
            Compute (x_k - x_{k-1})/T and updates the memory
        """
        integral = np.sum(self.T * np.array(self.x))
        self.x.append(val) # memory
        self.ix.append(integral) # memory
        self.time_update()
        return self.ix[-1]

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,val):
        """ Call like integ(error) """
        return self.integ(val)

```

```python
i4 = Integrator()

for sg in d4.dx:
    i4(sg)

fig, ax = plt.subplots(figsize=(10,5))
ax.plot(d4.t,d4.dx,label='dx4');
ax.plot(i4.t,i4.ix,label='i4');
ax.grid(True)
ax.set_title("Integral behavior")
ax.legend([r"$x$",r"$\int{x}dt$"]);
ax.set_xlabel("t(s)");
```

## Tunning a PID controller

Here we propose the tunning of the controller via de Ziegler Nichols method. The objective is to find $K_u$ the maximum gain and $T_u$ the oscillation period of the closed loop.

The main point of interest is when $K=4$ since the eigen values become imaginary meaning marginal stability

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

# Root locus
# sys = cnt.tf([1],coeffs)
# cnt.rlocus(sys);

r_k = []

K =  range(0,10)

# This loop is to find the critical gain K_u

for k in K:
    pol = Poly(expand((10*x+1)**4+k),x)
    coeffs = [float(x) for x in pol.coeffs()]
    r_root = [np.real(v) for v in np.roots(coeffs)]
    i_root = [np.imag(v) for v in np.roots(coeffs)]
    r_k.append((r_root,i_root))

pol = Poly(expand((10*x+1)**4+4),x)
coeffs = [float(x) for x in pol.coeffs()]
roots = np.roots(coeffs)

# Plot and decorations
fig, ax = plt.subplots(figsize=(10,10))
for i,k in zip(r_k,K):
    x,y = i
    ax.scatter(x,y,label=f"K = {k}")
ax.set_title("Root movement when K increases")
ax.set_xlabel("Real")
ax.set_ylabel("Imaginary")
plt.grid(True)
plt.legend();

```

```python
# System definition
g1,g2,g3,g4 = System(), System(), System(),System()

e = []
r = np.concatenate([np.zeros(10),np.ones(2000)])

for ct in r:
    err = ct - g4.x[-1]
    e.append(err)
    uct = err*4
    g4(g3(g2(g1(uct))))

## Plot and values
tu = np.arange(0,(len(u))*TS,TS)
fig, ax = plt.subplots(figsize=(10,5))
ax.plot(tu,u,label='r');
ax.plot(g4.t,g4.x,label='y');
ax.grid(True,which="both")
ax.legend();

```

By observation we infer the following values, $K_u$, $T_u$, and we can compute the constants for the PID. Check this link for more info [link](https://en.wikipedia.org/wiki/Ziegler–Nichols_method)

```python
# # By observation
K_u = 4 # Critical gain of oscillation
T_u = 70 # By observation

k_p = 0.3*K_u
k_i = 1.2*K_u/T_u
k_d = 3*K_u*T_u/40

print(f"K_p:{k_p}, K_i:{k_i} K_d:{k_d}")
```

    K_p:1.2, K_i:0.06857142857142857 K_d:21.0

## Implementing a PID controller

In the following the PID controller will be implemented. The idea is to explain the effects of saturations in the controller and how an antiwindup system may help for that. This full link explains the same idea but in MATLAB [link](https://www.mathworks.com/help/simulink/slref/anti-windup-control-using-a-pid-controller.html)

```python
# %%writefile -a pid.py

class PID:

    def __init__(self, k_p=k_p,k_i=k_i,k_d=k_d):

        # Ziegler Nichols method
        # Check here https://en.wikipedia.org/wiki/Ziegler–Nichols_method
        if k_d == 0:
            self.k_p = 0.45*K_u
            self.k_i = 0.54*K_u/T_u
            self.k_d = k_d
        else:
            self.k_p = k_p
            self.k_i = k_i
            self.k_d = k_d

#         self.k_p = 0.3*K_u
#         self.k_i = 1.2*K_u/T_u
#         self.k_d = 3*K_u*T_u/40

        self.T = TS # Sampling time
        self.t = [0]

        self.u_p = [0]# Proportional term
        self.u_i = [0]# Integral term
        self.u_d = [0] # Derivative term

        self.control = [0]# Control memory

        self.integ = Integrator()
        self.diff = Derivator()

    def apply_control(self,error):

        P = self.k_p * error
        self.u_p.append(P)
        I = self.k_i * self.integ(error)
        self.u_i.append(I)
        D = self.k_d * self.diff(error)
        self.u_d.append(D)

        u_f = self.u_p[-1]+self.u_i[-1]+self.u_d[-1]
        self.time_update()
        self.control.append(u_f)
        return u_f

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,error):
        """ Callable """
        return self.apply_control(error)

```

### Testing the closed loop system

Let consider the case where a reference is constant and we implement a PI controller. The plots in the figure illustrate the performance of the closed loop system and the effects on the output of the controller and the proportional and integral terms

```python
# System definition
pid = PID(k_d=0)
s1 = System()
s2 = System()
s3 = System()
s4 = System()

e = []
r = np.concatenate([np.zeros(10),np.ones(3000)])

for ct in r:
    err = ct - s4.x[-1]
    e.append(err)
    uct = pid(err)
    s4(s3(s2(s1(uct))))

## Plot and values
tr = np.arange(0,(len(r))*TS,TS)
fig, ax = plt.subplots(2,1,figsize=(10,10))
ax[0].plot(tr,r, label = "r");
ax[0].plot(s4.t,s4.x,label='y');
ax[0].grid(True,which="both")
ax[0].legend();
ax[0].set_xlabel("t(s)");
ax[0].set_title("Closed loop response");

ax[1].plot(pid.t,pid.control, label = "u");
ax[1].plot(pid.t,pid.u_p, label = "P");
ax[1].plot(pid.t,pid.u_i, label = "I");
ax[1].grid(True,which="both");
ax[1].legend();
ax[1].set_xlabel("t(s)");
ax[1].set_title("Control output , (P) Proportional, (I) Integral");

```

### Adding boundaries to the control signal

In the following implementation the output of the controller will be bounded by introducing a saturation before applying to the control

```python
# %%writefile -a pid.py

U_MAX = 10

class PIDlim:

    def __init__(self, k_p=k_p,k_i=k_i,k_d=k_d,u_max=U_MAX):

        # Ziegler Nichols method
        # Check here https://en.wikipedia.org/wiki/Ziegler–Nichols_method
        if k_d == 0:
            self.k_p = 0.45*K_u
            self.k_i = 0.54*K_u/T_u
            self.k_d = k_d
        else:
            self.k_p = k_p
            self.k_i = k_i
            self.k_d = k_d

#         self.k_p = 0.3*K_u
#         self.k_i = 1.2*K_u/T_u
#         self.k_d = 3*K_u*T_u/40

        self.T = TS # Sampling time
        self.t = [0]

        self.u_p = [0]# Proportional term
        self.u_i = [0]# Integral term
        self.u_d = [0] # Derivative term

        self.u_max = u_max
        self.u_min = -u_max

        self.control = [0]# Control memory
        self.control_bnd = [0]

        self.integ = Integrator()
        self.diff = Derivator()

    def apply_control(self,error):

        P = self.k_p * error
        self.u_p.append(P)
        I = self.k_i * self.integ(error)
        self.u_i.append(I)
        D = self.k_d * self.diff(error)
        self.u_d.append(D)

        u_f = self.u_p[-1]+self.u_i[-1]+self.u_d[-1]
        self.control.append(u_f)

        # Bound control
        u_f = max(self.u_min,min(u_f,self.u_max))
        self.control_bnd.append(u_f)

        self.time_update()

        return u_f

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,error):
        """ Callable """
        return self.apply_control(error)

```

```python
pid = PIDlim(k_d=0,u_max=10)
pid1 = PID(k_d=0)

s1,s2,s3,s4 = System(), System(),System(), System()
s5,s6,s7,s8 = System(), System(),System(), System()

e = []
A = 1
r = np.concatenate([A*np.ones(3000),(A+10)*np.ones(3000),A*np.ones(3000)])
tr = np.arange(0,(len(r))*TS,TS)

for ct in r:
    err = ct - s8.x[-1]
    e.append(err)
    uct = pid(err)
    s8(s7(s6(s5(uct)))) # Series system

for ct in r:
    err = ct - s4.x[-1]
    e.append(err)
    uct = pid1(err)
    s4(s3(s2(s1(uct)))) # Series system

## Plot and values

fig, ax = plt.subplots(3,1,figsize=(10,10))
ax[0].plot(tr,r, label = "r");
ax[0].plot(s4.t,s4.x,label='y1');
ax[0].plot(s8.t,s8.x,label='y2');
ax[0].grid(True,which="both")
ax[0].legend([r"$r$",r"$y_{ideal}$",r"$y_{sat}$"]);
ax[0].set_xlabel("t(s)");

ax[1].plot(pid1.t[2:],pid1.control[2:], label = "u");
ax[1].plot(pid.t[1:],pid.control_bnd[1:], label = "ubd");
# # ax[1].plot(pid.t,pid.u_d, label = "D");
ax[1].grid(True,which="both")
ax[1].legend([r"$u_{ideal}$",r"$u_{sat}$"]);
ax[1].set_xlabel("t(s)");

ax[2].plot(pid1.t[2:],pid1.u_i[2:], label = "u");
ax[2].plot(pid.t[2:],pid.u_i[2:], label = "ubd");
ax[2].grid(True,which="both")
ax[2].legend([r"$u_{ideal}$",r"$u_{sat}$"]);
ax[2].set_xlabel("t(s)");
```

In particular when the change in the reference is very high. The integral term tends to cumulate the error and causing a loss in performance of the output one alternative is to implement a reset of the integrator. The objective will be to reset with a slow time constant via the mechanisms explained in the Amstrom book.

```python
# %%writefile -a pid.py

class PIDantiwindup:

    def __init__(self, k_p=k_p,k_i=k_i,k_d=k_d,u_max=U_MAX):

        # Ziegler Nichols method
        # Check here https://en.wikipedia.org/wiki/Ziegler–Nichols_method
        if k_d == 0:
            self.k_p = 0.45*K_u
            self.k_i = 0.54*K_u/T_u
            self.k_d = k_d
        else:
            self.k_p = k_p
            self.k_i = k_i
            self.k_d = k_d

#         self.k_p = 0.3*K_u
#         self.k_i = 1.2*K_u/T_u
#         self.k_d = 3*K_u*T_u/40

        self.T = TS # Sampling time
        self.t = [0]

        self.u_p = [0]# Proportional term
        self.u_i = [0]# Integral term
        self.u_d = [0] # Derivative term

        self.u_max = u_max
        self.u_min = -u_max

        self.control = [0]# Control memory
        self.control_bnd = [0]

        self.T_t = 1 # Time constant for integration reset

        self.integ = Integrator()
        self.diff = Derivator()

    def apply_control(self,error):

        P = self.k_p * error
        self.u_p.append(P)

        wind_reset = (self.control_bnd[-1] - self.control[-1])/self.T_t

        I = self.integ(self.k_i * error + wind_reset) # Anti windup mechanism

        self.u_i.append(I)
        D = self.k_d * self.diff(error)
        self.u_d.append(D)

        u_f = self.u_p[-1]+self.u_i[-1]+self.u_d[-1]
        self.control.append(u_f)

        # Bound control
        u_f = max(self.u_min,min(u_f,self.u_max))
        self.control_bnd.append(u_f)

        self.time_update()

        return u_f

    def time_update(self):
        """ time vector"""
        self.t.append(self.t[-1]+self.T)

    def __call__(self,error):
        """ Callable """
        return self.apply_control(error)
```

```python
pid_sat = PIDlim(k_d=0,u_max=10)
pid_windup = PIDantiwindup(k_d=0,u_max=10)

s1,s2,s3,s4 = System(), System(),System(), System()
s5,s6,s7,s8 = System(), System(),System(), System()

e = []
A = 1
r = np.concatenate([A*np.ones(3000),(A+10)*np.ones(3000),A*np.ones(3000)])
tr = np.arange(0,(len(r))*TS,TS)

for ct in r:
    err = ct - s8.x[-1]
    e.append(err)
    uct = pid_sat(err)
    s8(s7(s6(s5(uct)))) # Series system

for ct in r:
    err = ct - s4.x[-1]
    e.append(err)
    uct = pid_windup(err)
    s4(s3(s2(s1(uct)))) # Series system

## Plot and values

fig, ax = plt.subplots(2,1,figsize=(10,10))
ax[0].plot(tr,r, label = "r");
ax[0].plot(s4.t,s4.x,label='y1');
ax[0].plot(s8.t,s8.x,label='y2');
ax[0].grid(True,which="both")
ax[0].legend([r"$r$",r"$y_{no-windup}$",r"$y_{windup}$"]);
ax[0].set_xlabel("t(s)");

ax[1].plot(pid_sat.t[2:],pid_sat.control_bnd[2:], label = "usat");
ax[1].plot(pid_windup.t[1:],pid_windup.control_bnd[1:], label = "uwd");
# # ax[1].plot(pid.t,pid.u_d, label = "D");
ax[1].grid(True,which="both")
ax[1].legend([r"$u_{no-windup}$",r"$u_{windup}$"]);
ax[1].set_xlabel("t(s)");
```

As observed the dynamical response of the system has less overshoot.


## Histograms of Oriented Gradients (HOG)

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

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

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

    The total number of elements in the HOG Feature Vector should be:  40 x 40 x 2 x 2 x 9 = 57600

    The HOG Descriptor has shape: (57600, 1)

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

    The feature vector has shape: (57600, 1)
    The reshaped feature vector has shape: (40, 40, 2, 2, 9)
    The average gradient array has shape:  (41, 41, 9)

    <IPython.core.display.Javascript object>

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!

```python

```


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

```python
sy.init_printing()

```

# 오일러법: 고차 상미분 방정식
Euler Method: Higher Order ODE

오일러법으로 1계 선형 미분 방정식의 근사해를 구하는 방법은 이미 살펴 보았다.

Previously, we numerically solved a first order linear ordinary equation using Euler method.

이번에는 미분 차수가 1보다 높은 경우들을 살펴보자.

This time, let's take a look at cases with order higher than one.

## 2계 (비선형) 상미분 방정식
Second order (Nonlinear) Ordinary Differential Equation

예를 들어 단진자가 어떻게 움직이는지 다음과 같이 묘사할 수 있다.

For example, we can describe how a simple pendulum moves as follows.

Ref : Wikipedia contributors, 'Pendulum (mathematics)', Wikipedia, The Free Encyclopedia, 2 June 2018, 13:28 UTC, <https://en.wikipedia.org/w/index.php?title=Pendulum_(mathematics)&oldid=844080803> [accessed 5 August 2018]

$$
\frac{d^2\theta}{dt^2} + \frac{g}{l}sin\theta = 0
$$

위 상미분 방정식은 $\theta$의 2계 미분과 $sin\theta$ 를 포함하고 있다.
The ordinary differential equation above includes a second order derivative of $\theta$ and $sin\theta$.

여기서 $sin\theta$ 는 비선형으로, 위 해당 식은 2계 비선형 상미분 방정식이다.
Here $sin\theta$ is nonlinear; thus the equation above is a second order nonlinear ordinary differential equation.

해당 방정식을 풀기 위해 다음과 같이 $x_0$, $x_1$ 와 같은 상태변수를 도입하자.
To solve the equation, let's introduce state variables $x_0$ and $x_1$ as follows.

$$
\begin{cases}
\begin{align}
    x_0 &= \theta\\
    x_1 &= \frac{d\theta}{dt}  = \frac{d}{dt}x_0\\
\end{align}
\end{cases}
$$

방정식의 각 항을 상태변수로 다시 써 보자.
Let's rewrite each term of the equation using the state variables.

$$
\begin{cases}
\begin{align}
    sin \theta &= sin x_0\\
    \frac{d^2\theta}{dt^2} &= \frac{d}{dt} \frac{d\theta}{dt}= \frac{d}{dt} x_1\\
\end{align}
\end{cases}
$$

다시 방정식에 대입해 보자.
Let's substitute back to the equation.

$$
\frac{dx_1}{dt} + \frac{g}{l}sinx_0 = 0 \\
\frac{dx_1}{dt} =- \frac{g}{l}sinx_0
$$

$x_0$와 $x_1$의 미분을 살펴 보자.
Let's take a look at the derivatives of $x_0$ and $x_1$.

$$
\frac{d}{dt}
\begin{pmatrix}
    x_0\\
    x_1
\end{pmatrix}
=
\begin{pmatrix}
    \frac{d}{dt}x_0\\
    \frac{d}{dt}x_1
\end{pmatrix}
=
\frac{d}{dt}
\begin{pmatrix}
    \theta\\
    \frac{d}{dt}\theta
\end{pmatrix}
=
\begin{pmatrix}
    x_1\\
    -\frac{g}{l}sinx_0
\end{pmatrix}
$$

이를 python 함수로 구현해 보자.
Let's implement this in a python function.

```python
g_mpsps = 9.8
l_m = 0.3

legends = ('$\\theta(deg)$', '$\\frac{d}{dt}\\theta(deg/s)$')
ylabel = ''

# Initial state
x_0 = np.array([np.deg2rad(90), 0])

def pendulum_NL(t, x):
    """
    Parameters
    ==========
    x: array of theta and d(theta)/dt
    t: time value

    Return Value
    ============
    One dimensional array of dx/dt
    """

    return np.array([x[1], (-g_mpsps/l_m)*np.sin(x[0])])

```

오일러법을 적용해 보자.
Let's apply the Euler Method.

```python
import ode_solver

```

```python
# Time step interval
delta_t = 0.1

# Time array
t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# *** ODE solver ***
t_01, x_01 = ode_solver.euler(pendulum_NL, t_sec_array, x_0)

```

```python
x_result_array = np.array(x_01).T
for label, x_result in zip(legends, x_result_array):
    py.plot(t_01, np.rad2deg(x_result), label=label)
# https://stackoverflow.com/questions/11481644/how-do-i-assign-multiple-labels-at-once-in-matplotlib

py.xlabel('t(sec)')
py.ylabel(ylabel)

py.legend(loc=0)
py.grid(True)

```

위 근사해는 불안정해 보인다. $\Delta t$를 줄여 보자.
The approximate solution looks unstable. Let's make $\Delta t$ smaller.

```python
# Time step interval
delta_t = 0.001

# Time array
t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# *** ODE solver ***
import ode_solver
t_01, x_01 = ode_solver.euler(pendulum_NL, t_sec_array, x_0)

```

```python
x_result_array = np.array(x_01).T
for label, x_result in zip(legends, x_result_array):
    py.plot(t_01, np.rad2deg(x_result), label=label)
# https://stackoverflow.com/questions/11481644/how-do-i-assign-multiple-labels-at-once-in-matplotlib

py.xlabel('t(sec)')
py.ylabel(ylabel)

py.legend(loc=0)
py.grid(True)

```

30cm 길이의 단진자로 시험해 보자.
Let's test using a 30cm simple pendulum.

## Scipy

```python
import scipy.integrate as si

```

```python
sol = si.solve_ivp(pendulum_NL, (t_sec_array[0], t_sec_array[-1]), x_0, t_eval=t_sec_array)

```

```python
for label, q_result, q_solve_ivp in zip(legends, x_result_array, sol.y.tolist()):
    py.plot(sol.t, q_solve_ivp, 'o', label='solve_ivp')
    py.plot(t_sec_array, q_result, label=label)
# https://stackoverflow.com/questions/11481644/how-do-i-assign-multiple-labels-at-once-in-matplotlib

py.xlabel('t(sec)')
py.ylabel(ylabel)

py.legend(loc=0)
py.grid(True)

```

## 높은 차수의 선형 상미분 방정식
Linear Ordinary Differential Equation of Higher Order

위 예에서는 어떻게 2차 (비선형) 상미분 방정식에 오일러법을 적용하는지 살펴보았다.

In the previous example, we observed how to apply Euler method to a 2nd order (nonlinear) ordinary differential equation.

일반적으로, 우변이 0인 (모든 가능한) $n$차 선형 상미분 방정식은 다음과 같이 쓸 수 있다.

In general, we can write (all possible) $n$th order linear ordinary differential equation with right side zero as follows.

$$
    a_0 \frac{d^nx}{dt^n} + a_1 \frac{d^{n-1}x}{dt^{n-1}} + a_2 \frac{d^{n-2}x}{dt^{n-2}} + \ldots + a_i  \frac{d^{n-i}x}{dt^{n-i}} + \ldots + a_{n-2} \frac{d^2x}{dt^2} + a_{n-1} \frac{dx}{dt} + a_n x = 0
$$

가장 높은 차수의 항만 남기고 모두 우변으로 옮겨 보자.

Let's move everyone to the right side except the highest order term.

$$
\begin{align}
    a_0 \frac{d^nx}{dt^n} &=
    - a_1 \frac{d^{n-1}x}{dt^{n-1}}
    - a_2 \frac{d^{n-2}x}{dt^{n-2}}
    - \ldots
    - a_i  \frac{d^{n-i}x}{dt^{n-i}}
    - \ldots
    - a_{n-2} \frac{d^2x}{dt^2}
    - a_{n-1} \frac{dx}{dt}
    - a_n x \\
    \frac{d^nx}{dt^n} &=
    - \frac{a_1}{a_0} \frac{d^{n-1}x}{dt^{n-1}}
    - \frac{a_2}{a_0} \frac{d^{n-2}x}{dt^{n-2}}
    - \ldots
    - \frac{a_i}{a_0}  \frac{d^{n-i}x}{dt^{n-i}}
    - \ldots
    - \frac{a_{n-2}}{a_0} \frac{d^2x}{dt^2}
    - \frac{a_{n-1}}{a_0} \frac{dx}{dt}
    - \frac{a_n}{a_0} x
\end{align}
$$

앞과 마찬가지로, 최고 차수인 $n$ 개의 상태변수를 도입해 보자.
As before, let's introduce $n$, the highest order, state variables.

$$
\mathbf{q}=
\begin{pmatrix}
    q_0 \\ q_1 \\ q_2 \\ \vdots \\ q_i \\ \vdots \\ q_{n-2} \\ q_{n-1}
\end{pmatrix}
=
\begin{pmatrix}
    x \\ \frac{d}{dt}x \\ \frac{d^2}{dt^2}x \\ \vdots \\ \frac{d^i}{dt^i}x \\ \vdots \\ \frac{d^{n-2}}{dt^{n-2}}x \\ \frac{d^{n-1}}{dt^{n-1}}x
\end{pmatrix}
$$

가장 높은 차수의 미분을 상태변수로 표시해 보자.
Let's indicate the highest order differentiation in the state variable form.

$$
    \frac{d^nx}{dt^n} =
    - \frac{a_1}{a_0} q_{n-1}
    - \frac{a_2}{a_0} q_{n-2}
    - \ldots
    - \frac{a_i}{a_0}  q_{n-i}
    - \ldots
    - \frac{a_{n-2}}{a_0} q_{2}
    - \frac{a_{n-1}}{a_0} q_{1}
    - \frac{a_n}{a_0} q_{0}
$$

시간에 대해 $\mathbf{q}$ 를 미분해 보자.
Let's differentiate $\mathbf{q}$ with respect to time.

$$
\frac{d}{dt}\mathbf{q}=
\begin{pmatrix}
    \frac{d}{dt}q_0 \\ \frac{d}{dt}q_1 \\ \frac{d}{dt}q_2 \\ \vdots \\ \frac{d}{dt}q_i \\ \vdots \\ \frac{d}{dt}q_{n-2} \\ \frac{d}{dt}q_{n-1}
\end{pmatrix}
=
\begin{pmatrix}
    \frac{d}{dt}x \\ \frac{d^2}{dt^2}x  \\ \frac{d^3}{dt^3}x \\ \vdots \\ \frac{d^i}{dt^i}x \\ \vdots \\ \frac{d^{n-1}}{dt^{n-1}}x \\ \frac{d^{n}}{dt^{n}}x
\end{pmatrix}
=
\begin{pmatrix}
q_1 \\ q_2 \\ q_3 \\ \vdots \\ q_{i+1} \\ \vdots \\ q_{n-1} \\
- \frac{a_1}{a_0} q_{n-1}
    - \frac{a_2}{a_0} q_{n-2}
    - \ldots
    - \frac{a_i}{a_0}  q_{n-i}
    - \ldots
    - \frac{a_{n-2}}{a_0} q_{2}
    - \frac{a_{n-1}}{a_0} q_{1}
    - \frac{a_n}{a_0} q_{0}
\end{pmatrix}
$$

행렬 형태로 다시 쓸 수도 있다.
We can rewrite in a matrix form.

$n=4$인 경우를 생각해 보자.
Let's think about a case of $n=4$.

$$
\frac{d\mathbf{q}}{dt}
=
\begin{pmatrix}
\frac{d}{dt} q_0 \\ \frac{d}{dt} q_1 \\ \frac{d}{dt} q_2 \\ \frac{d}{dt} q_3
\end{pmatrix}
=
\begin{pmatrix}
q_1 \\ q_2 \\ q_3 \\
- \frac{a_4}{a_0}q_0 - \frac{a_3}{a_0}q_1 - \frac{a_2}{a_0}q_2 - \frac{a_1}{a_0}q_3
\end{pmatrix}
=
\begin{bmatrix}
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1 \\
- \frac{a_4}{a_0} & - \frac{a_3}{a_0} & - \frac{a_2}{a_0} & - \frac{a_1}{a_0}
\end{bmatrix}
\begin{pmatrix}
q_0 \\ q_1 \\ q_2 \\ q_3
\end{pmatrix}
=
\mathbf{Aq}
$$

위 식의 의미는, $n$차 선형 상미분 방정식의 경우, $n$개의 1차 선형 상미분 방정식으로 바꾸어 풀 수 있다는 것이다.

The equation above means that we can solve a $n$th order linear ordinary differential equation by converting it to a system of $n$ first order linear differential equations.

다음 예를 살펴 보자.
Let's think about following example.

$$
         \frac{d^4x}{dt^4}
         + 12 \frac{d^3x}{dt^3}
         + 54 \frac{d^2x}{dt^2}
         + 108 \frac{dx}{dt}
         + 80 x = 0
$$

연립 1차 선형 상미분 방정식으로는 다음과 같이 다시 쓸 수 있다.
We can rewrite in a system of linear ODE's as follows.

$$
\frac{d\mathbf{q}}{dt}
=
\begin{bmatrix}
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1 \\
- 80 & - 108 & - 54 & -12
\end{bmatrix}
\begin{pmatrix}
q_0 \\ q_1 \\ q_2 \\ q_3
\end{pmatrix}
=
\mathbf{Aq}
$$

여기서 상태변수는 다음과 같다.
Here, state variables are as follows.

$$
\mathbf{q} = \begin{pmatrix}q_0 & q_1 & q_2 & q_3 \end{pmatrix}^T = \begin{pmatrix}x & \frac{dx}{dt} & \frac{d^2x}{dt^2} & \frac{d^3x}{dt^3} \end{pmatrix}^T
$$

python 함수 형태로도 써 보자.
Let's write a python function, too.

```python
matrix_A = np.array([
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1],
        [-80, -108, -54, -12],
])

legends = [f'$q_{k}$' for k in range(matrix_A.shape[0])]

ylabel = '$\mathbf{q}$'

def fourth_order(t, q):
    """
    Parameters
    ==========
    q: array of q_0, q_1, q_2, and q_3
    t: time value

    Return Value
    ============
    One dimensional array of dq/dt
    """

    q_column = np.array(q).T
    qdot_column = matrix_A @ q_column

    qdot_array = np.array(qdot_column.T).flatten()

    return qdot_array

```

오일러법을 적용해 보자.
Let's apply the Euler Method.

```python
# Time step interval and Time array
delta_t = 0.01
t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# Initial value of q
q_0 = np.array([1.0, 0, 0, 0])

# *** ODE solver ***
t, q_list = ode_solver.euler(fourth_order, t_sec_array, q_0)

```

반환값을 한번 살펴 보자.

Let's take a look at some of the results.

```python
len(t), len(q_list)

```

```python
assert len(t) == len(q_list), "Error : len(t) != len(q_list)"

```

`t` 와 `q_list`의 길이가 같다.
The lengths of `t` and `q_list` are the same.

```python
type(t[0]), type(q_list[0])

```

`q_list`에 들어 있는 것은 배열인 것으로 보인다.

`q_list` seems to contain arrays.

```python
q_0, q_list[0]

```

```python
import numpy.linalg as nl

assert 1e-7 > nl.norm(q_0 - q_list[0]), "Error : q_0 != q_list[0]"

```

`q`의 초기값과 결과 리스트의 첫번째 값이 같다.

Initial value of `q` is the same as the first item of the result list.

$q_0$, $q_1$, $q_2$, $q_3$ 별로 그리기 위해 배열의 리스트를 2차원 배열로 바꾼다.

To plot each $q_0$, $q_1$, $q_2$, and $q_3$, convert the list of arrays to a two dimensional array.

```python
q_result_array = np.array(q_list).T

```

2차원 배열의 크기를 확인한다.

Check the shape of the two dimensional array.

```python
q_result_array.shape

```

예를 들어 아래와 같이 각 $q_i$를 그려볼 수 있다.

For example, we can plot each $q_i$ as follows.

```python
for label, q_result in zip(legends, q_result_array):
    py.plot(t, q_result, label=label)
# https://stackoverflow.com/questions/11481644/how-do-i-assign-multiple-labels-at-once-in-matplotlib

py.xlabel('t(sec)')
py.ylabel(ylabel)

py.legend(loc=0)
py.grid(True)

```

## Scipy

```python
import scipy.integrate as si

```

```python
sol = si.solve_ivp(fourth_order, (t_sec_array[0], t_sec_array[-1]), q_0, t_eval=t_sec_array)

```

```python
for label, q_result, q_solve_ivp in zip(legends, q_result_array, sol.y.tolist()):
    py.plot(sol.t, q_solve_ivp, 'o', label='solve_ivp')
    py.plot(t, q_result, label=label)
# https://stackoverflow.com/questions/11481644/how-do-i-assign-multiple-labels-at-once-in-matplotlib

py.xlabel('t(sec)')
py.ylabel(ylabel)

py.legend(loc=4)
py.grid(True)

```

## 연습 문제
Exercise

도전 과제 1: 다음 2계 선형 상미분 방정식의 수치해를 오일러법으로 구하시오:

Try This 1: Find the numerical solutions of the following second order linear ordinary equation using Euler Method:

$$
\begin{align}
\frac{d^2}{dt^2}x(t) + 2\frac{d}{dt}x(t) + 2x(t) &= 0 \\
x(0) &= 0 \\
\frac{d}{dt}x(0) &= 1
\end{align}
$$

```python

```

도전 과제 2: 다음 2계 상미분 방정식의 수치해를 오일러법으로 구하시오:

Try This 2: Find the numerical solutions of the following second order ordinary equation using Euler Method:

$$
\begin{align}
\frac{d^2}{dt^2}x(t) + \cos(t)\cdot x(t) &= 0 \\
x(0) &= 1 \\
\frac{d}{dt}x(0) &= 0
\end{align}
$$

ref : Zill & Cullen

```python

```

도전 과제 3: 다음 2계 상미분 방정식의 수치해를 오일러법으로 구하시오:

Try This 3: Find the numerical solutions of the following second order ordinary equation using Euler Method: $0 < x\le 10$

$$
\begin{align}
x^2\frac{d^2}{dx^2}y(t)+ x\frac{d}{dx}y(t) + \left(x^2-2^2\right) y(x) &= 0 \\
y(0) &= 1 \\
\frac{d}{dx}y(0) &= 0
\end{align}
$$

ref : Zill & Cullen

```python

```

도전 과제 4: 다음 2계 상미분 방정식의 수치해를 오일러법으로 구하시오:

Try This 4: Find the numerical solutions of the following second order ordinary equation using Euler Method: $-1 \le x\le 1$, $n$ = 1, 2, 3, 4

$$
\begin{align}
\left(1-x^2\right)\frac{d^2}{dx^2}y(t)-2 x\frac{d}{dx}y(t) + n\left(n+1\right) y(x) &= 0 \\
y(-1) &= (-1)^{n+1} \\
\frac{d}{dx}y(-1) &= 0
\end{align}
$$

ref : Zill & Cullen

```python

```

## 부록: 단진자의 기울기 장
Appendix: Slope Field of the Simple Pendulum

$$
\frac{d}{dt}
\begin{pmatrix}
    x_0\\
    x_1
\end{pmatrix}
=
\begin{pmatrix}
    \frac{d}{dt}x_0\\
    \frac{d}{dt}x_1
\end{pmatrix}
=
\frac{d}{dt}
\begin{pmatrix}
    \theta\\
    \frac{d}{dt}\theta
\end{pmatrix}
=
\begin{pmatrix}
    x_1\\
    -\frac{g}{l}sinx_0
\end{pmatrix}
$$

단진자 사례의 상태변수는 $\left(\theta, \frac{d}{dt}\theta\right)$이다.
State variables of the simple pendulum example are $\left(\theta, \frac{d}{dt}\theta\right)$.

가로축을 $\theta$, 세로축을 $\frac{d}{dt}\theta$로 하는 2차원 좌표 평면을 생각해 보자.
Let's think about a 2D plane with $\theta$ as the horizontal axis and $\frac{d}{dt}\theta$ as the vertical axis.

각 상태에서 상태변수의 변화의 방향 $\left(\frac{d}{dt}\theta, \frac{d^2}{dt^2}\theta \right)$ 을 표시해 보자.
At each state, let's present the direction of state variable change $\left(\frac{d}{dt}\theta, \frac{d^2}{dt^2}\theta \right)$.

```python
import ode_plot

```

```python
time_list = []

# list of theta
theta_deg_array = np.arange(-540, 540+1, 30)
theta_rad_list = np.deg2rad(theta_deg_array)

# list of theta_dot
theta_dot_deg_array = np.arange(-540, 540+1, 45)
theta_dot_rad_list = np.deg2rad(theta_dot_deg_array)

# plot direction field
ode_plot.ode_slopes_2states_cartesian(pendulum_NL, theta_rad_list, theta_dot_rad_list, time_list)

# Plot the solution curve by Euler method
py.plot(py.rad2deg(x_result_array[0]), py.rad2deg(x_result_array[1]), 'r-')

ode_plot.set_axis(
    py.gca(),
    theta_deg_array[0], theta_deg_array[-1],
    theta_dot_deg_array[0], theta_dot_deg_array[-1],
)

ode_plot.title_axis_labels()

```

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```


```python
%matplotlib inline
```

# Nonlinear Schrödinger equation with loss

This example demonstrates how to perform simulations for the nonlinear
Schrödinger equation including loss.

.. codeauthor:: Oliver Melchert <melchert@iqo.uni-hannover.de>

We first import the functionality needed to perform the sequence of numerical
experiments:

```python
import sys
import numpy as np
from fmas.models import ModelBaseClass
from fmas.config import FTFREQ, FT, IFT, C0
from fmas.grid import Grid
from fmas.solver import SySSM
```

Next, we implement a model for the nonlinear Schrödinger equation.  In
particular, we here consider the standard nonlinear Schrödinger equation,
given by

\begin{align}\partial_z u = -\alpha u -i \frac{\beta_2}{2}\partial_t^2 u + i\gamma |u|^2 u,\end{align}

wherein $u = u(z, t)$ represents the slowly varying pulse envelope,
where $\alpha$ is the root-power attenuation constant accounting for fiber
loss, $\beta_2=-1$ is the second order dispersion parameter, and
$\gamma=1$ is the nonlinear parameter:

```python
class NSE(ModelBaseClass):
    def __init__(self, w, beta, alpha, gamma):
        super().__init__(w, beta_w=beta, alpha_w=alpha)
        self.gamma = gamma

    @property
    def Lw(self):
        return 1j * self.beta_w - self.alpha_w

    def N(self, uw):
        ut = IFT(uw)
        return 1j * self.gamma * FT(np.abs(ut) ** 2 * ut)
```

Next, we set up the computational domain, the model, an instance of a
symmetric split-step Fourier solver and prepare an initial condition given by
a fundamental soliton.

To construct the initial condition, we use the exact single-soliton solution
of the nonlinar Schrödinger equation, given by

\begin{align}u_{\rm{exact}}(z,t) = \sqrt{P_0} {\rm{sech}}(t/t_0)\,e^{-i\gamma P_0 z/2},\end{align}

with $P_0=|\beta_2|/(\gamma t_0^2)$. We here consider a fundamental
soliton of duration $t_0=1$ and use $u_{\rm{exact}}(0,t)$ as
initial condition.  The propagation is performed up to
$z_{\rm{max}}=\pi/2$, i.e.  for one soliton period.

```python
# -- SET MODEL PARAMETERS
t_max = -50.0
Nt = 2 ** 12
# ... PROPAGATION CONSTANT (POLYNOMIAL MODEL)
b2 = -1.0
beta = lambda w: 0.5 * b2 * w * w
# ... NONLINEAR PARAMETER
gamma = 1.0
# ... POWER ATTENUATION PARAMETER
alpha = 1.0
# ... SOLITON PARAMTERS
t0 = 1.0  # duration
P0 = np.abs(b2) / t0 / t0 / gamma  # peak-intensity
LD = t0 * t0 / np.abs(b2)  # dispersion length
# ... EXACT SOLUTION
u_exact = lambda z, t: np.sqrt(P0) * np.exp(0.5j * gamma * P0 * z) / np.cosh(t / t0)
```

We here measure the effect of fiber loss by monitoring the energy

\begin{align}E(z) = \sum_w |u_\omega(z)|^2\end{align}

which is expected to decay as

\begin{align}E(z) = E(0)\,e^{-2 \alpha z}\end{align}

```python
def Ce(i, zi, w, uw):
    return np.sum(np.abs(uw) ** 2)

# -- INITIALIZATION STAGE
# ... COMPUTATIONAL DOMAIN
grid = Grid(t_max=t_max, t_num=Nt)
t, w = grid.t, grid.w

model = NSE(w, beta(w), alpha, gamma)
# ... PROPAGATION ALGORITHM
solver = SySSM(model.Lw, model.N, user_action=Ce)
# ... INITIAL CONDITION
solver.set_initial_condition(w, FT(u_exact(0.0, t)))

# -- RUN SOLVER
solver.propagate(z_range=0.5 * np.pi * LD, n_steps=512, n_skip=2)  # propagation range
```

In the figure below, the top subfigure shows the time-domain propagation
dynamics of a fundamental soliton for the nonlinear Schrödinger equation in
the presence of fiber loss.
The subfigure at the bottom show the resulting decay of the energy in the
numerical experiment (solid line), along with the theoretical prediction
(dashed line).

```python
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.colors as col

f, (ax1, ax2) = plt.subplots(2, 1, figsize=(5, 3))
plt.subplots_adjust(left=0.15, right=0.8, bottom=0.15, top=0.96, hspace=0.2)
cmap = mpl.cm.get_cmap("jet")

def _setColorbar(im, refPos):
    """colorbar helper"""
    x0, y0, w, h = refPos.x0, refPos.y0, refPos.width, refPos.height
    cax = f.add_axes([x0 + 1.02 * w, y0, 0.025 * w, h])
    cbar = f.colorbar(im, cax=cax, orientation="vertical")
    cbar.ax.tick_params(
        color="k",
        labelcolor="k",
        bottom=False,
        direction="out",
        labelbottom=False,
        labeltop=True,
        top=True,
        size=4,
        pad=0,
    )
    cbar.ax.tick_params(which="minor", bottom=False, top=False)
    return cbar

# -- TOP SUB-FIGURE: TIME-DOMAIN PROPAGATION CHARACTERISTICS
It = np.abs(solver.utz) ** 2
It /= np.max(It)

im1 = ax1.pcolorfast(
    solver.z,
    grid.t,
    np.swapaxes(It[:-1, :-1], 0, 1),
    norm=col.Normalize(vmin=0, vmax=1),
    cmap=cmap,
)
cbar1 = _setColorbar(im1, ax1.get_position())
cbar1.ax.set_ylabel(r"$|u|^2/{\rm{max}}\left(|u|^2\right)$")
ax1.xaxis.set_ticks_position("bottom")
ax1.yaxis.set_ticks_position("left")
ax1.set_ylim(-5, 5)
ax1.set_xlim([0.0, solver.z.max()])
ax1.set_ylabel(r"$t~\mathrm{(fs)}$")
ax1.ticklabel_format(useOffset=False, style="plain")
ax1.tick_params(axis="x", labelbottom=False, length=4)

# -- BOTTOM SUB-FIGURE: ENERGY DECAY
Ez = lambda z: solver.ua_vals[0] * np.exp(-2*alpha * z)
ax2.plot(solver.z, solver.ua_vals / Ez(0), lw=1.5, label="simulation")
ax2.plot(
    solver.z, Ez(solver.z) / Ez(0), color="k", lw=2.5, dashes=[2, 2], label="theory"
)
ax2.xaxis.set_ticks_position("bottom")
ax2.yaxis.set_ticks_position("left")
ax2.set_xlim([0.0, solver.z.max()])
ax2.ticklabel_format(useOffset=False, style="sci")
ax2.set_xlabel(r"Propagation distance $z~\mathrm{(\mu m)}$")
ax2.set_ylabel(r"$E(z)/E(0)$")
ax2.legend()

plt.show()
```


```python
import numpy as np
from fun_doe import get_model_matrix
import pandas as pd
import plotly.express as px
```

```python
X = pd.DataFrame(np.linspace(-1, 1, 50), columns=['x1'])
XX = get_model_matrix(data=X, order=3, bias=True)
```

```python
XX
```

      1
      x1
      x1^2
      x1^3

      0
      1.0
      -1.000000
      1.000000
      -1.000000

      1
      1.0
      -0.959184
      0.920033
      -0.882481

      2
      1.0
      -0.918367
      0.843399
      -0.774550

      3
      1.0
      -0.877551
      0.770096
      -0.675798

      4
      1.0
      -0.836735
      0.700125
      -0.585819

      5
      1.0
      -0.795918
      0.633486
      -0.504203

      6
      1.0
      -0.755102
      0.570179
      -0.430543

      7
      1.0
      -0.714286
      0.510204
      -0.364431

      8
      1.0
      -0.673469
      0.453561
      -0.305459

      9
      1.0
      -0.632653
      0.400250
      -0.253219

      10
      1.0
      -0.591837
      0.350271
      -0.207303

      11
      1.0
      -0.551020
      0.303623
      -0.167303

      12
      1.0
      -0.510204
      0.260308
      -0.132810

      13
      1.0
      -0.469388
      0.220325
      -0.103418

      14
      1.0
      -0.428571
      0.183673
      -0.078717

      15
      1.0
      -0.387755
      0.150354
      -0.058301

      16
      1.0
      -0.346939
      0.120367
      -0.041760

      17
      1.0
      -0.306122
      0.093711
      -0.028687

      18
      1.0
      -0.265306
      0.070387
      -0.018674

      19
      1.0
      -0.224490
      0.050396
      -0.011313

      20
      1.0
      -0.183673
      0.033736
      -0.006196

      21
      1.0
      -0.142857
      0.020408
      -0.002915

      22
      1.0
      -0.102041
      0.010412
      -0.001062

      23
      1.0
      -0.061224
      0.003748
      -0.000229

      24
      1.0
      -0.020408
      0.000416
      -0.000008

      25
      1.0
      0.020408
      0.000416
      0.000008

      26
      1.0
      0.061224
      0.003748
      0.000229

      27
      1.0
      0.102041
      0.010412
      0.001062

      28
      1.0
      0.142857
      0.020408
      0.002915

      29
      1.0
      0.183673
      0.033736
      0.006196

      30
      1.0
      0.224490
      0.050396
      0.011313

      31
      1.0
      0.265306
      0.070387
      0.018674

      32
      1.0
      0.306122
      0.093711
      0.028687

      33
      1.0
      0.346939
      0.120367
      0.041760

      34
      1.0
      0.387755
      0.150354
      0.058301

      35
      1.0
      0.428571
      0.183673
      0.078717

      36
      1.0
      0.469388
      0.220325
      0.103418

      37
      1.0
      0.510204
      0.260308
      0.132810

      38
      1.0
      0.551020
      0.303623
      0.167303

      39
      1.0
      0.591837
      0.350271
      0.207303

      40
      1.0
      0.632653
      0.400250
      0.253219

      41
      1.0
      0.673469
      0.453561
      0.305459

      42
      1.0
      0.714286
      0.510204
      0.364431

      43
      1.0
      0.755102
      0.570179
      0.430543

      44
      1.0
      0.795918
      0.633486
      0.504203

      45
      1.0
      0.836735
      0.700125
      0.585819

      46
      1.0
      0.877551
      0.770096
      0.675798

      47
      1.0
      0.918367
      0.843399
      0.774550

      48
      1.0
      0.959184
      0.920033
      0.882481

      49
      1.0
      1.000000
      1.000000
      1.000000

```python
w = pd.DataFrame([3, 1 ,0.9, 1], index=XX.columns)
```

```python
(XX @ w).plot()
```

```python
import sympy as smp
```

```python
t, T = smp.symbols('t, T')
```

### Expression

```python
c_t = t**0 + t**1 + t**2
b_t = t**0 + t**1
step = smp.Piecewise((1, t < 0.3), (2, t < 0.5), (0, True))
```

```python
scaling = smp.integrate(c_t*b_t, (t, 0, T)).subs(T, 1)
```

```python
J_gcb = np.array([
    [1,-1, 1],
    [1,-1, 1],
    [-1,-1, 1],
    [1,-1, 1],
    [1,0, 1],
])
Z = np.matrix(np.append(np.ones((J_gcb.shape[0],1)), J_gcb * scaling, axis=1), dtype=float)
```

```python
np.linalg.det(Z.T @ Z)
```

    -4.4993051636947895e-12

```python
smp.integrate(c_t*b_t, (t, 0, T)).subs(T,1)
print()
```

$\displaystyle \frac{35}{12}$