Finding the best CHSH score can be seen as an optimization of a polynomial of non-commutative variables. Such optimization can be relaxed to a hierarchy of SDP [2], known as a NPA hierarchy [3]. We here use the Juia package `NCTSSOS`

to perform such a relaxation. More on that package can be found here [4].

The optimization problem is

\[\begin{align} \max_{E,\rho} \quad & \sum_{i,j=0}^1 (-1)^{ij} Tr[E_i E_j \rho] \\ \text{s.t.} \quad & Tr[\rho] = 1 \\ & E_i^\dagger = E_i \\ & E_i E_j = \delta_{ij} E_i, \quad E_i,E_j \in M_k, \forall M_k \\ & \sum_i E_i = 1 \\ & [E_i,E_j] = 0, \quad E_i\in [M_1,M_2],\quad E_j\in [M_3,M_4] \end{align} \]

We start by defining 8 non-commutative variables (2 projectors per measurement) using the `DynamicPolynomials`

package

```
using DynamicPolynomials
@ncpolyvar A1[1:2] A2[1:2] B1[1:2] B2[1:2]
A = [A1,A2]
B = [B1,B2]
```

We then define a function returning all the relevant constraints on the projectors composing the measurements

```
function constraints_projectors(A,B)
cons = Vector{Polynomial{false,Float64}}() #empty vector of polynomials
for M in [A,B]
for m in M
append!(cons,[m[1]*m[2]]) # orthogonality
for e in m
append!(cons,[e^2-e])
end
end
end
# commutativity
for a in A
for e in a
for b in B
for f in b
append!(cons,[e*f-f*e])
end
end
end
end
return cons
end
eq = constraints_projectors(A,B)
```

The objective function is a weighted sum of correlators. They are expressed using expectation value that we define with the function

```
expect(M_x;outcomes=[1,-1]) = outcomes[1]*M_x[1] + outcomes[2]*M_x[2]
C = expect.([A1,A2,B1,B2])
```

Finally we express the objective function as the CHSH score

```
obj = -(C[1]*C[3] + C[1]*C[4] + C[2]*C[3] - C[2]*C[4])
```

Note that the minus sign is due to `NCTSSOS`

that will minimize and not maximize the objective function.

Finally, we send everything to `NCTSSOS`

that will construct and solve the problem.

```
using NCTSSOS
level = 1 #level of the hierarchy
n_eq = length(eq)
pop = [obj; eq]
opt, data = nctssos_first(pop, [A1...,A2...,B1...,B2...], level, numeq=n_eq, TS="block");
```

Here is the result for the first level of the hierarchy

```
***************************NCTSSOS***************************
NCTSSOS is launching...
Starting to compute the block structure...
------------------------------------------------------
The sizes of PSD blocks:
[9]
[1]
------------------------------------------------------
Obtained the block structure in 0.000528174 seconds. The maximal size of blocks is 9.
There are 45 affine constraints.
Assembling the SDP...
SDP assembling time: 0.000512516 seconds.
Solving the SDP...
Problem
Name :
Objective sense : max
Type : CONIC (conic optimization problem)
Constraints : 45
Cones : 0
Scalar variables : 29
Matrix variables : 1
Integer variables : 0
Optimizer started.
Presolve started.
Linear dependency checker started.
Linear dependency checker terminated.
Eliminator started.
Freed constraints in eliminator : 0
Eliminator terminated.
Eliminator started.
Freed constraints in eliminator : 0
Eliminator terminated.
Eliminator - tries : 2 time : 0.00
Lin. dep. - tries : 1 time : 0.00
Lin. dep. - number : 0
Presolve terminated. Time: 0.00
Problem
Name :
Objective sense : max
Type : CONIC (conic optimization problem)
Constraints : 45
Cones : 0
Scalar variables : 29
Matrix variables : 1
Integer variables : 0
Optimizer - threads : 4
Optimizer - solved problem : the primal
Optimizer - Constraints : 45
Optimizer - Cones : 1
Optimizer - Scalar variables : 14 conic : 14
Optimizer - Semi-definite variables: 1 scalarized : 45
Factor - setup time : 0.00 dense det. time : 0.00
Factor - ML order time : 0.00 GP order time : 0.00
Factor - nonzeros before factor : 1035 after factor : 1035
Factor - dense dim. : 0 flops : 3.98e+04
ITE PFEAS DFEAS GFEAS PRSTATUS POBJ DOBJ MU TIME
0 1.0e+00 1.0e+00 1.0e+00 0.00e+00 0.000000000e+00 0.000000000e+00 1.0e+00 0.00
1 2.9e-01 2.9e-01 4.1e-01 -6.84e-01 -1.325948088e+00 -2.628521667e+00 2.9e-01 0.00
2 5.1e-02 5.1e-02 2.0e-02 4.00e-01 -3.126913608e+00 -3.133309605e+00 5.1e-02 0.01
3 1.6e-03 1.6e-03 1.4e-04 1.25e+00 -2.818018898e+00 -2.821554903e+00 1.6e-03 0.01
4 2.0e-05 2.0e-05 2.1e-07 9.90e-01 -2.828462112e+00 -2.828524906e+00 2.0e-05 0.01
5 1.5e-06 1.5e-06 4.3e-09 9.96e-01 -2.828433218e+00 -2.828437977e+00 1.5e-06 0.01
6 4.8e-08 4.8e-08 2.5e-11 9.99e-01 -2.828427384e+00 -2.828427541e+00 4.8e-08 0.01
7 4.7e-10 4.7e-10 2.4e-14 1.00e+00 -2.828427128e+00 -2.828427129e+00 4.7e-10 0.01
Optimizer terminated. Time: 0.01
SDP solving time: 0.014336663 seconds.
optimum = -2.8284271275550936
```

