```python
from thewalrus import hafnian, tor, quantum, samples, reduction, symplectic, threshold_detection_prob
import strawberryfields as sf
from strawberryfields.ops import *
import numpy as np
from sympy.utilities.iterables import multiset_permutations
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_formats=['svg']
import time
import random
import math
from itertools import *
```

```python
# functions

def prob_tor(cov, sub_mat):

    Husimi_big = quantum.Qmat(cov)

    dim = len(Husimi_big)

    det_hu =  np.linalg.det(Husimi_big)

    denominator = det_hu**0.5

    if np.any(sub_mat) == 0:

        return((1/denominator).real)

    else:

        Husimi_sub = quantum.Qmat(sub_mat)

        dim_sub = len(Husimi_sub)

        O = np.eye(dim_sub) - np.linalg.inv(Husimi_sub)

        Tor = tor(O)

        return (Tor/denominator).real

def prob_exact(M):

    m = len(M)
    stat =  np.zeros((m+1), dtype = np.float32)

    for i in range(m+1):
        permut = []
        list_0 = [1 for j in range(i)]
        list_0 += [0]*(m - i)
        permut += multiset_permutations(list_0)

        if i == 0:
            stat[i] += 1.

        else:
            for j in range(len(permut)):
                stat[i] += Z_i(M,  permut[j])

    for c in range(m):
        for h in range(c+1, m+1):
            stat[h] -= stat[c]*round(fact(m-c)/(fact(h-c)*fact(m - h)))

    s = 0.
    for i in range(m+1):
        s += stat[i]

    return stat/s

def prob_sectors_exact(M):

    m = len(M)
    Nu = 10*m
    dnu = 2*np.pi/Nu

    stat =  np.zeros((m+1, Nu), dtype = np.complex128)
    sectors =  np.zeros((m+1, Nu), dtype = np.complex128)

    for n in range(m+1):

        permut = []
        list_0 = [1 for j in range(n)]
        list_0 += [0]*(m - n)
        permut += multiset_permutations(list_0)

        if n == 0:
             for nu in range(Nu):
                stat[n,nu] +=  1.

        else:

            for nu in range(Nu):
                for k in range(len(permut)):
                    stat[n,nu] += Z_i_v(M, nu*dnu, permut[k])

    for k in range(m):
        for h in range(k+1, m+1):
            for nu in range(Nu):
                stat[h,nu] -= stat[k,nu]*round(fact(m-k)/(fact(h-k)*fact(m - h)))

    for n in range(m+1):
        for j in range(Nu):
            for k in range(Nu):
                sectors[n,j] += stat[n,k]*np.exp(-1j*j*k*dnu)/Nu

    return sectors.real

def Z_i(E, list_det):

    list_det_1 = covert_01_0123(list_det)

    Ei = red_mat(E, list_det_1)

    E_i = Ei.conjugate()@Ei

#     II = np.eye(len(E_i))

#     Zi = (np.linalg.det(II - 4*E_i))**(-0.5)

    eig = np.linalg.eigh(E_i)[0]

    Zi = 1.

    for i in range(len(eig)):

        Zi *= (1 - 4*eig[i])**(-0.5)

    return Zi.real

def Z(M):

    M = M.conjugate().T@M

#     II = np.eye(len(M))

#     z = (np.linalg.det(II - 4*M))**(-0.5)

    eig = np.linalg.eigh(M)[0]

    z = 1.

    for i in range(len(eig)):

        z *= (1 - 4*eig[i])**(-0.5)

    return z.real

def Z_i_v(E, nu, list_det):

    Zi = 1.

    list_det_1 = covert_01_0123(list_det)

    Ei = red_mat(E, list_det_1)

    E_i = Ei.conjugate()@Ei

#     II = np.eye(len(E_i))

#     Zi = (np.linalg.det(II - 4*np.exp(1j*nu)*E_i))**(-0.5)

    eig = np.linalg.eigh(E_i)[0]

    for i in range(len(eig)):
        p = 4*np.exp(1j*nu)*eig[i]
        Zi *= (1 - p)**(-0.5)

    return Zi

def covert_01_0123(list_det):

    #[0,1,1,0] - input
    #[1,2] - output

    list_det_1 = []

    for i in range(len(list_det)):
        if list_det[i] == 1:
            list_det_1.append(i)

    return list_det_1

def fact(x):

    res = 1

    for i in range(int(x)):
        res *=(i+1)

    return res

def red_mat(M_big, list_det): # [0, ..., n,k,l, ..., m-1] - numbers of clicked detectors

    n = len(list_det)

    small_mat =  np.zeros((n, n), dtype = np.complex128)

    # [0,+,0,+] == [1,3]

    for i in range(n):
        for j in range(n):
            ind_i = list_det[i]
            ind_j = list_det[j]
            small_mat[i,j] = M_big[ind_i,ind_j]

    return small_mat

```

```python
# parameters

path = r'data/demo'

data_VS = np.genfromtxt(path + '/Initial_state.dat')

m = len(data_VS) - 1

r = []
phi = []

for i in range(1, m+1):
    r.append(data_VS[i,1])

for i in range(1, m+1):
    phi.append(data_VS[i,2])

data_U = np.genfromtxt(path + '/Parmeters_of_interferometer.dat')

N_BS = len(data_U) - 1
N_PS = 2*N_BS

Phi_list_1 = np.zeros(N_BS)
Phi_list_2 = np.zeros(N_BS)
alfa_list = np.zeros(N_BS)

ind_list = []

for i in range(N_BS):
    ind_list.append([int(data_U[i+1,0]),int(data_U[i+1,1])])

for i in range(N_BS):
    Phi_list_1[i] = data_U[i+1,2]

for i in range(N_BS):
    Phi_list_2[i] = data_U[i+1,3]

for i in range(N_BS):
    alfa_list[i] = data_U[i+1,4]

data_M = np.genfromtxt(path + '/GBS_matrix.dat')

M =  np.zeros((m, m),dtype=np.complex128)

real_part = []
imaginary_part = []

for i in range(m):
    for k in range(0,2*m,2):
        real_part.append(data_M[i,k])

for i in range(m):
    for k in range(1,2*m+1,2):
        imaginary_part.append(data_M[i,k])

for i in range(m*m):
    M[i//m,i%m] = real_part[i] + 1j*imaginary_part[i]

print('Input:','\n')
print('n_modes = ', m ,'\n')
print('r = ', r,'\n')
print('phi = ', phi,'\n')
print('Interferometer:','\n')
print('N_bs = ',N_BS, ', N_ps = ', N_PS ,'\n')

```

    Input:

    n_modes =  8

    r =  [1.5, 1.5, 1.5, 1.5, 0.0, 0.0, 0.0, 0.0]

    phi =  [1.53672, 0.14297, 2.15673, 0.12049, 0.0, 0.0, 0.0, 0.0]

    Interferometer:

    N_bs =  320 , N_ps =  640

```python
possible_values = (0,1)
n_positions = m

sorted_combinations = combinations_with_replacement(possible_values, n_positions)

unique_permutations = set()

for combo in sorted_combinations:
    for p in permutations(combo):
        unique_permutations.add(p)

unique_permutations = sorted(unique_permutations)
print("Number of unique permutations: %i" % (len(unique_permutations)))
# for p in unique_permutations:
#     print(p)
```

    Number of unique permutations: 256

```python
# We check our exact results with the Walrus library https://the-walrus.readthedocs.io
# Here we want to prove that our expressions are in the same units as the conventional GBS problem.

prog = sf.Program(m)

eng = sf.Engine("gaussian")

with prog.context as q:

    for i in range(m):

        Sgate(r[i], phi[i])| q[i]

    for k in range(N_BS):

        Rgate(Phi_list_1[k]) | q[ind_list[k][0]]
        BSgate(alfa_list[k]) | (q[ind_list[k][1]], q[ind_list[k][0]])
        Rgate(Phi_list_2[k]) | q[ind_list[k][1]]

state = eng.run(prog).state

mu = state.means()
cov = state.cov()
```

