Basics
In this section you will be introduced to the basics of sending and receiving both classical and quantum information. As well as the first steps of writing programs and manipulating Qubits.
This chapter of the tutorial takes the user through the example examples/tutorial/1_Basics.
This chapter will focus only on application.py file.
The examples of this and the following sections contain the code snippets that are used in this tutorial. As such the examples may provide support in understanding the context of each of the snippets. Additionally all examples are fully functional. In order to run an example, one must first make the current example directory the active directory:
cd examples/tutorial/1_Basics
Afterwards one may run the simulation using:
python3 run_simulation.py
Application basics
In this section we will explain the basics of writing an application for SquidASM.
In the examples of this tutorial the application.py file will contain the programs that run on each node.
We define a separate meanings to program and application.
A program is the code running on a single node.
An application is the complete set of programs to achieve a specific purpose.
For example BQC is an application, but it consists of two programs, one program for the client and another for the server.
In this tutorial we will be creating a AliceProgram and a BobProgram that will run on a Alice and Bob node respectively.
Both the Alice and Bob program start with an unpacking of a ProgramContex object into
csocket (a classical socket), epr_socket and connection (a NetQASM connection).
The full AliceProgram is shown below, for now we will focus on introducing the objects in the highlighted section:
class AliceProgram(Program):
PEER_NAME = "Bob"
@property
def meta(self) -> ProgramMeta:
return ProgramMeta(
name="tutorial_program",
csockets=[self.PEER_NAME],
epr_sockets=[self.PEER_NAME],
max_qubits=1,
)
def run(self, context: ProgramContext):
# get classical socket to peer
csocket = context.csockets[self.PEER_NAME]
# get EPR socket to peer
epr_socket = context.epr_sockets[self.PEER_NAME]
# get connection to quantum network processing unit
connection = context.connection
# send a string message via a classical channel
message = "Hello"
csocket.send(message)
print(f"Alice sends message: {message}")
# Register a request to create an EPR pair, then apply a Hadamard gate on the epr qubit and measure
epr_qubit = epr_socket.create_keep()[0]
epr_qubit.H()
result = epr_qubit.measure()
yield from connection.flush()
print(f"Alice measures local EPR qubit: {result}")
# Qubits on a local node can be obtained, but require the connection to be initialized
local_qubit0 = Qubit(connection)
local_qubit1 = Qubit(connection)
# Apply a Hadamard gate
local_qubit0.H()
# Apply CNOT gate where q0 is the control qubit, q1 is the target qubit
local_qubit0.cnot(local_qubit1)
r0 = local_qubit0.measure()
r1 = local_qubit1.measure()
yield from connection.flush()
print(f"Alice measures local qubits: {r0}, {r1}")
return {}
In order to understand the role of csocket, epr_socket and connection,
we must introduce some overview context and concepts.
An important note is that the program runs on a host.
The host can be any type of classical computer.
The host is connected to a quantum network processing unit(QNPU),
that is responsible for local qubit operations and EPR pair generation with remote nodes.
The link between the host and quantum network processing unit is called a NetQASM connection.
The variable connection represents this NetQASM connection.
The NetQASM connection is used to communicate all instructions regarding qubit operations and entanglement generation. For many of these operations, this dependency is not explicit in the program code, but for certain actions it is required explicitly.
The csocket is a classical socket.
A socket represents the end point for sending and receiving data across a network to the socket of another node.
Note that the socket connects to one specific other node and socket.
The classical socket can be used to send classical information to the host of another node.
The epr_socket is instead a socket for generating entangled qubits on both nodes.
Behind the scenes the communication requests are sent to the quantum network processing unit.
Note
Most NetQASM objects, such as qubits and epr sockets, are initialized using a NetQASM connection and they store this NetQASM connection reference internally. These objects then forward instructions to a NetQASM connection behind the scenes.
Sending classical information
Classical information is done via the Socket object from netqasm.sdk.
The Socket objects represent an open connection to a peer.
So sending a classical message to a peer may be done by using the send() method of the classical socket.
# send a string message via a classical channel
message = "Hello"
csocket.send(message)
print(f"Alice sends message: {message}")
In order for Bob to receive the message, he must be waiting for a classical message at the same time using the recv() method.
# Bob listens for messages on his classical socket
message = yield from csocket.recv()
print(f"Bob receives message: {message}")
It is mandatory to include the yield from keywords when receiving messages for the application to work with SquidASM.
Running the simulation should results in:
Alice sends message: Hello
Bob receives message Hello
Creating EPR pairs between nodes
Creating an EPR pair follows a similar pattern as classical communication,
namely Alice must register a request using create_keep() to generate an EPR pair,
while Bob needs to be listening to such a request using recv_keep().
Both create_keep() and recv_keep() return a list of qubits so we select our local EPR qubit using [0].
By default the request only creates a single EPR pair,
but a request for multiple EPR pairs may be placed using create_keep(number=n).
epr_qubit = epr_socket.create_keep()[0]
epr_qubit.H()
result = epr_qubit.measure()
yield from connection.flush()
print(f"Alice measures local EPR qubit: {result}")
epr_qubit = epr_socket.recv_keep()[0]
epr_qubit.H()
result = epr_qubit.measure()
yield from connection.flush()
print(f"Bob measures local EPR qubit: {result}")
After the EPR pair is ready, we apply a Hadamard gate and measure the qubit.
It is then required to send these instructions to the QNPU using yield from connection.flush() for both Alice and Bob.
The next section, NetQASM, will go into more details regarding the connection.
Running the simulation results in either:
Alice measures local EPR qubit: 0
Bob measures local EPR qubit: 0
or:
Alice measures local EPR qubit: 1
Bob measures local EPR qubit: 1
Note
The EPR pairs as presented to the application are in the \(\ket{\Phi^+} = \frac{1}{\sqrt{2}}(\ket{00} + \ket{11}\) state. Behind the scenes the EPR pair might have been initially generated in a different bell state, but by applying the appropriate Pauli gates on both nodes, the state will be transformed into the \(\ket{\Phi^+}\) state.
Creating local Qubits
It is possible to request and use local qubits, without generating entanglement with a remote node.
This is done by initializing a Qubit object from netqasm.sdk.qubit.
This initialization requires the user to pass the NetQASM connection,
as instructions need to be sent to the QNPU that a particular qubit is reset and marked as in use.
We can use the Qubit object to create an EPR pair with both qubits on the same node:
# Qubits on a local node can be obtained, but require the connection to be initialized
local_qubit0 = Qubit(connection)
local_qubit1 = Qubit(connection)
# Apply a Hadamard gate
local_qubit0.H()
# Apply CNOT gate where q0 is the control qubit, q1 is the target qubit
local_qubit0.cnot(local_qubit1)
r0 = local_qubit0.measure()
r1 = local_qubit1.measure()
yield from connection.flush()
print(f"Alice measures local qubits: {r0}, {r1}")
The result of this code segment is either:
Alice measures local qubits: 0, 0
or:
Alice measures local qubits: 1, 1
Qubit gates
To apply a qubit gate, the methods representing the gates of the Qubit object may be used.
The Qubit object has a large selection of single qubit gates: X(), Y(), Z(), T(), H(), K(), S().
Three single qubit rotations: rot_X(n, d), rot_Y(n, d), rot_Z(n, d).
These required the specification of the magnitude of rotation via parameters n and d: \(\frac{n \pi}{2^d}\).
And it has two, two qubit operations: cnot(target) and cphase(target).
Where the control qubit is the qubit invoking the operation and the target qubit is the one given as argument.