# 2022-01-31 Formulation

Wednesday (Feb 2) we'll be back in person (if you want to be) in FLMG 157.

## Last time

* Derive Newton's method via fixed point convergence theory
* Newton methods in computing culture

## Today

* Breaking Newton's method
* Exploration
* Multiple roots
* Conditioning of the rootfinding problem
* Forward and backward stability

```julia
using Plots
default(linewidth=4, legendfontsize=12)

function newton_hist(f, fp, x0; tol=1e-12)
    x = x0
    hist = []
    for k in 1:100 # max number of iterations
        fx = f(x)
        fpx = fp(x)
        push!(hist, [x fx fpx])
        if abs(fx) < tol
            return vcat(hist...)
        end
        x = x - fx / fpx
    end
end
```

    fp (generic function with 1 method)

# Formulations are not unique (functions)

If $x = g(x)$ then
$$x = \underbrace{x + h(x) \big(g(x) - x\big)}_{g_3(x)}$$
for any smooth $h(x) \ne 0$. Can we choose $h(x)$ to make $\lvert g_3'(x) \rvert$ small?

```julia
g(x) = cos(x)
gp(x) = -sin(x)
h(x) = -1 / (gp(x) - 1)
g3(x) = x + h(x) * (g(x) - x)
plot([x-> x, cos, g3], ylims=(-5, 5))
```

* We don't know $g'(x_*)$ in advance because we don't know $x_*$ yet.
* This method converges very fast
* We actually just derived Newton's method.

# Newton's method by fixed point convergence

* A rootfinding problem $f(x) = 0$ can be converted to a fixed point problem $$x = x + f(x) =: g(x)$$ but there is no guarantee that $g'(x_*) = 1 + f'(x_*)$ will have magnitude less than 1.
* Problem-specific algebraic manipulation can be used to make $|g'(x_*)|$ small.
* $x = x + h(x) f(x)$ is also a valid formulation for any $h(x)$ bounded away from $0$.
* Can we choose $h(x)$ such that $$ g'(x) = 1 + h'(x) f(x) + h(x) f'(x) = 0$$ when $f(x) = 0$?