```python
# start_time = time.process_time()

print("P [s a m p l e] ", "         walrus", "        ours", '\n' )

comb = len(unique_permutations)

P_list_walrus = []
P_list_ours = []

P_sum_walrus = np.zeros(m+1)
P_sum_ours = prob_exact(M)

for p in unique_permutations:

    k = list(p).count(1)
    list_det = covert_01_0123(list(p))

    if k != 0:
        M_sub = red_mat(M, list_det)
        norm = Z(M_sub)/Z(M)
        P_list_ours.append(norm*prob_exact(M_sub)[k])

    else:

        P_list_ours.append(prob_exact(M)[k])

    if k == m:
        P_list_walrus.append(prob_tor(cov, cov))
        P_sum_walrus[k] = prob_tor(cov, cov)

    else:
        P_list_walrus.append(threshold_detection_prob(mu, cov, list(p))) #threshold_detection_prob_parallel
        P_sum_walrus[k] += threshold_detection_prob(mu, cov, list(p))

for k in range(comb):

    print('P', list(unique_permutations[k]), ' = ',  "{:.3e}".format(P_list_walrus[k]), ' | ', "{:.3e}".format(P_list_ours[k]) )

sum_prob_walrus = 0
sum_prob_ours = 0

for i in range(comb):
    sum_prob_walrus +=  P_list_walrus[i]
    sum_prob_ours +=  P_list_ours[i]

print('\n',"sum prob:","{:.3e}".format(sum_prob_walrus), '|', "{:.3e}".format(sum_prob_ours),  '\n')

for i in range(m+1):
    print('n =',i, '|', '  P_walrus = ', "{:.3e}".format(P_sum_walrus[i]), '  P_ours = ', "{:.3e}".format(P_sum_ours[i]))

# print('\n',"--- %s minutes ---" % ((time.process_time() - start_time)/60))
```

    P [s a m p l e]           walrus         ours

    /tmp/ipykernel_9/34118179.py:35: ComplexWarning: Casting complex values to real discards the imaginary part

    P [0, 0, 0, 0, 0, 0, 0, 0]  =  3.265e-02-3.366e-19j  |  3.265e-02
    P [0, 0, 0, 0, 0, 0, 0, 1]  =  2.008e-03+1.218e-18j  |  2.008e-03
    P [0, 0, 0, 0, 0, 0, 1, 0]  =  2.778e-04+3.372e-18j  |  2.778e-04
    P [0, 0, 0, 0, 0, 0, 1, 1]  =  7.032e-03+1.001e-18j  |  7.032e-03
    P [0, 0, 0, 0, 0, 1, 0, 0]  =  1.593e-05+5.658e-19j  |  1.593e-05
    P [0, 0, 0, 0, 0, 1, 0, 1]  =  1.801e-03+7.970e-19j  |  1.801e-03
    P [0, 0, 0, 0, 0, 1, 1, 0]  =  1.356e-03-6.354e-20j  |  1.356e-03
    P [0, 0, 0, 0, 0, 1, 1, 1]  =  1.378e-03+1.873e-18j  |  1.378e-03
    P [0, 0, 0, 0, 1, 0, 0, 0]  =  9.818e-04+2.048e-18j  |  9.818e-04
    P [0, 0, 0, 0, 1, 0, 0, 1]  =  5.467e-03-1.260e-18j  |  5.467e-03
    P [0, 0, 0, 0, 1, 0, 1, 0]  =  7.706e-03-3.006e-19j  |  7.706e-03
    P [0, 0, 0, 0, 1, 0, 1, 1]  =  7.151e-03+2.101e-18j  |  7.151e-03
    P [0, 0, 0, 0, 1, 1, 0, 0]  =  6.483e-04-4.510e-20j  |  6.483e-04
    P [0, 0, 0, 0, 1, 1, 0, 1]  =  7.654e-04-2.847e-19j  |  7.654e-04
    P [0, 0, 0, 0, 1, 1, 1, 0]  =  1.145e-03+2.629e-19j  |  1.145e-03
    P [0, 0, 0, 0, 1, 1, 1, 1]  =  3.587e-03-9.623e-19j  |  3.587e-03
    P [0, 0, 0, 1, 0, 0, 0, 0]  =  3.070e-05+8.215e-19j  |  3.070e-05
    P [0, 0, 0, 1, 0, 0, 0, 1]  =  7.499e-03+6.073e-19j  |  7.499e-03
    P [0, 0, 0, 1, 0, 0, 1, 0]  =  7.941e-04-4.154e-19j  |  7.941e-04
    P [0, 0, 0, 1, 0, 0, 1, 1]  =  3.704e-03+2.135e-18j  |  3.704e-03
    P [0, 0, 0, 1, 0, 1, 0, 0]  =  8.584e-04+2.636e-20j  |  8.584e-04
    P [0, 0, 0, 1, 0, 1, 0, 1]  =  1.917e-03+8.746e-19j  |  1.917e-03
    P [0, 0, 0, 1, 0, 1, 1, 0]  =  2.426e-04+1.549e-19j  |  2.426e-04
    P [0, 0, 0, 1, 0, 1, 1, 1]  =  1.303e-03-1.188e-18j  |  1.303e-03
    P [0, 0, 0, 1, 1, 0, 0, 0]  =  4.397e-04+2.415e-19j  |  4.397e-04
    P [0, 0, 0, 1, 1, 0, 0, 1]  =  4.401e-03+1.485e-18j  |  4.401e-03
    P [0, 0, 0, 1, 1, 0, 1, 0]  =  8.035e-04+8.786e-19j  |  8.035e-04
    P [0, 0, 0, 1, 1, 0, 1, 1]  =  1.613e-02+2.275e-18j  |  1.613e-02
    P [0, 0, 0, 1, 1, 1, 0, 0]  =  1.500e-04+9.998e-20j  |  1.500e-04
    P [0, 0, 0, 1, 1, 1, 0, 1]  =  1.730e-03+4.776e-19j  |  1.730e-03
    P [0, 0, 0, 1, 1, 1, 1, 0]  =  4.840e-04+2.448e-19j  |  4.839e-04
    P [0, 0, 0, 1, 1, 1, 1, 1]  =  7.176e-03+3.253e-18j  |  7.176e-03
    P [0, 0, 1, 0, 0, 0, 0, 0]  =  2.571e-03-3.059e-19j  |  2.571e-03
    P [0, 0, 1, 0, 0, 0, 0, 1]  =  1.165e-03+6.790e-19j  |  1.165e-03
    P [0, 0, 1, 0, 0, 0, 1, 0]  =  4.691e-03-1.089e-18j  |  4.691e-03
    P [0, 0, 1, 0, 0, 0, 1, 1]  =  8.239e-03+2.193e-18j  |  8.239e-03
    P [0, 0, 1, 0, 0, 1, 0, 0]  =  8.242e-04-5.374e-19j  |  8.242e-04
    P [0, 0, 1, 0, 0, 1, 0, 1]  =  1.855e-04-8.375e-20j  |  1.855e-04
    P [0, 0, 1, 0, 0, 1, 1, 0]  =  1.347e-03+1.211e-19j  |  1.347e-03
    P [0, 0, 1, 0, 0, 1, 1, 1]  =  1.791e-03+3.301e-19j  |  1.790e-03
    P [0, 0, 1, 0, 1, 0, 0, 0]  =  2.453e-03+3.357e-19j  |  2.453e-03
    P [0, 0, 1, 0, 1, 0, 0, 1]  =  1.163e-03-1.347e-18j  |  1.163e-03
    P [0, 0, 1, 0, 1, 0, 1, 0]  =  1.066e-02-1.466e-18j  |  1.066e-02
    P [0, 0, 1, 0, 1, 0, 1, 1]  =  1.354e-02+2.686e-18j  |  1.354e-02
    P [0, 0, 1, 0, 1, 1, 0, 0]  =  1.333e-04+9.042e-20j  |  1.333e-04
    P [0, 0, 1, 0, 1, 1, 0, 1]  =  6.896e-04+7.503e-19j  |  6.895e-04
    P [0, 0, 1, 0, 1, 1, 1, 0]  =  4.408e-03+1.976e-18j  |  4.408e-03
    P [0, 0, 1, 0, 1, 1, 1, 1]  =  4.610e-03+1.736e-18j  |  4.610e-03
    P [0, 0, 1, 1, 0, 0, 0, 0]  =  5.205e-06+7.785e-20j  |  5.201e-06
    P [0, 0, 1, 1, 0, 0, 0, 1]  =  1.155e-03+5.395e-19j  |  1.155e-03
    P [0, 0, 1, 1, 0, 0, 1, 0]  =  3.142e-04+7.166e-20j  |  3.142e-04
    P [0, 0, 1, 1, 0, 0, 1, 1]  =  5.751e-03-4.664e-18j  |  5.751e-03
    P [0, 0, 1, 1, 0, 1, 0, 0]  =  1.179e-04+1.520e-19j  |  1.179e-04
    P [0, 0, 1, 1, 0, 1, 0, 1]  =  4.665e-04+7.920e-19j  |  4.665e-04
    P [0, 0, 1, 1, 0, 1, 1, 0]  =  4.953e-04+2.498e-19j  |  4.953e-04
    P [0, 0, 1, 1, 0, 1, 1, 1]  =  1.666e-03+2.846e-18j  |  1.666e-03
    P [0, 0, 1, 1, 1, 0, 0, 0]  =  9.390e-05-2.008e-20j  |  9.390e-05
    P [0, 0, 1, 1, 1, 0, 0, 1]  =  1.228e-03-3.004e-19j  |  1.228e-03
    P [0, 0, 1, 1, 1, 0, 1, 0]  =  1.926e-03-7.707e-19j  |  1.926e-03
    P [0, 0, 1, 1, 1, 0, 1, 1]  =  1.990e-02+1.264e-18j  |  1.990e-02
    P [0, 0, 1, 1, 1, 1, 0, 0]  =  8.348e-05+1.675e-19j  |  8.349e-05
    P [0, 0, 1, 1, 1, 1, 0, 1]  =  1.001e-03-2.205e-18j  |  1.001e-03
    P [0, 0, 1, 1, 1, 1, 1, 0]  =  2.023e-03-4.905e-18j  |  2.023e-03
    P [0, 0, 1, 1, 1, 1, 1, 1]  =  8.115e-03-2.603e-18j  |  8.115e-03
    P [0, 1, 0, 0, 0, 0, 0, 0]  =  1.691e-03+1.708e-20j  |  1.691e-03
    P [0, 1, 0, 0, 0, 0, 0, 1]  =  2.702e-03+3.925e-19j  |  2.702e-03
    P [0, 1, 0, 0, 0, 0, 1, 0]  =  5.892e-04-1.003e-19j  |  5.892e-04
    P [0, 1, 0, 0, 0, 0, 1, 1]  =  1.900e-03+1.079e-18j  |  1.899e-03
    P [0, 1, 0, 0, 0, 1, 0, 0]  =  2.120e-04+1.223e-19j  |  2.120e-04
    P [0, 1, 0, 0, 0, 1, 0, 1]  =  6.259e-04-9.090e-20j  |  6.259e-04
    P [0, 1, 0, 0, 0, 1, 1, 0]  =  1.676e-04-3.098e-20j  |  1.676e-04
    P [0, 1, 0, 0, 0, 1, 1, 1]  =  1.285e-03-1.691e-18j  |  1.285e-03
    P [0, 1, 0, 0, 1, 0, 0, 0]  =  3.677e-03+1.325e-18j  |  3.677e-03
    P [0, 1, 0, 0, 1, 0, 0, 1]  =  1.855e-03+6.805e-19j  |  1.855e-03
    P [0, 1, 0, 0, 1, 0, 1, 0]  =  2.301e-03+1.026e-18j  |  2.301e-03
    P [0, 1, 0, 0, 1, 0, 1, 1]  =  4.694e-03-1.320e-18j  |  4.694e-03
    P [0, 1, 0, 0, 1, 1, 0, 0]  =  4.217e-04+1.364e-19j  |  4.217e-04
    P [0, 1, 0, 0, 1, 1, 0, 1]  =  7.328e-04+1.843e-18j  |  7.328e-04
    P [0, 1, 0, 0, 1, 1, 1, 0]  =  5.758e-04+8.166e-19j  |  5.758e-04
    P [0, 1, 0, 0, 1, 1, 1, 1]  =  3.127e-03+4.385e-18j  |  3.127e-03
    P [0, 1, 0, 1, 0, 0, 0, 0]  =  5.639e-03-5.830e-19j  |  5.639e-03
    P [0, 1, 0, 1, 0, 0, 0, 1]  =  6.705e-03-1.066e-18j  |  6.705e-03
    P [0, 1, 0, 1, 0, 0, 1, 0]  =  4.285e-04+6.237e-19j  |  4.285e-04
    P [0, 1, 0, 1, 0, 0, 1, 1]  =  2.809e-03-3.157e-18j  |  2.809e-03
    P [0, 1, 0, 1, 0, 1, 0, 0]  =  6.566e-04+1.341e-19j  |  6.566e-04
    P [0, 1, 0, 1, 0, 1, 0, 1]  =  3.883e-03+1.744e-19j  |  3.883e-03
    P [0, 1, 0, 1, 0, 1, 1, 0]  =  4.990e-04+5.612e-20j  |  4.990e-04
    P [0, 1, 0, 1, 0, 1, 1, 1]  =  1.870e-03+6.699e-18j  |  1.870e-03
    P [0, 1, 0, 1, 1, 0, 0, 0]  =  3.283e-03+1.045e-18j  |  3.283e-03
    P [0, 1, 0, 1, 1, 0, 0, 1]  =  1.243e-02-2.424e-18j  |  1.243e-02
    P [0, 1, 0, 1, 1, 0, 1, 0]  =  4.236e-03+6.295e-20j  |  4.236e-03
    P [0, 1, 0, 1, 1, 0, 1, 1]  =  2.482e-02+1.605e-17j  |  2.482e-02
    P [0, 1, 0, 1, 1, 1, 0, 0]  =  1.820e-03+8.229e-20j  |  1.820e-03
    P [0, 1, 0, 1, 1, 1, 0, 1]  =  8.396e-03+4.332e-18j  |  8.396e-03
    P [0, 1, 0, 1, 1, 1, 1, 0]  =  1.883e-03+1.220e-18j  |  1.883e-03
    P [0, 1, 0, 1, 1, 1, 1, 1]  =  2.522e-02+4.735e-18j  |  2.522e-02
    P [0, 1, 1, 0, 0, 0, 0, 0]  =  3.714e-03-5.448e-19j  |  3.714e-03
    P [0, 1, 1, 0, 0, 0, 0, 1]  =  6.055e-04+2.314e-19j  |  6.055e-04
    P [0, 1, 1, 0, 0, 0, 1, 0]  =  2.346e-03-6.696e-19j  |  2.346e-03
    P [0, 1, 1, 0, 0, 0, 1, 1]  =  2.725e-03-5.492e-19j  |  2.725e-03
    P [0, 1, 1, 0, 0, 1, 0, 0]  =  5.440e-04+1.969e-20j  |  5.440e-04
    P [0, 1, 1, 0, 0, 1, 0, 1]  =  5.167e-04+3.575e-20j  |  5.167e-04
    P [0, 1, 1, 0, 0, 1, 1, 0]  =  1.668e-03-5.143e-20j  |  1.668e-03
    P [0, 1, 1, 0, 0, 1, 1, 1]  =  1.191e-03+5.177e-19j  |  1.191e-03
    P [0, 1, 1, 0, 1, 0, 0, 0]  =  5.656e-04+2.593e-19j  |  5.656e-04
    P [0, 1, 1, 0, 1, 0, 0, 1]  =  2.941e-03+7.798e-19j  |  2.941e-03
    P [0, 1, 1, 0, 1, 0, 1, 0]  =  3.545e-03+8.309e-19j  |  3.545e-03
    P [0, 1, 1, 0, 1, 0, 1, 1]  =  8.900e-03-3.648e-18j  |  8.900e-03
    P [0, 1, 1, 0, 1, 1, 0, 0]  =  3.290e-04+4.770e-19j  |  3.290e-04
    P [0, 1, 1, 0, 1, 1, 0, 1]  =  9.242e-04-2.445e-18j  |  9.242e-04
    P [0, 1, 1, 0, 1, 1, 1, 0]  =  2.292e-03-7.315e-18j  |  2.292e-03
    P [0, 1, 1, 0, 1, 1, 1, 1]  =  6.172e-03-1.394e-17j  |  6.172e-03
    P [0, 1, 1, 1, 0, 0, 0, 0]  =  2.125e-03+7.101e-19j  |  2.125e-03
    P [0, 1, 1, 1, 0, 0, 0, 1]  =  3.652e-03+1.540e-18j  |  3.652e-03
    P [0, 1, 1, 1, 0, 0, 1, 0]  =  2.481e-03-1.515e-18j  |  2.481e-03
    P [0, 1, 1, 1, 0, 0, 1, 1]  =  8.932e-03-6.922e-18j  |  8.932e-03
    P [0, 1, 1, 1, 0, 1, 0, 0]  =  1.351e-03+5.250e-19j  |  1.351e-03
    P [0, 1, 1, 1, 0, 1, 0, 1]  =  2.994e-03-1.133e-18j  |  2.994e-03
    P [0, 1, 1, 1, 0, 1, 1, 0]  =  3.306e-03-1.242e-18j  |  3.306e-03
    P [0, 1, 1, 1, 0, 1, 1, 1]  =  5.427e-03+2.217e-18j  |  5.427e-03
    P [0, 1, 1, 1, 1, 0, 0, 0]  =  1.046e-03+1.423e-18j  |  1.046e-03
    P [0, 1, 1, 1, 1, 0, 0, 1]  =  1.087e-02+3.346e-18j  |  1.087e-02
    P [0, 1, 1, 1, 1, 0, 1, 0]  =  5.484e-03+3.136e-18j  |  5.484e-03
    P [0, 1, 1, 1, 1, 0, 1, 1]  =  3.875e-02+1.329e-17j  |  3.875e-02
    P [0, 1, 1, 1, 1, 1, 0, 0]  =  1.151e-03-1.625e-19j  |  1.151e-03
    P [0, 1, 1, 1, 1, 1, 0, 1]  =  1.243e-02-5.113e-18j  |  1.243e-02
    P [0, 1, 1, 1, 1, 1, 1, 0]  =  6.440e-03+8.596e-18j  |  6.439e-03
    P [0, 1, 1, 1, 1, 1, 1, 1]  =  4.901e-02+9.975e-17j  |  4.901e-02
    P [1, 0, 0, 0, 0, 0, 0, 0]  =  2.940e-04-7.697e-19j  |  2.940e-04
    P [1, 0, 0, 0, 0, 0, 0, 1]  =  6.437e-04-3.277e-19j  |  6.437e-04
    P [1, 0, 0, 0, 0, 0, 1, 0]  =  1.956e-03+3.622e-19j  |  1.956e-03
    P [1, 0, 0, 0, 0, 0, 1, 1]  =  2.101e-03-5.057e-20j  |  2.101e-03
    P [1, 0, 0, 0, 0, 1, 0, 0]  =  2.178e-04+1.908e-19j  |  2.178e-04
    P [1, 0, 0, 0, 0, 1, 0, 1]  =  1.822e-04-1.827e-19j  |  1.822e-04
    P [1, 0, 0, 0, 0, 1, 1, 0]  =  2.671e-04+1.598e-19j  |  2.671e-04
    P [1, 0, 0, 0, 0, 1, 1, 1]  =  1.420e-03-2.044e-19j  |  1.420e-03
    P [1, 0, 0, 0, 1, 0, 0, 0]  =  3.911e-04+1.264e-19j  |  3.911e-04
    P [1, 0, 0, 0, 1, 0, 0, 1]  =  2.475e-04-4.085e-19j  |  2.475e-04
    P [1, 0, 0, 0, 1, 0, 1, 0]  =  1.391e-03-1.246e-19j  |  1.391e-03
    P [1, 0, 0, 0, 1, 0, 1, 1]  =  3.290e-03-1.177e-18j  |  3.290e-03
    P [1, 0, 0, 0, 1, 1, 0, 0]  =  5.375e-05+1.310e-20j  |  5.375e-05
    P [1, 0, 0, 0, 1, 1, 0, 1]  =  6.339e-05+4.339e-19j  |  6.339e-05
    P [1, 0, 0, 0, 1, 1, 1, 0]  =  6.411e-04+3.943e-19j  |  6.411e-04
    P [1, 0, 0, 0, 1, 1, 1, 1]  =  3.145e-03+4.560e-18j  |  3.145e-03
    P [1, 0, 0, 1, 0, 0, 0, 0]  =  1.402e-04-1.103e-19j  |  1.402e-04
    P [1, 0, 0, 1, 0, 0, 0, 1]  =  3.690e-04+2.089e-19j  |  3.690e-04
    P [1, 0, 0, 1, 0, 0, 1, 0]  =  1.287e-04+2.260e-19j  |  1.287e-04
    P [1, 0, 0, 1, 0, 0, 1, 1]  =  1.396e-03-2.604e-18j  |  1.396e-03
    P [1, 0, 0, 1, 0, 1, 0, 0]  =  3.590e-05-4.962e-20j  |  3.590e-05
    P [1, 0, 0, 1, 0, 1, 0, 1]  =  1.655e-04+3.184e-19j  |  1.654e-04
    P [1, 0, 0, 1, 0, 1, 1, 0]  =  1.187e-04+2.259e-20j  |  1.187e-04
    P [1, 0, 0, 1, 0, 1, 1, 1]  =  8.393e-04+2.386e-18j  |  8.393e-04
    P [1, 0, 0, 1, 1, 0, 0, 0]  =  4.031e-05-2.796e-20j  |  4.031e-05
    P [1, 0, 0, 1, 1, 0, 0, 1]  =  4.118e-04-1.607e-20j  |  4.118e-04
    P [1, 0, 0, 1, 1, 0, 1, 0]  =  1.514e-04+4.419e-19j  |  1.514e-04
    P [1, 0, 0, 1, 1, 0, 1, 1]  =  6.862e-03+9.339e-18j  |  6.862e-03
    P [1, 0, 0, 1, 1, 1, 0, 0]  =  5.251e-05-2.093e-20j  |  5.251e-05
    P [1, 0, 0, 1, 1, 1, 0, 1]  =  1.672e-04+1.954e-19j  |  1.672e-04
    P [1, 0, 0, 1, 1, 1, 1, 0]  =  2.087e-04-7.650e-19j  |  2.087e-04
    P [1, 0, 0, 1, 1, 1, 1, 1]  =  6.317e-03-8.196e-18j  |  6.317e-03
    P [1, 0, 1, 0, 0, 0, 0, 0]  =  6.704e-04+3.904e-20j  |  6.704e-04
    P [1, 0, 1, 0, 0, 0, 0, 1]  =  1.098e-04+3.074e-19j  |  1.098e-04
    P [1, 0, 1, 0, 0, 0, 1, 0]  =  1.342e-03+1.553e-19j  |  1.342e-03
    P [1, 0, 1, 0, 0, 0, 1, 1]  =  4.310e-03-4.422e-18j  |  4.310e-03
    P [1, 0, 1, 0, 0, 1, 0, 0]  =  6.007e-05-2.741e-20j  |  6.007e-05
    P [1, 0, 1, 0, 0, 1, 0, 1]  =  8.887e-05+3.315e-19j  |  8.887e-05
    P [1, 0, 1, 0, 0, 1, 1, 0]  =  4.118e-04-6.450e-19j  |  4.118e-04
    P [1, 0, 1, 0, 0, 1, 1, 1]  =  1.745e-03+1.206e-18j  |  1.745e-03
    P [1, 0, 1, 0, 1, 0, 0, 0]  =  9.150e-05-1.092e-19j  |  9.150e-05
    P [1, 0, 1, 0, 1, 0, 0, 1]  =  1.686e-04+6.851e-19j  |  1.686e-04
    P [1, 0, 1, 0, 1, 0, 1, 0]  =  3.532e-03+3.003e-18j  |  3.532e-03
    P [1, 0, 1, 0, 1, 0, 1, 1]  =  6.066e-03+8.754e-18j  |  6.066e-03
    P [1, 0, 1, 0, 1, 1, 0, 0]  =  8.616e-05+3.020e-20j  |  8.617e-05
    P [1, 0, 1, 0, 1, 1, 0, 1]  =  7.441e-05-1.267e-18j  |  7.443e-05
    P [1, 0, 1, 0, 1, 1, 1, 0]  =  1.896e-03-1.153e-18j  |  1.896e-03
    P [1, 0, 1, 0, 1, 1, 1, 1]  =  6.178e-03-2.302e-17j  |  6.177e-03
    P [1, 0, 1, 1, 0, 0, 0, 0]  =  1.915e-05+6.245e-20j  |  1.915e-05
    P [1, 0, 1, 1, 0, 0, 0, 1]  =  1.982e-04-6.586e-20j  |  1.982e-04
    P [1, 0, 1, 1, 0, 0, 1, 0]  =  1.823e-04-1.450e-19j  |  1.823e-04
    P [1, 0, 1, 1, 0, 0, 1, 1]  =  3.049e-03+6.365e-18j  |  3.049e-03
    P [1, 0, 1, 1, 0, 1, 0, 0]  =  4.591e-05+1.089e-20j  |  4.590e-05
    P [1, 0, 1, 1, 0, 1, 0, 1]  =  9.490e-05-1.466e-18j  |  9.491e-05
    P [1, 0, 1, 1, 0, 1, 1, 0]  =  2.047e-04+4.632e-20j  |  2.047e-04
    P [1, 0, 1, 1, 0, 1, 1, 1]  =  1.683e-03+1.105e-18j  |  1.683e-03
    P [1, 0, 1, 1, 1, 0, 0, 0]  =  1.823e-05-3.279e-20j  |  1.823e-05
    P [1, 0, 1, 1, 1, 0, 0, 1]  =  1.694e-04+6.595e-19j  |  1.694e-04
    P [1, 0, 1, 1, 1, 0, 1, 0]  =  8.245e-04-5.268e-19j  |  8.245e-04
    P [1, 0, 1, 1, 1, 0, 1, 1]  =  7.296e-03+7.318e-18j  |  7.296e-03
    P [1, 0, 1, 1, 1, 1, 0, 0]  =  2.266e-05-1.166e-19j  |  2.268e-05
    P [1, 0, 1, 1, 1, 1, 0, 1]  =  1.294e-04+2.074e-18j  |  1.293e-04
    P [1, 0, 1, 1, 1, 1, 1, 0]  =  1.353e-03+6.948e-18j  |  1.353e-03
    P [1, 0, 1, 1, 1, 1, 1, 1]  =  8.512e-03+3.913e-17j  |  8.511e-03
    P [1, 1, 0, 0, 0, 0, 0, 0]  =  2.241e-03+1.075e-18j  |  2.241e-03
    P [1, 1, 0, 0, 0, 0, 0, 1]  =  1.249e-03-3.089e-20j  |  1.249e-03
    P [1, 1, 0, 0, 0, 0, 1, 0]  =  8.311e-04+2.282e-19j  |  8.311e-04
    P [1, 1, 0, 0, 0, 0, 1, 1]  =  4.482e-03+1.034e-19j  |  4.482e-03
    P [1, 1, 0, 0, 0, 1, 0, 0]  =  1.178e-04+1.384e-19j  |  1.178e-04
    P [1, 1, 0, 0, 0, 1, 0, 1]  =  7.676e-04+2.143e-19j  |  7.676e-04
    P [1, 1, 0, 0, 0, 1, 1, 0]  =  4.079e-04+6.638e-20j  |  4.079e-04
    P [1, 1, 0, 0, 0, 1, 1, 1]  =  4.906e-03+4.401e-18j  |  4.906e-03
    P [1, 1, 0, 0, 1, 0, 0, 0]  =  1.259e-03+1.252e-19j  |  1.259e-03
    P [1, 1, 0, 0, 1, 0, 0, 1]  =  5.276e-04+3.729e-19j  |  5.276e-04
    P [1, 1, 0, 0, 1, 0, 1, 0]  =  2.235e-03+1.884e-20j  |  2.235e-03
    P [1, 1, 0, 0, 1, 0, 1, 1]  =  6.477e-03+2.621e-18j  |  6.477e-03
    P [1, 1, 0, 0, 1, 1, 0, 0]  =  4.006e-04+3.589e-19j  |  4.006e-04
    P [1, 1, 0, 0, 1, 1, 0, 1]  =  4.321e-04-8.341e-19j  |  4.320e-04
    P [1, 1, 0, 0, 1, 1, 1, 0]  =  1.349e-03-1.731e-18j  |  1.349e-03
    P [1, 1, 0, 0, 1, 1, 1, 1]  =  1.306e-02-1.073e-17j  |  1.306e-02
    P [1, 1, 0, 1, 0, 0, 0, 0]  =  1.392e-03+3.166e-19j  |  1.392e-03
    P [1, 1, 0, 1, 0, 0, 0, 1]  =  2.555e-03+2.922e-19j  |  2.555e-03
    P [1, 1, 0, 1, 0, 0, 1, 0]  =  4.753e-04-7.285e-20j  |  4.753e-04
    P [1, 1, 0, 1, 0, 0, 1, 1]  =  3.827e-03+4.809e-18j  |  3.827e-03
    P [1, 1, 0, 1, 0, 1, 0, 0]  =  5.380e-04+1.576e-19j  |  5.380e-04
    P [1, 1, 0, 1, 0, 1, 0, 1]  =  2.939e-03+1.256e-18j  |  2.939e-03
    P [1, 1, 0, 1, 0, 1, 1, 0]  =  4.981e-04+1.633e-19j  |  4.981e-04
    P [1, 1, 0, 1, 0, 1, 1, 1]  =  6.525e-03-3.784e-18j  |  6.525e-03
    P [1, 1, 0, 1, 1, 0, 0, 0]  =  2.631e-03+8.952e-19j  |  2.631e-03
    P [1, 1, 0, 1, 1, 0, 0, 1]  =  4.512e-03+4.545e-18j  |  4.512e-03
    P [1, 1, 0, 1, 1, 0, 1, 0]  =  3.193e-03+4.798e-18j  |  3.193e-03
    P [1, 1, 0, 1, 1, 0, 1, 1]  =  1.257e-02-8.113e-19j  |  1.257e-02
    P [1, 1, 0, 1, 1, 1, 0, 0]  =  2.175e-03+1.783e-18j  |  2.175e-03
    P [1, 1, 0, 1, 1, 1, 0, 1]  =  6.010e-03+2.559e-18j  |  6.010e-03
    P [1, 1, 0, 1, 1, 1, 1, 0]  =  4.762e-03-9.731e-19j  |  4.762e-03
    P [1, 1, 0, 1, 1, 1, 1, 1]  =  2.782e-02+7.536e-17j  |  2.782e-02
    P [1, 1, 1, 0, 0, 0, 0, 0]  =  1.593e-03+1.292e-19j  |  1.593e-03
    P [1, 1, 1, 0, 0, 0, 0, 1]  =  4.302e-04+4.718e-20j  |  4.302e-04
    P [1, 1, 1, 0, 0, 0, 1, 0]  =  2.603e-03-8.021e-19j  |  2.603e-03
    P [1, 1, 1, 0, 0, 0, 1, 1]  =  3.721e-03+2.946e-18j  |  3.721e-03
    P [1, 1, 1, 0, 0, 1, 0, 0]  =  4.641e-04+2.329e-19j  |  4.641e-04
    P [1, 1, 1, 0, 0, 1, 0, 1]  =  6.090e-04+4.417e-19j  |  6.090e-04
    P [1, 1, 1, 0, 0, 1, 1, 0]  =  2.044e-03+3.820e-19j  |  2.044e-03
    P [1, 1, 1, 0, 0, 1, 1, 1]  =  3.957e-03-2.415e-18j  |  3.957e-03
    P [1, 1, 1, 0, 1, 0, 0, 0]  =  8.942e-04-1.490e-19j  |  8.942e-04
    P [1, 1, 1, 0, 1, 0, 0, 1]  =  8.406e-04-5.086e-19j  |  8.406e-04
    P [1, 1, 1, 0, 1, 0, 1, 0]  =  2.991e-03-1.779e-18j  |  2.991e-03
    P [1, 1, 1, 0, 1, 0, 1, 1]  =  1.377e-02+1.295e-17j  |  1.377e-02
    P [1, 1, 1, 0, 1, 1, 0, 0]  =  5.004e-04-1.686e-19j  |  5.004e-04
    P [1, 1, 1, 0, 1, 1, 0, 1]  =  5.333e-04+3.393e-18j  |  5.333e-04
    P [1, 1, 1, 0, 1, 1, 1, 0]  =  2.705e-03+1.527e-18j  |  2.705e-03
    P [1, 1, 1, 0, 1, 1, 1, 1]  =  2.259e-02+2.612e-17j  |  2.259e-02
    P [1, 1, 1, 1, 0, 0, 0, 0]  =  1.803e-03+1.165e-18j  |  1.803e-03
    P [1, 1, 1, 1, 0, 0, 0, 1]  =  1.744e-03-1.610e-18j  |  1.744e-03
    P [1, 1, 1, 1, 0, 0, 1, 0]  =  2.599e-03+7.010e-19j  |  2.599e-03
    P [1, 1, 1, 1, 0, 0, 1, 1]  =  9.942e-03+1.976e-17j  |  9.942e-03
    P [1, 1, 1, 1, 0, 1, 0, 0]  =  1.467e-03-1.052e-18j  |  1.467e-03
    P [1, 1, 1, 1, 0, 1, 0, 1]  =  3.587e-03+4.007e-18j  |  3.587e-03
    P [1, 1, 1, 1, 0, 1, 1, 0]  =  5.921e-03+3.961e-18j  |  5.921e-03
    P [1, 1, 1, 1, 0, 1, 1, 1]  =  1.290e-02+1.930e-17j  |  1.290e-02
    P [1, 1, 1, 1, 1, 0, 0, 0]  =  2.215e-03-7.218e-19j  |  2.215e-03
    P [1, 1, 1, 1, 1, 0, 0, 1]  =  6.033e-03+3.134e-18j  |  6.033e-03
    P [1, 1, 1, 1, 1, 0, 1, 0]  =  4.396e-03-3.884e-18j  |  4.396e-03
    P [1, 1, 1, 1, 1, 0, 1, 1]  =  3.100e-02-5.153e-17j  |  3.100e-02
    P [1, 1, 1, 1, 1, 1, 0, 0]  =  4.327e-03+5.781e-18j  |  4.327e-03
    P [1, 1, 1, 1, 1, 1, 0, 1]  =  1.054e-02-1.708e-17j  |  1.054e-02
    P [1, 1, 1, 1, 1, 1, 1, 0]  =  1.127e-02-1.760e-17j  |  1.127e-02
    P [1, 1, 1, 1, 1, 1, 1, 1]  =  7.251e-02  |  7.251e-02

     sum prob: 1.000e+00+2.917e-16j | 1.000e+00

    n = 0 |   P_walrus =  3.265e-02   P_ours =  3.265e-02
    n = 1 |   P_walrus =  7.869e-03   P_ours =  7.869e-03
    n = 2 |   P_walrus =  6.553e-02   P_ours =  6.553e-02
    n = 3 |   P_walrus =  8.247e-02   P_ours =  8.247e-02
    n = 4 |   P_walrus =  1.448e-01   P_ours =  1.448e-01
    n = 5 |   P_walrus =  1.969e-01   P_ours =  1.969e-01
    n = 6 |   P_walrus =  2.236e-01   P_ours =  2.236e-01
    n = 7 |   P_walrus =  1.736e-01   P_ours =  1.736e-01
    n = 8 |   P_walrus =  7.251e-02   P_ours =  7.251e-02

