Challenge 3: Let’s get serious

Author

Le magicien quantique

Published

May 12, 2024

1 Before we begin

Here are versions of the utility functions for 1 and 2 qubits.

from perceval import pdisplay, PS, BS, Circuit, BasicState, Processor, PERM
from perceval.components import Unitary
from perceval.backends import BackendFactory
from perceval.algorithm import Analyzer, Sampler
import perceval as pcvl
from exqalibur import FockState

from qiskit.visualization import plot_bloch_multivector
from qiskit.quantum_info import Statevector

import matplotlib.pyplot as plt
from numpy import pi, cos, sin, sqrt
import numpy as np

from typing import List, Dict, Tuple, Union, Optional

qubits = {
    "0": BasicState([1, 0]),
    "1": BasicState([0, 1]),
    "00": BasicState([1, 0, 1, 0]),
    "01": BasicState([1, 0, 0, 1]),
    "10": BasicState([0, 1, 1, 0]),
    "11": BasicState([0, 1, 0, 1])
}
qubits_ = {qubits[k]: k for k in qubits}
sqlist = [qubits["0"], qubits["1"]]
mqlist = [qubits["00"], qubits["01"], qubits["10"], qubits["11"]]

def analyze(circuit: Circuit, input_states: Optional[FockState] = None, output_states: Optional[FockState] = None) \
        -> None:
    if len(circuit.depths()) == 2:
        states = sqlist
    else:
        states = mqlist

    if input_states is None:
        input_states = states
    if output_states is None:
        output_states = states

    p = Processor("Naive", circuit)
    a = Analyzer(p, input_states, output_states, mapping=qubits_)
    pdisplay(a)

def amplitudes(circuit: Circuit, input_state: Optional[FockState] = None, output_states: Optional[FockState] = None) \
        -> (complex, complex):
    if input_state is None:
        if len(circuit.depths()) == 2:
            input_state = qubits["0"]
        else:
            input_state = qubits["00"]

    if output_states is None:
        if len(circuit.depths()) == 2:
            output_states = sqlist
        else:
            output_states = mqlist

    b = BackendFactory.get_backend("Naive")
    b.set_circuit(circuit)
    b.set_input_state(input_state)
    return {qubits_[k]: roundc(b.prob_amplitude(k)) for k in output_states}

def measure2p(processor: Processor, input_state: Optional[FockState] = None) -> None:
    if input_state is None:
        input_state = qubits["00"]

    # We enforce the rule: the sum of photons per pair of rails must be equal to 1.
    processor.set_postselection(pcvl.utils.PostSelect("[0,1]==1 & [2,3]==1"))
    processor.min_detected_photons_filter(0)

    # Finally, we take the measurement:
    processor.with_input(input_state)
    measure2p_s = pcvl.algorithm.Sampler(processor)

    print(f"Input: {qubits_[input_state]}")
    for k, v in measure2p_s.probs()["results"].items():
        print(f"> {qubits_[k]}: {round(v, 2)}")

def roundc(c, decimals: int = 2):
    return round(c.real, decimals) + round(c.imag, decimals) * 1j

2 Multiple Systems

dicaprio_laugh_meme.jpg

The concepts discussed for simple systems, concatenation of gates, measurements, probabilities…, are still valid for multiple systems. To add a qubit, you will need to apply tensor products to the gates as well as to the basis states.

Suppose we have a qubit \(A\) and a qubit \(B\), respectively in states \(|\psi\rangle\) and \(|\phi\rangle\). Then the system \((A, B)\) is in the state \(|\psi\rangle \otimes |\phi\rangle = |\psi \otimes \phi\rangle\) (depending on the writing conventions).

The canonical basis then becomes: \[ |00\rangle = |0\rangle \otimes |0\rangle = \begin{pmatrix} 1 \\ 0 \end{pmatrix}\otimes \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} 1 \times \begin{pmatrix} 1 \\ 0 \end{pmatrix} \\ 0 \times \begin{pmatrix} 1 \\ 0 \end{pmatrix} \end{pmatrix} = \begin{pmatrix} 1 \\ 0 \\ 0 \\ 0 \end{pmatrix}, \] \[ |01\rangle = \begin{pmatrix} 0 \\ 1 \\ 0 \\ 0 \end{pmatrix}, \] \[ |10\rangle = \begin{pmatrix} 0 \\ 0 \\ 1 \\ 0 \end{pmatrix}, \] \[ |11\rangle = \begin{pmatrix} 0 \\ 0 \\ 0 \\ 1 \end{pmatrix} \]