We observe a maximum score of 2.8284271275550936 corresponding to 2√2, the expected results.

[1] Clauser, J. F.; Horne, M. A.; Shimony, A. & Holt, R. A. Proposed Experiment to Test Local Hidden-Variable Theories. Physical Review Letters, 1969, 23, 880-884.

[2] Pironio, S.; Navascués, M. & Acín, A. Convergent Relaxations of Polynomial Optimization Problems with Noncommuting Variables. Siam Journal on Optimization, 2010, 20, 2157–2180.

[3] Navascués, M.; Pironio, S. & Acín, A. A convergent hierarchy of semidefinite programs characterizing the set of quantum correlations. New Journal of Physics, 2008, 10, 073013.

[4] Wang, J. & Magron, V. Exploiting term sparsity in Noncommutative Polynomial Optimization. Arxiv:2010.06956

PICOS is the Python interface I recommend to write/solve LP/SDP problem. It is under the GPLv3 free-license, probably supports your favorite solver, has crucial features for quantum information and, most importantly, has an attentive and friendly developer community.

Regarding SDP solvers, I had best performance with MOSEK. Sadly, this solver is proprietary (if price is of your concern, MOSEK offers a free one-year license for students). CVXOPT is a relatively good free-licensed alternative to MOSEK. However, in my experience, CVXOPT might lead to some issues (memory-leakage, unstable with complex SDP), and an important optimization time compare to MOSEK.

For this section we need the following import

```
import picos as pcs
import numpy as np
from itertools import product
```

Consider a scenario where a state is shared between `N`

parties, each having `m`

measurement choices (inputs) with `d`

possible results (outputs). The correlations between the parties admit a local hidden-variable model iff the correlations belong to a *local polytope* [1,2].

To know if correlations of a given scenario are local, it is sufficient to check if these correlations belong, i.e. are “inside”, the local polytope.

Correlations are in a vector of probability \[p = {p(a_1 \dots a_N|x_1 \dots x_N)}\] for all inputs x_i and output a_i.
The vertices of the local polytope are the deterministic strategies `p_d`

, i.e. `p`

is composed of 0s and 1s.
The local polytope can be written as the set of its vertices `L=p_d`

.

Here a simple function that construct such a set

```
def local_polytope(N,m,d):
"""Local Polytope generation.
Generate :math:`d^{m.N}` vertices of the Local Polytpe for a system with ``N`` partite, each having ``m`` measurement/input and ``d`` output.
Parameters
----------
N : int
number of partite
m : int
number of input of one partite
d : int
number of output of one partite
Returns
-------
vertices : array_like
Polytope. Each row is a vertex (corresponding to a deterministic strategy), each column is the :math:`n^{th}` element to that vertice
"""
D = np.zeros((d**m,m), dtype=int)
for _ in range(d**m):
D[_][:] = np.array(np.unravel_index(_,(d,)*m))
vertices = np.zeros(((d**(m*N),)+(m,)*N+(d,)*N))
c = 0
for _ in product(range(d**m), repeat=N):
for x in product(range(m), repeat=N):
vertices[(c,)+x+tuple([D[_[i]][x[i]] for i in range(N)])]=1
c += 1
shape = np.prod(np.delete(vertices.shape[:],0,0))
polytope = vertices.reshape(vertices.shape[0],shape)
return polytope
```