```python
n_cl = [i for i in range(m+1)]
plt.plot(n_cl, P_sum_walrus, label = 'the walrus')
plt.plot(n_cl, P_sum_ours, '.', label = 'our scheme')
plt.yscale('log')
plt.legend(prop={'size':13}, loc='lower center')
plt.xlabel(r'n',fontsize=20)
plt.ylabel(r'P',fontsize=20)
plt.show()
```

```python
# Calculation the probabilities via sectors

print("P [s a m p l e] ", "         exact", "   sum over sectors", '\n' )

# P[random sample]

N_comb = unique_permutations.index((1,1,0,1,0,1,0,1))

list_det = list(unique_permutations[N_comb])

n_tar = list_det.count(1)

list_det_ = covert_01_0123(list_det)

M_sub = red_mat(M, list_det_)

P_sectors =  prob_sectors_exact(M_sub)

norm = 1/Z(M)

P_ = 0

for nu in range(10*n_tar):
    P_ += P_sectors[n_tar,nu]*norm

print('P',list_det, ' = ', "{:.3e}".format(P_list_ours[N_comb]) ,'  ', "{:.3e}".format(P_))

```

    P [s a m p l e]           exact    sum over sectors

    P [1, 1, 0, 1, 0, 1, 0, 1]  =  2.939e-03    2.939e-03