It’s the same for logic gates: if we apply \(U_1\) to \(A\) and \(U_2\) to \(B\), then we apply \(U_1 \otimes U_2\) to \(A \otimes B\).

For example, if we apply \(H\) to \(|0\rangle\) and nothing (i.e., the identity) to \(|1\rangle\). The matrix of the operation on the system is thus \(H \otimes I\). The circuit is as follows:

c = Circuit(4) // (0, BS.H())
pdisplay(c)

We expect to have: \[ \DeclareMathOperator{\H}{H} \DeclareMathOperator{\I}{I} \]

\[ |0\rangle \otimes |1\rangle \rightarrow \begin{cases} \text{Top: } |0\rangle \longrightarrow \H \longrightarrow \frac{1}{\sqrt{2}}|0\rangle+\frac{1}{\sqrt{2}}|1\rangle \\ \text{Bottom: } |1\rangle \longrightarrow \I \longrightarrow |1\rangle \end{cases} \rightarrow \left(\frac{1}{\sqrt{2}}|0\rangle+\frac{1}{\sqrt{2}}|1\rangle\right)\otimes|1\rangle \]

This gives the state: \[ \left(\frac{1}{\sqrt{2}}|0\rangle+\frac{1}{\sqrt{2}}|1\rangle\right)\otimes|1\rangle = \frac{1}{\sqrt{2}}|01\rangle+\frac{1}{\sqrt{2}}|11\rangle \]

In practice, we determine the logic gate \(U\), where \(U = H \otimes I\), so: \[ U = \frac{1}{\sqrt{2}}\begin{pmatrix}1 & 1 \\ 1& -1\end{pmatrix} \otimes \begin{pmatrix} 1&0 \\ 0& 1 \end{pmatrix} = \frac{1}{\sqrt{2}}\begin{pmatrix} 1\times\begin{pmatrix} 1&0 \\ 0& 1 \end{pmatrix}&1\times\begin{pmatrix} 1&0 \\ 0& 1 \end{pmatrix}\\1\times\begin{pmatrix} 1&0 \\ 0& 1 \end{pmatrix}&-1\times\begin{pmatrix} 1&0 \\ 0& 1 \end{pmatrix} \end{pmatrix}= \frac{1}{\sqrt{2}}\begin{pmatrix} 1&0&1&0\\0&1&0&1\\1&0&-1&0\\0&1&0&-1\end{pmatrix} \]

This gives us with our input: \[ |01\rangle = \begin{pmatrix} 0\\1\\0\\0\end{pmatrix}, \] \[ \frac{1}{\sqrt{2}}\begin{pmatrix} 1&0&1&0\\0&1&0&1\\1&0&-1&0\\0&1&0&-1\end{pmatrix} \begin{pmatrix} 0\\1\\0\\0\end{pmatrix} = \frac{1}{\sqrt{2}}\begin{pmatrix} 0\\1\\0\\1\end{pmatrix} = \frac{1}{\sqrt{2}} |01\rangle + \frac{1}{\sqrt{2}} |11\rangle \]

Which corresponds exactly to the expected result.

If you would like a more detailed explanation, you can check out this course offered by IBM: https://learning.quantum.ibm.com/course/basics-of-quantum-information/multiple-systems.

2.1 Quantum Entanglement

A very interesting property of qubits is that they can be separated, but also entangled, meaning they are dependent on each other. This allows, for example, to act on one qubit and obtain information about the second one.

If we take the entangled state \(|\psi\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)\), and we measure the first qubit (we have a 50% chance of getting \(0\) and correspondingly \(1\)), the state of the second qubit is entirely determined without measuring it! Indeed, if we measure \(0\) for the first qubit, then the second is necessarily also \(0\). However, since the measurement of the first qubit is random, this does not allow information to be teleported faster than the speed of light, as from the other perspective, one does not know the state of the measurement (before receiving the information through a classical channel, for example).

A counterexample is the state \(|\psi\rangle = \frac{1}{2}(|00\rangle + |01\rangle + |10\rangle + |11\rangle) = \frac{1}{\sqrt{2}}(|0\rangle + |1\rangle) \otimes \frac{1}{\sqrt{2}}(|0\rangle + |1\rangle)\). Measuring the first qubit does not give any information about the second; the two qubits are independent or separable.

2.2 The Controlled-NOT (CNOT) Gate

The flagship gate of quantum entanglement is the controlled NOT gate (or CNOT or cX). It acts on 2 qubits, and performs the NOT operation on the second qubit only when the first qubit is \(|1\rangle\), otherwise it leaves it unchanged. Its matrix is as follows:

\[ \DeclareMathOperator{\CNOT}{CNOT} \]