A correlation vector `p`

is described by a local model iff `p`

can be written as a convex sum of the local-polytope vertices.
I.e. iff `p`

is inside the local polytope.
This is a linear programming problem: find a vector `v`

such that `v*L=p`

, with `sum(v)=1`

(convexity) and `v>0`

(each element of `v`

positive).
If `v`

exists then `p`

is compatible with a local model.

Using `PICOS`

we write this linear program

```
def is_local(correlation,polytope):
pb = pcs.Problem() # Create a picos Problem instance
# The two constants of our problem
L = pcs.Constant("L", polytope) # local polytope
p = pcs.Constant("p", correlation) # correlation vector
# The variable v (the coefficient of the convex sum)
dim = L.shape[0] # number of vertices
v = pcs.RealVariable("v", dim) # v is a real vector with dim element
# Constraints
pb.add_constraint(v >= 0) # all elements of v are positive
pb.add_constraint(sum(v) == 1) # sum of elements of v is 1
pb.add_constraint(L.T*v == p) # v is the coefficient of the convex sum of all L vertices
# Objective
pb.set_objective("min", 1 | v) # dummy objective
# Solving the LP
pb.solve(solver="cvxopt",primals=False)
if pb.status == "optimal":
return True # if an optimal solution exists, p belongs to L
else:
return False # otherwise p is non-local
```

To test our LP implementation let’s now create a function that returns a local correlation vector `p`

, by construction.

```
def local_correlation(polytope):
nb_vertices = len(polytope) # Number of vertices composing the polytope
coeff = np.random.random(nb_vertices) # Random coefficients, one per vertices
coeff /= np.sum(coeff) # Normalised coefficients
p = np.sum([c*v for (c,v) in zip(coeff,polytope)],axis=0) # p is a convex sum of the polytopes vertices
return p
```

We verify our implementation with two examples, one local, one non-local

```
L = local_polytope(2,2,2)
p_l = local_correlation(L)
p_nl = L[1]+1e-3 # A non local correlation -> we take a vertice and we add 0.01 to each of its elements
print(is_local(p_l,L)) # Output True
print(is_local(p_nl,L)) # Output False
```

For this section, import and constant definition needed

```
import numpy as np
import picos as pcs
# Paulis
Z = np.array([[1.,0.],[0.,-1.]])
X = np.array([[0.,1.],[1.,0.]])
# Kroenecker product
K = np.kron
```

Consider a scenario where Alice and Bob share a two-qubit state `rho`

. They each have two measurements : `A0,A1`

for Alice, `B0,B1`

for Bob.
These measurements are of the form \[M_i = \cos(\theta)\sigma_x + (-1)^i \sin(\theta)\sigma_z\] with `i`

the measurement choice, 0 or 1, and `θ`

the angle between measurement 0 and 1. Alice set `θ=a`

while Bob set `θ=b`

.

A bell operator can be seen as a linear combination of the four correlators `AxBy`

. For example the well-known CHSH operator `S=A0(B0+B1)+A1(B0-B1)`

. Here is a function to generate such operators (return CHSH by default)

```
def bell_op(a=np.pi/4,b=np.pi/4,coeff=[1,1,1,-1]):
""" Return Bell operator, for specific angle between measurements (for each partite resp.)
Parameters
----------
a : float
Angle between observabe, Alice side.
b : float
Angle between observabe, Bob side.
coeff : array
Numpy array of four elements, for A0B0, A1B0, A0B1, A1B1.
Return
------
bell : ndarray
Bell operator
"""
O = lambda x,a : np.cos(a)*X + (-1)**x*np.sin(a)*Z
if coeff == [1,1,1,-1]:
#CHSH case
bell = K(O(0,a),O(0,b)+O(1,b)) + K(O(1,a),O(0,b)-O(1,b))
else:
bell = coeff[0]*K(O(0,a),O(0,b))+coeff[1]*K(O(1,a),O(0,b))+coeff[2]*K(O(0,a),O(1,b))+coeff[3]*K(O(1,a),O(1,b))
return bell
```

The CHSH score achieved by `rho`