In other words,
$$ x_{k+1} = x_k + \underbrace{\frac{-1}{f'(x_k)}}_{h(x_k)} f(x_k)  . $$

# Quadratic convergence!

$$ \left\lvert \frac{e_{k+1}}{e_k} \right\rvert \to \lvert g'(x_*) \rvert $$

* What does it mean that $g'(x_*) = 0$?
* It turns out that Newton's method has _locally **q-quadratic**_ convergence to simple roots,
$$\lim_{k \to \infty} \frac{|e_{k+1}|}{|e_k|^2} < \infty.$$
* "The number of correct digits doubles each iteration."
* Now that we know how to make a good guess accurate, the effort lies in getting a good guess.

# Rootfinding methods outlook

* Newton methods are immensely successful
  * Convergence theory is local; we need good initial guesses (activity)
  * Computing the derivative $f'(x)$ is *intrusive*
    * Avoided by secant methods (approximate the derivative; activity)
    * Algorithmic or numerical differentiation (future topics)
  * Bisection is robust when conditions are met
  * Line search (activity)
  * When does Newton diverge?

* More topics
  * Find *all* the roots
  * Use Newton-type methods with bounds
  * Times when Newton converges slowly

# Exploratory rootfinding

* Find a function $f(x)$ that models something you're interested in. You could consider nonlinear physical models (aerodynamic drag, nonlinear elasticity), behavioral models, probability distributions, or anything else that that catches your interest. Implement the function in Julia or another language.

* Consider how you might know the output of such functions, but not an input. Think from the position of different stakeholders: is the equation used differently by an experimentalist collecting data versus by someone making predictions through simulation? How about a company or government reasoning about people versus the people their decisions may impact?

* Formulate the map from known to desired data as a rootfinding problem and try one or more methods (Newton, bisection, etc., or use a rootfinding library).

* Plot the inverse function (output versus input) from the standpoint of one or more stakeholder. Are there interesting inflection points? Are the methods reliable?

* If there are a hierarchy of models for the application you're interested in, consider using a simpler model to provide an initial guess to a more complicated model.

# Equation of state example

Consider an [equation of state](https://en.wikipedia.org/wiki/Real_gas#Beattie%E2%80%93Bridgeman_model) for a real gas, which might provide pressure $p(T, \rho)$ as a function of temperature $T$ and density $\rho = 1/v$.

* An experimentalist can measure temperature and pressure, and will need to solve for density (which is difficult to measure directly).
* A simulation might know (at each cell or mesh point, at each time step) the density and internal energy, and need to compute pressure (and maybe temperature).
* An analyst might have access to simulation output and wish to compute entropy (a thermodynamic property whose change reflects irreversible processes, and can be used to assess accuracy/stability of a simulation or efficiency of a machine).

The above highlights how many equations are incomplete, failing to model how related quantities (internal energy and entropy in this case) depend on the other quantities. Standardization bodies (such as NIST, here in Boulder) and practitioners often prefer models that intrinsically provide a complete set of consistent relations. An elegent methodology for equations of state for gasses and fluids is by way of the [Helmholtz free energy](http://www.coolprop.org/fluid_properties/PurePseudoPure.html#pure-and-pseudo-pure-fluid-properties), which is not observable, but whose partial derivatives define a complete set of thermodynamic properties. The [CoolProp](http://www.coolprop.org) software has highly accurate models for many gasses, and practitioners often build less expensive models for narrower ranges of theromdynamic conditions.

# Roots with multiplicity

There are multiple ways to represent (monic) polynomials

\begin{align}
f(x) &= (x - a_1) (x - a_2) & g(x) &= x^2 \underbrace{- (a_1 + a_2)}_{b_1} x + \underbrace{a_1 a_2}_{b_2}
\end{align}

```julia
poly_eval_prod(x, a) = prod(x .- a)
function poly_eval_sum(x, b)
    sum = 1
    for c in b
        # This is known as Horner's rule
        sum = x * sum + c
    end
    sum
end

eps = 1e-10
a = [1e5, 1e5*(1)] # tiny perturbation to root
b = [-(a[1] + a[2])*(1), # tiny perturbation to monomial coefficent
    a[1]*a[2]]
f(x) = poly_eval_prod(x, a)
g(x) = poly_eval_sum(x, b)
```

    g (generic function with 1 method)

```julia
plot([f, g], xlims=(a[1]-2, a[2]+2), ylims=(-3, 3))
plot!(zero, color=:black, label=:none)
```

* Perturbing the coefficient in the tenth digit made the difference between one root and two.
* The distance between the roots was big when $b_1$ was perturbed.

# How did the roots move?
Take two roots at $x=a$ and perturb the middle coefficient.
\begin{align}
x^2 \underbrace{- 2a(1 + \epsilon)}_{b_1} x + \underbrace{a^2}_{b_2} = 0
\end{align}

Analytically, we know the roots are at

\begin{align}
\frac{b_1}{2} \pm \sqrt{\frac{b_1^2}{4} - b_2}
&= a(1+\epsilon) \pm \sqrt{a^2(1+\epsilon)^2 - a^2} \\
&= a(1+\epsilon) \pm a \sqrt{2 \epsilon + \epsilon^2} \\
&\approx a \Bigl(1 + \epsilon \pm \sqrt{2 \epsilon} \Bigr).
\end{align}

```julia
plot([x->x, sqrt], xlims=(0, .1))
```

Note that condition number is well behaved.
$$ \kappa(\sqrt{\epsilon}) = \frac{1}{2\sqrt{\epsilon}} \frac{\epsilon}{\sqrt{\epsilon}} = \frac 1 2$$

# What does Newton find?

```julia
hist = newton_hist(g, x -> 2*x + b[1], 1.1e5; tol=1e-14)
```

    25×3 Matrix{Float64}:
     110000.0             1.0e8        20000.0
     105000.0             2.5e7        10000.0
     102500.0             6.25e6        5000.0
     101250.0             1.5625e6      2500.0
     100625.0        390625.0           1250.0
          1.00312e5   97656.2            625.0
          1.00156e5   24414.1            312.5
          1.00078e5    6103.52           156.25
          1.00039e5    1525.88            78.125
          1.0002e5      381.47            39.0625
          1.0001e5       95.3674          19.5312
          1.00005e5      23.8419           9.76562
          1.00002e5       5.96046          4.88281
          1.00001e5       1.49012          2.44141
          1.00001e5       0.372528         1.2207
          1.0e5           0.093132         0.610353
          1.0e5           0.023283         0.305179
          1.0e5           0.00582123       0.152593
     100000.0             0.00145531       0.0762955
     100000.0             0.000364304      0.0381463
     100000.0             9.15527e-5       0.0190459
     100000.0             2.28882e-5       0.00943207
     100000.0             5.72205e-6       0.0045788
     100000.0             1.90735e-6       0.00207943
     100000.0             0.0              0.000244935

```julia
plot(hist[:,1], hist[:,2], seriestype=:path, marker=:auto)
plot!(g, title="Root $(hist[end,1])", alpha=.5)
plot!(zero, color=:black, label=:none)
```

* Convergence is kinda slow.
* The solution is not nearly as accurate as machine precision.

# Using `Polynomials`

Julia has a nice package to evaluate and manipulate polynomials. The coefficients are given in the other order $b_0 + b_1 x + b_2 x^2 + \dotsb$.

```julia
using Polynomials
@show fpoly = Polynomial([2, -3, 1])
@show gpoly = fromroots([1, 2] .+ 1e5)
derivative(gpoly)
```

    fpoly = Polynomial([2, -3, 1]) = Polynomial(2 - 3*x + x^2)
    gpoly = fromroots([1, 2] .+ 100000.0) = Polynomial(1.0000300002e10 - 200003.0*x + 1.0*x^2)

-200003.0 + 2.0∙x

```julia
newton_hist(gpoly, derivative(gpoly), 3)
```

    23×3 Matrix{Float64}:
          3.0            9.9997e9     -199997.0
      50002.2            2.49993e9     -99998.5
      75001.9            6.24981e8     -49999.3
      87501.7            1.56245e8     -24999.6
      93751.6            3.90613e7     -12499.8
      96876.5            9.76533e6      -6249.91
      98439.0            2.44133e6      -3124.95
      99220.3            6.10333e5      -1562.48
      99610.9            1.52583e5       -781.239
      99806.2        38145.7             -390.62
      99903.8         9536.37            -195.311
      99952.7         2384.03             -97.6582
      99977.1          595.945            -48.8342
      99989.3          148.924            -24.4273
      99995.4           37.1686           -12.2341
      99998.4            9.23006           -6.15794
      99999.9            2.24666           -3.16017
          1.00001e5      0.505424          -1.7383
          1.00001e5      0.0845398         -1.15679
     100001.0            0.0053409         -1.01063
     100001.0            2.79285e-5        -1.00006
     100001.0            7.85803e-10       -1.0
     100001.0            0.0               -1.0

# Finding all roots of polynomials

```julia
roots(Polynomial([1e10, -2e5*(1 + 1e-10), 1]))
```

    2-element Vector{Float64}:
      99998.58579643762
     100001.41422356237

```julia
p = fromroots([0., 1., 2, 3] .+ 1e3)
xs = roots(p)
scatter(real(xs), imag(xs), color=:red)
for i in 1:100
    r = randn(length(p)) # normally distributed mean 0, stddev 1
    q = copy(p)
    q[:] .*= 1 .+ 1e-10 * r
    xs = roots(q)
    scatter!(real(xs), imag(xs), markersize=1)
end
plot!(legend=:none)
```

> ## [Fundamental Theorem of Algebra](https://en.wikipedia.org/wiki/Fundamental_theorem_of_algebra)
>
> Every non-zero, single-variable, degree $n$ polynomial with complex coefficients has, counted with multiplicity, exactly $n$ complex roots.

# Wilkinson's polynomial

Suppose we add more terms
$$ w(x) = \prod_{k=1}^{20} (x - k) = (x - 1) (x - 2) \dotsb (x - 20) = \sum_{k=0}^{20} b_k x^k $$

```julia
n = 20
a = collect(1.:n)
w = fromroots(a)
w[10] *= 1 + 1e-12
plot(x -> abs(w(x)), xlims=(0, n+1), yscale=:log10)
```

```julia
w = fromroots(a)
scatter(a, zero(a), color=:red)
for i in 1:100
    r = randn(length(w))
    q = copy(w)
    q[:] .*= 1 .+ 1e-10 * r
    xs = roots(q)
    scatter!(real(xs), imag(xs), markersize=1)
end
plot!(legend=:none)
```

# Which is better to model inputs to a rootfinder?

* A: coefficients $a_k$ in $$p(x) = \prod_k (x - a_k)$$

* B: coefficients $b_k$ in $$p(x) = \sum_k b_k x^k$$

## Figure from Trefethen and Bau (1999)

# [Forward vs backward error and stability](https://fncbook.github.io/fnc/intro/stability.html#backward-error)

## Stability
**"nearly the right answer to nearly the right question"**
$$ \frac{\lvert \tilde f(x) - f(\tilde x) \rvert}{| f(\tilde x) |} \in O(\epsilon_{\text{machine}}) $$
for some $\tilde x$ that is close to $x$

## Backward Stability
**"exactly the right answer to nearly the right question"**
$$ \tilde f(x) = f(\tilde x) $$
for some $\tilde x$ that is close to $x$

* Every backward stable algorithm is stable.
* Not every stable algorithm is backward stable.

# Map angle to the unit circle

```julia
theta = 1 .+ LinRange(0, 3, 100)
scatter(cos.(theta), sin.(theta), legend=:none, aspect_ratio=:equal)
```

```julia
theta = LinRange(1., 1+2e-5, 100)
mysin(t) = cos(t - (1e10+1)*pi/2)
plot(cos.(theta), sin.(theta), aspect_ratio=:equal)
scatter!(cos.(theta), mysin.(theta))
```

## Are we observing A=stabiltiy, B=backward stability, or C=neither?

* The numbers $(\widetilde\cos \theta, \widetilde\sin \theta = (\operatorname{fl}(\cos \theta), \operatorname{fl}(\sin\theta))$ do not lie exactly on the unit circles.
  * There does not exist a $\tilde\theta$ such that $(\widetilde\cos \theta, \widetilde \sin\theta) = (\cos\tilde\theta, \sin\tilde\theta)$

# Accuracy of backward stable algorithms (Theorem)

A backward stable algorithm for computing $f(x)$ has relative accuracy
$$ \left\lvert \frac{\tilde f(x) - f(x)}{f(x)} \right\rvert \lesssim \kappa(f) \epsilon_{\text{machine}} . $$
Backward stability is generally the best we can hope for.

In practice, it is rarely possible for a function to be backward stable when the output space is higher dimensional than the input space.


# Random Signals and LTI-Systems

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## The Wiener Filter

The [Wiener filter](https://en.wikipedia.org/wiki/Wiener_filter), named after [*Nobert Wiener*](https://en.wikipedia.org/wiki/Norbert_Wiener), aims at estimating an unknown random signal by filtering a noisy distorted observation of the signal. It has a wide range of applications in signal processing. For instance, noise reduction, system identification, deconvolution and signal detection. The Wiener filter is frequently used as basic building block for algorithms that denoise audio signals, like speech, or remove noise from a image.

### Signal Model

The following signal model is underlying the Wiener filter

The random signal $s[k]$ is subject to distortion by the linear time-invariant (LTI) system $G(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and additive noise $n[k]$, resulting in the observed signal $x[k] = s[k] * g[k] + n[k]$. The additive noise $n[k]$ is assumed to be uncorrelated from $s[k]$. It is furthermore assumed that all random signals are wide-sense stationary (WSS). This distortion model holds for many practical problems, like e.g. the measurement of a physical quantity by a sensor.

The basic concept of the Wiener filter is to apply the LTI system $H(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ to the observed signal $x[k]$ such that the output signal $y[k] = x[k] * h[k]$ matches $s[k]$ as close as possible. In order to quantify the deviation between $y[k]$ and $s[k]$, the error signal

\begin{equation}
e[k] = y[k] - s[k]
\end{equation}

is introduced. The error $e[k]$ equals zero, if the output signal $y[k]$ perfectly matches $s[k]$. In general, this goal cannot be achieved in a strict sense. As alternative one could aim at minimizing the linear average of the error $e[k]$. However, this quantity is not very well suited for optimization since it is signed. Instead, the quadratic average of the error $e[k]$ is used in the Wiener filter and other techniques. This measure is known as [*mean squared error*](https://en.wikipedia.org/wiki/Mean_squared_error) (MSE). It is defined as

\begin{equation}
E \{ |e[k]|^2 \} = \frac{1}{2 \pi} \int\limits_{-\pi}^{\pi} \Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \,\mathrm{d}\Omega = \varphi_{ee}[\kappa] \Big\vert_{\kappa = 0}
\end{equation}

the equalities are deduced from the properties of the power spectral density (PSD) and the auto-correlation function (ACF). Above equation is referred to as [*cost function*](https://en.wikipedia.org/wiki/Loss_function). We aim at minimizing the cost function, hence minimizing the MSE between the original signal $s[k]$ and its estimate $y[k]$. The solution is referred to as [minimum mean squared error](https://en.wikipedia.org/wiki/Minimum_mean_square_error) (MMSE) solution.

### Transfer Function of the Wiener Filter

At first, the Wiener filter shall only have access to the observed signal $x[k]$ and selected statistical measures. It is assumed that the cross-power spectral density $\Phi_{xs}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ between the observed signal $x[k]$ and the original signal $s[k]$, and the PSD of the observed signal $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ are known. This knowledge can either be gained by estimating both from measurements taken at an actual system or by using suitable statistical models.

The optimal filter is found by minimizing the MSE $E \{ |e[k]|^2 \}$ with respect to the transfer function $H(\mathrm{e}^{\,\mathrm{j}\,\Omega})$. The solution of this optimization problem goes beyond the scope of this notebook and can be found in the literature, e.g. [[Girod et. al](../index.ipynb#Literature)]. The transfer function of the Wiener filter is given as

\begin{equation}
H(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}{\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})} = \frac{\Phi_{xs}(\mathrm{e}^{\,-\mathrm{j}\,\Omega})}{\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}
\end{equation}

for $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \neq 0$.
No knowledge on the actual distortion process is required besides the assumption of an LTI system and additive noise. Only the PSDs $\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ have to be known in order to estimate $s[k]$ from $x[k]$ in the MMSE sense. Care has to be taken that the filter $H(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ is causal and stable in practical applications.

**Example - Denoising of an Deterministic Signal**

The following example considers the estimation of the original signal from a distorted observation. It is assumed that the original signal is a deterministic signal $s[k] = \sin[\Omega_0\,k]$ which is distorted by an LTI system and additive wide-sense ergodic normal distributed zero-mean white noise with PSD $N_0 = 0.1$. The PSDs $\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ are estimated from $s[k]$ and $x[k]$ using the [Welch technique](../spectral_estimation_random_signals/welch_method.ipynb). The Wiener filter is applied to the observation $x[k]$ in order to compute the estimate $y[k]$ of $s[k]$. Note that the discrete signals have been illustrated by continuous lines for ease of illustration.

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.signal as sig

N = 2**14  # number of samples
M = 256  # length of Wiener filter
Om0 = 0.1*np.pi  # frequency of original signal
N0 = 0.1  # PSD of additive white noise

# generate original signal
s = np.cos(Om0 * np.arange(N))
# generate observed signal
g = 1/20*np.asarray([1, 2, 3, 4, 5, 4, 3, 2, 1])
np.random.seed(1)
n = np.random.normal(size=N, scale=np.sqrt(N0))
x = np.convolve(s, g, mode='same') + n
# estimate (cross) PSDs using Welch technique
f, Pxx = sig.csd(x, x, nperseg=M)
f, Psx = sig.csd(s, x, nperseg=M)
# compute Wiener filter
H = Psx/Pxx
H = H * np.exp(-1j*2*np.pi/len(H)*np.arange(len(H)) *
               (len(H)//2))  # shift for causal filter
h = np.fft.irfft(H)
# apply Wiener filter to observation
y = np.convolve(x, h, mode='same')

# plot (cross) PSDs
Om = np.linspace(0, np.pi, num=len(H))

plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.plot(Om, 20*np.log10(np.abs(.5*Pxx)),
         label=r'$| \Phi_{xx}(e^{j \Omega}) |$ in dB')
plt.plot(Om, 20*np.log10(np.abs(.5*Psx)),
         label=r'$| \Phi_{sx}(e^{j \Omega}) |$ in dB')
plt.title('(Cross) PSDs')
plt.xlabel(r'$\Omega$')
plt.legend()
plt.axis([0, np.pi, -60, 40])
plt.grid()

# plot transfer function of Wiener filter
plt.subplot(122)
plt.plot(Om, 20*np.log10(np.abs(H)))
plt.title('Transfer function of Wiener filter')
plt.xlabel(r'$\Omega$')
plt.ylabel(r'$| H(e^{j \Omega}) |$ in dB')
plt.axis([0, np.pi, -150, 3])
plt.grid()
plt.tight_layout()

# plot signals
idx = np.arange(500, 600)
plt.figure(figsize=(10, 4))
plt.plot(idx, x[idx], label=r'observed signal $x[k]$')
plt.plot(idx, s[idx], label=r'original signal $s[k]$')
plt.plot(idx, y[idx], label=r'estimated signal $y[k]$')
plt.title('Signals')
plt.xlabel(r'$k$')
plt.axis([idx[0], idx[-1], -1.5, 1.5])
plt.legend()
plt.grid()
```

Let's listen to the observed signal $x[k]$ and the output of the Wiener filter $y[k]$

```python
from scipy.io import wavfile

fs = 8000
wavfile.write('Wiener_observed_signal.wav', fs, np.int16(x*16384))
wavfile.write('Wiener_output.wav', fs, np.int16(y*16384))
```

**Observed signal**

<audio src="./Wiener_observed_signal.wav" controls>Your browser does not support the audio element.</audio>[./Wiener_observed_signal.wav](./Wiener_observed_signal.wav)

**Output of Wiener filter**

<audio src="./Wiener_output.wav" controls>Your browser does not support the audio element.</audio>[./Wiener_output.wav](./Wiener_output.wav)

**Exercise**

* Take a look at the PSDs and the resulting transfer function of the Wiener filter. How does the Wiener filter remove the noise from the observed signal?
* Change the frequency `Om0` of the original signal $s[k]$ and the noise power `N0` of the additive noise. What changes?

Solution: The cross-PSD $\Phi_{sx}(e^{j \Omega})$ has its maximum at the frequency $\Omega_0$ of the original signal $s[k]$, besides this frequency the PSD $\Phi_{xx}(e^{j \Omega})$ has approximately the value of the PSD $\Phi_{nn}(e^{j \Omega}) = N_0$ of the additive noise. The resulting Wiener filter $G(e^{j \Omega})$ has an attenuation of 0 dB at $\Omega_0$ which increases off this frequency. It has the characteristics of a band-pass filter centered at $\Omega_0$. By filtering the observed signal $x[k]$ with the Wiener filter, the major portion of the additive white noise is removed. However, around $\Omega_0$ both the original signal $s[k]$ and noise pass the filter. This explains the remaining small deviations between the estimated signal $y[k]$ and the original signal $y[k]$. The adaption of the Wiener filter to the original signal and additive noise changes when changing its frequency and PSD.

### Wiener Deconvolution

As discussed above, the general formulation of the Wiener filter is based on the knowledge of the PSDs $\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and $\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ characterizing the distortion process and the observed signal, respectively. These PSDs can be derived from the PSDs of the original signal $\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ and the noise $\Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$, and the transfer function $G(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of the distorting system.

Under the assumption that $n[k]$ is uncorrelated from $s[k]$, the PSD $\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ can be derived as

\begin{equation}
\Phi_{sx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \cdot G(\mathrm{e}^{\,-\mathrm{j}\,\Omega})
\end{equation}

and

\begin{equation}
\Phi_{xx}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \cdot  |G(\mathrm{e}^{\,\mathrm{j}\,\Omega})|^2 + \Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})
\end{equation}

Introducing these results into the general formulation of the Wiener filter yields

\begin{equation}
H(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \cdot G(\mathrm{e}^{\,-\mathrm{j}\,\Omega})}{\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) \cdot  |G(\mathrm{e}^{\,\mathrm{j}\,\Omega})|^2 + \Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}
\end{equation}

This specialization is also known as [*Wiener deconvolution filter*](https://en.wikipedia.org/wiki/Wiener_deconvolution). The filter can be derived from the PSDs of the original signal $s[k]$ and the noise $n[k]$, and the transfer function $G(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of the distorting system. This form is especially useful when the PSDs can be modeled reasonably well by analytic probabilty density functions (PSDs). In many cases, the additive noise can be modeled as white noise $\Phi_{nn}(e^{j \Omega}) = N_0$.

### Interpretation

The Wiener deconvolution filter derived above is also useful for an interpretation of the operation of the Wiener filter. It can be rewritten by introducing the frequency dependent [signal-to-noise ratio](https://en.wikipedia.org/wiki/Signal-to-noise_ratio) $\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}{\Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}$ between the original signal and the noise as

\begin{equation}
H(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{1}{G(\mathrm{e}^{\,\mathrm{j}\,\Omega})} \cdot \left(   \frac{|G(\mathrm{e}^{\,\mathrm{j}\,\Omega})|^2}{|G(\mathrm{e}^{\,\mathrm{j}\,\Omega})|^2 + \frac{1}{\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}} \right)
\end{equation}

Now two special cases are discussed:

1. If there is no additive noise $\Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = 0$, the bracketed expression is equal to 1. Hence, the Wiener filter is simply given as the inverse system to the distorting system $$H(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{1}{G(\mathrm{e}^{\,\mathrm{j}\,\Omega})}$$

2. If the distorting system is just a pass-through $G(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = 1$, the Wiener filter is given as

    $$H(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}{\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) + 1}=\frac{\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}{\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})+\Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})}$$
    Two extreme cases are considered
    * for $\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})\gg \Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ (high $\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$) at a given frequency $\Omega$ the transfer function approaches 1
    * for $\Phi_{ss}(\mathrm{e}^{\,\mathrm{j}\,\Omega})\ll \Phi_{nn}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ (low $\text{SNR}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$) at a given frequency $\Omega$ the transfer function approaches 0

The discussed cases explain the operation of the Wiener filter for specific situations, the general operation is a combination of these.

**Example - Denoising of a deterministic signal with the Wiener deconvolution filter**

The preceding example of the general Wiener filter will now be reevaluated with the Wiener deconvolution filter.

```python
N = 2**14  # number of samples
M = 256  # length of Wiener filter
Om0 = 0.1*np.pi  # frequency of original signal
N0 = .1  # PSD of additive white noise

# generate original signal
s = np.cos(Om0 * np.arange(N))
# generate observed signal
g = 1/20*np.asarray([1, 2, 3, 4, 5, 4, 3, 2, 1])
np.random.seed(1)
n = np.random.normal(size=N, scale=np.sqrt(N0))
x = np.convolve(s, g, mode='same') + n
# estimate PSD
f, Pss = sig.csd(s, s, nperseg=M)
f, Pnn = sig.csd(n, n, nperseg=M)
# compute Wiener filter
G = np.fft.rfft(g, M)
H = 1/G * (np.abs(G)**2 / (np.abs(G)**2 + N0/Pss))
H = H * np.exp(-1j*2*np.pi/len(H)*np.arange(len(H)) *
               (len(H)//2-8))  # shift for causal filter
h = np.fft.irfft(H)
# apply Wiener filter to observation
y = np.convolve(x, h, mode='same')

# plot (cross) PSDs
Om = np.linspace(0, np.pi, num=len(H))

plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.plot(Om, 20*np.log10(np.abs(.5*Pss)),
         label=r'$| \Phi_{ss}(e^{j \Omega}) |$ in dB')
plt.plot(Om, 20*np.log10(np.abs(.5*Pnn)),
         label=r'$| \Phi_{nn}(e^{j \Omega}) |$ in dB')
plt.title('PSDs')
plt.xlabel(r'$\Omega$')
plt.legend()
plt.axis([0, np.pi, -60, 40])
plt.grid()

# plot transfer function of Wiener filter
plt.subplot(122)
plt.plot(Om, 20*np.log10(np.abs(H)))
plt.title('Transfer function of Wiener filter')
plt.xlabel(r'$\Omega$')
plt.ylabel(r'$| H(e^{j \Omega}) |$ in dB')
plt.axis([0, np.pi, -150, 10])
plt.grid()
plt.tight_layout()

# plot signals
idx = np.arange(500, 600)
plt.figure(figsize=(10, 4))
plt.plot(idx, x[idx], label=r'observed signal $x[k]$')
plt.plot(idx, s[idx], label=r'original signal $s[k]$')
plt.plot(idx, y[idx], label=r'estimated signal $y[k]$')
plt.title('Signals')
plt.xlabel(r'$k$')
plt.axis([idx[0], idx[-1], -1.5, 1.5])
plt.legend()
plt.grid()
```

**Exercise**

* Compare the transfer function of the Wiener deconvolution filter to the transfer function of the general Wiener filter from the preceding example.
* What is different compared to the general Wiener filter? Why?

Solution: The transfer function $H(e^{j \Omega})$ of the Wiener deconvolution filter is much smoother than the transfer function of the general Wiener filter from the preceding example. This is due to the knowledge of the transfer function $G(e^{j \Omega})$ of the disturbing system. The general Wiener filter inherently estimates this from the noisy observations.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples*.


# Quiz - Week 3B

## Q1.
* * *
* Suppose we hash the elements of a set S having 20 members, to a bit array of length 99. The array is initially all-0's, and we set a bit to 1 whenever a member of S hashes to it. The hash function is random and uniform in its distribution. What is the expected fraction of 0's in the array after hashing? What is the expected fraction of 1's? You may assume that 99 is large enough that asymptotic limits are reached

## Solution 1.
* * *
* Throwing Darts approximation - since we know that in this case we have t = 99, d = S*20, the fraction of 0's that remain will follow the following equation:

\begin{align}
(1 - 1/t)^{t(d/t)} &~= e^{-d/t} \\
&= e^{ \frac{-20*S}{99} }
\end{align}

* So in this cae, we can derive the fraction of 1's that remain will be $ 1 - e^{ \frac{-20*S}{99} }$

## Q2.
* * *
* A certain Web mail service (like gmail, e.g.) has $10^8$ users, and wishes to create a sample of data about these users, **occupying $10^{10}$ bytes**. Activity at the service can be viewed as a **stream of elements**, each of which is an email. The element contains the ID of the sender, which must be one of the $10^8$ users of the service, and other information, e.g., the recipient(s), and contents of the message. The plan is to pick a **subset of the users** and collect in the $10^{10}$ bytes records of **length 100 bytes** about every email sent by the users in the selected set (and nothing about other users).
* The method of Section 4.2.4 will be used. User ID's will be **hashed to a bucket number**, from 0 to 999,999. At all times, **there will be a threshold t** such that the 100-byte records for all the users whose ID's hash to **t or less** will be retained, and other users' records will not be retained. You may assume that each user generates emails at exactly the **same rate** as other users. **As a function of n**, the number of emails in the stream so far, **what should the threshold t be in order that the selected records will not exceed the $10^{10}$ bytes available to store records**? From the list below, identify the true statement about a value of n and its value of t.

## Solution 2.
* * *
* Since in this problem, basically we are going to partition the data stream by its value. Thus the relationship betwee n and t will be as folloing[note that hash value starts at 0]:

\begin{align}
\frac{t}{buckets\#} = \frac{10^{10}}{n}
\end{align}

# Quiz - Week 3A(Advanced)

## Q1.

   C -- D -- E
 / |    |    | \
A  |    |    |  B
 \ |    |    | /
   F -- G -- H

* * *

Write the adjacency matrix A, the degree matrix D, and the Laplacian matrix L. For each, find the sum of all entries and the number of nonzero entries. Then identify the true statement from the list below.

```python
## Solution 1.
import numpy as np

A = np.array([[0, 0, 1, 0, 0, 1, 0, 0], #A
              [0, 0, 0, 0, 1, 0, 0, 1], #B
              [1, 0, 0, 1, 0, 1, 0, 0], #C
              [0, 0, 1, 0, 1, 0, 1, 0], #D
              [0, 1, 0, 1, 0, 0, 0, 1], #E
              [1, 0, 1, 0, 0, 0, 1, 0], #F
              [0, 0, 0, 1, 0, 1, 0, 1], #G
              [0, 1, 0, 0, 1, 0, 1, 0]  #H
             ])
print "Adjacent Matrix A:"
print A
print "Zero elements in A:"
print A.shape[1]* A.shape[0] - A.sum()
print "Sum as well as Non zero elements in A:"
print A.sum()
print "=========================================="

D = np.diag([2,2,3,3,3,3,3,3])
print "Degree Matrix D:"
print D

print "=========================================="

L = D - A

print "Laplacian Matrix L:"
print L

```

    Adjacent Matrix A:
    [[0 0 1 0 0 1 0 0]
     [0 0 0 0 1 0 0 1]
     [1 0 0 1 0 1 0 0]
     [0 0 1 0 1 0 1 0]
     [0 1 0 1 0 0 0 1]
     [1 0 1 0 0 0 1 0]
     [0 0 0 1 0 1 0 1]
     [0 1 0 0 1 0 1 0]]
    Zero elements in A:
    42
    Sum as well as Non zero elements in A:
    22
    ==========================================
    Degree Matrix D:
    [[2 0 0 0 0 0 0 0]
     [0 2 0 0 0 0 0 0]
     [0 0 3 0 0 0 0 0]
     [0 0 0 3 0 0 0 0]
     [0 0 0 0 3 0 0 0]
     [0 0 0 0 0 3 0 0]
     [0 0 0 0 0 0 3 0]
     [0 0 0 0 0 0 0 3]]
    ==========================================
    Laplacian Matrix L:
    [[ 2  0 -1  0  0 -1  0  0]
     [ 0  2  0  0 -1  0  0 -1]
     [-1  0  3 -1  0 -1  0  0]
     [ 0  0 -1  3 -1  0 -1  0]
     [ 0 -1  0 -1  3  0  0 -1]
     [-1  0 -1  0  0  3 -1  0]
     [ 0  0  0 -1  0 -1  3 -1]
     [ 0 -1  0  0 -1  0 -1  3]]

## Q2.
* Given the following graph

      2 -----6
     /  \    |
    1    4   |
     \  /  \ |
      3      5

* The goal is to find two clusters in this graph using Spectral Clustering on the Laplacian matrix. Compute the Laplacian of this graph. Then compute the **second eigen vector of the Laplacian** (the one corresponding to the second smallest eigenvalue).

* To cluster the points, we decide to split at the mean value. We say that a node is a tie if its value in the eigen-vector is exactly equal to the mean value. Let's assume that if a point is a tie, we choose its cluster at random. Identify the true statement from the list below.

```python
import numpy as np

A = np.array([[0, 1, 1, 0, 0, 0],
              [1, 0, 0, 1, 0, 0],
              [1, 0, 0, 1, 0, 0],
              [0, 1, 1, 0, 1, 0],
              [0, 0, 0, 1, 0, 1],
              [0, 1, 0, 0, 1, 0]
            ])

D = np.diag([2, 2, 2, 3, 2, 2])
L = (D - A)
print "=========================================="
print "Graph Laplacian Matrix: "
print L
values, vectors = np.linalg.eig(L)
print "Eigen values: "
values = np.around(values, decimals=4)
print values
print "=========================================="
print "Eigen vectors: "
vectors = np.around(vectors, decimals=4)
print vectors
print "=========================================="
print "Second Smallest Eigen vectors: "
print vectors[:, 2]
print "=========================================="
print "Mean of each row in vectors: "
print np.mean(vectors[:, 2])
```

    ==========================================
    Graph Laplacian Matrix:
    [[ 2 -1 -1  0  0  0]
     [-1  2  0 -1  0  0]
     [-1  0  2 -1  0  0]
     [ 0 -1 -1  3 -1  0]
     [ 0  0  0 -1  2 -1]
     [ 0 -1  0  0 -1  2]]
    Eigen values:
    [ 4.618  0.     1.     3.     2.382  2.   ]
    ==========================================
    Eigen vectors:
    [[ 0.277  -0.4082 -0.3536  0.5774 -0.5804 -0.4082]
     [-0.3626 -0.4082 -0.1768 -0.2887  0.1108 -0.4082]
     [-0.3626 -0.4082 -0.1768 -0.2887  0.1108  0.4082]
     [ 0.6723 -0.4082  0.1768 -0.2887  0.538   0.4082]
     [-0.3626 -0.4082  0.7071  0.5774  0.1108  0.4082]
     [ 0.277  -0.4082  0.5303 -0.2887 -0.5804 -0.4082]]
    ==========================================
    Second Smallest Eigen vectors:
    [-0.3536 -0.1768 -0.1768  0.1768  0.7071  0.5303]
    ==========================================
    Mean of each row in vectors:
    0.117833333333

## Q3.
* We wish to estimate the surprise number (2nd moment) of a data stream, using the method of AMS. It happens that our stream consists of **ten different values**, which we'll call 1, 2,..., 10, that cycle repeatedly. That is, at timestamps 1 through 10, the element of the stream equals the timestamp, at timestamps 11 through 20, the element is the timestamp minus 10, and so on. **It is now timestamp 75**, and a 5 has just been read from the stream. As a start, you should calculate the surprise number for this time.

* For our estimate of the surprise number, we shall choose **three timestamps at random**, and estimate the surprise number from each, using the AMS approach (length of the stream times 2m-1, where **m is the number of occurrences of the element of the stream at that timestamp**, considering all times from that timestamp on, to the current time). Then, our estimate will be the **median of the three resulting values**.

* You should discover the simple rules that **determine the estimate** derived from any given timestamp and from any set of three timestamps. Then, identify from the list below the set of three "random" timestamps that **give the closest estimate**.

### Some hints:
1. Be sure you have the surprise number correct. Notice that some of the elements appear 8 times and others appear 7 times. The surprise number is the sum over all elements that appear of the square of the number of times they appear.
2. Remember that for any given timestamp, the estimate is the length of the stream (75 in this example) times 2m-1, where m is the number of times the element at that timestamp appears, at that time or later.

```python
def cal_supprise(current_t = 75):
    common = ( current_t / 10 ) % 10
    more_set = ( current_t ) % 10

    less_set = 10 - more_set

    return more_set * (common+1)**2 + less_set * (common**2)

def AMS(time_list, current_t = 75):

    estimates = []
    for t in time_list:
        delta = current_t - t

        elem = t % 10
        threshold = current_t % 10

        common = (delta / 10) % 10

        if elem > threshold:
            estimates.append( ( 2*common - 1)*current_t )
        else:
            estimates.append( ( 2*common + 1)*current_t )

    return estimates

print cal_supprise()
print np.mean(AMS([31,32,44]))
print np.mean(AMS([14,35,42]))
print np.mean(AMS([32,48,50]))
print np.mean(AMS([22,42,62]))
```

```python
#buffer
```

## Q4
* We wish to use the Flagolet-Martin lgorithm of Section 4.4 to count the number of distinct elements in a stream. Suppose that there are **ten possible elements, 1, 2,..., 10, that could appear in the stream**, but only four of them have actually appeared. To make our estimate of the count of distinct elements, we **hash each element to a 4-bit binary number**. The element x is hashed to 3x + 7 (modulo 11). For example, element 8 hashes to 3*8+7 = 31, which is 9 modulo 11 (i.e., the remainder of 31/11 is 9). Thus, the 4-bit string for element 8 is 1001.

* A set of **four of the elements 1 through 10 could give an estimate that is exact** (if the estimate is 4), or too high, or too low. You should figure out under what circumstances a set of four elements falls into each of those categories. Then, identify in the list below the set of four elements that gives the exactly correct estimate.

```python
def hash1(x):
    return (3*x + 7) % 11

def printBinary(x):
    print "- The binary result of {0} is:".format(str(x)) + str(bin(x))

def countTrailingZerosInBinary(num):
    bnum = str(bin(num))
    return len(bnum) - len(bnum.rstrip('0'))

def FlagoletMatrtin(hash_list):
    maxnum = 0
    for val in hash_list:
        num = countTrailingZerosInBinary(val)
        if num > maxnum:
            maxnum = num
    return 2**maxnum

print FlagoletMatrtin(([hash1(x) for x in [1,3,6,8]]))
print FlagoletMatrtin(([hash1(x) for x in [2,4,6,10]]))
print FlagoletMatrtin(([hash1(x) for x in [2,6,8,10]]))
print FlagoletMatrtin(([hash1(x) for x in [3,4,8,10]]))

print "================="

print FlagoletMatrtin(([hash1(x) for x in [2,6,8,9]]))
print FlagoletMatrtin(([hash1(x) for x in [4,6,9,10]]))
print FlagoletMatrtin(([hash1(x) for x in [1,5,8,9]]))
print FlagoletMatrtin(([hash1(x) for x in [1,6,7,10]]))

print "================="

print FlagoletMatrtin(([hash1(x) for x in [1,2,3,9]]))
print FlagoletMatrtin(([hash1(x) for x in [1,3,9,10]]))
print FlagoletMatrtin(([hash1(x) for x in [3,4,8,10]]))
print FlagoletMatrtin(([hash1(x) for x in [4,6,9,10]]))

print "================="

print FlagoletMatrtin(([hash1(x) for x in [1,4,7,9]]))
print FlagoletMatrtin(([hash1(x) for x in [4,6,9,10]]))
print FlagoletMatrtin(([hash1(x) for x in [1,6,7,10]]))
print FlagoletMatrtin(([hash1(x) for x in [4,5,6,10]]))

```

    2
    8
    4
    8
    =================
    2
    8
    2
    4
    =================
    2
    4
    8
    8
    =================
    8
    8
    4
    8

## Q5

* Suppose we are using the DGIM algorithm of Section 4.6.2 to estimate the number of 1's in suffixes of a sliding window of length 40. The current timestamp is 100, and we have the following buckets stored:

    End Time    100  98  95  92  87  80  65
        Size    1    1   2   2   4   8   8

* **Note**: we are showing timestamps as absolute values, rather than modulo the window size, as DGIM would do.
* Suppose that at times 101 through 105, 1's appear in the stream. Compute the set of buckets that would exist in the system at time 105. Then identify one such bucket from the list below. Buckets are represented by pairs (end-time, size).

## Solution5.

step 1:
    101 100 95 87 80 65
    1   2   4  4  8  8
step 2:
    102 101 100 95 87 80 65
    1   1   2   4  4  8  8
step 3:
    103 102 100 95 87 80 65
    1   2   2   4  4  8  8
step 4:
    104 103 102 100 95 87 80 65
    1   1   2   2   4  4  8  8
step 5:
    105 104 102 95 80
    1   2   4   8  8

```python

```


## Augmented Reality

This notebook aims to give a brief explaination of the theory and math behind the present *augmented reality* project.
The content is based on the articles ([part1](https://bitesofcode.wordpress.com/2017/09/12/augmented-reality-with-python-and-opencv-part-1/) and [part2](https://bitesofcode.wordpress.com/2018/09/16/augmented-reality-with-python-and-opencv-part-2/)) written by *Juan Gallostra*, while the code that you can find in this project is a fork from his [project](https://github.com/juangallostra/augmented-reality) too.

```python
# import all the required module for the notebook
import cv2
import numpy as np
```

Other interesting articles:
[glyph recognition](https://rdmilligan.wordpress.com/2015/07/19/glyph-recognition-using-opencv-and-python/)
[AR with python OpenCV and OpenGL](https://rdmilligan.wordpress.com/2015/10/15/augmented-reality-using-opencv-opengl-and-blender/)
[pygame OBJ loader](http://www.pygame.org/wiki/OBJFileLoader)
[3D object search](https://clara.io/library)

---

## Introduction

The aim of the project is to display in a screen a 3D model of a figure whose position and orientation matches the position and orientation of some predefined flat surface. Furthermore, we want to do it in real time, so that if the surface changes its position or orientation the projected model does so accordingly.

To achieve this we first have to be able to identify the flat surface of reference in an image or video frame. Once identified, we can easily determine the transformation from the reference surface image (2D) to the target image (2D). This transformation is called [homography](https://en.wikipedia.org/wiki/Homography_(computer_vision)). However, if what we want is to project a 3D model placed on top of the reference surface to the target image we need to extend the previous transformation to handle cases were the height of the point to project in the reference surface coordinate system is different than zero. This can be achieved with a bit of algebra. Finally, we should apply this transformation to our 3D model and draw it on the screen. Bearing the previous points in mind our project can be divided into:

1.  [Recognize the reference flat surface.](#1.-Recognition-of-the-reference-target)
2.  [Estimate the homography.](#2.-Homography-estimation)
3.  [Derive from the homography the transformation (pose estimation) from the reference surface coordinate system to the target image coordinate system.](#3.-Pose-estimation-from-a-plane)
4.  [Project our 3D model in the image (pixel space) and draw it.](#4.-Model-projection)

The main tools we will use are *Python* and *OpenCV* because they are both open source, easy to set up and use and it is fast to build prototypes with them. For the needed algebra bit I will be using *numpy*.

**insert image of 1.2.3.4. scheme here**

### 1. Recognition of the reference target

From the many possible techniques that exist to perform object recognition I decided to tackle the problem with a feature based recognition method. This kind of methods, without going into much detail, consist in three main steps:

1. [feature detection or extraction.](#1.1.-Feature-extraction)
2. [feature description.](#1.2.-Feature-description)
3. [feature matching.](#1.3.-Feature-matching)

#### 1.1. Feature extraction

Roughly speaking, this step consists in first looking in both the reference and target images for features that stand out and, in some way, describe part the object to be recognized. This features can be later used to find the reference object in the target image. We will assume we have found the object when a certain number of positive feature matches are found between the target and reference images. For this to work it is important to have a reference image where the only thing seen is the object (or surface, in this case) to be found.  We don’t want to detect features that are not part of the surface. And, although we will deal with this later, we will use the dimensions of the reference image when estimating the pose of the surface in a scene.

For a region or point of an image to be labeled as feature it should fulfill two important properties: first of all, it should present some uniqueness at least locally. Good examples of this could be corners or edges. Secondly, since we don’t know beforehand which will be, for example, the orientation, scale or brightness conditions of this same object in the image where we want to recognize it a feature should, ideally, be invariant to transformations; i.e, invariant against scale, rotation or brightness changes. As a rule of thumb, the more invariant the better.

*Example of features extracted (right) from a reference target (left).*

#### 1.2. [Feature description](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_orb/py_orb.html)

Once features have been found we should find a suitable representation of the information they provide. This will allow us to look for them in other images and also to obtain a measure of how similar two detected features are when being compared. This is were descriptors roll in. A descriptor provides a representation of the information given by a feature and its surroundings. Once the descriptors have been computed the object to be recognized can then be abstracted to a [feature vector](https://en.wikipedia.org/wiki/Feature_(machine_learning)),  which is a vector that contains the descriptors of the keypoints found in the image with the reference object.

This is for sure a nice idea, but how can it actually be done? There are many algorithms that extract image features and compute its descriptors and, since I won’t go into much more detail (a whole post could be devoted only to this) if you are interested in knowing more take a look at [SIFT](http://aishack.in/tutorials/sift-scale-invariant-feature-transform-introduction/), [SURF](http://www.vision.ee.ethz.ch/~surf/eccv06.pdf), or [Harris](http://aishack.in/tutorials/harris-corner-detector/). The one we will be using was developed at the OpenCV Lab and it is called [ORB](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_orb/py_orb.html) (Oriented FAST and Rotated BRIEF). The shape and values of the descriptor depend on the algorithm used and, in our case,  the descriptors obtained will be binary strings.

With *OpenCV*, extracting features and its descriptors via the ORB detector is as easy as:

```python
img = cv2.imread('../reference/glyph_01.png', 0)

# Initiate ORB detector
orb = cv2.ORB_create()

# find the keypoints with ORB
kp = orb.detect(img, None)

# compute the descriptors with ORB
kp, des = orb.compute(img, kp)

# draw keypoints location, size and orientation
img2 = cv2.drawKeypoints(img, kp, img, color=(0,255,0), flags=4)

# resize image
width  = int(img2.shape[1] * 0.8)
height = int(img2.shape[0] * 0.8)
img3 = cv2.resize(img2, (width, height))

# display image
cv2.imshow('keypoints', img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

#### 1.3. [Feature matching](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html)

Once we have found the features of both the object and the scene were the object is to be found and computed its descriptors it is time to look for matches between them. The simplest way of doing this is to take the descriptor of each feature in the first set, compute the distance to all the descriptors in the second set and return the closest one as the best match (I should state here that it is important to choose a way of measuring distances suitable with the descriptors being used. Since our descriptors will be binary strings we will use [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance)). This is a brute force approach, and more sophisticated methods exist.

For example, and this is what we will be also using, we could check that the match found as explained before is also the best match when computing matches the other way around, from features in the second set to features in the first set. This means that both features match each other. Once the matching has finished in both directions we will take as valid matches only the ones that fulfilled the previous condition.

Another option to reduce the number of false positives would be to check if the distance to the second to best match is below a certain threshold.  If it is, then the match is considered valid.

**image**

Finally, after matches have been found, we should define some criteria to decide if the object has been found or not. For this I defined a threshold on the minimum number of matches that should be found. If the number of matches is above the threshold, then we assume the object has been found. Otherwise we consider that there isn’t enough evidence to say that the recognition was successful.

With OpenCV all this recognition process can be done in a few lines of code:

```python
MIN_MATCHES = 30

# read images in grayscale mode
cap   = cv2.imread('images/scene_ticket.jpg', 0)
model = cv2.imread('../reference/ticket.jpg', 0)

# ORB keypoint detector
orb = cv2.ORB_create()
# create brute force  matcher object
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

# Compute model keypoints and its descriptors
kp_model, des_model = orb.detectAndCompute(model, None)
# Compute scene keypoints and its descriptors
kp_frame, des_frame = orb.detectAndCompute(cap, None)

# Match frame descriptors with model descriptors
matches = bf.match(des_model, des_frame)
# Sort them in the order of their distance
matches = sorted(matches, key=lambda x: x.distance)

if len(matches) > MIN_MATCHES:
    # draw first 15 matches.
    cap = cv2.drawMatches(model, kp_model, cap, kp_frame,
                          matches[:MIN_MATCHES], 0, flags=2)
    # resize
    width  = int(cap.shape[1] * 0.3)
    height = int(cap.shape[0] * 0.3)
    cap2 = cv2.resize(cap, (width, height))
    # show result
    cv2.imshow('frame', cap2)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
else:
    print("Not enough matches found - %d/%d" % (len(matches), MIN_MATCHES))
```

On a final note and before stepping into the next step of the process I must point out that, since we want a real time application, it would have been better to implement a tracking technique and not just plain recognition. This is due to the fact that object recognition will be performed in each frame independently without taking into account previous frames that could add valuable information about the location of the reference object. Another thing to take into account is that, the easier to found the reference surface the more robust detection will be. In this particular sense, the reference surface I’m using might not be the best option, but it helps to understand the process.

### 2. [Homography estimation](https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_feature_homography/py_feature_homography.html)

Once we have identified the reference surface in the current frame and have a set of valid matches we can proceed to estimate the homography between both images. As explained before, we want to find the transformation that maps points from the target surface plane to the image plane. This transformation will have to be updated each new frame we process.

Since we have already found a set of matches between both images we can certainly find directly by any of the existing methods (I advance we will be using [RANSAC](https://en.wikipedia.org/wiki/Random_sample_consensus)) an homogeneous transformation that performs the mapping.

What we have is an object (a plane in this case) with known coordinates in the *World* coordinate system and we take a picture of it with a camera located at a certain position and orientation with respect to the World coordinate system. We will assume the camera works following the [pinhole model](https://en.wikipedia.org/wiki/Pinhole_camera_model), which roughly means that the rays passing through a 3D point *p* and the corresponding 2D point *u* intersect at *c*, the camera center. A good resource if you are interested in knowing more about the pinhole model can be found [here](http://alumni.media.mit.edu/~maov/classes/comp_photo_vision08f/lect/09_image_formation.pdf).

Although not entirely true, the pinhole model assumption eases our calculations and works well enough for our purposes. The *u*, *v* coordinates (coordinates in the image plane) of a point *p* expressed in the Camera coordinate system if we assume a pinhole camera can be computed as (the derivation of the equation is left as an exercise to the reader):

$$
\begin{equation}
    \begin{bmatrix}
        u \cdot k \\
        v \cdot k \\
        k
    \end{bmatrix}
    =
    A
    \begin{bmatrix}
        x_{cam} \\
        y_{cam} \\
        z_{cam}
    \end{bmatrix}
    \label{eq:pin_hole_model}
    \tag{1}
\end{equation}
$$

where the matrix

$$
\begin{equation}
    A =
    \begin{bmatrix}
        f_u & 0   & u_0\\
        0   & f_v & v_0\\
        0   & 0   & 1
    \end{bmatrix}
\end{equation}
$$

is know as *calibration matrix*, $(u_0, v_0)$ are the *projection of the optical center*, which is the position of the optical center in the image plane, $k$ is a scaling factor and $f_u, f_v$ are the *focal lengths*, the distance from the pinhole to the image plane.

Equation ($\ref{eq:pin_hole_model}$) tells us how the image is formed. However, as stated before, we know the coordinates of the point *p* in the World coordinate system and not in the Camera coordinate system, so we have to add another transformation that maps points from the World coordinate system to the Camera coordinate system. The transformation that tells us the coordinates in the image plane of a point *p* in the World coordinate system is then:

$$
\begin{equation}
    \begin{bmatrix}
        u \cdot k \\
        v \cdot k \\
        k
    \end{bmatrix}
    =
    A
    \begin{bmatrix}
        R & t
    \end{bmatrix}
    \begin{bmatrix}
        x_{world} \\
        y_{world} \\
        z_{world} \\
        1
    \end{bmatrix}
    \label{eq:pin_hole_model_extended}
    \tag{2}
\end{equation}
$$

Where $R$ is a $3 \times 3$ rotation matrix and $t$ is the translation vector that specifies the position of the reference target.

Luckily for us, since the points in the reference surface plane do always have its *z* coordinate equal to 0 (it is a surface!) we can simplify the transformation that we found above. It can be easily seen that the product of the *z* coordinate and the third column of the projection matrix will always be $0$ so we can drop this column and the *z* coordinate from the previous equation.

$$
\begin{equation}
    \begin{bmatrix}
        u \cdot k \\
        v \cdot k \\
        k
    \end{bmatrix}
    =
    A
    \begin{bmatrix}
        R_1 & R_2 & R_3 & t
    \end{bmatrix}
    \begin{bmatrix}
        x_{world} \\
        y_{world} \\
        0 \\
        1
    \end{bmatrix}
    =
    A
    \begin{bmatrix}
        R_1 & R_2 & t
    \end{bmatrix}
    \begin{bmatrix}
        x_{world} \\
        y_{world} \\
        1
    \end{bmatrix}
    =
    H
    \begin{bmatrix}
        x_{world} \\
        y_{world} \\
        1
    \end{bmatrix}
    \label{eq:pin_hole_model_simplified}
    \tag{3}
\end{equation}
$$

There are several methods that allow us to estimate the values of the homography matrix $H$, and you might be familiar with some of them. The one we will be using is **RANdom SAmple Consensus** ([RANSAC](https://en.wikipedia.org/wiki/Random_sample_consensus)).  RANSAC is an iterative algorithm used for model fitting in the presence of a large number of outliers and can be schematize as follows.

1. Choose a small subset of points uniformly at random.
2. Fit a model to that subset.
3. Find all remaining points that are "close" to the model and reject the rest as outliers.
4. Do this many times and choose the best model.

Since we cannot guarantee that all the matches we have found are actually valid matches we have to consider that there might be some false matches (which will be our outliers) and, hence, we have to use an estimation method that is robust against outliers.

For homography estimation the algorithm can be outlined with the following steps:

1. Randomly sample 4 matches.
2. Estimate Homography H.
3. Verify homography: search for other matches consistent with H.
4. Iterate until convergence.

Before seeing how OpenCV can handle this for us we should  discuss one final aspect of the algorithm, which is what does it mean that a match is consistent with $H$. What this mainly means is that if after estimating an homography we project into the target image the matches that were not used to estimate it then the projected points from the reference surface should be close to its matches in the target image. How close they should be to be considered consistent is up to you.
In OpenCV estimating the homography with RANSAC is as easy as:

```python
# assuming matches stores the matches found and
# returned by bf.match(des_model, des_frame)
# differenciate between source points and destination points
src_pts = np.float32([kp_model[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
dst_pts = np.float32([kp_frame[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
# compute Homography
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
```

Where 5.0 is the threshold distance to determine if a match is consistent with the estimated homography. If after estimating the homography we project the four corners of the reference surface on the target image and connect them with a line we should expect the resulting lines to enclose the reference surface in the target image. We can do this by:

```python
model = cv2.imread('../reference/ticket.jpg', 0)
frame = cv2.imread('images/scene_ticket.jpg', 0)
# Draw a rectangle that marks the found model in the frame
h, w = model.shape
pts = np.float32([[0, 0], [0, h - 1],
                  [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
# project corners into frame
dst = cv2.perspectiveTransform(pts, M)
# connect them with lines
img = cv2.polylines(frame, [np.int32(dst)], True, 255, 3, cv2.LINE_AA)

img2 = cv2.drawMatches(model, kp_model, img, kp_frame, matches[:MIN_MATCHES], 0, flags=2)
# resize
width  = int(img2.shape[1] * 0.4)
height = int(img2.shape[0] * 0.4)
img2 = cv2.resize(img2, (width, height))
# show
cv2.imshow('frame', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

### 3. Pose estimation from a plane

To locate the 3D model in the camera frame we need its pose estimation with respect the camera frame, that is a rotation matrix $R$ and a position vector $t$, that is a homogeneus transformation from points in the *World* frame to the *Camera* frame.
This values are the same of equation ($\ref{eq:pin_hole_model_simplified}$), and since at this point the homography matrix $H$ has been already estimated we could invert equation ($\ref{eq:pin_hole_model_simplified}$) and obtain:

$$
\begin{equation}
    \begin{bmatrix}
        R_1 & R_2 & t
    \end{bmatrix}
    =
    A^{-1}H
    \label{eq:homogeneus_transformation}
    \tag{4}
\end{equation}
$$

Equation ($\ref{eq:homogeneus_transformation}$) has still some problem.
First of all matrix $A$ is unknown, there are methods to evaluate an estimation (see [this](https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_calib3d/py_calibration/py_calibration.html), [this](https://www.learnopencv.com/approximate-focal-length-for-webcams-and-cell-phone-cameras/) and [this](http://ksimek.github.io/2012/08/13/introduction/)), but for now the code assume that the matrix is known *a priori* with some "rational" values.
Another problem is the absence of $R_3$, which is the estmate of the *z-axis* of the *World* frame. So it is necessary to add a step to evalluate $R_3$. It can be evaluated as $R_1 \times R_2$, however the values of $R_1$ and $R_2$ are affected by some estimation error and they can not result to be *orthonormal*, so its needed to correct them too.

*Remark: the correction of $R_1$ and $R_2$ is not important to the estimate of $R_3$ but for the representation of the entire matrix $R$! if $R_1$, $R_2$ and $R_3$ are not orthonormal the transformation $R$ rotates and **deforms** the points transformated resulting in the rotation and **deformation** of the 3D object rendered!*

The estimation of the basis considers the assumption that, since $R_1$ and $R_2$ are estimates of the real ${R_1}'$ and ${R_2}'$ (which are orthonormal), the angle between $R_1$ and $R_2$ will be not 90 degrees. Furthermore, the modulus of each of this vectors will be close to 1. From $R_1$ and $R_2$ we can easily compute an orthogonal basis -this meaning that the angle between the basis vectors will exactly be 90 degrees- that will be rotated approximately 45° degrees clockwise with respect to the basis formed by $R_1$ and $R_2$. This basis is the one formed by $c=R_1+R_2$ and  $d=c \times p=(R_1+R_2)\times (R_1\times R_2)$ in figure below. If the vectors that form this new basis $(c,d)$ are made unit vectors and rotated 45° degrees counterclockwise (note that once the vectors have been transformed into unit vectors rotating the basis is as easy as d’ = c / ||c|| + d / ||d|| and  c’ = c / ||c|| – d / ||d||), guess what? We will have an orthogonal basis which is pretty close to our original basis $(R_1,R_2)$. If we normalize this rotated basis we will finally get the pair of vectors we were looking for.

Once this basis $({R_1}',{R_2}')$ has been obtained it is trivial to get the value of $R_3$ as $R_1' \times R_2'$.
Below you can find the function which compute the estimate.

```python
def projection_matrix(camera_parameters, homography):
"""
 From the camera calibration matrix and the estimated homography
 compute the 3D projection matrix
 """
# Compute rotation along the x and y axis as well as the translation
homography = homography * (-1)
rot_and_transl = np.dot(np.linalg.inv(camera_parameters), homography)
col_1 = rot_and_transl[:, 0]
col_2 = rot_and_transl[:, 1]
col_3 = rot_and_transl[:, 2]
# normalise vectors
l = math.sqrt(np.linalg.norm(col_1, 2) * np.linalg.norm(col_2, 2))
rot_1 = col_1 / l
rot_2 = col_2 / l
translation = col_3 / l
# compute the orthonormal basis
c = rot_1 + rot_2
p = np.cross(rot_1, rot_2)
d = np.cross(c, p)
rot_1 = np.dot(c / np.linalg.norm(c, 2) + d / np.linalg.norm(d, 2), 1 / math.sqrt(2))
rot_2 = np.dot(c / np.linalg.norm(c, 2) - d / np.linalg.norm(d, 2), 1 / math.sqrt(2))
rot_3 = np.cross(rot_1, rot_2)
# finally, compute the 3D projection matrix from the model to the current frame
projection = np.stack((rot_1, rot_2, rot_3, translation)).T
return np.dot(camera_parameters, projection)
```

### 4. Model projection

The program currently only uses simple models in `.obj` format, because they are easy to process and render directly with bare Python without having to make use of other libraries such as OpenGL. The problem with complex models is that the amount of processing they require is way more than what my computer can handle. Since I want my application to be real-time, this limits the complexity of the models I am able to render.

The code I used to load the models is based on this [OBJFileLoader](http://www.pygame.org/wiki/OBJFileLoader) script that I found on Pygame’s website. I stripped out any references to OpenGL and left only the code that loads the geometry of the model. Once the model is loaded we just have to implement a function that reads this data and projects it on top of the video frame with the projection matrix we obtained in the previous section. To do so we take every point used to define the model and multiply it by the projection matrix. One this has been done, we only have to fill with color the faces of the model. The following function can be used to do so:

```python
def render(img, obj, projection, model, color=False):
    vertices = obj.vertices
    scale_matrix = np.eye(3) * 3
    h, w = model.shape

    for face in obj.faces:
        face_vertices = face[0]
        points = np.array([vertices[vertex - 1] for vertex in face_vertices])
        points = np.dot(points, scale_matrix)
        # render model in the middle of the reference surface. To do so,
        # model points must be displaced
        points = np.array([[p[0] + w / 2, p[1] + h / 2, p[2]] for p in points])
        dst = cv2.perspectiveTransform(points.reshape(-1, 1, 3), projection)
        imgpts = np.int32(dst)
        if color is False:
            cv2.fillConvexPoly(img, imgpts, (137, 27, 211))
        else:
            color = hex_to_rgb(face[-1])
            color = color[::-1] # reverse
            cv2.fillConvexPoly(img, imgpts, color)

    return img
```

There are two things to be highlighted from the previous function:

1. The scale factor: Since we don’t know the actual size of the model with respect to the rest of the frame, we may have to scale it (manually for now) so that it haves the desired size. The scale matrix allows us to resize the model.
2. I like the model to be rendered on the middle of the reference surface frame. However, the reference frame of the models is located at the center of the model. This means that if we project directly the points of the OBJ model in the video frame our model will be rendered on one corner of the reference surface. To locate the model in the middle of the reference surface we have to, before projecting the points on the video frame, displace the x and y coordinates of all the model points by half the width and height of the reference surface.
3. There is an optional color parameter than can be set to True. This is because some models also have color information that can be used to color the different faces of the model. I didn’t test it enough and setting it to True might result in some problems. It is not 100% guaranteed this feature will work.

### 5 TODO(ed): Kalman Filter

[This](https://docs.opencv.org/master/dc/d2c/tutorial_real_time_pose.html#gsc.tab=0) is an interesting article about using Kalman Filter in a AR project.


## 4.4 Decentralized SVRG with Exact Diffusion

### 4.4.1 Problem and decentralized stochastic algorithms

Consider the optimization problem:

$$\min_{x \in \mathbb{R}^d} \quad f(x) = \frac{1}{n}\sum_{i=1}^n [f_i(x) = \mathbb{E}_{\xi_i \sim D_i}\{F(x,\xi_i)\}]$$

where $f_i(x)$ is local to node $i$, and random variable $\xi_i$ denotes the local data that follows distribution $D_i$. Each node $i$ can locally evaluate stochastic gradient $\nabla F(x;\xi_i)$ (not the real gradient $\nabla f_i(x)$); it must communicate to access information from other nodes.

In Section 3.6, we discussed one widely-used approach - ATC-DSGD to solve the above problem. Given the network topology and the weight matrix, the ATC-DSGD will update as follows:

\begin{align}
x_i^{(k+1)} =  \sum_{j=1}^n w_{ij} \Big(x_j^{(k)} - \alpha \nabla F(x_j^{(k)};\xi_j^{(k)}) \Big), \quad  i=1,\cdots,n.
\end{align}

where $\xi_i^{(k)}$ is a realization of the random variable $\xi_i$ at iteration $k$. Furthermore, it is established in Section 3.6 that under smoothness and strongly-convex assumptions, ATC-DSGD suffers from a limiting bias as follows:

\begin{align}\label{dsgd-convergence}
\lim_{k\to \infty}\frac{1}{n}\sum_{i=1}^n \mathbb{E}\|x_i^{(k)} - x^\star\|^2 = O\Big(\underbrace{\frac{\alpha \sigma^2}{n} + \frac{\rho^2 \alpha^2 \sigma^2}{1-\rho}}_{\rm sto.\ bias} + \underbrace{\frac{\rho^2 \alpha^2 b^2}{(1-\rho)^2}}_{\rm inconsis.\ bias} \Big). \hspace{1cm} \mbox{(ATC-DSGD-Limiting-Bias)}
\end{align}

It is observed that ATC-DSGD suffers from two sources of bias:

- A stochastic limiting bias caused by the stochastic gradient noise $\sigma^2$;

- An inconsistency bias caused by the data heterogeneity $b^2 = \frac{1}{n}\sum_{i=1}^n\|\nabla f_i(x^\star)\|^2$

This motivates us that, to correct the limiting bias, we have to design algorithms that can remove both the stochastic  bias and inconsistency bias. In Section 4.2, we have discussed that SVRG can remove the stochastic bias when the problem is in the finite-sum form. In Section 4.3, we have discussed that Exact-diffusion can remove the inconsistency bias. A natural idea is to integrate SVRG to Exact-diffusion so that the new algorithm can converge **exactly** to the global solution $x^\star$ without any limiting bias.

### 4.4.2 Exact Diffusion with SVRG

In this section we disscuss the new algorithm. First, we assume each local $f_i(x) = \mathbb{E}_{\xi_i \sim D_i}\{F(x,\xi_i)\} = \frac{1}{M}\sum_{m=1}^M F_i(x; \xi_{m,i})$. In other words, we assume each $f_i(x)$ is in a **finite-sum** form. The Diffusion-SVRG algorithm is shown as follows. Each node $i$ will run the following algorithm in a parallel manner:

> \begin{align}
& \mbox{For $s = 1,2,\cdots$}\\
& \hspace{5mm} \tilde{x}_i = \tilde{x}_{i,s-1} \\
& \hspace{5mm} \tilde{\mu}_i = \frac{1}{M}\sum_{m=1}^M \nabla F(\tilde{x}_i, \xi_{m,i}) \\
& \hspace{5mm} x_{i}^0 = \tilde{x}_i \\
& \hspace{5mm} \mbox{For $k = 1,2,\cdots,K$}\\
& \hspace{15mm}\mbox{Randomly pick $m_{k} \in \{1,\cdots, M\}$ and compute the stochastic gradient as}\\
& \hspace{15mm}g^k_{i} = \nabla F(x_i^{k-1};\xi_{m_k,i}) - \nabla F(\tilde{x}_i;\xi_{m_k,i}) + \tilde{\mu}_i \\
& \hspace{15mm}\mbox{Update local variables $x_i$ as follows} \\
& \hspace{15mm}\psi_i^{k} = x_i^{k} - \alpha \nabla g^k_{i}  \\
& \hspace{15mm}\phi_i^{k} = \psi_i^{k} + x_i^{k} - \psi_i^{k-1} \\
& \hspace{15mm}x_i^{k+1} = \sum_{j=1}^n \bar{w}_{ij} \phi_j^{k} \\
& \hspace{5mm} \mbox{Set $\tilde{x}_{i,s} = x^K_i$}
\end{align}

When each $f_i(x)$ is smooth and strongly convex, and the gradient noise is unbiased and bounded, one can show the above Diffusion-SVRG algorithm can converge to the global solution $x^\star$ in a linear rate.

### 4.4.3 An example: least-square problem

In this section, we will show a demo on how to solve a decentralized least-square problem with decentralized SVRG with Exact Diffusion. Suppose $n$ computing nodes collaborate to solve the following problem:

$$\min_x \quad \frac{1}{n}\sum_{i=1}^n \|A_i x - b_i\|^2$$

where $\{A_i, b_i\}$ are local data held in node $i$.

#### 4.4.3.1 Set up Environment

```
import ipyparallel as ipp

rc = ipp.Client(profile="bluefog")
dview = rc[:]  # A DirectView of all engines
dview.block = True
```

```
%%px
import numpy as np
import bluefog.torch as bf
import torch
from bluefog.common import topology_util
import networkx as nx
import matplotlib.pyplot as plt

%matplotlib inline

bf.init()
```

#### 4.4.4.2 Generate local data $A_i$ and $b_i$

```
%%px

def generate_data(m, n):

    A = torch.randn(m, n).to(torch.double)
    x_o = torch.randn(n, 1).to(torch.double)
    ns = 0.1 * torch.randn(m, 1).to(torch.double)
    b = A.mm(x_o) + ns

    return A, b
```

```
%%px

def distributed_grad_descent(A, b, maxite=5000, alpha=1e-1):

    x_opt = torch.zeros(n, 1, dtype=torch.double)

    for _ in range(maxite):
        # calculate local gradient
        grad_local = A.t().mm(A.mm(x_opt) - b)

        # global gradient
        grad = bf.allreduce(grad_local, name="gradient")

        # distributed gradient descent
        x_opt = x_opt - alpha * grad

    grad_local = A.t().mm(A.mm(x_opt) - b)
    grad = bf.allreduce(grad_local, name="gradient")  # global gradient

    # evaluate the convergence of distributed gradient descent
    # the norm of global gradient is expected to 0 (optimality condition)
    global_grad_norm = torch.norm(grad, p=2)
    if bf.rank() == 0:
        print(
            "[Distributed Grad Descent] Rank {}: global gradient norm: {}".format(
                bf.rank(), global_grad_norm
            )
        )

    return x_opt
```

In the following code we run distributed gradient descent to achieve the global solution $x^\star$ to the optimization problem. To validate whether $x^\star$ is optimal, it is enough to examine $\frac{1}{n}\sum_{i=1}^n \nabla f_i(x^\star) = 0$.

```
%%px

m, n = 500, 10
A, b = generate_data(m, n)
x_opt = distributed_grad_descent(A, b, maxite=1000, alpha=1e-4)
```

    [stdout:3] [Distributed Grad Descent] Rank 0: global gradient norm: 1.4937122611144555e-12

#### 4.4.4.3 One step of SVRG with Exact Diffusion
In this section, we depict the convergence curve of the SVRG algorithm with Exact Diffusion  in a decentralized environment. We will utilize the $x^\star$ achieved by distributed gradient descent as the optimal solution. First, we define one step of the SVRG with Exact Diffusion.

```
%%px

def SVRG_exact_diffusion_one_step(x, A, b, pre_psi, alpha=1e-2):
    m = A.size()[0]
    x_0 = x.clone()
    grad_0 = A.t().mm(A.mm(x_0) - b)/m # Precompute the full gradient of the step
    for _ in range(m):
        # Select a random sample
        i = torch.randint(m,(1,)).item()
        sample = A[i:i+1]
        # Gradient estimation using SVRG
        grad_est_i =  sample.t()*(sample.mm(x)-b[i])
        grad_est_0 =   sample.t()*(sample.mm(x_0)-b[i])
        grad_est = grad_est_i - grad_est_0 + grad_0
        # Correct bias using Exact Diffusion
        psi = x-alpha*grad_est
        phi = psi + x - pre_psi
        # Communication with neighbors
        x = bf.neighbor_allreduce(phi)
        pre_psi = psi.clone()
    return x, psi
```

Next we run the SVRG algorithm with Exact Diffusion.

```
%%px

# Set topology as ring topology.
G = topology_util.RingGraph(bf.size())
bf.set_topology(G)

max_iter = 300

alpha = 5e-4

x = torch.zeros(n, 1, dtype=torch.double)  # Initialize x for exact diffusion
psi = x
rel_error = torch.zeros((max_iter, 1))

for ite in range(max_iter):
    if ite % 10 == 0:
        if bf.rank() == 0:
            print("Progress {}/{}".format(ite, max_iter))
    x, psi = SVRG_exact_diffusion_one_step(x, A, b, psi, alpha=alpha)
    rel_error[ite] = torch.norm(x - x_opt, p=2) / torch.norm(x_opt, p=2)
```

    [stdout:3]
    Progress 0/300
    Progress 10/300
    Progress 20/300
    Progress 30/300
    Progress 40/300
    Progress 50/300
    Progress 60/300
    Progress 70/300
    Progress 80/300
    Progress 90/300
    Progress 100/300
    Progress 110/300
    Progress 120/300
    Progress 130/300
    Progress 140/300
    Progress 150/300
    Progress 160/300
    Progress 170/300
    Progress 180/300
    Progress 190/300
    Progress 200/300
    Progress 210/300
    Progress 220/300
    Progress 230/300
    Progress 240/300
    Progress 250/300
    Progress 260/300
    Progress 270/300
    Progress 280/300
    Progress 290/300

```
# collect relative error
rel_error = dview.pull("rel_error", block=True)
rel_error_avg = sum(rel_error) / len(rc.ids)
```

```
import matplotlib.pyplot as plt

%matplotlib inline

plt.semilogy(rel_error_avg)

plt.xlabel("Iteration", fontsize=16)
plt.ylabel("Relative error", fontsize=16)
```

It can be observed from the above figure that the SVRG with Exact Diffusion can precisely converge to the optimal solution in a stochastic decentralized scenarios. The two segments of the convergence rate demonstrates two bias sources (from stochastic and decentralized optimization) are corrected by SVRG algorithm with Exact Diffusion during the optimization.

```

```


```
%matplotlib inline
```

```
# vector addition pt a = (a0,a1), pt b = (b0,b1)
a0,b0 = .5,.1
a1,b1=np.random.rand(2,1000)
```

```
idx = (a0+a1 > b0+b1)
fig,ax=plt.subplots()
ax.plot(a1[idx],b1[idx],'o',alpha=.1)
ax.plot(a0,b0,'rs',ms=15,alpha=.3)
ax.plot(0,a0-b0,'r^',ms=15,alpha=.3)
ax.plot(1-(a0-b0),1,'r^',ms=15,alpha=.3)
ax.arrow(0,0,a0,b0,width=.002,length_includes_head=True)
ax.arrow(a0,b0,0,a0-b0,width=.002,length_includes_head=True)
ax.arrow(a0,b0,1+b0-a0,1,width=.002,length_includes_head=True,color='g')
ax.arrow(0,0,1+b0-a0,1,width=.002,length_includes_head=True,color='g')
ax.axis((0,1.2,0,1.2))
ax.set_aspect(1)
ax.set_xlabel('a',fontsize=18)
ax.set_ylabel('b',fontsize=18)
ax.plot(np.linspace(0,2,3),np.linspace(0,2,3),'k--',lw=3.)
ax.set_title('prob = %3.3f,phat=%3.3f'%(1-(1-a0+b0)**2/2.,idx.np.mean()))
ax.add_patch(Rectangle((0,0),1,1,alpha=.2,lw=2.,color='gray'))
```

```
b0,b1 = np.random.rand(2,1000)
a0,a1 = np.random.rand(2,1000)
np.mean((a0+b0) > (a1+b1))
```

    0.50800000000000001

```
idx = (a0>b0)
np.mean(a0[idx]+a1[idx] > b0[idx]+b1[idx])
```

    0.76771653543307083

```
np.logical_and( a0>b0, (a0+a1) > (b0+b1)).np.mean() # 3/8 is exact solution
```

    0.38

```
np.logical_and( a0>b0, (a0+a1) > (b0+b1)).np.mean()
```

    0.39400000000000002

```
(a0>b0).np.mean()
```

    0.52000000000000002

```
import sympy as S
```

```
a0,b0 = S.symbols('a0 b0')
expr = S.integrate((1-(1-a0+b0)**2/2),(a0,b0,1))
S.integrate(expr,(b0,0,1)) # prob of (a0>b0) AND (a0+a1>b0+b1)
print 'conditional probability P(a0+a1>b0+b1|a0>b0) = %3.3f' % (float(_)/(0.5))
```

    conditional probability P(a0+a1>b0+b1|a0>b0) = 0.750

```

```


# Import

```python
#source
#region

#import
#region
import math
from sympy import *
import matplotlib.pyplot as plt
from numpy import linspace
import numpy as np
from sympy.codegen.cfunctions import log10
from sympy.abc import x,t,y
from sympy.plotting import plot
#endregion

#symbol declaration
#region
x, t = symbols('x t')
f = symbols('f', cls=Function)
#endregion
```

# Read Input

```python
#input, output
#region
def ReadInput(file):
    f = file.readline()
    (lowT, upT) = map(lambda s: N(s), file.readline().split(","))
    (lowX, upX) = map(lambda s: N(s), file.readline().split(","))
    (t0, x0) = map(lambda s: N(s), file.readline().split(","))
    epsilon = N(file.readline())
    return (f, lowT, upT, lowX, upX, t0, x0, epsilon)
#endregion
```

# Main Function

```python
def Pica1(f, deltaT, deltaX, t0, x0, M, L, epsilon, mode = ""):

    N = GetN(M, L, deltaT, deltaX, epsilon)
    xn = SymbolicIntegrate(f, t0, x0, N, mode)

    return xn

def Pica2(f, deltaT, t0, x0, M, L, epsilon, length = 69, mode = ""):

    xn = []
    segmentLength = 2 * deltaT / length
    n = (int)(length / 2)
    for i in range(-n, n + 1):
        xn.append([t0 + i * segmentLength, x0])

    xn = NumericIntegrate(f, xn, x0, segmentLength, epsilon, mode)

    return xn

def Pica(filename, length = None, M = None, L = None, deltaT = None, mode = ""):
    try:
        file = open(filename, "r")
        (f, lowT, upT, lowX, upX, t0, x0, epsilon) = ReadInput(file)
        f = sympify(f)
    except:
        raise ValueError("invalid Pica input")
    file.close()

    if not lowX< x0 <upX or not lowT< t0< upT:
        raise ValueError("invalid Pica input")

    if M is None:
        M = GetM(x, lowT, upT, lowX, upX)
    else:
        if M <= 0:
            raise ValueError("invalid Pica input")

    if L is None:
        L = GetL(x, lowT, upT, lowX, upX)
    else:
        if L < 0:
            raise ValueError("invalid Pica input")
    if L == 0:
        SymbolicIntergrate(f, t0, x0, 1, mode)

    deltaX = min(x0 - lowX, upX - x0)
    if deltaT is None:
        deltaT = min(deltaX / M, 1 / (2 * L), t0 - lowT, upT - t0)
    interval = (float(t0-deltaT), float(t0+deltaT))
    if length is None:
        return (Pica1(f, deltaT, deltaX, t0, x0, M, L, epsilon, mode), interval)
    return Pica2(f, deltaT, t0, x0, M, L, epsilon, length, mode)
```

# Main loop (integrate)

```python

#region
def NumericIntegrate(f, xn, x0, segmentLength, epsilon, mode = ""):
    n = (int) (len(xn)/2)
    segmentLength /=2
    maxError = -math.inf
    loop = 0
    while abs(maxError) > epsilon:
        if mode == "test":
            dx = []
        loop += 1
        maxError = -math.inf

        integral = 0
        for i in range(n, 0, -1):
            integral = integral - segmentLength * (f.subs([(t, xn[i][0]), (x, xn[i][1])]) + f.subs([(t, xn[i - 1][0]), (x, xn[i - 1][1])]))
            newValue = x0 + integral
            error = abs(xn[i - 1][1] - newValue)
            xn[i - 1][1] = newValue
            if(error > maxError): maxError = error
            if mode == 'test':
                dx.append((xn[i][0], error))

        integral = 0
        for i in range(n, 2 * n):
            integral = integral + segmentLength * (f.subs([(t, xn[i][0]), (x, xn[i][1])]) + f.subs([(t, xn[i + 1][0]), (x, xn[i + 1][1])]))
            newValue = x0 + integral
            error = abs(xn[i + 1][1] - newValue)
            xn[i + 1][1] = x0 + integral
            if(error > maxError): maxError = error
            if mode == 'test':
                dx.append((xn[i][0], error))

        if mode == "test":
            print("Lặp lần ", loop, " với max error = ", maxError)
            PlotPairs(dx)
            plt.show()
    return xn

def GetN(M, L, deltaT, deltaX, epsilon, mode = ""):
    h = deltaT * L
    N = 1
    error = M * deltaT
    while error > epsilon:
        N+=1
        error = error * h / N
    return N

def SymbolicIntegrate(f, t0, x0, N, mode = ''):
     xn = x0
     for i in range(0,N):
        if mode == 'test':
            print(xn.evalf(2))
        xn = x0 + integrate(f.replace(x,xn), (t,t0,t))
     return xn

#endregion

```

# Not implemented supremum finder

```python
# sup
#region
def GetM(f, lowT, upT, lowX, upX):
    #not implemented
    return 10
def GetL(f, lowT, upT, lowX, upX):
    #not implemented
    return 10

#endregion
```

# Plot

```python
#plot
#region
def PlotPairs(pairList):
    t,x = zip(*pairList)
    plt.scatter(t,x)

def PlotSymbol(symbolOutput):
    func, interval = symbolOutput
    #t = linspace(interval[0], interval[1], 1000)
    #func = t**3/3 + t**7/67
    plot((func, (t, interval[0], interval[1])))

def PlotBoth(symbolOutput, pairList):
    t1, x1 = zip(*pairList)
    plt.scatter(t1,x1)
    func, interval = symbolOutput
    t_vals = linspace(interval[0], interval[1], 1000)
    lam_x = lambdify(t, func, modules=['numpy'])
    x_vals = lam_x(t_vals)
    plt.plot(t_vals, x_vals)

def Plot(f, interval, label = ""):
    t_vals = linspace(interval[0], interval[1], 1000)
    lam_x = lambdify(t, f, modules = ['numpy'])
    x_vals = lam_x(t_vals)
    plt.plot(t_vals, x_vals)
    #plt.show()
#endregion

#Program
#region
```

# Test

```python
filename = "input2.txt"

result = Pica(filename, M = 2.5, L = 1)
result1 = Pica(filename, M = 2.5, L = 1, length = 31, mode = 'test')

print(result[0].evalf(2))
print("Khoảng hội tụ:", result[1])
PlotBoth(result, result1)
```

```python
filename = "input3.txt"

result = Pica(filename, M = 5, L = 10)
result1 = Pica(filename, length = 31, M = 12, L = 10)

print(result[0].evalf(2))
print("Khoảng hội tụ:", result[1])
PlotBoth(result, result1)

```

```python
filename = "input4.txt"

#result = Pica(filename, M = 15, L = 1.5)
result1 = Pica(filename, length = 31, M = 15, L = 1.5)

PlotPairs(result1)
#print(result)
#PlotBoth(result, result1)
```

```python
filename = "input1.txt"
result = Pica(filename, M = 2, L = 1)
result1 = Pica(filename, length = 31, M = 2, L = 1, mode = 'test')

print(result[0].evalf(2))
print("Khoảng hội tụ:", result[1])
PlotBoth(result, result1)
#Plot(sin(10*t) + cos(10*t), result[1])
#plt.show()
```

```python
filename = "input5.txt"

result1 = Pica(filename, length = 222, M = 50, L = 1)
PlotPairs(result1)
interval = (float(result1[0][0]), float(result1[len(result1)-1][0]))
Plot(cos(300*t), interval)
plt.show()
```

```python
filename = "input6.txt"

result = Pica(filename, M = 250, L = 100)
result1 = Pica(filename, length = 31, M = 250, L = 100, mode = 'test')

print(result[0].evalf(2))
print("Khoảng hội tụ:", result[1])
PlotBoth(result, result1)
#interval = (float(result1[0][0]), float(result1[len(result1)-1][0]))
#Plot(sin(100*t), interval)
```

```python

```

```python

```

Topic 5 - Solving Differential Equations/26.1.Pica/.ipynb_checkpoints/Pica-checkpoint.ipynb


### SETUP
Paste the code in [coupled_euler.py](../coupled_euler.py) in a python file in the working directory and make another new .py file

Now, use this command to import all the functions and class definitions in coupled_euler.py

```python
from coupled_euler import *
```

Also make the following imports,

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

### THE PROBLEM

#### CIRCUIT DIAGRAM

#### MATH

Applying KVL in two inner loops,
$$
\begin{equation}
    L\frac{dI_b}{dt} = C'^{-1}Q_2 - C^{-1}Q_3 - R I_b
\end{equation}
$$
$$
\begin{equation}
    L\frac{dI_b}{dt} = C'^{-1}Q_2 - C^{-1}Q_3 - R I_b
\end{equation}
$$

Now, applying KCL at node b,
$$
\begin{equation}
    I_a = I_b + I_c
\end{equation}
$$
In our case setting internal resistance R of inductors zero
$$
\begin{equation*}
    \frac{dQ_1}{dt} = -I_a, \frac{dQ_2}{dt} = -I_a - I_b, \frac{dQ_3}{dt} = I_b
\end{equation*}
$$
#### CODE
Now to play with initial value problem understand and use the following code which can also be found in [here](../coupled_oscillators.py).

```python

#l stands for inductance L , c1, c2 corresponds to C and C'
l,c1,c2 = 0.1,1,1.3
#resistance
r=0
ivp = set_problem(
    f=[lambda t,x,y,q1,q2,q3 : (q1/c1 - q2/c2 -r*x)/l, #Function associated with (1)
    lambda t,x,y,q1,q2,q3 :(q2/c2 - q3/c1 -r*y)/l, #Function associated with (2)
    lambda t,x,y,q1,q2,q3 : -x , #Function associated with dq1/dt
    lambda t,x,y,q1,q2,q3 : -x-y, #Function associated with dq2/dt
    lambda t,x,y,q1,q2,q3 : y], #Function associated with dq3/dt
    dom=(0,15), # Time Domain
    ini=(0,10000,10000,1,0,-1), #initial conditions in ordered tuple(t,I_a,I_b,q1,q2,q3)
    N=int(20000),# No. of nodes/ control step size
    vars = ("t","$I_a$","$I_b$","q1","q2","q3") # var names for labels
    )

d=ivp.rk4() # rk4 called to solve the ivp problem
fig,ax = plt.subplots(1,1)
ivp.jt_plot(ax,1) # plots I_a vs t on ax
ivp.jt_plot(ax,2) # plots I_b vs t on ax

#ivp.jt_plot(ax,3) # plots q1 vs t
#ivp.jt_plot(ax,5) # plots q3 vs t
#ivp.kj_plot(ax,3,5) # plots q3 vs q1
#ivp.kj_plot(ax,1,2) # plots I_b vs I_a on ax
plt.legend()
plt.show()
```

The result that I obtain for specifically the above Initial value problem,

which is clearly not what one expects.


## Measuring $b^{\dagger}b$

```python
import sys
sys.path.append('..')
from src.sympy_utilities import *#

import sympy as sp
import pickle

ho_ham = pickle.load(open("DATA/ho_hamiltonian.p", "rb"))

observable = bdag*b
print(observable)

fq = max_sympy_exponent(ho_ham)

xf = sp.Symbol('X^'+str(fq),commutative=False)
yf = sp.Symbol('Y^'+str(fq),commutative=False)
zf = sp.Symbol('Z^'+str(fq),commutative=False)
idf = sp.Symbol('I^'+str(fq),commutative=False)

nq = fq+1
b_ps = 0.5*xf+0.5*complex(0,1)*yf
bdag_ps = 0.5*xf-0.5*complex(0,1)*yf
commutation_lhs = xf*yf-yf*xf
commutation_rhs = 2*complex(0,1)*zf

```

    bd*b

```python
obs_ps = sp.simplify(observable.subs({b: b_ps, bdag: bdag_ps})).subs(commutation_lhs, commutation_rhs)
obs_ps = sp.expand(obs_ps)
obs_ps = sp.simplify(obs_ps.subs({xf*xf: idf, yf*yf: idf}))

obs_ps
```

$\displaystyle 0.5 I^{3} + 0.25 i X^{3} Y^{3} - 0.25 i Y^{3} X^{3}$

So this tells us that
\begin{equation}
    \langle b^{\dagger} b \rangle = \langle 0.5 I^3 \rangle + \langle 0.25iX^3Y^3 \rangle - \langle 0.25 i Y^3 X^3 \rangle.
\end{equation}
By substituting in the pauli matrices this is just
\begin{equation}
    \langle b^{\dagger} b \rangle = \begin{pmatrix} 0 & 0 \\ 0 & 1 \end{pmatrix}
\end{equation}

which is not unitary?!?!  But I guess observables only need to be Hermitian which this is with eigenvalues 0 and 1.  And three quantum circuits measuring each of the three expectectation values will make three unitary circuits.

```python
from src.sympy_utilities import identity_qubit_padded_H

identity_qubit_padded_H(obs_ps)
```

$\displaystyle 0.5 I^{0} I^{1} I^{2} I^{3} + 0.25 i I^{0} I^{1} I^{2} X^{3} Y^{3} - 0.25 i I^{0} I^{1} I^{2} Y^{3} X^{3}$

```python
from qiskit.aqua.operators import I,X,Y,Z

str_to_op = {'I': I, 'X': X, 'Y': Y, 'Z': Z}

tst = str(identity_qubit_padded_H(obs_ps))
print(tst)
```

    0.5*I^0*I^1*I^2*I^3 + 0.25*I*I^0*I^1*I^2*X^3*Y^3 - 0.25*I*I^0*I^1*I^2*Y^3*X^3

```python
import re

res = re.split('(\+|\-)',tst)
print(res)
term_lst = res[1].split('*')

coef = 1.
ops = [I for i in range(nq)]

for t in term_lst:
    if(len(t.split('^'))==1):
        if(t=='I'):
            coef*=1.0j
        else:
            coef*=float(t)
    else:
        op = t.split('^')
     #   print(op)
        ops[int(op[1])] = ops[int(op[1])] @ str_to_op[op[0]]
    #print(ops[3])

res = ops[0]
for i in range(1,len(ops)):
    res = res^ops[i]
res = coef*res

print(res)
```

```python
def observable_term_to_qiskit(expr, nq):
    print('expr={}'.format(expr))
    terms = expr.split('*')
    coef = 1.
    ops = [I for i in range(nq)]

    for t in terms:
        if(len(t.split('^'))==1):
            if(t=='I'):
                coef*=1.0j
            else:
                coef*=float(t)
        else:
            op = t.split('^')
     #   print(op)
            ops[int(op[1])] = ops[int(op[1])] @ str_to_op[op[0]]
    #print(ops[3])

    res = ops[0]
    for i in range(1,len(ops)):
        res = res^ops[i]

    res = coef*res
    return res
```

```python
from qiskit.aqua.operators import StateFn

tstSplit=re.split('(\+|\-)',tst)
circuit = 0
sign=1
for t in tstSplit:
    if(t=='+'):
        continue
    elif(t=='-'):
        sign*=-1
        continue
    print(observable_term_to_qiskit(t,nq))
    circuit+=sign*observable_term_to_qiskit(t,nq)
print(circuit)
observableCircuit = StateFn(circuit).adjoint()
print(observableCircuit)
```

    expr=0.5*I^0*I^1*I^2*I^3
    0.5 * IIII
    expr=0.5*I^0*I^1*I^2*I^3
    expr= 0.25*I*I^0*I^1*I^2*X^3*Y^3
    (-0.25+0j) * IIIZ
    expr= 0.25*I*I^0*I^1*I^2*X^3*Y^3
    expr= 0.25*I*I^0*I^1*I^2*Y^3*X^3
    (0.25+0j) * IIIZ
    expr= 0.25*I*I^0*I^1*I^2*Y^3*X^3
    SummedOp([
      0.5 * IIII,
      (-0.25+0j) * IIIZ,
      (-0.25+0j) * IIIZ
    ])
    OperatorMeasurement(SummedOp([
      0.5 * IIII,
      (-0.25-0j) * IIIZ,
      (-0.25-0j) * IIIZ
    ]))

```python
import pickle

ho_ham = pickle.load(open("DATA/ho_hamiltonian.p", "rb"))
aho_ham = pickle.load(open("DATA/aho_hamiltonian.p", "rb"))

import sys
sys.path.append('..')
from src.qiskit_utilities import *

trotter_step = pauli_string_to_trotter_step(ho_ham,1)

print(trotter_step)
```

    global phase: -4
          ┌───────┐   ┌───────┐
    q_0: ─┤ RZ(1) ├───┤ RZ(1) ├──
          ├───────┴┐  ├───────┴┐
    q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
          ├────────┤  ├────────┤
    q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
         ┌┴────────┴┐┌┴────────┴┐
    q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
         └──────────┘└──────────┘

```python
from qiskit import QuantumCircuit, transpile
from qiskit import (
    execute,
    Aer)
from qiskit.providers.aer.noise import NoiseModel
from qiskit import IBMQ
from qiskit.visualization import plot_histogram

provider = IBMQ.load_account()
```

    /home/chrisculver/miniconda3/envs/QuantumComputing/lib/python3.9/site-packages/qiskit/providers/ibmq/ibmqfactory.py:192: UserWarning: Timestamps in IBMQ backend properties, jobs, and job results are all now in local time instead of UTC.
      warnings.warn('Timestamps in IBMQ backend properties, jobs, and job results '

```python
#observableCircuit = StateFn(observable_term_to_qiskit('0.5*I^0*I^1*I^2*I^3',4)-observable_term_to_qiskit('0.5*I^0*I^1*I^2*Z^3',4)).adjoint()

fullCircuit = observableCircuit @ trotter_step @ Zero
print(fullCircuit)
```

    ComposedOp([
      OperatorMeasurement(SummedOp([
        0.5 * IIII,
        (-0.25-0j) * IIIZ,
        (-0.25-0j) * IIIZ
      ])),
      CircuitStateFn(
      global phase: -4
            ┌───────┐   ┌───────┐
      q_0: ─┤ RZ(1) ├───┤ RZ(1) ├──
            ├───────┴┐  ├───────┴┐
      q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
            ├────────┤  ├────────┤
      q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
           ┌┴────────┴┐┌┴────────┴┐
      q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
           └──────────┘└──────────┘
      )
    ])

```python
fullCircuit.eval()
```

    0j

```python
fullCircuit[1].to_circuit().draw()
```

global phase: -4
      ┌───────┐   ┌───────┐
q_0: ─┤ RZ(1) ├───┤ RZ(1) ├──
      ├───────┴┐  ├───────┴┐
q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
      ├────────┤  ├────────┤
q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
     ┌┴────────┴┐┌┴────────┴┐
q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
     └──────────┘└──────────┘

```python
obs1=StateFn(observable_term_to_qiskit('0.5*I^0*I^1*I^2*I^3',4)).adjoint()
circuit=(obs1@trotter_step@Zero)
print(circuit)
```

    expr=0.5*I^0*I^1*I^2*I^3
    ComposedOp([
      OperatorMeasurement(0.5 * IIII),
      CircuitStateFn(
      global phase: -4
            ┌───────┐   ┌───────┐
      q_0: ─┤ RZ(1) ├───┤ RZ(1) ├──
            ├───────┴┐  ├───────┴┐
      q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
            ├────────┤  ├────────┤
      q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
           ┌┴────────┴┐┌┴────────┴┐
      q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
           └──────────┘└──────────┘
      )
    ])

```python
circuit[1].to_circuit().draw()
```

global phase: -4
      ┌───────┐   ┌───────┐
q_0: ─┤ RZ(1) ├───┤ RZ(1) ├──
      ├───────┴┐  ├───────┴┐
q_1: ─┤ RZ(-2) ├──┤ RZ(-2) ├─
      ├────────┤  ├────────┤
q_2: ─┤ RZ(-1) ├──┤ RZ(-1) ├─
     ┌┴────────┴┐┌┴────────┴┐
q_3: ┤ RZ(-0.5) ├┤ RZ(-0.5) ├
     └──────────┘└──────────┘

```python
from qiskit import (execute, Aer)
from qiskit import ClassicalRegister
simulator = Aer.get_backend('qasm_simulator')
cr = ClassicalRegister(4,'nc')
print(nq)

qc = circuit[1].to_circuit()
qc.add_register(cr)
qc.measure([i for i in range(nq)],[i for i in range(nq)])
job = execute(qc, simulator, shots=1000)
result = job.result()
counts = result.get_counts(qc)
plot_histogram(counts)
```

```python
from qiskit.aqua.operators import CircuitSampler
from qiskit import BasicAer
import numpy as np
```

```python
sampler = CircuitSampler(backend=BasicAer.get_backend('qasm_simulator'))

sampled_circuit = sampler.convert(fullCircuit)
```

```python
sampled_circuit_results = sampled_circuit.eval()
```

```python
print('Sampled results:\n{}'.format(np.real(sampled_circuit_results)))
```

    Sampled results:
    0.0

```python
print(sampled_circuit)
```

    ComposedOp([
      OperatorMeasurement(SummedOp([
        0.5 * IIII,
        (-0.25-0j) * IIIZ,
        (-0.25-0j) * IIIZ
      ])),
      DictStateFn({'0000': 1.0})
    ])

```python

```

```python

```