\[ \CNOT = \begin{pmatrix}1&0&0&0\\0&1&0&0\\0&0&0&1\\0&0&1&0\end{pmatrix} \] Ou encore : \[ \CNOT = \begin{cases} |00 \rangle \xrightarrow[]{I \otimes I} |00 \rangle \\ |01 \rangle \xrightarrow[]{I \otimes I} |01 \rangle \\ |10 \rangle \xrightarrow[]{I \otimes NOT} |11 \rangle \\ |11 \rangle \xrightarrow[]{I \otimes NOT} |10 \rangle \end{cases} \]

Its implementation with photons is quite technical, and it hides problems related to our way of encoding qubits. Therefore, we will rely on the definition proposed by Quandela to use this gate in our circuits.

from perceval.components import catalog
cnot = catalog["klm cnot"].build_circuit()
# https://github.com/Quandela/Perceval/blob/main/perceval/components/core_catalog/klm_cnot.py
pdisplay(cnot)

2.3 One last point before we go

As you have seen, the CNOT gate defined above involves 8 rails instead of the expected 4. This is where the technical peculiarity related to photonics lies. For our encoding to work, the sum of the photons in a pair of rails must be equal to 1.

For example, for the state \(|01\rangle\), we have \(1\) photon in the first rail, \(0\) in the second and third rails, and \(1\) in the fourth rail. If we end up with \(1\) photon in the second rail and \(1\) photon in the third rail at the end, we know we have the state \(|10\rangle\). But what happens if we get \(2\) photons in the first rail and \(0\) in the others? Well, that does not correspond to any logical state. It makes sense physically, but not informatically; we can no longer assign qubits to our photonic state.

To address this issue, we add control states that will nullify certain results. In our case, everything will be handled automatically through heralded gates and ancilla states.

2.4 To summarize:

  • We let the heralded gates and ancilla states handle the issues.
  • If we end up with an inconsistent number of photons at the end of our experiment, there’s no need to overthink it; the experiment is invalid and needs to be redone.

To manage the heralded gates and ancilla states, we will work directly with processors.

More information here: https://perceval.quandela.net/docs/notebooks/Tutorial.html#3.-Two-qubit-gates

p = Processor("Naive", cnot)
p.min_detected_photons_filter(0)
p.add_herald(4, 0)
p.add_herald(5, 1)
p.add_herald(6, 0)
p.add_herald(7, 1)
p.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))
p.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))
pdisplay(p, recursive=True)
measure2p(p)
measure2p(p, input_state=qubits["10"])
measure2p(p, input_state=qubits["01"])
measure2p(p, input_state=qubits["11"])

3 Your Turn!

We have seen how to prepare a qubit in any quantum state. Now we will move on to setting up two-qubit states!

To start, let’s try to prepare what are called Bell states. They are widely used because they correspond to entangled states and are relatively easy to manipulate. They correspond to the following basis:

\[ |\Phi^+\rangle = \frac{1}{\sqrt{2}} (|00\rangle + |11\rangle) \] \[ |\Phi^-\rangle = \frac{1}{\sqrt{2}} (|00\rangle - |11\rangle) \] \[ |\Psi^+\rangle = \frac{1}{\sqrt{2}} (|01\rangle + |10\rangle) \] \[ |\Psi^-\rangle = \frac{1}{\sqrt{2}} (|01\rangle - |10\rangle) \]

Let’s try to prepare the state \(|\Phi^+\rangle\). A possible approach is as follows: we seek to have a superposed state, so we will need a Hadamard gate or equivalent, and we also need entangled states, so we will need a \(\CNOT\) gate. In practice, this results in:

phi_plus = Circuit(8).add(0, BS.H()).add(0, cnot)

# Processor Preparation
p_plus = Processor("Naive", phi_plus)  # Step 1: Create the processor with the correct circuit
p_plus.min_detected_photons_filter(0)       # Step 2: Create the filter to discard failed experiments
p_plus.add_herald(4, 0)                     # Add heralds on rails 4, 5, 6, 7
p_plus.add_herald(5, 1)                     
p_plus.add_herald(6, 0)                     
p_plus.add_herald(7, 1)                     
p_plus.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))  # (Step 4): Optionally specify that we 
p_plus.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))  #      are using rail encoding
pdisplay(p_plus, recursive=True)            # Final step: Admire the result/ cry if it does not work
measure2p(p_plus)

One can also cheat with:

e = pcvl.utils.stategenerator.StateGenerator(encoding=pcvl.Encoding.DUAL_RAIL)
b = e.bell_state("phi-")
print(b)

4 Step 1: Creating Quantum States

1.a) Create the state: \[ |\psi\rangle = |11\rangle \]