```python
# Sectors for the last point of P(n) ( P[1,1,1,1,1,1] )

P_sectors =  prob_sectors_exact(M)

plt.plot(n_cl, P_sum_ours, '-', label = 'our scheme')

for nu in range(m,m*10,m):
    plt.plot([i for i in range(m+1)], [P_sectors[j,nu]/Z(M) for j in range(m+1)],'--' ,label = 'k='+str(2*nu))
plt.yscale('log')
plt.legend(prop={'size':10}, loc='lower left')
plt.xlabel(r'n',fontsize=20)
plt.ylabel(r'P',fontsize=20)
plt.ylim([10**(-6),10**(-0.3)])
plt.show()
```

```python
# Moments calculation

# Import minors

Nu = 10*m

data_minors = np.genfromtxt(path + r'/Minors0-1.dat')
data_minors2 = np.genfromtxt(path + r'/Minors2.dat')
data_minors3 = np.genfromtxt(path + r'/Minors3.dat')
data_minors4 = np.genfromtxt(path + r'/Minors4.dat')

p2 = round(fact(m)/(fact(m-2)*2))
p3 = round(fact(m)/(fact(m - 3)*fact(3)))
p4 = round(fact(m)/(fact(m - 4)*fact(4)))

Z_v_0 = np.zeros((Nu),dtype=np.complex128)

Z_v_1 = np.zeros((m, Nu),dtype=np.complex128)

Z_v_2 = np.zeros((p2, Nu),dtype=np.complex128)

Z_v_3 = np.zeros((p3, Nu),dtype=np.complex128)

Z_v_4 = np.zeros((p4, Nu),dtype=np.complex128)

for j in range(Nu):
     Z_v_0[j] =  data_minors[j,1:2] + 1j*data_minors[j,2:3]

for j in range(Nu):
    for n in range(0,2*m,2):
        Z_v_1[n//2,j] =  data_minors[j,int(3+n)] + 1j*data_minors[j,int(4+n)]

for j in range(Nu):
    for n in range(0,2*p2,2):
        Z_v_2[n//2,j] =  data_minors2[j,int(1+n)] + 1j*data_minors2[j,int(2+n)]

for j in range(Nu):
    for n in range(0,2*p3,2):
        Z_v_3[n//2,j] =  data_minors3[j,int(1+(n))] + 1j*data_minors3[j,int(2+(n))]

for j in range(Nu):
    for n in range(0,2*p4,2):
        Z_v_4[n//2,j] =  data_minors4[j,int(1+(n))] + 1j*data_minors4[j,int(2+(n))]

Z_v_0f = np.fft.fft(Z_v_0)/Nu
Z_v_1f = np.fft.fft(Z_v_1)/Nu
Z_v_2f = np.fft.fft(Z_v_2)/Nu
Z_v_3f = np.fft.fft(Z_v_3)/Nu
Z_v_4f = np.fft.fft(Z_v_4)/Nu
```