is given by the linear operation \[\text{Tr}[S\rho]\]. Since all two-qubits state are semi-positive definite and the CHSH value is linear (in `rho`

) we can use semi-definite programming (SDP) to find the maximum CHSH score a two-qubit state can achieve. Such an SDP reads
\[ \max_\rho \qquad \text{Tr}[S\rho]\]
\[ \text{s.t.} \qquad \rho \succeq 0\]
\[\qquad \quad \text{Tr}[\rho]=1\]
with `rho`

a 4x4 Hermitian matrix.

Using `PICOS`

, we can write this SDP as follow

```
def max_bell(operator):
""" Compute the max(Tr[operator*rho]) over all rho = two-qubits state
Parameters
----------
operator : np.ndarray
Two-qubits operator
"""
sdp = pcs.Problem()
#SDP "constant", the bell operator
bell_operator = pcs.Constant(operator)
#SDP variable (quantum state)
rho = pcs.HermitianVariable('rho',4)
#Constraints of the SDP :
#semi positive definite
sdp.add_constraint(rho >> 0)
#Trace 1
sdp.add_constraint(pcs.trace(rho) == 1)
# Objective of the SDP: Tr[Bell*rho]
obj = pcs.trace(bell_operator*rho).real
sdp.set_objective('max',obj)
# Some print stuff
print("Solving: ")
print(sdp)
sol = sdp.solve(solver='cvxopt',verbosity=0)
print(f"Solved with {sol.claimedStatus} status")
print(f"Optimal violation: {obj}")
print(f"State reaching this violation:\n{rho}")
```

Let’s test our implementation

```
S = bell_op()
max_bell(S)
```

Running this output

```
Solving max(Tr[CHSH*rho] over all two-qubit states
Solving:
Complex Semidefinite Program
maximize Re(tr([4×4]·rho))
over
4×4 hermitian variable rho
subject to
rho ≽ 0
tr(rho) = 1
Solved with optimal status
Optimal violation: 2.8284271145023476
State reaching this violation:
[ 7.32e-02-j0.00e+00 1.77e-01-j1.85e-17 1.77e-01+j5.72e-18 -7.32e-02+j2.73e-17]
[ 1.77e-01+j1.85e-17 4.27e-01-j0.00e+00 4.27e-01+j5.60e-17 -1.77e-01+j4.77e-17]
[ 1.77e-01-j5.72e-18 4.27e-01-j5.60e-17 4.27e-01-j0.00e+00 -1.77e-01+j7.19e-17]
[-7.32e-02-j2.73e-17 -1.77e-01-j4.77e-17 -1.77e-01-j7.19e-17 7.32e-02-j0.00e+00]
```

We obtain the expected violation of `2.8284...`

which corresponds to 2√2 achieved by Bell states [2].

[1] Pironio S., J. Phys. A: Math. theor. 47 (424020)

[2] Brunner N. et. al., Rev. Mod. Phys. 86, (419)

`dnscrypt-proxy`

supports the encryption of your DNS request over HTTPS, and makes use of the DNSCrypt protocol.
Here is a small guide to set this up alongside NetworkManager.
Install the dnscrypt package provided by your distribution:

+ dnscrypt-proxy for ArchLinux

+ dnscrypt-proxy for Debian-based distribution

In its default config, NetworkManager overwrite `/etc/resolv.conf`

.
In order to stop this behaviour, create a configuration file under `/etc/NetworkManager/conf.d/`

, e.g. `/etc/NetworkManager/conf.d/00-dns.conf`

, containing

```
[main]
dns=none
```

Restart `NetworkManager.service`

```
> sudo systemctl restart NetworkManager.service
```

`/etc/resolv.conf`

is now a dead symlink.
Remove the file and create a new one with nameserver pointing to your localhost.

```
> sudo rm /etc/resolv.conf
> sudoedit /etc/resolv.conf
```

```
nameserver ::1
nameserver 127.0.0.1
options edns0 single-request-reopen
```

Make sure no other service makes use of port `:53`

by running

```
> ss -lp 'sport = :domain'
```

If the command output something more than one line (starting with `Netid`

), a process is using the port 53 (common ones are `dnsmasq`

and `systemd-resolv`

).

By default, `dnscrypt-proxy`

will chose the fastest resolver from the servers listed in `/etc/dnscrypt-proxy/dnscrypt-proxy.toml`

under `[sources]`

.
Optionally you can setup specific resolvers.
For an up-to-date list of server see the upstream page.

Enable and start the `dnscrypt-proxy`

service.