Starting from our base state, which is \(|00\rangle\). The qubits are accessible via the qubits dictionary for testing, although the default input is already \(|00\rangle\).

There are multiple ways to do this. All methods are accepted.

step_one = ...
raise NotImplementedError

pdisplay(step_one)

If you didn’t use a CNOT gate, you can verify with:

if len(step_one.depths()) == 4:
    analyze(step_one)
    print(f"Result : {amplitudes(step_one)}")
    print("Solution: {'00': 0j, '01': 0j, '10': 0j, '11': (1+0j)}")

If you used a CNOT gate, you need to set up a processor:

if len(step_one.depths()) == 8:
    p_step_one = Processor("Naive", step_one)
    p_step_one.min_detected_photons_filter(0)
    p_step_one.add_herald(4, 0)
    p_step_one.add_herald(5, 1)
    p_step_one.add_herald(6, 0)
    p_step_one.add_herald(7, 1)
    p_step_one.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))
    p_step_one.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))
    pdisplay(p_step_one, recursive=True)
if len(step_one.depths()) == 8:
    measure2p(p_step_one)
    print("Solution:\n> 11: 1.0")

1.b) Create the following state: \[ |\psi\rangle = -\cos\frac{\pi}{6}|00\rangle-\sin\frac{\pi}{6}|11\rangle \]

step_one_more = ...
raise NotImplementedError

p_step_one_more = pcvl.Processor("Naive", step_one_more)
p_step_one_more.min_detected_photons_filter(0)
p_step_one_more.add_herald(4, 0)
p_step_one_more.add_herald(5, 1)
p_step_one_more.add_herald(6, 0)
p_step_one_more.add_herald(7, 1)
p_step_one_more.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))
p_step_one_more.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))
pdisplay(p_step_one_more, recursive=True)
measure2p(p_step_one_more)

5 Step 2: Bell States

Once we have successfully created the Bell states, we still need to know how to measure them in order to use them. Similarly to before, to measure in an arbitrary basis \(\mathcal{B}\), we will create the transition matrix from \(\mathcal{B}\) to \(\mathcal{B}_c\), our canonical basis, and then measure in this known basis.

Combine different gates to obtain the basis change gate, from the Bell basis to the canonical basis. The tests are below.

step_two = Circuit(8, "Transition Bell -> Canonical") // ...
raise NotImplementedError
test_passage = Circuit(8).add(0, BS.H()).add(0, cnot).add(0, step_two)
p_test_passage = pcvl.Processor("Naive", test_passage)
p_test_passage.min_detected_photons_filter(0)
p_test_passage.add_herald(4, 0)
p_test_passage.add_herald(5, 1)
p_test_passage.add_herald(6, 0)
p_test_passage.add_herald(7, 1)
p_test_passage.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))
p_test_passage.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))
pdisplay(p_test_passage, recursive=True)

The test circuit transitions to the Bell basis, then returns to the canonical basis. We therefore expect to find the identity (with some rounding errors).

Warning, for the moment the tests require the circuit to have 8 rails.

measure2p(p_test_passage)
measure2p(p_test_passage, qubits["01"])
measure2p(p_test_passage, qubits["10"])
measure2p(p_test_passage, qubits["11"])

6 Step 3: Trivial?

Create the following state: \[ |\psi\rangle = \frac{1}{\sqrt{3}}(|01\rangle + |10\rangle + |11\rangle) \]

step_three = ...
raise NotImplementedError

pdisplay(step_three)
p_step_three = Processor("Naive", step_three)
p_step_three.min_detected_photons_filter(0)
p_step_three.add_herald(4, 0)
p_step_three.add_herald(5, 1)
p_step_three.add_herald(6, 0)
p_step_three.add_herald(7, 1)
p_step_three.add_port(0, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "0"))
p_step_three.add_port(2, pcvl.Port(pcvl.Encoding.DUAL_RAIL, "1"))
pdisplay(p_step_three, recursive=True)
measure2p(p_step_three)

7 Flag recovery

def circuit_to_list(circuit: Circuit) -> List[List[Tuple[float, float]]]:
    return [[(x.real, x.imag) for x in l] for l in np.array(circuit.compute_unitary())]

d = {
    "step_one": circuit_to_list(step_one),
    "step_one_more": circuit_to_list(step_one_more),
    "step_two": circuit_to_list(step_two),
    "step_three": circuit_to_list(step_three)
}
import requests as rq

URL = ...
# URL = "https://perceval.challenges.404ctf.fr"
rq.get(URL + "/healthcheck").json()
rq.post(URL + "/challenges/3", json=d).json()