```python
# Moments calculation

mean_ = np.zeros(Nu)
disp_ = np.zeros(Nu)
m3_ = np.zeros(Nu)
m4_ = np.zeros(Nu)
m5_ = np.zeros(Nu)

def moment_formula(n, *args):

    m = 0

    for x in args:
            moments = x

    if n == 2:
        m = moments[0] + 2*moments[1] - moments[0]**2

    if n == 3:
        m = moments[0] + 6*moments[1] + 6*moments[2] - 3*mean_[nu]*(moments[0] + 2*moments[1]) +  2*moments[0]**3

    if n == 4:
        m_2 = moments[0] + 2*moments[1]

        m_3 = moments[0] + 6*moments[1] + 6*moments[2]

        m_4 = moments[0] + 14*moments[1] + 36*moments[2] + 24*moments[3]

        m =  m_4 - 4*m_3*moments[0]- 3*m_2**2 + 12*m_2*moments[0]**2 - 6*moments[0]**4

    return m

n_ij_v =  np.zeros(Nu)
n_ijk_v = np.zeros(Nu)
n_ijkl_v = np.zeros(Nu)
n_ijklp_v = np.zeros(Nu)

ind_2 = []
ind_3 = []
ind_4 = []

for i in range(m):
        for j in range(i+1, m):
            ind_2.append([i,j])

for i in range(m):
        for j in range(i+1, m):
            for k in range(j+1, m):
                ind_3.append([i,j,k])

for i in range(m):
    for j in range(i+1, m):
        for k in range(j+1, m):
            for l in range(k+1, m):
                ind_4.append([i,j,k,l])

for z in range(Nu):
    for j in range(m):
        mean_[z] += 1 - (Z_v_1f[j,z]/Z_v_0f[z]).real

for nu in range(Nu):
    i_ = 0
    for i in range(m):
        for j in range(i+1, m):
            n_ij_v[nu] += 1 - (( Z_v_1f[j,nu] + Z_v_1f[i,nu] - Z_v_2f[i_,nu])/Z_v_0f[nu]).real
            i_ += 1
    disp_[nu] =  moment_formula(2, [mean_[nu], n_ij_v[nu]])

for nu in range(Nu):
    i_= 0
    for i in range(m):
        for j in range(i+1, m):
            for k in range(j+1, m):

                z1 = ind_2.index([i,j])
                z2 = ind_2.index([i,k])
                z3 = ind_2.index([j,k])

                n_ijk_v[nu] += 1 - ((Z_v_1f[i,nu] + Z_v_1f[j,nu] + Z_v_1f[k,nu] - Z_v_2f[z1,nu] - Z_v_2f[z2,nu] - Z_v_2f[z3,nu] + Z_v_3f[i_,nu])/Z_v_0f[nu]).real
                i_ += 1

    m3_[nu] = moment_formula(3, [mean_[nu], n_ij_v[nu], n_ijk_v[nu]])

for nu in range(Nu):
    i_= 0
    for i in range(m):
        for j in range(i+1, m):
            for k in range(j+1, m):
                for l in range(k+1, m):

                    z1 = ind_2.index([i,j])
                    z2 = ind_2.index([i,k])
                    z3 = ind_2.index([i,l])

                    z4 = ind_2.index([j,k])
                    z5 = ind_2.index([k,l])
                    z6 = ind_2.index([j,l])

                    h1 = ind_3.index([i,j,k])
                    h2 = ind_3.index([j,k,l])
                    h3 = ind_3.index([i,k,l])
                    h4 = ind_3.index([i,j,l])

                    n_ijkl_v[nu] += 1 - ((Z_v_1f[i,nu] + Z_v_1f[j,nu] + Z_v_1f[k,nu] + Z_v_1f[l,nu] - Z_v_2f[z1,nu] - Z_v_2f[z2,nu] - Z_v_2f[z3,nu] - Z_v_2f[z4,nu] - Z_v_2f[z5,nu] - Z_v_2f[z6,nu] + Z_v_3f[h1,nu] + Z_v_3f[h2,nu] +  Z_v_3f[h3,nu] + Z_v_3f[h4,nu] -  Z_v_4f[i_,nu])/Z_v_0f[nu]).real

                    i_ += 1

    m4_[nu] = moment_formula(4, [mean_[nu], n_ij_v[nu], n_ijk_v[nu], n_ijkl_v[nu]])
```

