# S1 - Sample-based implementation of Blahut-Arimoto iteration
This notebook is part of the supplementary material for:
Genewein T., Leibfried F., Grau-Moya J., Braun D.A. (2015) *Bounded rationality, abstraction and hierarchical decision-making: an information-theoretic optimality principle*, Frontiers in Robotics and AI.

More information on how to run the notebook on the accompanying [github repsitory](https://github.com/tgenewein/BoundedRationalityAbstractionAndHierarchicalDecisionMaking) where you can also find updated versions of the code and notebooks.

This notebook in mentioned in Section 2.3. Due to time- and space-limitations the results of this notebook are not in the paper.

## Disclaimer
This notebook provides a proof-of-concept implementation of a naive sample-based Blahut-Arimoto iteration scheme. Neither the code nor the notebook have been particularly polished or tested. There is a short theory-bit in the beginning of the notebook but most of the explanations are brief and mixed into the code as comments.

## Free energy rejection sampling
The solution to a free energy variational problem (Section 2.1 in the paper) has the form of a Boltzmann distribution
$$p(y) = \frac{1}{Z}p_0(y)e^{\beta U(y)},$$
where $Z=\sum_y p_0(y)e^{\beta U(y)}$ denotes the partition sum, $p_0(y)$ is a prior distribution and $U(y)$ is the utility function. The inverse temperature $\beta$ can be interpreted as a resource parameter and it governs how far the posterior $p(y)$ can deviate from the prior (measured as a KL-divergence) - see the paper Section 2.1 for details.

For a decision-maker, it suffices to obtain a sample from $p(y)$ and act according to that sample, rather than computing the full distribution $p(y)$. A simple scheme to sample from $p(y)$ is given by rejection sampling.

**Rejection sampling**
Goal: get a sample $y$ from the distribution $f(y)$. Draw from a uniform distribution $u\sim \mathcal{U}(0,1)$ and from a proposal distribution $y\sim g(y)$. If $u < \frac{f(y)}{M g(y)}$, accept the sample as a sample from $f(y)$, otherwise reject the sample and repeat. $M$ is a constant that ensures that $M g(y) \geq f(y)~\forall y$. Note that rejection sampling also works for sampling from an unnormalized distribution as long as $M$ is chosen accordingly.

For the free-energy problem, we want a sample from $p(y)\propto f(y) = p_0(y)e^{\beta U(y)}$. We choose $g(y)=p_0(y)$ and set $M=e^{\beta U_{max}}$, where $U_{max}=\underset{y}{max}~U(y)$

**Finally we get the following rejection sampling scheme:**
*  draw from a uniform distribution $u\sim \mathcal{U}(0,1)$
*  draw from the proposal distribution $x\sim p_0(y)$ (*the prior*)
  *  if $u < \frac{\exp(\beta U(y))}{\exp(\beta U_\mathrm{max})}$ accept the sample as a sample from the posterior $p(y)$
  *  otherwise reject the sample (and re-sample).

## Rate distortion rejection sampling
The solution to the rate distortion problem looks very similar to the Boltzmann distribution in the free-energy case. However, there is one crucial difference: in the free-energy case, the prior is an arbitrary distribution - in the rate distortion case, the prior is replaced by the marginal distribution, which leads to a set of self-consistent equations
$$\begin{align}
p^*(a|w)&=\frac{1}{Z(w)}p(a)e^{\beta U(a,w)} \\
p(a)&=\sum_w p(w)p(a|w)
\end{align}$$
*After* convergence of the Blahut-Arimoto iterations, the marginal $p(a)$ can just be treated like a prior and the rejection sampling scheme described above can straightforwardly be used. However, when initializing with an arbitary marginal distribution $\hat{p}(a)$ the iterations must be performed in a sample-based manner until convergence.

Here, we do this in a naive and very straightforward way: we represent $\hat{p}(a)$ simply through counters (a categorical distribution). Then we do the following:
1.  Draw a number of samples (a batch) from $\hat{p}^*(a|w)=\frac{1}{Z(w)}\hat{p}(a)e^{\beta U(a,w)}$ using the rejection sampling scheme.
2.  Update $\hat{p}(a)$ with the accepted samples obtained in step 1. There are different possibilities for the update step
    1.  Simply increase the counters for each accepted $a$ and re-normalize (no-forgetting)
    2.  Reset the counters for $a$ and use only the last batch of accepted samples to empirically estimate $p(a)$ (full-forgetting)
    3.  Use an exponentially decaying window over the last samples to update the empirical estimate of $p(a)$ (not implemented in this notebook).
    4.  Use a parametric model for $p_theta(a)$ and then perform some moment-matching or use a gradient-based update rule to adjust the parameters $\theta$ (not implemented in this notebook).
3.  Repeat until convergence (or here for simplicity: for a fixed number of steps)

Additionally, this notebook allows for some burn-in time, where after a certain number of iterations of 1. and 2. (i.e. after the "burn-in") the counters for $\hat{p}(a)$ are reset. This naive scheme seems to work but it is unclear how to choose the batch-size (number of samples from $\hat{p}^*(a|w)$ to take before performing an update step on $\hat{p}(a)$), how to set the burn-in phase, etc.

In the notebook below, you can try different batch-sizes and different burn-in times and you can compare full-forgetting against no forgetting (i.e. no resetting of the counters).

```julia
#only run this once
include("RateDistortionDecisionMaking.jl")
```

## Load the taxonomy example as a testbed

```julia
#load taxonomy example
using RateDistortionDecisionMaking, DataFrames, Gadfly, Distributions

#set up taxonomy example
include("TaxonomyExample.jl")
w_vec, w_strings, a_vec, a_strings, p_w, U = setuptaxonomy()

#pre-compute utilities, find maxima
U_pre, Umax = setuputilityarrays(a_vec,w_vec,U)

#initialize p(a) uniformly
num_acts = length(a_vec)
pa_init = ones(num_acts)/num_acts;

```

## Set up functions for sampling and run on the example from above

```julia
#Performs rejection sampling with a constant (scaled uniform) envelope
#using a softmax acceptance-rejection criterion.
#prop_dist .. proposal distribution (must be an instance of type ::Distribution)
#nsamps ..... desired number of samples (scalar)
#maxsteps ... maximum number of acceptance-rejection steps (scalar, must be ≧ nsamps)
#β .......... softmax parameter
#lh ......... likelihood value (vector of length N)
#maxlh ...... maximum value that the likelihood can take (scalar)
function rej_samp_const(prop_dist::Distribution, nsamps::Integer, maxsteps::Integer, β::Number, lh::Vector, maxlh::Number)
    #initialize
    samps = zeros(nsamps)
    acc_cnt = 0  #acceptance-counter
    if(maxsteps < nsamps)
        maxsteps = nsamps
    end

    k=0 #use this to make sure that k is still available after the loop
    for k in 1:maxsteps
        u=rand(1) #sample from uniform between (0,1)
        index = rand(prop_dist) #sample from proposal

        ratio = exp(β*lh[index])/exp(β*maxlh)
        if u[1]<ratio #explicit indexing is needed to get a float, since the >= can not handle arrays
            #if we enter here, accept the sample
            acc_cnt = acc_cnt + 1
            samps[acc_cnt] = index

            if(acc_cnt == nsamps)
                #we have enough samples, exit loop
                break
            end
        end
    end

    if(k==maxsteps)
        warn("[RejSampConst] Maximum number of steps reached - number of samples is potentially lower than nsamps!\n")
    end

    #store all accepted samples (this can be less than nsamps if maxsteps is too low or acceptance-rate is low)
    samples = samps[1:acc_cnt]

    #compute acceptance ratio
    acc_ratio = acc_cnt/k

    return samples, acc_ratio
end
```

```julia
#marginal is simply represented by counters (i.e. by frequencies)
function init_marginal_representation_ctrs(pa_init::Vector)
    return pa_init
end

#this updates the marginal over actions p(a) using a counter-representation
#this function counts the number of times each action-index occurs in sampled_indices
#these counts are then added to the current marginal_ctrs. Optionally, the counters are reset
#before adding the new samples (=hard forgetting).
function update_marginal_ctrs(sampled_indices::Vector, marginal_ctrs::Vector; reset_ctrs::Bool=false)
    #TODO: perhaps replace hard-resetting with an exponential decay?

    p_ctrs = marginal_ctrs
    card_p = length(p_ctrs)

    #reset counters for marginal? (make sure every entry is non-zero!)
    if reset_ctrs
        p_ctrs = ones(card_p)/card_p
    end

    #update marginal counters using a histogram to do the counting (bin-borders have to be set manually!)
    e,p_counts = hist(sampled_indices,0.5:1:(card_p+0.5))
    p_ctrs = p_ctrs + p_counts

    #normalize to get the updated marginal
    p_sampled = p_ctrs / sum(p_ctrs)

    return p_sampled, p_ctrs  #return the probability-vector, but also the representation of the marginal (as counts)
end

#function for BA sampling
#burnin_ratio specifies the ratio of outer iterations that will not count
#towards computation of the final marginal distribution (counters will be blocked)
#reset_marginal_ctrs specifies whether the marginal is computed with the samples of the last
#iteration only (=hard forgetting by resetting counters) or whether the marginal
#is computed with all samples of all iterations (=no forgetting)
function BAsampling(pa_init::Vector, β::Number, U_pre::Matrix, Umax::Vector, pw::Vector,
                    nsteps_marginalupdate::Integer, nsteps_conditionalupdate::Integer;
                    burnin_ratio::Real=0.7, max_rejsamp_steps::Integer=200,
                    compute_performance::Bool=false, performance_as_dataframe::Bool=false,
                    performance_per_iteration::Bool=false,
                    init_marg_func::Function=init_marginal_representation_ctrs,
                    update_marg_func::Function=update_marginal_ctrs, update_func_args...)

    #compute cardinality, check size of U_pre
    card_a = length(pa_init)
    card_w = length(pw)
    if size(U_pre) != (card_a, card_w)
        error("Size mismatch of U_pre and pa_init or pw!")
    end

    #check that burnin_ratio is really a ratio
    if (burnin_ratio < 0) || (burnin_ratio > 1)
        error("burnin_ratio must be a number between 0 and 1.")
    end

    #if performance measures don't need to be returned, don't compute them per iteration
    if compute_performance==false
        performance_per_iteration = false
    end
    #preallocate if necessary
    if performance_per_iteration
        I_i = zeros(maxiter)
        Ha_i = zeros(maxiter)
        Hagw_i = zeros(maxiter)
        EU_i = zeros(maxiter)
        RDobj_i = zeros(maxiter)
    end

    #initialize sampling distributions
    pw_dist = Categorical(pw) #proposal distribution
    pagw_ctrs = ones(card_a, card_w) #counters for conditional distribution
    pa_sampled = pa_init #marginal distribution

    #initialize the marginal representation
    pa_ctrs = init_marg_func(pa_init)

    burnin_triggered=false
    #outer loop - in each iteration the marginal is updated
    iter=0
    for iter in 1:nsteps_marginalupdate
        a_samples = zeros(nsteps_conditionalupdate)  #this will hold the samples from p(a|w) during inner loop

        #inner loop - in each step a sample is drawn from the conditional and stored for
        #for the batch-update of the marginal
        for j in 1:nsteps_conditionalupdate
            #draw a w sample
            w_samp = rand(pw_dist)

            #draw a sample from p(a|w) using the current estimate of p(a) as proposal distribution using rejection sampling
            agw_samp, acc_ratio = rej_samp_const(Categorical(pa_sampled), 1, max_rejsamp_steps, β, U_pre[:,w_samp], Umax[w_samp])
            a_samples[j] = agw_samp[1]

            #update conditional counters
            pagw_ctrs[agw_samp, w_samp] += 1
        end

        #very simple burn-in: simply reset counters
        if (iter >(nsteps_marginalupdate)*burnin_ratio) && (!burnin_triggered)
            burnin_triggered = true
            pagw_ctrs = ones(card_a, card_w)
        end

        #update marginal with samples drawn in inner loop
        pa_sampled, pa_ctrs = update_marg_func(a_samples, pa_ctrs; update_func_args...)

        #compute entropic quantities (if requested with additional parameter)
        if performance_per_iteration
            #compute sample-based conditional p(a|w)
            pagw_sampled = zeros(card_a, card_w)
            for i in 1:card_w
                pagw_sampled[:,i] = pagw_ctrs[:,i] / sum(pagw_ctrs[:,i])
            end
            I_i[iter], Ha_i[iter], Hagw_i[iter], EU_i[iter], RDobj_i[iter] = analyzeBAsolution(pw, pa_sampled, pagw_sampled, U_pre, β)
        end
    end

    #compute conditionals using the sample-counts of the previous inner loops
    #the burn-in parameter specifies how many of the inner loops are discarded
    pagw_sampled = zeros(card_a, card_w)
    for i in 1:card_w
        pagw_sampled[:,i] = pagw_ctrs[:,i] / sum(pagw_ctrs[:,i])
    end

    #return results
    if compute_performance == false
        return pagw_sampled, pa_sampled
    else
        if performance_per_iteration == false
            #compute performance measures for final solution
            I, Ha, Hagw, EU, RDobj = analyzeBAsolution(pw, pa_sampled, pagw_sampled, U_pre, β)
        else
            #"cut" valid results from preallocated vector
            I = I_i[1:iter]
            Ha = Ha_i[1:iter]
            Hagw = Hagw_i[1:iter]
            EU = EU_i[1:iter]
            RDobj = RDobj_i[1:iter]
        end

        #if needed, transform to data frame
        if performance_as_dataframe == false
            return pagw_sampled, pa_sampled, I, Ha, Hagw, EU, RDobj
        else
            performance_df = performancemeasures2DataFrame(I, Ha, Hagw, EU, RDobj)
            return pagw_sampled, pa_sampled, performance_df
        end
    end

end
```

```julia
#example call and also plot evolution of performance measueres
maxiter = 10000
β = 1.2
nsteps_marg = 500
nsteps_cond = 750
pagw_s,pa_s,perf = BAsampling(pa_init, β, U_pre, Umax, p_w, nsteps_marg, nsteps_cond,
                              burnin_ratio=0.7, max_rejsamp_steps=500, reset_ctrs=false,
                              compute_performance=true, performance_as_dataframe=true, performance_per_iteration=true)

plt_cond = visualizeBAconditional(pagw_s,a_vec,w_vec,a_strings,w_strings)

#instead of using a range of β-values (as for the standard-performance plot),
#use a vector indicating the iteration
niter = size(perf,1)
plt_perf_entropy, plt_perf_utility, plt_rateutility = plotperformancemeasures(perf,collect(1:niter),
                                                      suppress_vis=true, xlabel_perf="Iteration")

#TODO: somehow the "Iteration" label above doesn't seem to work!

display(vstack(plt_perf_entropy, plt_perf_utility))
display(plt_cond)
```

## [Interact] Change the parameters in the code-cell above to explore the sampling scheme and its solutions

## Compare sampling-solutions against analytical results

Below, we will average over several sampling-runs at different temperatures $\beta$ to see a difference between the analytical solutions and the sample-based solutions (with and without forgetting).

```julia
#compute theoretical result for rate-dutility curve
ε = 0.0001 #convergence critetion for BAiterations
maxiter = 10000
β_sweep = collect(0.01:0.05:3)
nβ = length(β_sweep)

#preallocate
I = zeros(nβ)
Ha = zeros(nβ)
Hagw = zeros(nβ)
EU = zeros(nβ)
RDobj = zeros(nβ)

#sweep through β values and perfomr Blahut-Arimoto iterations for each value
for i=1:nβ
    pagw, pa, I[i], Ha[i], Hagw[i], EU[i], RDobj[i] = BAiterations(pa_init, β_sweep[i], U_pre, p_w, ε, maxiter,compute_performance=true)
end

#show rate-utility curve (shaded region is theoretically infeasible)
perf_res_analytical = performancemeasures2DataFrame(I, Ha, Hagw, EU, RDobj);
plot_perf_entropy, plot_perf_util, plot_rateutility = plotperformancemeasures(perf_res_analytical, β_sweep, suppress_vis=true);
display(plot_rateutility)
```

```julia
#run the smapling for different temperatures and repeat each run n-times
#then plot these results against the rate-utility curve (based on the closed-form solutions)
βrange_samp = [0.1, 0.25, 0.5, 0.8, 1.2, 1.4, 1.6, 2]
#βrange_samp = [1.2, 2]

nruns = 10; #number of runs per β point

nsteps_marg = 500
nsteps_cond = 750
burnin_ratio = 0.8
max_rejsamp_steps=500 #maximum number of steps for sampling from the conditional

nconditions = size(βrange_samp,1)*nruns
I_sampled = zeros(2*nconditions)
Ha_sampled = zeros(2*nconditions)
EU_sampled = zeros(2*nconditions)
βval = zeros(2*nconditions)
ResetCtrs = falses(2*nconditions)

#first run with reset_ctrs to false
reset_ctrs = false #if true, the ctrs for the marginal are reset in each iteration (="hard" forgetting)
for b in 1:nconditions
    println("BA Sampling, run $b of $(2*nconditions)")
    β = βrange_samp[round(Int,ceil(b/nruns))]

    pagw_s, pa_s, I, Ha, Hagw, EU, RDobj = BAsampling(pa_init, β, U_pre, Umax, p_w, nsteps_marg, nsteps_cond,
                                               reset_ctrs=reset_ctrs, burnin_ratio=burnin_ratio,
                                               max_rejsamp_steps=max_rejsamp_steps, compute_performance=true)

    I_sampled[b] = I
    Ha_sampled[b] = Ha
    EU_sampled[b] = EU
    βval[b] = β
    ResetCtrs[b] = reset_ctrs
end

#second run with reset_ctrs to true
reset_ctrs = true #if true, the ctrs for the marginal are reset in each iteration (="hard" forgetting)
for b in 1:nconditions
    println("BA Sampling, run $(nconditions+b) of $(2*nconditions)")
    β = βrange_samp[round(Int,ceil(b/nruns))]

    pagw_s, pa_s, I, Ha, Hagw, EU, RDobj = BAsampling(pa_init, β, U_pre, Umax, p_w, nsteps_marg, nsteps_cond,
                                               reset_ctrs=reset_ctrs, burnin_ratio=burnin_ratio,
                                               max_rejsamp_steps=max_rejsamp_steps, compute_performance=true)

    I_sampled[nconditions+b] = I
    Ha_sampled[nconditions+b] = Ha
    EU_sampled[nconditions+b] = EU
    βval[nconditions+b] = β
    ResetCtrs[nconditions+b] = reset_ctrs
end

#wrap data in DataFrame for convenient plotting
res_sampled = DataFrame(β=βval, I_aw=I_sampled, H_a=Ha_sampled, E_U=EU_sampled, Forgetting=ResetCtrs)

```

```julia
#compute theoretical result for same set of temperatures
ε = 0.0001 #convergence critetion for BAiterations
maxiter = 10000
nβ = length(βrange_samp)

#preallocate
I = zeros(nβ)
Ha = zeros(nβ)
Hagw = zeros(nβ)
EU = zeros(nβ)
RDobj = zeros(nβ)

#sweep through β values and perfomr Blahut-Arimoto iterations for each value
for i=1:nβ
    pagw, pa, I[i], Ha[i], Hagw[i], EU[i], RDobj[i] = BAiterations(pa_init, βrange_samp[i], U_pre, p_w, ε, maxiter,compute_performance=true)
end

#show rate-utility curve (shaded region is theoretically infeasible)
perf_res_analytical_samp = performancemeasures2DataFrame(I, Ha, Hagw, EU, RDobj)
perf_res_analytical_samp[:β] = βrange_samp;
```

```julia
#plot the solutions from the sampling runs into the (analytical) rate-utility plot
plot(Guide.ylabel("E[U]"), Guide.xlabel("I(A;W) [bits]"),
     Guide.title("Rate-Utility curve (dots show sampling solutions)"), BAtheme(), BAdiscretecolorscale(2),
     layer(res_sampled,y="E_U",x="I_aw",Geom.point,color="Forgetting"),
     layer(perf_res_analytical_samp,y="E_U",x="I_aw",Geom.point),
     layer(perf_res_analytical,y="E_U",x="I_aw",Geom.line),
     layer(perf_res_analytical,y="E_U",x="I_aw",ymin="E_U",ymax=ones(size(perf_res_analytical,1))*maximum(perf_res_analytical[:E_U]),
     Geom.ribbon,BAtheme(default_color=colorant"green"))
    )
```

```julia
#compute the mean for each group of points (that were produced with the same beta and same forgetting setting)
res_samp_aggregated = aggregate(res_sampled, [:β,:Forgetting], mean)

#plot the mean-solutions from the sampling runs into the (analytical) rate-utility plot
plt_samp = plot(Guide.ylabel("E[U]"), Guide.xlabel("I(A;W) [bits]"),
     Guide.title("Rate-Utility curve (dots show mean sampling solutions)"), BAtheme(), BAdiscretecolorscale(2),
     layer(res_samp_aggregated,y="E_U_mean",x="I_aw_mean",Geom.point,Geom.line(preserve_order=true),color="Forgetting"),
     layer(perf_res_analytical_samp,y="E_U",x="I_aw",Geom.point),
     layer(perf_res_analytical,y="E_U",x="I_aw",Geom.line),
     layer(perf_res_analytical,y="E_U",x="I_aw",ymin="E_U",ymax=ones(size(perf_res_analytical,1))*maximum(perf_res_analytical[:E_U]),
     Geom.ribbon,BAtheme(default_color=colorant"green"))
    )
```

```julia
#store the plots
#draw(SVG("Figures/RateUtilityCurve.svg", 8.5cm, 7cm), plot_rateutility)

#draw(SVG("Figures/SamplingCond.svg", 8.5cm, 9cm), plt_cond)
#draw(SVG("Figures/BASampling.svg", 13cm, 11cm), plt_samp)

#plot_samp = vstack(plt_samp, plt_cond)
#draw(SVG("Figures/SamplingResults.svg", 18cm,16cm),plot_samp)
```

```julia
#try changing the number of inner-/outer-loop iterations
#try changing the burn-in ratio
#try soft-forgetting (with exponential-decay window)

#forgetting seems to do better than no forgetting (in terms of being closer to the rate-utility curve),
#but it also seems that the points with forgetting tend to have a lower I(A;O) (and also a lower E[U]) - even though
#the temperatures are the same.
```


```python
#Importando sympy
from sympy import *

#Impresión para "jupyter notebook"
init_printing(use_latex = 'mathjax')
```

```python
#Variables del Hamiltoniano
var('x m hbar')
```

$$\left ( x, \quad m, \quad \hbar\right )$$

```python
#Definición de función: user_variables
def user_variables(yn):
    if yn == 1:
        variable = ''
        a = 0
        print("\nAñada las variables simbólicas que requiera. Cuando haya finalizado, ingrese 'done'.")
        while (variable != 'done'):
            a = a + 1
            variable = input("var_"+str(a)+" = ")
            if variable != 'done':
                var(variable)
```

## Teoría de perturbaciones

Consiste en resolver un sistema perturbado(se conoce la solución al no perturbado), y donde el interés es conocer la contribución de la parte perturbada $H'$ al nuevo sistema total.

$$ H = H^{0} + H'$$

Para sistemas no degenerados, la corrección a la energía a primer orden se calcula como

$$ E_{n}^{(1)} = \int\psi_{n}^{(0)*} H' \psi_{n}^{(0)}d\tau$$

** Tarea 1 : Programar esta ecuación si conoces $H^{0}$ y sus soluciones. **

```python
#Añadir variables simbólicas del usuario
print("El programa ya reconoce las variables simbolicas x, m y hbar.")
print("\n¿Desea declarar más variables? (0:No; 1:Sí)")
user_variables(int(input()))
```

    El programa ya reconoce las variables simbolicas x, m y hbar.

    ¿Desea declarar más variables? (0:No; 1:Sí)
    1

    Añada las variables simbólicas que requiera. Cuando haya finalizado, ingrese 'done'.
    var_1 = c1
    var_2 = c2
    var_3 = alpha
    var_4 = pi
    var_5 = done

```python
print("Introduzca la función de onda del sistema no perturbado.")
psi = sympify(input("psi = "))
```

    Introduzca la función de onda del sistema no perturbado.
    psi = ((alpha/pi)**(1/4))*(exp((-(alpha*x**2))/2))

```python
psi
```

$$\frac{\sqrt[4]{\alpha}}{\sqrt[4]{\pi}} e^{- \frac{\alpha x^{2}}{2}}$$

```python
print("Introduzca la perturbación del sistema.")
Hp = sympify(input("H' = "))
```

    Introduzca la perturbación del sistema.
    H' = c1*x**3+c2*x**4

```python
Hp
```

$$c_{1} x^{3} + c_{2} x^{4}$$

```python
E1 = integrate(psi*Hp*psi, (x, -oo, oo))
E1
```

$$\begin{cases} \frac{3 c_{2}}{4 \alpha^{2}} & \text{for}\: \left|{\operatorname{periodic_{argument}}{\left (\alpha,\infty \right )}}\right| < \frac{\pi}{2} \\\int_{-\infty}^{\infty} \frac{\sqrt{\alpha}}{\sqrt{\pi}} \left(c_{1} x^{3} + c_{2} x^{4}\right) e^{- \alpha x^{2}}\, dx & \text{otherwise} \end{cases}$$

Y la corrección a la función de onda, también a primer orden, se obtiene como:

 $$ \psi_{n}^{(1)} = \sum_{m\neq n} \frac{\langle\psi_{m}^{(0)} | H' | \psi_{n}^{(0)} \rangle}{E_{n}^{(0)} - E_{m}^{(0)}} \psi_{m}^{(0)}$$

**Tarea 2: Programar esta ecuación si conoces $H^{0}$ y sus soluciones. **

```python
### Solución
```

**Tarea 3: Investigue las soluciones a segundo orden y también programe las soluciones. **

```python
### Solución

```

**Tarea 4.  Resolver el átomo de helio aplicando los programas anteriores.**

```python

```

**Tarea 5: Método variacional-perturbativo. **

Este método nos permite estimar de forma precisa $E^{(2)}$ y correcciones perturbativas de la energía de órdenes más elevados para el estado fundamental del sistema, sin evaluar sumas infinitas. Ver ecuación 9.38 del libro.

**Resolver el átomo de helio, considerando este método (sección 9.4), como mejor le parezca. **

**Tarea 6. Revisar sección 9.7. **

Inicialmente a mano, y sengunda instancia favor de intentar programar sección del problema, i.e. integral de Coulomb  e integral de intercambio.

## Siguiente: Segunda parte, Octubre
Simetrías moleculares y Hartree-Fock

```python

```


```python
import numpy as np
import sympy as sym
import numba
import pydae.build as db

```

```python
S_b = 90e3
U_b = 400.0
Z_b = U_b**2/S_b
I_b = S_b/(np.sqrt(3)*U_b)
Omega_b = 2*np.pi*50
R_s = 0.023/Z_b
R_r = 0.024/Z_b
X_s = 0.086/Z_b
X_r = 0.196/Z_b
X_m = 3.7/Z_b

X_0 = X_s + X_m
X1  = X_s + X_r*X_m/(X_r + X_m)
T10 = (X_r + X_m)/(Omega_b*R_r)
X1
```

    0.15307854209445587

```python
S_b = 90e3

U_b = 400.0
Z_b = U_b**2/S_b
I_b = S_b/(np.sqrt(3)*U_b)
Omega_b = 2*np.pi*50
R_s = 0.023/Z_b
R_r = 0.024/Z_b
X_s = 0.086/Z_b
X_r = 0.196/Z_b
X_m = 3.7/Z_b

X_0 = X_s + X_m
X1  = X_s + X_r*X_m/(X_r + X_m)
#X1  = X_s+X_m - X_m**2/(X_r+X_m)
X1
```

    0.15307854209445587

```python
S_b = 90e3
U_b = 400.0
Z_b = U_b**2/S_b
I_b = S_b/(np.sqrt(3)*U_b)
Omega_b = 2*np.pi*50
R_s = 0.023/Z_b
R_r = 0.024/Z_b
X_s = 0.086/Z_b
X_r = 0.196/Z_b
X_m = 3.7/Z_b

X_0 = X_s + X_m
X1  = X_s + X_r*X_m/(X_r + X_m)
X1  = X_s - X_m**2/X_r

T10 = (X_r + X_m)/(Omega_b*R_r)

params = {'S_b':S_b,'U_b':U_b,'I_b':I_b,
          'R_s':R_s,'X_0':X_0,'X1':X1,'T10':T10,   # synnchronous machine d-axis parameters
          'H_m':3.5,'Omega_b':2*np.pi*50,
          'v_0':1,'theta_0':0.0,
          'X_l':0.05}

u_ini_dict = {'P_h':0.1, 'Q_h':0.0}  # for the initialization problem
u_run_dict = {'tau_m':0.8,'Q_c':0.0}  # for the running problem (here initialization and running problem are the same)

x_list = ['omega_r','e1d','e1q']    # [inductor current, PI integrator]
y_ini_list = ['i_d','i_q','v_h','theta_h','tau_m','Q_c'] # for the initialization problem
y_run_list = ['i_d','i_q','v_h','theta_h','P_h','Q_h'] # for the running problem (here initialization and running problem are the same)

sys_vars = {'params':params,
            'u_list':u_run_dict,
            'x_list':x_list,
            'y_list':y_run_list}

exec(db.sym_gen_str())  # exec to generate the required symbolic varables and constants
```

```python
v_d =  -v_h*sin(theta_h)
v_q =  v_h*cos(theta_h)

tau_e = e1d*i_d + e1q*i_q

sigma = 1-omega_r

domega_r = 1/(2*H_m)*(tau_m - tau_e)
de1d = Omega_b*sigma*e1q - (e1d + (X_0 - X1)*i_q)/T10
de1q =-Omega_b*sigma*e1d - (e1q - (X_0 - X1)*i_d)/T10

g_1 = R_s*i_d - X1*i_q - v_d + e1d
g_2 = R_s*i_q + X1*i_d - v_q + e1q
g_3 = -P_h - (v_h*v_0*sin(theta_h - theta_0))/X_l
g_4 = -Q_c - Q_h + (v_h*v_0*cos(theta_h - theta_0))/X_l - v_h**2/X_l
g_5 = -P_h - (v_d*i_d + v_q*i_q)
g_6 = -Q_h - (v_q*i_d - v_d*i_q)

h_1 = I_b*(i_d*i_d + i_q*i_q)**0.5

sys = {'name':'imib_milano_3rd',
       'params':params,
       'f':[domega_r,de1d,de1q],
       'g':[g_1,g_2,g_3,g_4,g_5,g_6],
       'g_ini':[g_1,g_2,g_3,g_4,g_5,g_6],
       'x':x_list,
       'y_ini':y_ini_list,
       'y':y_run_list,
       'u_run_dict':u_run_dict,
       'u_ini_dict':u_ini_dict,
       'h':[h_1]}

sys = db.system(sys)
db.sys2num(sys)
```

```python
sys['f']
```

$\displaystyle \left[\begin{matrix}\frac{- e1d i_{d} - e1q i_{q} + \tau_{m}}{2 H_{m}}\\e1q \left(314.159265358979 - 314.159265358979 \omega_{r}\right) - \frac{e1d + 1.97654645790554 i_{q}}{T_{10}}\\e1d \left(314.159265358979 \omega_{r} - 314.159265358979\right) - \frac{e1q - 1.97654645790554 i_{d}}{T_{10}}\end{matrix}\right]$

```python

```


```python
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from scipy.stats import norm
```

    /home/adam/.local/lib/python2.7/site-packages/scipy/special/__init__.py:640: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._ufuncs import *
    /home/adam/.local/lib/python2.7/site-packages/scipy/linalg/basic.py:17: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._solve_toeplitz import levinson
    /home/adam/.local/lib/python2.7/site-packages/scipy/linalg/__init__.py:207: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._decomp_update import *
    /home/adam/.local/lib/python2.7/site-packages/scipy/special/_ellip_harm.py:7: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._ellip_harm_2 import _ellipsoid, _ellipsoid_norm
    /home/adam/.local/lib/python2.7/site-packages/scipy/interpolate/_bsplines.py:10: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from . import _bspl
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/lil.py:19: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from . import _csparsetools
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/csgraph/__init__.py:165: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._shortest_path import shortest_path, floyd_warshall, dijkstra,\
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/csgraph/_validation.py:5: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._tools import csgraph_to_dense, csgraph_from_dense,\
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/csgraph/__init__.py:167: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._traversal import breadth_first_order, depth_first_order, \
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/csgraph/__init__.py:169: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._min_spanning_tree import minimum_spanning_tree
    /home/adam/.local/lib/python2.7/site-packages/scipy/sparse/csgraph/__init__.py:170: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._reordering import reverse_cuthill_mckee, maximum_bipartite_matching, \
    /home/adam/.local/lib/python2.7/site-packages/scipy/spatial/__init__.py:95: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from .ckdtree import *
    /home/adam/.local/lib/python2.7/site-packages/scipy/spatial/__init__.py:96: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from .qhull import *
    /home/adam/.local/lib/python2.7/site-packages/scipy/spatial/_spherical_voronoi.py:18: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from . import _voronoi
    /home/adam/.local/lib/python2.7/site-packages/scipy/spatial/distance.py:122: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from . import _hausdorff
    /home/adam/.local/lib/python2.7/site-packages/scipy/optimize/_trlib/__init__.py:1: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._trlib import TRLIBQuadraticSubproblem
    /home/adam/.local/lib/python2.7/site-packages/scipy/optimize/_numdiff.py:10: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from ._group_columns import group_dense, group_sparse
    /home/adam/.local/lib/python2.7/site-packages/scipy/stats/_continuous_distns.py:18: RuntimeWarning: numpy.dtype size changed, may indicate binary incompatibility. Expected 96, got 88
      from . import _stats

```python
x = np.matrix([[0.0, 0.0, 0.0, 0.0]]).T
print(x, x.shape)
P = np.diag([1000.0, 1000.0, 1000.0, 1000.0])
print(P, P.shape)

dt = 0.1 # Time Step between Filter Steps
F = np.matrix([[1.0, 0.0, dt, 0.0],
              [0.0, 1.0, 0.0, dt],
              [0.0, 0.0, 1.0, 0.0],
              [0.0, 0.0, 0.0, 1.0]])
print(F, F.shape)

H = np.matrix([[0.0, 0.0, 1.0, 0.0],
              [0.0, 0.0, 0.0, 1.0]])
print(H, H.shape)

ra = 10.0**2

R = np.matrix([[ra, 0.0],
              [0.0, ra]])
print(R, R.shape)
```

    (matrix([[0.],
            [0.],
            [0.],
            [0.]]), (4, 1))
    (array([[1000.,    0.,    0.,    0.],
           [   0., 1000.,    0.,    0.],
           [   0.,    0., 1000.,    0.],
           [   0.,    0.,    0., 1000.]]), (4, 4))
    (matrix([[1. , 0. , 0.1, 0. ],
            [0. , 1. , 0. , 0.1],
            [0. , 0. , 1. , 0. ],
            [0. , 0. , 0. , 1. ]]), (4, 4))
    (matrix([[0., 0., 1., 0.],
            [0., 0., 0., 1.]]), (2, 4))
    (matrix([[100.,   0.],
            [  0., 100.]]), (2, 2))

```python
ra = 0.09
R = np.matrix([[ra, 0.0],
              [0.0, ra]])
print(R, R.shape)

sv = 0.5
G = np.matrix([[0.5*dt**2],
               [0.5*dt**2],
               [dt],
               [dt]])
Q = G*G.T*sv**2
from sympy import Symbol, Matrix
from sympy.interactive import printing
printing.init_printing()
dts = Symbol('dt')
Qs = Matrix([[0.5*dts**2],[0.5*dts**2],[dts],[dts]])
Qs*Qs.T
```

    (matrix([[0.09, 0.  ],
            [0.  , 0.09]]), (2, 2))

$$\left[\begin{matrix}0.25 dt^{4} & 0.25 dt^{4} & 0.5 dt^{3} & 0.5 dt^{3}\\0.25 dt^{4} & 0.25 dt^{4} & 0.5 dt^{3} & 0.5 dt^{3}\\0.5 dt^{3} & 0.5 dt^{3} & dt^{2} & dt^{2}\\0.5 dt^{3} & 0.5 dt^{3} & dt^{2} & dt^{2}\end{matrix}\right]$$

```python
I = np.eye(4)
print(I, I.shape)
```

    (array([[1., 0., 0., 0.],
           [0., 1., 0., 0.],
           [0., 0., 1., 0.],
           [0., 0., 0., 1.]]), (4, 4))

```python
m = 200 # Measurements
vx= 20 # in X
vy= 10 # in Y

mx = np.array(vx+np.random.randn(m))
my = np.array(vy+np.random.randn(m))
measurements = np.vstack((mx,my))

print(measurements.shape)
print('Standard Deviation of Acceleration Measurements=%.2f' % np.std(mx))
print('You assumed %.2f in R.' % R[0,0])

fig = plt.figure(figsize=(16,5))
plt.step(range(m),mx, label='$\dot x$')
plt.step(range(m),my, label='$\dot y$')
plt.ylabel(r'Velocity $m/s$')
plt.title('Measurements')
plt.legend(loc='best',prop={'size':18})
```

```python
xt = []
yt = []
dxt= []
dyt= []
Zx = []
Zy = []
Px = []
Py = []
Pdx= []
Pdy= []
Rdx= []
Rdy= []
Kx = []
Ky = []
Kdx= []
Kdy= []

def savestates(x, Z, P, R, K):
    xt.append(float(x[0]))
    yt.append(float(x[1]))
    dxt.append(float(x[2]))
    dyt.append(float(x[3]))
    Zx.append(float(Z[0]))
    Zy.append(float(Z[1]))
    Px.append(float(P[0,0]))
    Py.append(float(P[1,1]))
    Pdx.append(float(P[2,2]))
    Pdy.append(float(P[3,3]))
    Rdx.append(float(R[0,0]))
    Rdy.append(float(R[1,1]))
    Kx.append(float(K[0,0]))
    Ky.append(float(K[1,0]))
    Kdx.append(float(K[2,0]))
    Kdy.append(float(K[3,0]))
```

```python
for n in range(len(measurements[0])):

    # Time Update (Prediction)
    # ========================
    # Project the state ahead
    x = F*x

    # Project the error covariance ahead
    P = F*P*F.T + Q

    # Measurement Update (Correction)
    # ===============================
    # Compute the Kalman Gain
    S = H*P*H.T + R
    K = (P*H.T) * np.linalg.pinv(S)

    # Update the estimate via z
    Z = measurements[:,n].reshape(2,1)
    y = Z - (H*x)                            # Innovation or Residual
    x = x + (K*y)

    # Update the error covariance
    P = (I - (K*H))*P

    # Save states (for Plotting)
    savestates(x, Z, P, R, K)
```

```python
def plot_x():
    fig = plt.figure(figsize=(16,9))
    plt.step(range(len(measurements[0])),dxt, label='$estimateVx$')
    plt.step(range(len(measurements[0])),dyt, label='$estimateVy$')

    plt.step(range(len(measurements[0])),measurements[0], label='$measurementVx$')
    plt.step(range(len(measurements[0])),measurements[1], label='$measurementVy$')

    plt.axhline(vx, color='#999999', label='$trueVx$')
    plt.axhline(vy, color='#999999', label='$trueVy$')

    plt.xlabel('Filter Step')
    plt.title('Estimate (Elements from State Vector $x$)')
    plt.legend(loc='best',prop={'size':11})
    plt.ylim([0, 30])
    plt.ylabel('Velocity')
plot_x()
```

```python
def plot_xy():
    fig = plt.figure(figsize=(16,16))
    plt.scatter(xt,yt, s=20, label='State', c='k')
    plt.scatter(xt[0],yt[0], s=100, label='Start', c='g')
    plt.scatter(xt[-1],yt[-1], s=100, label='Goal', c='r')

    plt.xlabel('X')
    plt.ylabel('Y')
    plt.title('Position')
    plt.legend(loc='best')
    plt.axis('equal')
plot_xy()
```

```python

```


# Realization of Recursive Filters

*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).*

## Quantization of Variables and Operations

As for [non-recursive filters](../nonrecursive_filters/quantization_effects.ipynb#Quantization-Effects), the practical realization of recursive filters may suffer from the quantization of variables and algebraic operations. The effects of [coefficient quantization](quantization_of_coefficients.ipynb) were already discussed. This section takes a look at the quantization of variables.  We limit the investigations to the recursive part of a second-order section (SOS), since any recursive filter of order $N \geq 2$ can be [decomposed into SOSs](cascaded_structures.ipynb).

The computation of the output signal $y[k] = \mathcal{H}\{ x[k] \}$ by a difference equation involves a number of multiplications and additions. As discussed already for [non-recursive filters](../nonrecursive_filters/quantization_effects.ipynb#Quantization-of-Signals-and-Operations), multiplying two numbers in a binary representation (e.g. [two's complement](https://en.wikipedia.org/wiki/Two's_complement) or [floating point](https://en.wikipedia.org/wiki/Floating_point)) requires requantization of the result to keep the word length constant. The addition of two numbers may fall outside the maximum/minimum values of the representation and may suffer from clipping.

The resulting round-off and clipping errors depend on the number and sequence of algebraic operations. These depend on the structure used for implementation of the SOSs. For ease of illustration we limit our discussion to the [direct form I and II](direct_forms.ipynb). Similar insights can be achieved in a similar manner for other structures.

### Analysis of Round-Off Errors

Round-off errors are a consequence of reducing the word length after a multiplication. In order to investigate the influence of these errors on a recursive filter, the statistical model for [round-off errors in multipliers](../nonrecursive_filters/quantization_effects.ipynb#Model-for-round-off-errors-in-multipliers) as introduced for non-recursive filters is used. We furthermore neglect clipping.

The difference equation for the recursive part of a SOS realized in direct form I or II is given as

\begin{equation}
y[k] =  x[k] - a_1 \, y[k-1] - a_2 \, y[k-2]
\end{equation}

where $a_0 = 1$, $a_1$ and $a_2$ denote the coefficients of the recursive part. Introducing the requantization after the multipliers into the difference equation yields the output signal $y_Q[k]$

\begin{equation}
y_Q[k] = x[k] - \mathcal{Q} \{ a_{1} \, y[k-1] \} - \mathcal{Q} \{ a_{2} \, y[k-2] \}
\end{equation}

where $\mathcal{Q} \{ \cdot \}$ denotes the requantizer. Requantization is a non-linear process which results in a requantization error. If the value to be requantized is much larger that the quantization step $Q$, the average statistical properties of this error can be modeled as additive uncorrelated white noise. Introducing the error into above difference equation gives

\begin{equation}
y_Q[k] = x[k] - a_1 \, y[k-1] - e_1[k] - a_2 \, y[k-2] - e_2[k]
\end{equation}

where the two white noise sources $e_1[k]$ and $e_2[k]$ are assumed to be uncorrelated to each other. This difference equation can be split into a set of two difference equations

\begin{align}
y_Q[k] &= y[k] + e[k] \\
y[k] &=  x[k] - a_1 \, y[k-1] - a_2 \, y[k-2] \\
e[k] &= - e_1[k] - e_2[k] - a_1 \, e[k-1] - a_2 \, e[k-2]
\end{align}

The first difference equation computes the desired output signal $y[k]$ as a result of the input signal $x[k]$. The second one the additive error $e[k]$ due to requantization as a result of the requantization error $- (e_1[k] + e_2[k])$ injected into the recursive filter.
The power spectral density (PSD) $\Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$ of the error $e[k]$ is then given as

\begin{equation}
\Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = | H(\mathrm{e}^{\,\mathrm{j}\,\Omega})|^2 \cdot (\Phi_{e_1 e_1}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) + \Phi_{e_2 e_2}(\mathrm{e}^{\,\mathrm{j}\,\Omega}))
\end{equation}

According to the model for the requantization errors, their PSDs are given as $\Phi_{e_1 e_1}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \Phi_{e_2 e_2}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \frac{Q^2}{12}$. Introducing this together with the transfer function of the SOS yields

\begin{equation}
\Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \left| \frac{1}{1 + a_1 \, \mathrm{e}^{\,-\mathrm{j}\,\Omega} + a_2 \, \mathrm{e}^{\,-\mathrm{j}\,2\,\Omega}} \right|^2 \cdot \frac{Q^2}{6}
\end{equation}

#### Example - Round-off error of a SOS

The following example evaluates the error $e[k] = y_Q[k] - y[k]$ for a SOS which only consists of a recursive part. The desired system response $y[k]$ is computed numerically by floating point operations with double precision, $y_Q[k]$ is computed by applying a uniform midtread quantizer after the multiplications. The system is excited by uniformly distributed white noise. Besides the PSD $\Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega})$, the signal-to-noise ratio (SNR) $10 \cdot \log_{10} \left( \frac{\sigma_y^2}{\sigma_e^2} \right)$ in dB of the filter is evaluated.

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

N = 8192  # length of signals
w = 8  # wordlength for requantization of multiplications

def uniform_midtread_quantizer(x):
    # linear uniform quantization
    xQ = Q * np.floor(x/Q + 1/2)

    return xQ

def no_quantizer(x):

    return x

def sos_df1(x, a, requantize=None):
    y = np.zeros(len(x)+2)  # initial value appended
    for k in range(len(x)):
        y[k] = x[k] - requantize(a[1]*y[k-1]) - requantize(a[2]*y[k-2])

    return y[0:-2]

# cofficients of the SOS
p = 0.90*np.array([np.exp(1j*np.pi/3), np.exp(-1j*np.pi/3)])
a = np.poly(p)
# quantization step
Q = 1/(2**(w-1))

# compute input signal
x = np.random.uniform(low=-1, high=1, size=N)
# compute output signals w and w/o requantization
yQ = sos_df1(x, a, requantize=uniform_midtread_quantizer)
y = sos_df1(x, a, requantize=no_quantizer)
# compute requantization error
e = yQ-y
# Signal-to-noise ratio
SNR = 10*np.log10(np.var(y)/np.var(e))
print('SNR due to requantization: %f dB' % SNR)

# estimate PSD of requantization error
nf, Pxx = sig.welch(e, window='hamming', nperseg=256, noverlap=128)
Pxx = .5*Pxx  # due to normalization in scipy.signal
Om = 2*np.pi*nf
# compute frequency response of system
w, H = sig.freqz([1, 0, 0], a)

# plot results
plt.figure(figsize=(10, 4))
plt.plot(Om, Pxx/Q**2 * 12, 'b', label=r'$|\hat{\Phi}_{ee}(e^{j \Omega})|$')
plt.plot(w, np.abs(H)**2 * 2, 'g', label=r'$|H(e^{j \Omega})|^2$')
plt.title('Estimated PSD and transfer function of requantization noise')
plt.xlabel(r'$\Omega$')
plt.ylabel(r'$Q^2/12$')
plt.axis([0, np.pi, 0, 100])
plt.legend()
plt.grid()
```

    SNR due to requantization: 45.470119 dB

### Small Limit Cycles

Besides the requantization noise, recursive filters may be subject to periodic oscillations present at the output. These undesired oscillations are termed *limit cycles*. Small limit cycles emerge from the additive round-off noise due to requantization after a multiplication. The feedback in a recursive filter leads to a feedback of the requantization noise. This may lead to a periodic output signal with an amplitude range of some quantization steps $Q$, even after the input signal is zero. The presence, amplitude and frequency of small limit cycles depends on the location of poles and the structure of the filter. A detailed treatment of this phenomenon is beyond the scope of this notebook and can be found in the literature.

#### Example - Small limit cycles of a SOS

The following example illustrates small limit cycles for the system investigated in the previous example. The input signal is uniformly distributed white noise till time-index $k=256$ and zero for the remainder.

```python
# compute input signal
x = np.random.uniform(low=-1, high=1, size=256)
x = np.concatenate((x, np.zeros(1024)))
# compute output signal
yQ = sos_df1(x, a, requantize=uniform_midtread_quantizer)

# plot results
np.seterr(divide='ignore')
plt.figure(figsize=(10, 3))
plt.plot(20*np.log10(np.abs(yQ)))
plt.title('Level of output signal')
plt.xlabel(r'$k$')
plt.ylabel(r'$|y_Q[k]|$ in dB')
plt.grid()

plt.figure(figsize=(10, 3))
k = np.arange(1000, 1050)
plt.stem(k, yQ[k]/Q, use_line_collection=True)
plt.title('Output signal for zero input')
plt.xlabel(r'$k$')
plt.ylabel(r'$y_Q[k] / Q$ ')
plt.axis([k[0], k[-1], -3, 3])
plt.grid()
```

**Exercise**

* Estimate the period of the small limit cycles. How is it related to the poles of the system?
* What amplitude range is spanned?

Solution: The period of the small limit cycles can be estimated from the second illustration as $P = 6$. The normalized frequency of a harmonic exponential signal with the same periodicity is given as $\Omega_0 = \frac{2 \pi}{P} = \frac{\pi}{3}$. The poles of the system can be extracted from the code of the first example as $z_{\infty 0,1} = 0.9 \cdot e^{\pm j \frac{\pi}{3}}$. The periodicity of the small limit cycles is hence linked to the normalized frequency of the poles. The amplitude range spanned by the small limit cycles is $\pm 2 Q$.

### Large Limit Cycles

Large limit cycles are periodic oscillations of a recursive filter due to overflows in the multiplications/additions. As for small limit cycles, large limit cycles may be present even after the input signal is zero. Their level is typically in the range of the minimum/maximum value of the requantizer. Large limit cycles should therefore be avoided in a practical implementation. The presence of large limit cycles depends on the scaling of input signal and coefficients, as well as the strategy used to cope for clipping. Amongst others, they can be avoided by proper scaling of the coefficients to prevent overflow. Again, a detailed treatment of this phenomenon is beyond the scope of this notebook and can be found in the literature.

#### Example - Large limit cycles of a SOS

The following example illustrates large limit cycles for the system investigated in the first example. In order to trigger large limit cycles, the coefficients of the filter have been doubled. The input signal is uniformly distributed white noise till time-index $k=256$ and zero for the remainder.

```python
def uniform_midtread_quantizer(x, xmin=1):
    # limiter
    x = np.copy(x)
    if x <= -xmin:
        x = -1
    if x > xmin - Q:
        x = 1 - Q
    # linear uniform quantization
    xQ = Q * np.floor(x/Q + 1/2)

    return xQ

# compute input signal
x = np.random.uniform(low=-1, high=1, size=256)
x = np.concatenate((x, np.zeros(1024)))
# compute output signal
yQ = sos_df1(x, 2*a, requantize=uniform_midtread_quantizer)

# plot results
plt.figure(figsize=(10, 3))
plt.plot(20*np.log10(np.abs(yQ)))
plt.title('Level of output signal')
plt.xlabel(r'$k$')
plt.ylabel(r'$|y_Q[k]|$ in dB')
plt.grid()

plt.figure(figsize=(10, 3))
k = np.arange(1000, 1050)
plt.stem(k, yQ[k], use_line_collection=True)
plt.title('Output signal for zero input')
plt.xlabel(r'$k$')
plt.ylabel(r'$y_Q[k]$ ')
plt.grid()
```

**Exercise**

* Determine the period of the large limit cycles. How is it related to the poles of the system?

Solution: The period of the large limit cycles can be estimated from the second illustration as $P = 6$. The normalized frequency of a harmonic exponential signal with the same periodicity is given as $\Omega_0 = \frac{2 \pi}{P} = \frac{\pi}{3}$. The poles of the system can be extracted from the code of the first example as $z_{\infty 0,1} = 0.9 \cdot e^{\pm j \frac{\pi}{3}}$. The periodicity of the large limit cycles is hence linked to the normalized frequency of the poles.

**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*.


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

# continuum dispersion

```python
import kwant
import numpy as np
import scipy.linalg as la

import matplotlib.pyplot as plt
%matplotlib inline
```

```python
a = 0.5
g0 = 1

bands = ['gamma_6c', 'gamma_8v', 'gamma_7v']

model = semicon.models.ZincBlende(
    components=['foreman'],
    bands=bands,
    default_databank='winkler'
)

params = model\
    .parameters(material='InAs')\
    .renormalize(new_gamma_0=g0)

# define continuum dispersion function
continuum = kwant.continuum.lambdify(str(model.hamiltonian), locals=params)

# define tight-binding dispersion function
template = kwant.continuum.discretize(model.hamiltonian, grid_spacing=a)
syst = kwant.wraparound.wraparound(template).finalized()
p = lambda k_x, k_y, k_z: {'k_x': k_x, 'k_y': k_y, 'k_z': k_z, **params}
tb = lambda k_x, k_y, k_z: syst.hamiltonian_submatrix(params=p(k_x, k_y, k_z))

# get dispersions
k = np.linspace(-np.pi/a, np.pi/a, 101)
e = np.array([la.eigvalsh(continuum(k_x=ki, k_y=0, k_z=0)) for ki in k])
e_tb = np.array([la.eigvalsh(tb(k_x=a*ki, k_y=0, k_z=0)) for ki in k])
```

```python
plt.plot(k, e, 'r-');
plt.plot(k, e_tb, 'k-');

plt.ylim(-1, 2)
plt.grid();
```


# Le-Net 1 based architecture

We start with 50X50 (I) after first convolution we have 46X46 (L1). The next pooling layer takes twice as big steps (instead of 1 pixel) reducing to an output image of 23X23 with 2x2 pooling kernels (L2). Then we apply different types of convolution 4x4 kernels on the L2 layer resulting in 20x20 (L3) . Then followed by pooling 5X5 resulting in 4x4 output map (L4). So we have 16 connection for each element in layer L4 (which depend on the amount of different Covolutions in L3)

\begin{equation}
f(x)=\frac{1}{1+e^{-ax}} \\
F_{k}= f( \sum_{i} \mathbf{W_{i} \cdot y_{i}}-b_{k})
\end{equation}

\begin{equation}
E=\sum_{k} \frac{1}{2}|t_k-F_{k}|^{2} \\
\Delta W_{ij}= - \eta \frac{dE}{d W_{ij}}
\end{equation}

\begin{equation}
\Delta W_{ij}= \sum_{k} - \eta \frac{dE}{d F_{k}} \frac{dF_{k}}{dx_{k}} \frac{dx_{k}}{dW_{ij}}=\sum_{k} \eta (t_{k}-F_{k})\frac{ae^{-ax_{k}}}{(1+e^{-ax_{k}})^{2}} \frac{dx_{k}}{dW_{ij}} \\
= \eta (t_{k}-F_{k})\frac{ae^{-ax_{k}}}{(1+e^{-ax_{k}})^{2}} y_{ij}
\end{equation}

\begin{equation}
\Delta b_{k}= - \eta \frac{dE}{d F_{k}} \frac{dF_{k}}{dx_{k}} \frac{dx_{k}}{b_{k}}=\eta (t_{k}-F_{k})\frac{ae^{-ax_{k}}}{(1+e^{-ax_{k}})^{2}} \cdot-1
\end{equation}

\begin{equation}
\begin{split}
    \Delta a_{k}=- \eta \frac{dE}{d F_{k}} \frac{dF_{k}}{da_{k}}= \eta (t_{k}-F_{k})\frac{x_{k}e^{-ax_{k}}}{(1+e^{-ax_{k}})^{2}}
\end{split}
\end{equation}

```
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from numpy import linalg as lin
import scipy.signal as sig
import Image
RGB=2 #selects layer R=0,G=1,B=2

img = Image.open('Forest\F1.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedF1=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Forest\F5.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedF2=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Forest\F3.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedF3=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Forest\F4.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedF4=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Forest\F5.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedF5=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('City\C3.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedC1=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('City\C4.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedC2=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('City\C5.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedC3=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('City\C6.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedC4=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('City\C7.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedC5=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Water\W1.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedW1=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Water\W2.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedW2=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Water\W3.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedW3=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

img = Image.open('Water\W4.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedW4=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

k=1
img = Image.open('Water\W5.png')
data=img.convert('RGB')
data = np.asarray( data, dtype="int32" )
patchSize=50 # patchsize this must be 50 since our network can only handle this value
Yamount=data.shape[0]/patchSize # Counts how many times the windowsize fits in the picture
Xamount=data.shape[1]/patchSize # Counts how many times the windowsize fits in the picture
dataPatchedW5=np.array([[data[j*patchSize:(j+1)*patchSize,i*patchSize:(i+1)*patchSize,RGB] for i in range(0,Xamount)] for j in range(0,Yamount)])

```

```
dataPatchedW4.shape
```

    (11L, 26L, 50L, 50L)

```
def Sigmoid(x,a): # a is used to adjust the slope of the function
    return (1/(1+np.exp(-x*a)))

def Pool(I,W):
    PoolImg=np.zeros((len(I)/len(W),len(I)/len(W))) # W must fit an integer times into I.
    for i in range(0,len(PoolImg)):
        for j in range(0,len(PoolImg)):
            SelAr=I[i*len(W):(i+1)*len(W),j*len(W):(j+1)*len(W)]
            PoolImg[i,j]=np.inner(SelAr.flatten(),W.flatten()) # Now this is just an inner product since we have vectors
    return PoolImg

scale=1*10**-6
scale_WP=1*10**-1
WC11=np.array([
    [1,1,1,1,1],
    [1,2,2,2,1],
    [1,2,4,2,1],
    [1,2,2,2,1],
    [1,1,1,1,1],
    ])

WC12=np.array([
    [3,2,1,2,3],
    [2,1,1,2,2],
    [1,1,1,1,1],
    [2,2,1,2,2],
    [3,2,1,2,3],
    ])

WC13=np.array([
    [3,2,1,2,3],
    [3,2,1,2,3],
    [3,2,1,2,3],
    [3,2,1,2,3],
    [3,2,1,2,3],
    ])

WC14=np.array([
    [0,0,1,2,2],
    [0,0,1,2,2],
    [0,0,1,2,2],
    [0,0,1,2,2],
    [0,0,1,2,2],
    ])

WC15=np.array([
    [2,2,2,2,2],
    [2,2,2,2,2],
    [1,1,1,1,1],
    [0,0,0,0,0],
    [0,0,0,0,0]
    ])

WC16=np.array([
    [2,2,2,2,1],
    [2,2,2,1,0],
    [2,2,1,0,0],
    [2,1,0,0,0],
    [1,0,0,0,0]
    ])

# WC16=np.array([
#     [0.5,0,0,0,0],
#     [1,0.5,0,0,0],
#     [1,1,0.5,0,0],
#     [1,1,1,0.5,0],
#     [1,1,1,1,0.5]
#     ])

WC17=np.array([
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1]
    ])

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

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

# averaging kernel in L2
WP=np.array([
    [1,1],
    [1,1],
    ])

WC31=np.array([
    [1,1,1,1],
    [1,1,1,1],
    [1,1,1,1],
    [1,1,1,1],
    ])

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

WP4=np.array([
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1],
    [1,1,1,1,1],
    ])*scale_WP

# we initialize random weight to prevent oscillations while training
W1_1=np.random.rand(1,16)
W1_2=np.random.rand(1,16)
W1_3=np.random.rand(1,16)
W1_4=np.random.rand(1,16)
W1_5=np.random.rand(1,16)
W1_6=np.random.rand(1,16)
W1_7=np.random.rand(1,16)
W1_8=np.random.rand(1,16)

W2_1=np.random.rand(1,16)
W2_2=np.random.rand(1,16)
W2_3=np.random.rand(1,16)
W2_4=np.random.rand(1,16)
W2_5=np.random.rand(1,16)
W2_6=np.random.rand(1,16)
W2_7=np.random.rand(1,16)
W2_8=np.random.rand(1,16)

W3_1=np.random.rand(1,16)
W3_2=np.random.rand(1,16)
W3_3=np.random.rand(1,16)
W3_4=np.random.rand(1,16)
W3_5=np.random.rand(1,16)
W3_6=np.random.rand(1,16)
W3_7=np.random.rand(1,16)
W3_8=np.random.rand(1,16)

b1=0
b2=0
b3=0

a1=1
a2=1
# a3=1

data_list=([[dataPatchedF1,dataPatchedF2,dataPatchedF3,dataPatchedF4,dataPatchedF5],
           [dataPatchedC1,dataPatchedC2,dataPatchedC3,dataPatchedC4,dataPatchedC5],
           [dataPatchedW1,dataPatchedW2,dataPatchedW3,dataPatchedW4,dataPatchedW5]
            ])

class_list=[[1,0,0],
            [0,1,0],
            [0,0,1]
            ]

i=0
j=0
d1=0
d2=0

for u in range(0,4000):
    if(d1==3):
        d1=0
        d2=d2+1
    if(d2==5):
        d2=0
        if(j==24 and i==9):
            print('stop')
            break # Searchspace of 2000x2000 is achieved and the loop will stop
        if(i==9): # i must increase to cover all patches over y-direction
            j=j+1 # selects new patch in x direction
            i=0
        i=i+1

    inputPatch=data_list[d1][d2][i,j,:,:]
    t1,t2,t3=class_list[d1]
#     print(t1,t2)
    d1=d1+1
    if(t3==1):continue
    #L1
    ConvImg1=sig.convolve(inputPatch,WC11,mode='valid')
    ConvImg2=sig.convolve(inputPatch,WC12,mode='valid')
    ConvImg3=sig.convolve(inputPatch,WC13,mode='valid')
    ConvImg4=sig.convolve(inputPatch,WC14,mode='valid')
    ConvImg5=sig.convolve(inputPatch,WC15,mode='valid')
    ConvImg6=sig.convolve(inputPatch,WC16,mode='valid')
    ConvImg7=sig.convolve(inputPatch,WC17,mode='valid')
    ConvImg8=sig.convolve(inputPatch,WC18,mode='valid')
    ConvImg9=sig.convolve(inputPatch,WC19,mode='valid')

    #L2
    PoolImg1=Pool(ConvImg1,WP) # reducing dimensionality by 2.
    PoolImg2=Pool(ConvImg2,WP)
    PoolImg3=Pool(ConvImg3,WP)
    PoolImg4=Pool(ConvImg4,WP)
    PoolImg5=Pool(ConvImg5,WP)
    PoolImg6=Pool(ConvImg6,WP)
    PoolImg7=Pool(ConvImg7,WP)
    PoolImg8=Pool(ConvImg8,WP)
    PoolImg9=Pool(ConvImg9,WP)

    #L3
    ConvImg1_31=sig.convolve(PoolImg1,WC31,mode='valid')
    ConvImg2_31=sig.convolve(PoolImg2,WC31,mode='valid')
    ConvImg3_31=sig.convolve(PoolImg3,WC31,mode='valid')
    ConvImg4_31=sig.convolve(PoolImg4,WC31,mode='valid')
    ConvImg5_31=sig.convolve(PoolImg5,WC31,mode='valid')
    ConvImg6_31=sig.convolve(PoolImg6,WC31,mode='valid')
    ConvImg7_31=sig.convolve(PoolImg7,WC31,mode='valid')
    ConvImg8_31=sig.convolve(PoolImg8,WC31,mode='valid')
    ConvImg9_31=sig.convolve(PoolImg9,WC31,mode='valid')

    #L4
    # reducing dimensionality by 2.
    PoolImg1_41=Pool(ConvImg1_31,WP4)*scale
    PoolImg2_41=Pool(ConvImg1_31,WP4)*scale
    PoolImg3_41=Pool(ConvImg2_31,WP4)*scale
    PoolImg4_41=Pool(ConvImg2_31,WP4)*scale
    PoolImg5_41=Pool(ConvImg3_31,WP4)*scale
    PoolImg6_41=Pool(ConvImg3_31,WP4)*scale
    PoolImg7_41=Pool(ConvImg4_31,WP4)*scale
    PoolImg8_41=Pool(ConvImg4_31,WP4)*scale
    PoolImg9_41=Pool(ConvImg4_31,WP4)*scale

#     print(PoolImg1_41)

#     plt.figure()
#     plt.imshow(PoolImg1_41)

    n1=2*10**-2
    n2=1*10**0
    n3=1*10**-2

    # Now we have the outputs
    x1=(
        np.inner(PoolImg1_41.flatten(),W1_1)+
        np.inner(PoolImg2_41.flatten(),W1_2)+
        np.inner(PoolImg3_41.flatten(),W1_3)

        )
    x2=(
        np.inner(PoolImg4_41.flatten(),W2_1)+
        np.inner(PoolImg5_41.flatten(),W2_2)+
        np.inner(PoolImg6_41.flatten(),W2_3)
        )
#     x3=(
#         np.inner(PoolImg7_41.flatten(),W3_1)+
#         np.inner(PoolImg8_41.flatten(),W3_2)+
#         np.inner(PoolImg9_41.flatten(),W3_3)
#         )
    x1=x1[0]
    x2=x2[0]
#     x3=x3[0]
    f1=Sigmoid(x1-b1,a1)
    f2=Sigmoid(x2-b2,a2)
#     f3=Sigmoid(x3-b3,a3)

    f11=(np.exp(-a1*x1)/((1+np.exp(-a1*x1))**2))
    f22=(np.exp(-a2*x2)/((1+np.exp(-a2*x2))**2))
    f33=(np.exp(-a3*x3)/((1+np.exp(-a3*x3))**2))

    W1_1=W1_1+ n1*(t1-f1)*PoolImg1_41.flatten()
    W1_2=W1_2+ n1*(t1-f1)*PoolImg2_41.flatten()
    W1_3=W1_3+ n1*(t1-f1)*PoolImg3_41.flatten()

    W2_1=W2_1+ n1*(t2-f2)*PoolImg4_41.flatten()
    W2_2=W2_2+ n1*(t2-f2)*PoolImg5_41.flatten()
    W2_3=W2_3+ n1*(t2-f2)*PoolImg6_41.flatten()

#     W3_1=W3_1+ n1*(t3-f3)*PoolImg7_41.flatten()
#     W3_2=W3_2+ n1*(t3-f3)*PoolImg8_41.flatten()
#     W3_3=W3_3+ n1*(t3-f3)*PoolImg9_41.flatten()

    b1=b1-n2*(t1-f1)*a1*f11
    b2=b2-n2*(t2-f2)*a2*f22
#     b3=b3-n2*(t3-f3)*a3*f33

    a1=a1+n3*(t1-f1)*x1*f11
    a2=a2+n3*(t2-f2)*x2*f22
#     a3=a3+n3*(t3-f3)*x3*f33
#     print(x1,x2,x3)
    if(u%10==0): print(t1,t2,'|',f1,f2)

```

```
print(x1,a1,b1,W1_1)
print(x2,a2,b2,W2_1)
print(x3,a3,b3,W3_1)
```

    (5.1082153914136157, 0.88357034591764272, -0.041571562509730008, array([[ 0.12548644,  0.12435387, -0.02916368, -0.68134182,  0.83634774,
             0.6536961 , -0.46682376, -0.1838609 ,  0.60459763,  0.13026754,
            -0.46514862, -0.39128289,  0.43959054, -0.23451106, -1.10508909,
            -0.09890063]]))
    (1.4553505724687028, 0.61523641737923762, 0.0088807056695710852, array([[ 0.0472142 ,  0.17779603,  1.21360529,  0.61738222, -0.31280446,
             0.49011907,  0.26865029, -0.41569453, -1.06330305,  0.10401362,
            -0.072053  , -0.27021989, -0.73922872,  0.63354082,  0.76385381,
             0.06167666]]))
    (-5.2394938672417126, 1.0662452701052707, 0.064108423516966273, array([[ 0.47248456, -0.11093047, -0.89613024, -0.07104968,  0.14020274,
            -0.26866677,  0.02431033,  0.18967934,  0.02899224,  0.04424406,
             0.05184127,  0.16060613,  0.33891533, -0.4402418 , -0.43514422,
            -0.15758227]]))

```
plt.figure()
plt.imshow(ConvImg1)
plt.imshow(ConvImg2)
plt.imshow(ConvImg3)
plt.show()

```

```

```


## Raytracing Fundamentals

In this tutorial, we will show the fundamentals of using prysm to perform sequential raytracing.  At the moment, this capability is in the experimental submodule, which provides no guarantees of testing, documentation, or future compatability.  However, the core is based on the classic Spencer and Murty paper in JOSA 1961, and is known to work correctly under limited tests.

Raytracing begins by importing a few pieces of machinery,

```python
import numpy as np

from prysm.experimental.raytracing.surfaces import Surface
from prysm.experimental.raytracing.spencer_and_murty import raytrace
from prysm.experimental.raytracing.raygen import generate_collimated_ray_fan
from prysm.experimental.raytracing.opt import paraxial_image_solve
from prysm.experimental.raytracing.plotting import plot_rays, plot_optics
```

The first "unit" of a rayrace is the prescription, a series of surfaces with what should be a familiar description of their geometry, and a less familiar global position `P` for the local coordinate origin.

The local coordinate origin is taken to be in z if only a single number is given, This makes the most common case of coaxial designs more ergonomic.

We'll make our prescription a single reflective sphere for now

```python
mirror_semidiameter = 4
pres = [
    #          curvature reflect|refract position n(wvl), rotation (None=non-tilted)
    Surface.sphere(-0.05, 'reflect', P=5, n=None, R=None)
]
```

and use a paraxial image solve to add another planar surface at the focus.  This solve needs to known either the entrance pupil diameter, or the object position and object NA, to work.  We're using a collimated input, so we provide an EPD of 2 mm.

```python
P_img = paraxial_image_solve(pres, z=0, epd=2*mirror_semidiameter)
pres.append(Surface.plane(P_img, 'eval'))
```

Now we can view a raytrace through the prescription.  The fundamental unit of raytracing is a ray, which is defined as a pair of length 3 vectors,
$$
\begin{align}
P &= \langle X,Y,Z \rangle \\
S &= \langle k,l,m \rangle
\end{align}
$$

$P$ is the location of the ray, and $S$ its direction cosines.  You can simply give the raytrace function any $P$ and $S$, and it will trace that one ray.  Or, you can provide it an ensemble of $N$ $P$ and $S$ with a pair of arrays of shape `(N,3)`, and it will trace all the rays at once.  To create a fan of rays, we can use one of the handy generation functions,

```python
# starting z is not important, since the ray it collimated.
# it will only affect the raytrace plots
P, S = generate_collimated_ray_fan(nrays=4, maxr=mirror_semidiameter, z=0)
np.set_printoptions(precision=2, suppress=True)
print(P)
```

Then we simply trace the rays through the prescription, collecting the history of $P$ and $S$ for each ray through each surface,

```python
# a wavelength is needed, though it does not matter
# since this is all-reflective
phist, shist = raytrace(pres, P, S, 0.6328)
fig, ax = plot_rays(phist)
plot_optics(pres, phist, fig=fig, ax=ax)
```

We can trace another field point, one offset by a few degrees in the y axis:

```python
# 10 degrees clockwise advance in y
P2, S2 = generate_collimated_ray_fan(nrays=4, maxr=mirror_semidiameter, z=0, yangle=10)
phist2, shist2 = raytrace(pres, P2, S2, 0.6328)

fig, ax = plot_rays(phist)
plot_rays(phist2, c='b', fig=fig, ax=ax)
plot_optics(pres, phist, fig=fig, ax=ax)
```

Notice that these rays hit the surface lower on average than the red, on-axis ray bundle.  At the moment, there is nothing in prysm's ray tracing code that knows about aperture stops, and there is no automatic ray-aiming.  "Manual" ray-aiming does exist, and will be covered in a more advanced tutorial.  Because no bounding geometry was specified for the surface, `plot_optics` used the extrema of the raytrace as the surface bound.  Since it was given only the trace history for the off-axis bundle, it does not appear to cover some of the on-axis bundle.

## Wrap-Up

In this tutorial, we showed how to trace rays through a spherical reflecting mirror using prysm.  In it, we showed how to create an optical prescription, use paraxial image solves to locate the image, and plot ray fans from on and off-axis bundles in the same plot.  More advanced tutorials will cover how to construct the prescription for more complex systems and use the raytrace results to perform analysis.

```python

```


# Single-Qubit System Measurements Tutorial Workbook

**What is this workbook?**
A workbook is a collection of problems, accompanied by solutions to them.
The explanations focus on the logical steps required to solve a problem; they illustrate the concepts that need to be applied to come up with a solution to the problem, explaining the mathematical steps required.

Note that a workbook should not be the primary source of knowledge on the subject matter; it assumes that you've already read a tutorial or a textbook and that you are now seeking to improve your problem-solving skills. You should attempt solving the tasks of the respective kata first, and turn to the workbook only if stuck. While a textbook emphasizes knowledge acquisition, a workbook emphasizes skill acquisition.

This workbook describes the solutions to the problems offered in the [Single-Qubit System Measurements tutorial](./SingleQubitSystemMeasurements.ipynb).
Since the tasks are offered as programming problems, the explanations also cover some elements of Q# that might be non-obvious for a first-time user.

**What you should know for this workbook**

You should be familiar with the following concepts before tackling the Single-Qubit System Measurements tutorial (and this workbook):
1. Basic linear algebra
2. The concept of a qubit
3. Single-qubit gates
$\newcommand{\ket}[1]{\left|#1\right>}$
$\newcommand{\bra}[1]{\left<#1\right|}$

### Exercise 1: The probability outcomes for a specific state
The qubit is in the following state:
$$\ket \psi = 0.6 \ket 0 + 0.8 \ket 1 \equiv \begin{bmatrix} 0.6 \\ 0.8\end{bmatrix}.$$

If this qubit is measured in the computational basis, what are the outcome probabilities?

### Solution
The given state $\ket \psi$ is normalized, since $0.6^2 + 0.8^2 = 1$. Hence, the probability of measuring $0$ is $|0.6|^2 = 0.36$, and the probability of measuring $1$ is $|0.8|^2 = 0.64$.

[Return to exercise 1 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-1:-The-probability-outcomes-for-a-specific-state)

### Exercise 2: Distinguish $|0\rangle$ and $|1\rangle$

**Input:** A qubit which is guaranteed to be in either the $|0\rangle$ or the $|1\rangle$ state.

**Output:**  `true` if the qubit was in the $|0\rangle$ state, or `false` if it was in the $|1\rangle$ state. The state of the qubit at the end of the operation does not matter.

### Solution

The input qubit is guaranteed to be either in basis state $|0\rangle$ or $|1\rangle$. This means that when measuring the qubit in the computational basis, the measurement will report the input state without any doubt.

In Q# the operation [`M`](https://docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.intrinsic.m) can be used to measure a single qubit in the computational basis. The measurement result is a value of type `Result`: the operation `M` will return `One` if the input qubit was in the $|1\rangle$ state and `Zero` if the input qubit was in the $|0\rangle$ state. Since we need to encode the first case as `false` and the second one as `true`, we can return the result of equality comparison between measurement result and `Zero`.

```qsharp
%kata T2_IsQubitZero

operation IsQubitZero (q : Qubit) : Bool {
    return M(q) == Zero;
}
```

[Return to exercise 2 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-2:-Distinguish-$|0\rangle$-and-$|1\rangle$)

### Exercise 3: Distinguish $|+\rangle$ and $|-\rangle$ using the `Measure` operation

**Input**: A qubit which is guaranteed to be in either the $\ket +$ state, or the $\ket -$ state.

**Output**: `true` if the qubit is in the $\ket +$ state, or `false` if it was in the $\ket -$ state.

### Solution

The input qubit is guaranteed to be either in basis state $|+\rangle$ or $|-\rangle$. This means that when measuring the qubit in the Pauli $X$ basis, the measurement will report the input state without any doubt. (Recall that these states are eigenstates for the Pauli $X$ matrix).

In Q# the operation [`Measure`](https://docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.intrinsic.measure) can be used to measure a qubit in Pauli basis of the user's choice. The operation returns a value of type `Result`, and is `Zero` if the measured state corresponds to the eigenvalue $+1$, and `One` if it corresponds to the eigenvalue $-1$ of the Pauli operator.

Since the states $\ket +$ and $\ket -$ correspond to the eigenvalues $+1$ and $-1$ of the Pauli X operator, we can return the result of equality comparison between the measurement result and `One`.
Note that since `Measure` operation generally works with multiple qubits to perform multi-qubit measurements, it takes array parameters. To do a single-qubit measurement, you need to pass two arrays of one element, `[PauliX]` and `[q]`, rather than individual values.

```qsharp
%kata T3_IsQubitMinus

operation IsQubitMinus (q : Qubit) : Bool {
    return Measure([PauliX], [q]) == One;
}
```

[Return to exercise 3 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-3:-Distinguish-$|+\rangle$-and-$|-\rangle$-using-the-Measure-operation)

### Exercise 4: The outcome probabilities for a measurement in a specified basis

What are the outcome probabilities of measuring a qubit in the $\ket{0}$ state in the Pauli X basis, i.e., the $\{ \ket +, \ket -\}$ basis?

What are the outcome probabilities of measuring a qubit in the $0.6\ket{0} + 0.8 \ket{1}$ state in the Pauli Y basis, i.e., the $\{ \ket i, \ket{-i}\}$ basis?

### Solution

1. To find the probabilities of measuring $+$ and $-$, we first need to express the state $\ket 0$ in terms of $\ket +$ and $\ket -$. Using the fact that $\ket{\pm} = \frac{1}{\sqrt{2}}  (\ket{0} \pm \ket{1})$, we can show that
$$
\ket 0 = \frac{1}{\sqrt{2}} \ket{+} + \frac{1}{\sqrt{2}} \ket{-}.
$$

  Thus, the probability of measuring $+$ is $|\frac1{\sqrt2}|^2 = 0.5$, and similarly, the probability of measuring $-$ is $0.5$.

2. Similar to the first part, we need to express the state $\ket \psi = 0.6 \ket 0 + 0.8 \ket 1$ in the $\ket{\pm i}$ basis. For this calculation, we use the projection matrix approach.
First, we recall that the states $\ket{\pm i}$ are given by
$$
\ket{\pm i} = \frac1{\sqrt2} (\ket 0 \pm i \ket 1).
$$
We can now construct the two projectors $P_{\pm i}$ onto states $\ket {\pm i}$ as follows:
\begin{align}
P_{i} &= \ket{i}\bra{i} = \frac{1}{2} \begin{bmatrix} 1 \\ i \end{bmatrix} \begin{bmatrix} 1 & -i \end{bmatrix} = \frac{1}{2} \begin{bmatrix}1 & -i \\ i & 1\end{bmatrix}; \\
P_{-i} &=\ket{-i}\bra{-i} = \frac{1}{2} \begin{bmatrix} 1 \\ -i \end{bmatrix} \begin{bmatrix} 1 & i \end{bmatrix} = \frac{1}{2} \begin{bmatrix}1 & i \\ -i & 1\end{bmatrix}
\end{align}
Recalling that the probabilities of measuring $\pm i$ are equal to the norm of the vectors $P_{\pm i}\ket \psi$, we now apply $P_{\pm i}$ to $\ket \psi$:
\begin{align}
P_{+i} \ket \psi &= \frac{1}{2} \begin{bmatrix}1 & -i \\ i & 1\end{bmatrix} \begin{bmatrix} 0.6 \\ 0.8 \end{bmatrix} = \frac{1}{2} \begin{bmatrix} 0.6 - 0.8i \\ 0.8 + 0.6i \end{bmatrix} ;\\
P_{-i} \ket \psi &= \frac{1}{2} \begin{bmatrix}1 & i \\ -i & 1\end{bmatrix} \begin{bmatrix} 0.6 \\ 0.8 \end{bmatrix} = \frac{1}{2} \begin{bmatrix} 0.6 + 0.8i \\ 0.8 - 0.6i \end{bmatrix}.
\end{align}
Hence, the probabilities of measuring $\pm i$, which we denote by $p(\pm i)$, are
\begin{align}
p(+i)& = |P_{+i} \ket \psi|^2 = \frac{1}{4}(|0.6 - 0.8i|^2 + |0.8 + 0.6i|^2) = \frac{1}{2}; \\
p(-i)& = |P_{-i} \ket \psi|^2 = \frac{1}{4}(|0.6 + 0.8i|^2 + |0.8 - 0.6i|^2) = \frac{1}{2}.
\end{align}

[Return to exercise 4 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-4:-The-outcome-probabilities-for-a-measurement-in-a-specified-basis)

### Exercise 5: Distinguishing orthogonal states - 1

**Inputs:** A qubit which is guaranteed to be in either the $\ket {\psi_+}$ or the $\ket{\psi_-} $ state, where $\ket {\psi_+} = 0.6\ket 0 + 0.8 \ket 1 $ and $\ket {\psi_-} = 0.8\ket 0 - 0.6 \ket 1$.

**Output:** `true` if the qubit was in the $\ket {\psi_+}$ state, or `false` if it was in the $\ket{\psi_-} $ state. The state of the qubit at the end of the operation does not matter.

### Solution
We can distinguish between the states $\ket{\psi_\pm}$ if we implement a measurement in the $\{ \ket{\psi_+}, \ket{\psi_-}\}$ basis. This can be done if we construct a unitary transformation which maps the $\ket{\psi_+}$ state to the $\ket{0}$ state, and the $\ket{\psi_{-}}$ state to the $\ket{1}$ state.

We can notice that the $R_y$ rotation gate with $\theta = 2 \arctan \frac{0.8}{0.6}$ is an appropriate transformation:

\begin{align}
R_y(\theta) \ket 0 &= 0.6 \ket 0 + 0.8 \ket 1 = \ket {\psi_+} \\
R_y(\theta) \ket 1 &= -0.8 \ket 0 + 0.6 \ket 1 = \ket{\psi_-}
\end{align}

Thus, the inverse (adjoint) transformation $R_y(-\theta)$ maps the $\ket{\psi_\pm}$ basis to the computational basis, i.e.,
\begin{align}
R_y(-\theta) \ket {\psi_+} &= \ket 0 \\
R_y(-\theta) \ket {\psi_-} &= \ket 1
\end{align}

Hence, if we apply $R_y(-\theta)$ to the qubit, its state will be transformed to one of the computational basis states, at which point we can measure it using `M`. If `M` returns `Zero`, the rotated state is $\ket{0}$, which means that the original state of the qubit was $\ket{\psi_+}$. Similarly, an output of `One` indicates that the qubit was originally in the state $\ket{\psi_-}$.

```qsharp
%kata T5_IsQubitPsiPlus

open Microsoft.Quantum.Math;

operation IsQubitPsiPlus (q : Qubit) : Bool {
    Ry(-2.0 * ArcTan2(0.8, 0.6), q);
    return M(q) == Zero;
}
```

[Return to exercise 5 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-5:-Distinguishing-orthogonal-states---1)

### Exercise 6: Distinguishing orthogonal states - 2

**Inputs:**

1. Angle $\alpha$, in radians, represented as a `Double`.
2. A qubit which is guaranteed to be in either the $|A\rangle$ or the $|B\rangle$ state, where $|A\rangle = \cos \alpha |0\rangle - i \sin \alpha |1\rangle$ and $|B\rangle = - i \sin \alpha |0\rangle + \cos \alpha |1\rangle$.

**Output:** `true` if the qubit was in the $|A\rangle$ state, or `false` if it was in the $|B\rangle$ state. The state of the qubit at the end of the operation does not matter.

### Solution
We can distinguish between the states $\ket{A}$ and $\ket B$ if we implement a measurement in the $\{ \ket{A}, \ket{B}\}$ basis.

We can notice that the $R_x$ rotation gate with $\theta = 2 \alpha$ is an appropriate transformation which maps the $\ket 0 $ state to the $\ket A$ state, and the $\ket 1$ state to the $\ket B$ state:

\begin{align}
R_x(\theta) \ket 0 &= \cos \alpha \ket 0 -i \sin \alpha \ket 1 = \ket {A} \\
R_x(\theta) \ket 1 &= -i \sin \alpha \ket 0 + \cos \alpha \ket 1 = \ket{B}
\end{align}

Thus, the inverse transformation $R_x(-\theta)$ maps the $A/B$ basis to the $0/1$ basis.

Therefore, if we apply $R_x(-\theta)$ to the qubit and measure it using `M`, a measurement result of `Zero` will correspond to the qubit's original state being $\ket{A}$, while a result of `One` will correspond to the qubit's original state being $\ket B$.

```qsharp
%kata T6_IsQubitA

operation IsQubitA (alpha: Double, q : Qubit) : Bool {
    Rx(-2.0 * alpha, q);
    return M(q) == Zero;
}
```

[Return to exercise 6 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-6:-Distinguishing-orthogonal-states---2)

### Exercise 7: Measurement in the $\ket A$, $\ket B$ basis

**Inputs:**
1. Angle $\alpha$, in radians, represented as a `Double`.
1. A qubit in some unknown state.

**Output:** Implement a measurement in the $\{\ket A$, $\ket B\}$ basis. Same as in the previous exercise, $|A\rangle = \cos \alpha |0\rangle - i \sin \alpha |1\rangle$ and $|B\rangle = - i \sin \alpha |0\rangle + \cos \alpha |1\rangle$. Return `Zero` if the measurement outcome is $A$, and `One` if the outcome is $B$.
The state of the qubit after the measurement should correspond to the measurement result.

### Solution
For this problem, we follow the procedure given in the [arbitrary basis measurement section](./SingleQubitSystemMeasurements.ipynb#Arbitrary-basis-measurements-implementation) of the tutorial.
As noted in the solution of problem 6, the gate $R_x(-\theta)$, with $\theta = 2\alpha$ transforms the $\ket A/\ket B$ states to the $\ket 0/\ket 1$ states:
\begin{align}
R_x(-\theta) \ket A &= \ket 0 \\
R_X(-\theta) \ket B &= \ket 1
\end{align}
Hence, we first apply $R_x(-\theta)$ to the qubit. Next, we measure in the computational basis using the `M` operation.
If the `M` operation returned `Zero`, we get measurement outcome $A$, and if it returned `One`, we get measurement outcome $B$.

After the measurement, we apply the inverse of the $R_x(-\theta)$ gate, which is the $R_x(\theta)$ gate.
The final rotation ensures that the state of the qubit is in the state corresponding to the measurement outcome.

```qsharp
%kata T7_MeasureInABBasis

operation MeasureInABBasis (alpha: Double, q : Qubit) : Result {
    Rx(-2.0 * alpha, q);
    let measurementResult = M(q);
    Rx(2.0 * alpha, q);
    return measurementResult;
}
```

[Return to exercise 7 of the Single-Qubit System Measurements tutorial.](./SingleQubitSystemMeasurements.ipynb#Exercise-7:-Measurement-in-the-$\ket-A$,-$\ket-B$-basis)