```
> sudo systemctl start dnscrypt-proxy.service
> sudo systemctl enable dnscrypt-proxy.service
```

NetworkManager still shows the DNS server reported by your router:

```
> nmcli dev show | grep IP4.DNS
```

However, using `nslookup`

should show localhost on port 53 as the resolver:

```
> nslookup valcarce.fr
Server: ::1
Address: ::1#53
Non-authoritative answer:
Name: valcarce.fr
Address: 51.15.121.4
Name: valcarce.fr
Address: 2001:bc8:1824:e4e::1
```

Finally, you can test your DNS via dnsleak websites such as dnsleaktest.com or mullvad.net.

Notice for Firefox user: by default (in the US) Firefox uses DNS-over-HTTPS (DoH) and send all your DNS requests to CloudFlare.
To prevent this, open Firefox `Preferences`

, search for `Network Settings`

, click on `Settings...`

and disable `Enable DNS over HTTPS`

.

This is a pet project to help me understand the relatively new programming language Julia. I don’t guarantee that the code in this blog post is optimal, nor respect all Julia conventions.

An implementation of the one-time pad encryption algorithm, based on the XOR gate. The code should be able to encrypt any given file by generating a key of this file’s size and XORing the file with the key, bit-by-bit. We want the code to also be able to do the reverse; given a key and an encrypted file, run the XOR to get the original file.

```
HELP = "xore.jl usage:
xore.jl [enc|dec] [filename [keyfile]]
Options:
enc : encrypt file. Generated file will be the filename.enc and generated key filename.key
dec : decrypt file. [keyfile] argument required
"
# Test arguments
@assert (length(ARGS)==2 || length(ARGS)==3) HELP
@assert (ARGS[1]=="enc" || ARGS[1]=="dec") HELP
@assert isfile(ARGS[2]) "Can not find provided file"
if ARGS[1]=="dec"
@assert length(ARGS)==3 "Keyfile argument missing"
@assert isfile(ARGS[3]) "Can not find provided keyfile"
end
# Filling variable
filename = ARGS[2]
extension = findlast(isequal('.'),ARGS[2])
if extension!=nothing
file = filename[1:extension-1]
else
file = filename
end
```

This the meta part of our program. Our program takes 2 or 3 arguments. The first one, either enc or dec set the mode of our program (decryption or encryption mode). The second one is the file to encrypt/decrypt. Finally, if the decryption mode is trigge, we need the file containing the key . Some checks are made using assert to ensure that provided arguments, and arguments number, are correct.

Let’s now study how one can read a given file and store it into a variable.

```
# Read file to byte arrays
f = open(filename, "r")
data = UInt8[]
readbytes!(f,data,Inf)
close(f)
```

This open the file with the open function — argument r is for read only. We then create an array of type UInt8. Finally, we read the file by byte, and store them in the previously declared array. The Inf argument is to read to entire file — this way we don’t need to provide the length (number of byte) of our file.

Here is the encryption section in which we generate the key, encrypt the file and save both the encrypted version of the file and the key.

```
if ARGS[1]=="enc"
# Genearting key of the file's size
key = rand(UInt8,size(data)[1])
# Generating encrypted file
enc = UInt8[]
for (i,d) in enumerate(data)
push!(enc,xor(d,key[i]))
end
# Saving everything
@assert !isfile(file*".scrt") "enc.scrt file already exist in current directory"
enc_file = open(file*".scrt", "w")
write(enc_file, enc)
close(enc_file)
@assert !isfile(file*".key") "enc.key file already exist in current directory"
key_file = open(file*".key", "w")
write(key_file, key)
close(key_file)
```

The key is generated using the rand function. The key consists of an UInt8 array of the file’s size (number of byte). We then proceed to the encryption by XOR-ing the file and the key, storing everything in the newly created enc array. The XOR bit-wise operation is done by the built-in xor function. Finally, we save the encrypted file as well as the key using the name of the file (without the extension). A small check is made for the existence of such file, in order not to erase unintentionally potential files.

```
else
# Read the keyfile
k = open(ARGS[3], "r")
key = UInt8[]
readbytes!(k,key,Inf)
close(k)
# Decryption
dec = UInt8[]
for (i,d) in enumerate(data)
push!(dec,xor(d,key[i]))
end
#Saving decrypted file
dec_file = open(file*".clear", "w")
write(dec_file, dec)
close(dec_file)
end
```

This is the decryption mode. It works in a similar manner as the encryption process, no further explanation are required to understand this part of the code.

]]>