```python
# Approximation
old_settings = np.seterr(all='ignore')
def gauss_fun(x, *args):

    for c in args:
        c = args

    if len(c) == 3:

        res = c[0]*np.exp(-(x - c[1])**2/(2*c[2]))

    if len(c) == 4:

        res = c[0]*np.exp(-(x - c[1])**2/(2*c[2])) * np.exp(+ c[3]*(x - c[1])**3/(6*c[2]**3))

    if len(c) == 5:

        res = c[0]*np.exp(-(x - c[1])**2/(2*c[2])) * np.exp(+ c[3]*(x - c[1])**3/(6*c[2]**3)) * np.exp(+ c[4]*(x - c[1])**4/(8*c[2]**4))

    return res

mu0 = np.zeros(Nu)
mu1 = np.zeros(Nu)
mu2 = np.zeros(Nu)
mu3 = np.zeros(Nu)
mu4 = np.zeros(Nu)

for nu in range(Nu):
    mu0[nu] = (Z_v_0f[nu]/Z_v_0[0]).real
    mu1[nu] = mean_[nu]
    mu2[nu] = disp_[nu]
    mu3[nu] = m3_[nu]
    mu4[nu] = m4_[nu]

n_cut = int(m+1)

# 2 order

A_2 = np.zeros(Nu)
Mu1_2 = np.zeros(Nu)
Mu2_2 = np.zeros(Nu)

for nu in range(Nu):

    A_2[nu] =  mu0[nu]
    Mu1_2[nu] = mu1[nu]
    Mu2_2[nu] = mu2[nu]

    for z in range(300):
        s0 = 0
        s1 = 0
        s2 = 0
        s3 = 0
        s4 = 0

        for j in range(n_cut):

            s0 +=  gauss_fun(j, A_2[nu], Mu1_2[nu], Mu2_2[nu])
            s1 +=  gauss_fun(j, A_2[nu], Mu1_2[nu], Mu2_2[nu])* j
            s2 +=  gauss_fun(j, A_2[nu], Mu1_2[nu], Mu2_2[nu])* j**2

        if s0==s0:

            mu0_ = s0
            mu1_ = s1/s0
            mu2_ = s2/s0 - mu1_**2

            A_2[nu] += 0.1*(mu0[nu]  - mu0_ )
            Mu1_2[nu] += 0.1*(mu1[nu] - mu1_)
            Mu2_2[nu] += 0.1*(mu2[nu] - mu2_)

        else:

            A_2[nu] += 0
            Mu1_2[nu] += 0
            Mu2_2[nu] += 0

# 3 order

A_3 = np.zeros(Nu)
Mu1_3 = np.zeros(Nu)
Mu2_3 = np.zeros(Nu)
Mu3_3 = np.zeros(Nu)

for nu in range(Nu):

    A_3[nu] = mu0[nu]
    Mu1_3[nu] = mu1[nu]
    Mu2_3[nu] = mu2[nu]
    Mu3_3[nu] = 0

    for z in range(500):
        s0 = 0
        s1 = 0
        s2 = 0
        s3 = 0

        for j in range(n_cut):

            s0 += gauss_fun(j, A_3[nu], Mu1_3[nu], Mu2_3[nu], Mu3_3[nu])
            s1 += gauss_fun(j, A_3[nu], Mu1_3[nu], Mu2_3[nu], Mu3_3[nu])* j
            s2 += gauss_fun(j, A_3[nu], Mu1_3[nu], Mu2_3[nu], Mu3_3[nu])* j**2
            s3 += gauss_fun(j, A_3[nu], Mu1_3[nu], Mu2_3[nu], Mu3_3[nu])* j**3

        if s0==s0:

            mu0_ = s0
            mu1_ = s1/s0
            mu2_ = s2/s0 - mu1_**2
            mu3_ = s3/s0 - 3*mu2_*mu1_ - mu1_**3

            A_3[nu] += 0.05*(mu0[nu] - mu0_)
            Mu1_3[nu] += 0.1*(mu1[nu] - mu1_ )
            Mu2_3[nu] += 0.1*(mu2[nu] - mu2_)
            Mu3_3[nu] += 0.05*(mu3[nu] - mu3_)

        else:
            A_3[nu] += 0
            Mu1_3[nu] += 0
            Mu2_3[nu] += 0
            Mu3_3[nu] += 0

A_4 = np.zeros(Nu)
Mu1_4 = np.zeros(Nu)
Mu2_4 = np.zeros(Nu)
Mu3_4 = np.zeros(Nu)
Mu4_4 = np.zeros(Nu)

for nu in range(Nu):

    A_4[nu] = mu0[nu]
    Mu1_4[nu] = mu1[nu]
    Mu2_4[nu] = mu2[nu]
    Mu3_4[nu] = 0
    Mu4_4[nu] = 0

    for z in range(700):

        s0 = 0
        s1 = 0
        s2 = 0
        s3 = 0
        s4 = 0

        for j in range(n_cut):

            s0 +=  gauss_fun(j, A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu])
            s1 +=  gauss_fun(j, A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu])* j
            s2 +=  gauss_fun(j, A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu])* j**2
            s3 +=  gauss_fun(j, A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu])* j**3
            s4 +=  gauss_fun(j, A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu])* j**4

        if s0==s0:
            mu0_ = s0
            mu1_ = s1/s0
            mu2_ = s2/s0 - mu1_**2
            mu3_ = s3/s0 - 3*mu2_*mu1_ - mu1_**3
            mu4_ = s4/s0 - 4*mu3_*mu1_ - 3*mu2_**2 - 6*mu2_*mu1_**2 - mu1_**4

            step_ini_0 =  0.1
            step_ini_1 =  0.1
            step_ini_2 =  0.1
            step_ini_3 =  0.05
            step_ini_4 =  0.008

            A_4[nu] +=  step_ini_0*(mu0[nu] - mu0_)
            Mu1_4[nu] +=  step_ini_1*(mu1[nu] - mu1_)
            Mu2_4[nu] +=  step_ini_2*(mu2[nu] - mu2_)
            Mu3_4[nu] +=  step_ini_3*(mu3[nu] - mu3_)
            Mu4_4[nu] +=  step_ini_4*(mu4[nu] - mu4_)

        else:
            A_4[nu] += 0
            Mu1_4[nu] += 0
            Mu2_4[nu] += 0
            Mu3_4[nu] += 0
            Mu4_4[nu] += 0
```

```python
P_sectors =  prob_sectors_exact(M)
k_ex = int(m)

for nu in range(k_ex,k_ex+1):
    iN = 100
    di = (m+0.1)/iN

    line1, = plt.plot([i for i in range(m+1)], [P_sectors[j,nu]/Z(M) for j in range(m+1)],'.' ,label = 'exact')

    line2, = plt.plot([i*di for i in range(iN)],[gauss_fun(i*di,A_2[nu], Mu1_2[nu], Mu2_2[nu]) for i in range(iN)],':', label = '2nd order')
    line3, = plt.plot([i*di for i in range(iN)],[gauss_fun(i*di,A_3[nu], Mu1_3[nu], Mu2_3[nu], Mu3_3[nu]) for i in range(iN)],'-.',  label = '3rd order')
    line4, = plt.plot([i*di for i in range(iN)],[gauss_fun(i*di,A_4[nu], Mu1_4[nu], Mu2_4[nu], Mu3_4[nu], Mu4_4[nu]) for i in range(iN)],'--', label = '4th order')

plt.yscale('log')
plt.title('k='+str(2*k_ex), size = 15)
plt.legend(handles=[line1, line2,line3,line4], loc='upper left', prop={'size':13}, title_fontsize = '13' )
plt.xlabel('n', fontsize=20)
plt.ylabel('P', fontsize=20)
plt.xlim([4,8.3])
plt.ylim([10**(-3),10**(-1.5)])
plt.show()
```

```python
x_start = int(Nu*0.05)
x_fin = int(Nu/2)

line1, = plt.plot([2*i for i in range(x_start,x_fin, 1)], [P_sectors[m,j]/Z(M) for j in range(x_start,x_fin)],'.' ,label = 'exact')

line2, = plt.plot([2*i for i in range(x_start,x_fin, 1)], [gauss_fun(m, A_2[j], Mu1_2[j], Mu2_2[j]) for j in range(x_start,x_fin, 1)] , linewidth=2, linestyle = ':', label = '2nd order')

line3, = plt.plot([2*i for i in range(x_start,x_fin, 1)], [gauss_fun(m, A_3[j], Mu1_3[j], Mu2_3[j], Mu3_3[j]) for j in range(x_start,x_fin, 1)] , linestyle = '-.', label = '3rd order')

line4, = plt.plot([2*i for i in range(x_start,x_fin, 1)], [gauss_fun(m, A_4[z], Mu1_4[z], Mu2_4[z], Mu3_4[z], Mu4_4[z]) for z in range(x_start,x_fin, 1)] , linestyle = '--', label = '4th order')

plt.yscale('log')
plt.legend(handles=[line1, line2,line3,line4], prop={'size':13}, loc='lower center', title_fontsize = '13' )
plt.xlabel('k',fontsize=20)
plt.ylabel('P',fontsize=20)
plt.ylim([10**(-4),10**(-2.2)])

plt.show()
```

```python
# Warning!
# Choosing of k_0 and k_cut (cut off of the number of sectors) is heuristic
# The 3rd and 4th order might give the 'nan' result  due to the function's kind.

cut_off_ = np.zeros(Nu)
for i in range(Nu):
    if gauss_fun(m, A_4[i],Mu1_4[i], Mu2_4[i],Mu3_4[i],Mu4_4[i] ) == gauss_fun(m, A_4[i],Mu1_4[i], Mu2_4[i],Mu3_4[i],Mu4_4[i] ): # excludes 'nan'
        cut_off_[i] = gauss_fun(m, A_4[i],Mu1_4[i], Mu2_4[i],Mu3_4[i],Mu4_4[i] )

k_max = list(cut_off_).index(np.max(cut_off_))

# You can vary 'accur' to obtain more precise results

accur = 1000

# Let's find the left cut off over sectors
k_0 = 1

for j in range(int(Nu/10)):

    if  gauss_fun(m, A_4[j],Mu1_4[j], Mu2_4[j],Mu3_4[j], Mu4_4[j] ) < 10**(-12): # > 10**(-12) and A_2[i,j]!= 0 :
        k_0 = j

# Let's find the right cut off over sectors

p_4 = 0

i = k_max

while cut_off_[k_max]/cut_off_[i] < accur and i < Nu - 1:

    p_4 += gauss_fun(m, A_4[k], Mu1_4[k], Mu2_4[k], Mu3_4[k], Mu4_4[k])

    i += 1

k_cut = i

# The probability computation for different orders of approximation

p_2 = 0
p_3 = 0
p_4 = 0

for k in range(k_0, k_cut):
    p_2 += gauss_fun(m, A_2[k], Mu1_2[k], Mu2_2[k])
    p_3 += gauss_fun(m, A_3[k], Mu1_3[k], Mu2_3[k], Mu3_3[k])
    p_4 += gauss_fun(m, A_4[k], Mu1_4[k], Mu2_4[k], Mu3_4[k], Mu4_4[k])

p_4_new = p_4

if p_4 > 10*p_2 or (p_4 == p_4)==False:
    while p_4_new > 10*p_2 and (p_4 == p_4)==False:
        k_cut -= 1

        p_4_new = 0
        for k in range(k_0, k_cut):
            p_4_new += gauss_fun(m, A_4[k], Mu1_4[k], Mu2_4[k], Mu3_4[k], Mu4_4[k])

    p_4 = p_4_new

p_3 = 0
for k in range(k_0, k_cut):
    p_3 += gauss_fun(m, A_3[k], Mu1_3[k], Mu2_3[k], Mu3_3[k])

print('K_cut_off = (' , k_0*2, ',', k_cut*2,')\n')
print('P_exact = ', "{:.3e}".format(P_list_ours[len(unique_permutations)-1]), '\n','   P_2 = ' , "{:.3e}".format(p_2), '\n', '   P_3 = ' , "{:.3e}".format(p_3), '\n','   P_4 = ' , "{:.3e}".format(p_4), '\n',)

```

    K_cut_off = ( 4 , 98 )

    P_exact =  7.251e-02
        P_2 =  7.322e-02
        P_3 =  7.314e-02
        P_4 =  7.202e-02

```python

```


[comment]: <> (Written by Andrés Cárdenas)
[comment]: <> (July 2021)
[comment]: <> (www.fysixs.com)
[comment]: <> (andres.cardenas@gmail.com)

    **A Julia/Jupyter Companion**

  *Andrés Cárdenas, 2021*, www.fysixs.com

***Principles of Quantum Mechanics***
 by R. Shankar  (text on Amazon)

 SOURCE FILE FOR THE BOOK

---
###  Imports

```julia
using LinearAlgebra
using StaticArrays
using SymPy
using Gadfly
using LaTeXStrings
```

---
###  Julia Environment Setup

```julia
# Symbolic declarations
@syms θ::real
@syms α₁, α₂, α₃
@syms θ₁::real, θ₂::real, θ₃::real
@syms ω, Ω₁₁, Ω₁₂, Ω₂₁, Ω₂₂
@syms x₁, x₂, x₃

# Bra-ket types
const ket{N, T} = SArray{Tuple{N, 1}, T, 2, N};
const bra{N, T} = SArray{Tuple{1, N}, T, 2, N};
@inline ket(x::NTuple{N,Any})  where {N}    = ket{N}(x)
@inline ket{N}(x::NTuple{N,T}) where {N, T} = ket{N, T}(x)
@inline bra(x::NTuple{N,Any})  where {N}    = bra{N}(x)
@inline bra{N}(x::NTuple{N,T}) where {N, T} = bra{N, T}(x)

# Define these basis elements here
î = ket(1, 0, 0)
ĵ = ket(0, 1, 0)
k̂ = ket(0, 0, 1)

# Add an array dispatch to simplify and expand
import SymPy: simplify, expand
function simplify(A::T) where {T <: AbstractArray}
    szA = size( A )

    if szA |> length == 2
        n = szA[1]
        m = szA[2]
        vals = [ simplify(A[i,j]) for j ∈ 1:m for i ∈ 1:n ]
    elseif szA |> length == 1
        n = szA[1]
        vals = [ simplify(A[i]) for i ∈ 1:n ]
    end

    if typeof(A) <: StaticArray
        Aout = typeof(A)( Tuple( Sym(x) for x in vals ) )
    else
        if szA |> length == 2
            Aout   = typeof(A)( undef, n, m )
            k = 1
            for j ∈ 1:m
                for i ∈ 1:n
                    Aout[i,j] = Sym(vals[k])
                    k += 1
                end
            end
        else
            Aout = typeof(A)( [ Sym(x) for x in vals ] )
        end
    end

    return Aout
end

function expand(A::T) where {T <: AbstractArray}
    szA = size( A )

    if szA |> length == 2
        n = szA[1]
        m = szA[2]
        vals = [ expand(A[i,j]) for j ∈ 1:m for i ∈ 1:n ]
    elseif szA |> length == 1
        n = szA[1]
        vals = [ expand(A[i]) for i ∈ 1:n ]
    end

    if typeof(A) <: StaticArray
        Aout = typeof(A)( Tuple( Sym(x) for x in vals ) )
    else
        if szA |> length == 2
            Aout   = typeof(A)( undef, n, m )
            k = 1
            for j ∈ 1:m
                for i ∈ 1:n
                    Aout[i,j] = Sym(vals[k])
                    k += 1
                end
            end
        else
            Aout = typeof(A)( [ Sym(x) for x in vals ] )
        end
    end

    return Aout
end;

# Better TeX printing for the REPL
TeX(expr) = expr |> sympy.latex |> latexstring;
disp(str) = display("text/markdown", str);
```

---

###  Mathematical Definitions

```julia
import Base: |
"""
Define the inner product by
overloading the vertical bar to make bra-kets
"""
|(a::ket, b::ket) = (a'*b)[1];
|(a::bra, b::ket) = (a*b)[1];

"""
Define the commutator between operators Ω and Λ
"""
Com(Ω, Λ) = (Ω)Λ - (Λ)Ω

"""
Create a unit ket
"""
function uvec(a::ket)
    â = a / sqrt( a|a )
    return â .|> Sym
end

"""
Create a unit bra
"""
function uvec(b::bra)
    b̂ = b / sqrt( b|b' )
    return b̂ .|> Sym
end

"""
Gram-Schmidt Algorithm
"""
function gramschmidt(ℬ)
    ℬ′ = typeof(ℬ)()

    push!( ℬ′, uvec( ℬ[1] ) )
    for i ∈ 2:length(ℬ)
        ℬ′ᵢ = ℬ[i]
        for j ∈ 1:i-1
            ℬ′ᵢ += - (ℬ′[j]|ℬ[i])ℬ′[j]
        end
        push!( ℬ′, uvec( ℬ′ᵢ ) )
    end
    return ℬ′
end

"""
Rotation Operators
3D rotation matrix about axis n̂
by an angle θ
"""
function R(θ, n̂::ket)
    R₀ = @SMatrix[
        1    0               0;
        0    cos(θ)          -sin(θ);
        0    sin(θ)          cos(θ);
        ]
    R₁ = @SMatrix[
        cos(θ)    0          -sin(θ);
        0         1          0;
        sin(θ)    0          cos(θ);
        ]
    R₂ = @SMatrix[
        cos(θ)    -sin(θ)    0;
        sin(θ)    cos(θ)     0;
        0         0          1;
        ]

    î = ket(1, 0, 0)
    ĵ = ket(0, 1, 0)
    k̂ = ket(0, 0, 1)

    if (n̂ == î)
        op = R₀
    elseif (n̂ == ĵ)
        op = R₁
    elseif (n̂ == k̂)
        op = R₂
    end

    return op
end

"""
Obtain the matrix elements of an operator in a given basis
"""
function mat( Ω, ℬ )
    dim  = length(ℬ)
    vals = [ ( ℬ[i]|(Ω*ℬ[j]) ) for j ∈ 1:dim for i ∈ 1:dim ]
    mat  = typeof(Ω)( Tuple( Sym(x) for x in vals ) )
    return mat
end

"""
Projection operator for ket V
"""
ℙ(V::ket) = (V)V'
```

```julia
#END SOURCE
```


# COMP90051 Workshop 5
## Logistic regression in TensorFlow
***
In this worksheet, you'll get some hands-on experience with TensorFlow by implementing logistic regression using the low-level API.
We'll assume you're already familiar with logistic regression from Worksheet 3, although we'll consider the more general case of multi-class classification.
By the end of this worksheet, you should be able to:
* use placeholders to feed data into TensorFlow
* define a simple graph (e.g. to represent the logistic regression model)
* use the built-in optimisers to fit a model

Let's begin by importing the required packages (including TensorFlow).

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

### 1. MNIST data set

MNIST is a multi-class classification data set where:
* the features are images of handwritten digits (28×28 pixels with a single 8-bit channel)
* the target is a label in the set $\{0, 1, \ldots, 9\}$

The data is already split into training and test sets. The training set contains 60,000 instances and the test set contains 10,000 instances.

Below we load the data into NumPy arrays using a built-in function from TensorFlow.

**Question:** How are the arrays structured? Which index is used to access individual instances? What is the type of the arrays? \[Hint: use `array.dtype` to check\]

```python
from tensorflow.keras.datasets import mnist
(images_train, labels_train), (images_test, labels_test) = mnist.load_data()
```

Before using the data for logistic regression, we need to do some basic pre-processing:
 * We rescale the images so that each pixel is represented as a float between 0 and 1 (note that the images are already normalised)
 * Unroll the 2D image arrays into 1D arrays

```python
features_train = np.reshape(images_train.astype('float32')/255, [images_train.shape[0], -1])
features_test = np.reshape(images_test.astype('float32')/255, [images_test.shape[0], -1])
NUM_FEATURES = features_train.shape[1]
NUM_CLASSES = 10
```

You can explore the training instances using the code block below.

```python
train_id = 4 # change this
plt.imshow(images_train[train_id], cmap="binary")
plt.axis('off')
plt.show()
print("This instance has label {}".format(labels_train[train_id]))
```

### 2. Batching the data
To fit the logistic regression model, we'll be using gradient descent with mini-batches.
In other words, at each iteration we update the parameters using a small sample (mini-batch) of points from the training set.

To make our code more readable, we implement a lightweight wrapper for the training data which returns mini-batches when requested.
This functionality can be implemented elegantly using a Python iterator (see below).

**Note:** For large-scale data (particularly when using a GPU) we recommend using the new Dataset API in Tensorflow (available at `tf.data.Dataset`).
It can represents a generic input pipeline (which might include batching as well as transformations).

```python
class DatasetIterator:
    """
    An iterator that returns randomized batches from a data set (with features and labels)
    """
    def __init__(self, features, labels, batch_size):
        assert(features.shape[0]==labels.shape[0])
        assert(batch_size > 0 and batch_size <= features.shape[0])
        self.features = features
        self.labels = labels
        self.num_instances = features.shape[0]
        self.batch_size = batch_size
        self.num_batches = self.num_instances//self.batch_size
        if (self.num_instances%self.batch_size!=0):
            self.num_batches += 1
        self._i = 0
        self._rand_ids = None

    def __iter__(self):
        self._i = 0
        self._rand_ids = np.random.permutation(self.num_instances)
        return self

    def __next__(self):
        if self.num_instances - self._i >= self.batch_size:
            this_rand_ids = self._rand_ids[self._i:self._i + self.batch_size]
            self._i += self.batch_size
            return self.features[this_rand_ids], self.labels[this_rand_ids]
        elif self.num_instances - self._i > 0:
            this_rand_ids = self._rand_ids[self._i::]
            self._i = self.num_instances
            return self.features[this_rand_ids], self.labels[this_rand_ids]
        else:
            raise StopIteration()
```

Below we define a batch size and set up an iterator for the training set.

```python
batch_size = 100
train_iterator = DatasetIterator(features_train, labels_train, batch_size)
```

### 3. TensorFlow placeholders

Placeholders represent tensors in the dataflow graph that will be fed values from outside TensorFlow (e.g. NumPy arrays).

Below we define some placeholders for features and labels.
Note that we've used `None` for the first dimension of both placeholders.
This indicates that the size of the first dimension (i.e. the number of instances) is not fixed.
This allows us to feed in data with varying numbers of instances (e.g. a batch might only have 100 instances, while the test set has 10,000 instances).

```python
X = tf.placeholder(dtype=tf.float32, shape=[None, NUM_FEATURES], name="features")
Y = tf.placeholder(dtype=tf.uint8, shape=[None,], name="labels")
```

### 4. Model

We'll shortly specify the logistic regression model as part of the dataflow graph.
However, first we consider how to generalize binary logistic regression to multi-class softmax logistic regression.
***
It's convenient to encode the multi-class labels (corresponding to the digits 0-9) as one-hot vectors.
A one-hot vector has length equal to the number of classes. Whichever class is "active" is set to $1$, while the remaining classes are set to $0$.

$$
\begin{align}
y = 0 &\to \mathbf{y} = \begin{bmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix} \\
y = 1 &\to \mathbf{y} = \begin{bmatrix}
0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix} \\
&\vdots \\
y = 9 &\to \mathbf{y} = \begin{bmatrix}
0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1
\end{bmatrix}
\end{align}
$$

For training instance $(\mathbf{x}_i, \mathbf{y}_i)$:

* Define $\mathbf{z}_i = \mathbf{x}_i  \mathbf{W} + \mathbf{b}$ where $\mathbf{W}$ is a `num_features` × `num_classes` weight matrix and $\mathbf{b}$ is a bias vector of length `num_classes`.
* The predicted label is $\hat{y}_i = \arg \max_{j} \sigma(\mathbf{z}_i)_{j}$ where $\sigma(\mathbf{z})_j = \frac {e^{z_{j}}}{\sum _{k=1}^{K} e^{z_{k}}}$ is the softmax function.

The corresponding loss function is the softmax cross-entropy:
$$
L[\mathbf{W}, \mathbf{b}] = - \sum_{i} \mathbf{y}_i \log (\sigma(\mathbf{z}_i))
$$
***

To implement this model in TensorFlow, we need to define Tensor variables to store the weight matrix and the bias vector.
We do this using `tf.get_variable` (although you may see `tf.Variable` used elsewhere).
Note that we had to specify the shape of the tensors, the type, and an initializer (we initialise to zero here).

Once we've defined the variables, we can define additional tensors using TensorFlow operations (e.g. `tf.matmul` for matrix multiplication).
Ultimately, we need to tell TensorFlow how to compute the loss, so that we can minimize it.

```python
with tf.variable_scope("logistic-regression", reuse=tf.AUTO_REUSE):
    W = tf.get_variable("weights", shape=[NUM_FEATURES, NUM_CLASSES], \
                        dtype=tf.float32, initializer=tf.zeros_initializer())
    b = tf.get_variable("bias", shape=[NUM_CLASSES], dtype=tf.float32, \
                        initializer=tf.zeros_initializer())
    logits = tf.matmul(X, W) + b
    Y_pred = tf.nn.softmax(logits)
    Y_one_hot = tf.one_hot(Y, NUM_CLASSES)
    loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=Y_one_hot))
```

### 5. Running gradient descent

Now that we've told TensorFlow how to compute the loss (by defining the `loss` tensor) we can apply a built-in optimizer.

Note that we don't have to specify how to compute the gradient of the loss. This is one of the advantages of expressing the computation as a data flow graph—we can get derivatives using automatic differentiation.

```python
opt = tf.train.GradientDescentOptimizer(0.01)
opt_operation = opt.minimize(loss)
num_epochs = 50
```

Below we set up an initializer for the global variables (including `W` and `b`)

```python
init = tf.global_variables_initializer()
```

And open a session to run operations on the graph we've defined.

```python
with tf.Session() as sess:
    sess.run(init)
    #writer = tf.summary.FileWriter("./tensorboard.log", sess.graph) # uncomment to use TensorBoard

    # Run gradient descent for multiple epochs
    for epoch in range(num_epochs):
        avg_loss = 0 # average loss over all batches
        for X_batch, Y_batch in train_iterator:
            _, l = sess.run([opt_operation, loss], feed_dict={X: X_batch, Y: Y_batch})
            avg_loss += l / train_iterator.num_batches
        print("Epoch {}: loss={:.9f}".format(epoch, avg_loss))
    print("Optimization complete.")

    # Evaluate the trained model
    correct_prediction = tf.equal(tf.argmax(Y_pred, 1), tf.argmax(Y_one_hot, 1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    print("Accuracy on the test set: {}.".format(accuracy.eval({X: features_test, Y: labels_test})))
```

### 6. Extension activities
* Experiment with using different built-in optimizers (e.g. `tf.train.AdamOptimizer`) and/or step sizes. Does the rate of convergence vary?
* Try setting up TensorBoard (a web-app that allows you to view the graph and visualize metrics during training).
* Adapt the code in this notebook to implement linear regression (it should only require minimal changes). You'll need to import data that's appropriate for regression.