cunqa.circuit
This module defines CunqaCircuit. Many circuit design models already
exist, the best-known being Qiskit’s QuantumCircuit. The motivation behind the design of
CunqaCircuit does not stem from identifying problems in existing models, but from the lack of
communication directives, which are so vital in CUNQA as a DQC platform. In this way,
CunqaCircuit class allows users to describe quantum circuits that
include not only local quantum operations, but also classical and quantum communication directives
between distributed circuits. A CunqaCircuit can, therefore,
represent both computation and communication in DQC scenarios.
- class CunqaCircuit(num_qubits, num_clbits=None, id=None)
Quantum circuit abstraction for the CUNQA API. This class allows the design of quantum circuits to be executed in the vQPUs. Upon initialization, the circuit is defined by its number of qubits and, optionally, its number of classical bits and a user-defined identifier. If no identifier is provided, a unique one is generated automatically. The circuit identifier is later used to reference the circuit in communication-related instructions.
Once created, instructions can be appended to the circuit using the provided methods, including single- and multi-qubit gates, measurements, classically controlled operations, and remote communication primitives.
Supported operations Group
Category
Operations
Unitary operations
Single-qubit gates with no parameters
id,x,y,z,h,s,sdg,sx,sxdg,sy,sydg,sz,szdg,t,tdg,p0,p1,v,vdg,kSingle-qubit gates with one parameter
Single-qubit gates with two parameters
Single-qubit gates with three parameters
Single-qubit gates with four parameters
Two-qubit gates with no parameters
swap,iswap,fusedswap,cx,cy,cz,ch,csx,csxdg,csy,csz,cs,csdg,ecr,ct,dcxTwo-qubit gates with one parameter
Two-qubit gates with two parameters
Two-qubit gates with three parameters
Two-qubit gates with four parameters
Three-qubit gates with no parameters
Multicontrol gates with no parameters
Multicontrol gates with one parameter
Multicontrol gates with four parameters
Special gates
unitary,randomunitary,diagonal,multipauli,multipaulirotation,sparsematrix,amplitudedampingnoise,bitflipnoise,dephasingnoise,depolarizingnoise,independentxznoise,twoqubitdepolarizingnoiseLocal non-unitary operations
Local non-unitary operations
cif,endcif,measure,measure_all,resetRemote operations
Classical communication
Quantum communication
Attributes
- id : str
Returns circuit id.
- info : dict
Information of the instance attributes, given in a dictionary.
- num_qubits : tuple[int, int]
Returns a pair with the number of data qubits on the first spot and the number of the communication qubits on the second spot.
- num_clbits : int
Number of classical bits of the circuit.
- instructions: list[dict]
Set of operations applied to the circuit.
- is_dynamic: bool
Whether the circuit has local non-unitary operations.
- data_regs: dict
Dictionary of quantum registers with data qubits as
{"name": [assigned qubits]}.
- classical_regs: dict
Dictionary of classical registers of the circuit as
{"name": [assigned clbits]}.
- sending_to: set[str]
Set of circuit ids to which the current circuit is sending measurement outcomes or qubits.
Operations
Classical communication directives
Quantum communication directives
Non-unitary operations
Unitary operations
- Parameters:
num_qubits (Union[int, tuple[int, int]])
num_clbits (Optional[int])
id (Optional[str])
Transformations
Besides all quantum operations and communication directives, it is common for users to want to
combine two circuits or, conversely, to create several circuits from a single one.
This module exists to enable this capability, defining the following functions. See the following
explanations of the add,
union and hsplit
functions to understand how they empower the study of DQC algorithms.
The function add takes an iterable of circuits as
input and returns a circuit that has the instructions of each circuit of the iterable in order.
Its purpose is to build circuits modularly from simple parts.
- add(circuits)
This function concatenates the instructions of two circuits.
It appends the gates from a list of circuits into a final resulting circuit. The order of the list passed as an argument is important, since the instructions will be appended in that same order.
In this operation, if two circuits in the list contain communication directives between them, an exception will be raised to prevent potential deadlocks and undefined or incorrectly specified behavior.
This operation is the inverse of the
vsplit.- Parameters:
circuits (list[CunqaCircuit])
- Return type:
Warning
As communications greatly depend on the order of execution, adding circuits does not mesh well with communications. In particular, if two circuits that communicate with eachother were added, execution would stall as the circuit would wait to communicate with the next subcircuit, which wouldn’t respond until execution progressed, waiting indefinitely.
For this reason, if two circuits on the iterable contain comunications between them, an exception would be raised.
The add function can be used to avoid redundant code, for example when creating a circuit for the Grover algorithm:
grover_circuit = add([oracle, diffusor] * repeat_times)
Full example of the add function:
import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA
from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qjob import gather
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import add
from pprint import pprint
# ---------------------------
# Acquiring the resources
# ---------------------------
family = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu] = get_QPUs(co_located=True, family=family)
# ---------------------------
# Original circuits are:
#
# | circuit1 | circuit2 |
# q0:| ─[H]─ | ──●── |
# | | | |
# q1:| | ─[X]─ |
#
# And, then, added_circuit is a Bell pair:
#
# added_circuit.q0: ─[H]──●──[M]─
# |
# added_circuit.q1: ─────[X]─[M]─
# ---------------------------
circuit1 = CunqaCircuit(1, id = "circuit1") # adding ancilla
circuit1.h(0)
circuit2 = CunqaCircuit(2, id = "circuit2")
circuit2.cx(0,1)
added_circuit = add([circuit1, circuit2])
added_circuit.measure_all()
qjob = run(added_circuit, qpu, shots = 1024)# non-blocking call
results = qjob.result
print(f"\nResult after addition: {results.counts}\n")
# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family)
Given two circuits with n and m qubits, their union returns a circuit with n + m qubits, where the operations of the former are applied to the first n qubits and those of the latter are applied to the last m qubits. If originally there were distributed instructions between the circuits, they would be replaced by local ones. In the following example we observe the union of two simple circuits.
- union(circuits)
Union of circuits (addition of qubits).
This function joins the qubits of several
CunqaCircuitobjects into a single circuit. Circuits connected by quantum communication protocols have those protocols collapsed into their equivalent local operations:Telegate (a
cat_entangler/ remote-controlled gate(s) /cat_disentanglerblock). The whole protocol is removed and the gate(s) the receiver applied controlled on its comm qubit are reissued as direct gate(s) controlled on the sender’s data qubit.Teledata (a
qsend/qrecvblock). The whole protocol is removed and replaced by aswapbetween the sender’s and the receiver’s data qubits.
This operation is the inverse of the
hsplit.- Parameters:
circuits (list[CunqaCircuit]) – circuits to join.
- Return type:
Basic usage:
c1 = CunqaCircuit(2, id="circuit1")
c1.h(0)
c1.cx(0,1)
c2 = CunqaCircuit(1, id="circuit2")
c2.x(0)
union_circuit = union([c1, c2])
Full example of the union function:
import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA
from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qc_protocols import cat_entangler, cat_disentangler
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import union
from cunqa.qjob import gather
# ---------------------------
# Acquiring resources
# ---------------------------
family_separated = qraise(2, "00:10:00", simulator="Aer", quantum_comm=True, co_located=True)
qpus_separated = get_QPUs(co_located=True, family=family_separated)
family_union = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu_union] = get_QPUs(co_located=True, family=family_union)
try:
# ---------------------------
# Communicated circuits created and executed.
# The remote control of the gate (a remote CX) is implemented with the
# cat-entangler / cat-disentangler telegate protocol.
#
# circuit1.data: ─[H]──●──[M]─
# $
# circuit2.data: ─────[X]─[M]─
# Where $ represents the remote control of the gate
# ---------------------------
circuit1 = CunqaCircuit((1, 1), 1, id="circuit1") # (data qubits, comm qubits), clbits
circuit2 = CunqaCircuit((1, 1), 1, id="circuit2")
data_1, comm_1 = circuit1.get_qubits()
data_2, comm_2 = circuit2.get_qubits()
circuit1.h(data_1[0])
cat_entangler(
[circuit1, circuit2],
data_1[0],
[comm_1[0], comm_2[0]],
[0, 0],
tag="telegate"
)
circuit2.cx(comm_2[0], data_2[0])
cat_disentangler(
[circuit1, circuit2],
data_1[0],
[comm_2[0]],
[0],
[0]
)
circuit1.measure(data_1[0], 0)
circuit2.measure(data_2[0], 0)
qjobs = run([circuit1, circuit2], qpus_separated, shots=1024)
results = gather(qjobs)
for i, result in enumerate(results):
print(f"\nResult before union (circuit{i + 1}): {result.counts}")
# ---------------------------
# Take the union of the circuits and execute it on a single QPU.
# The communication directives (gen_ent/send/recv) are replaced by local
# operations, yielding a single equivalent circuit.
#
# union_circuit.q0: ─[H]──●──[M]─
# |
# union_circuit.q1: ─────[X]─[M]─
# ---------------------------
union_circuit = union([circuit1, circuit2])
qjob = run(union_circuit, qpu_union, shots=1024) # non-blocking call
results = qjob.result
print(f"\nResult after union: {results.counts}\n")
except Exception as error:
raise error
finally:
# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family_union)
qdrop(family_separated)
The function hsplit divides the set of qubits of a circuit into subcircuits, preserving the instructions and substituing local 2-qubit gates by distributed gates if they involve qubits from different subcircuits. The name hspit stands for horizontal split, as in the conventional way to visually represent a circuit one would have to draw a horizontal line to separate the qubits of the circuit in two subsets.
To divide a circuit circuit_to_divide, one should provide an additional argument that determines how the circuits should be divided. This argument can be a list with the number of qubits for each subcircuit (the lenght of the list determines the number of subcircuits), or an int specifying the number of subcircuits, which would get an equal number of qubits except possibly the last one, which would get the remainder if the number of qubits is not cleanly divided by the int provided.
- hsplit(circuit, qubits_or_sections)
Horizontal split of a quantum circuit.
This function splits a circuit into a given number of subcircuits. This number is determined by the qubits_or_sections argument. If it is a list, then it specifies the number of qubits each subcircuit will have; however, if it is an int, it specifies the number of subcircuits to be created (each having the same number of qubits, except for one in case the split is not exact, which will take the remainder as its number of qubits).
This operation is the inverse of the
union.- Parameters:
circuit (CunqaCircuit) – circuit to be splited.
qubits_or_sections (list[int], int) – if is a list, qubits in which to split, if an int, number of subcircuits that result of the split.
- Return type:
list[CunqaCircuit]
Basic syntax:
# List with the number of qubits per subcircuit
[c1, c2] = hsplit(circuit_to_divide, [2, 7])
# Int specifying the number of resulting subcircuits
[c1, c2] = hsplit(circuit_to_divide, 2)
In particular, it could be checked that union and hsplit are inverses of eachother:
# New circuit equivalent to circ is returned
union(hsplit(circ, 2))
# New circuits equal to circ1 and circ2 are returned
hsplit(union(circ1, circ2), [circ1.num_qubits, circ2.num_qubits])
Full example of the hsplit function:
import os, sys
# In order to import cunqa, we append to the search path the cunqa installation path
sys.path.append(os.getenv("HOME")) # HOME as install path is specific to CESGA
from cunqa.qpu import get_QPUs, qraise, qdrop, run
from cunqa.qjob import gather
from cunqa.circuit import CunqaCircuit
from cunqa.circuit.transformations import hsplit
# ---------------------------
# Acquiring resources
# ---------------------------
family_original = qraise(1, "00:10:00", simulator="Aer", co_located=True)
[qpu_original] = get_QPUs(co_located=True, family=family_original)
family_separated = qraise(2, "00:10:00", simulator="Aer", quantum_comm=True, co_located=True)
qpus_separated = get_QPUs(co_located=True, family=family_separated)
try:
# ---------------------------
# Original circuit
# union_circuit.q0: ─[H]──●──[M]─
# |
# union_circuit.q1: ─────[X]─[M]─
# ---------------------------
circuit = CunqaCircuit(2, id="circuit")
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
qjob = run(circuit, qpu_original, shots=1024) # non-blocking call
results = qjob.result
print(f"\nResult before hsplit: {results.counts}")
# ---------------------------
# Split original circuit to create two communicated circuits, and execute them
# circuit1.q0: ─[H]──●──[M]─
# $
# circuit2.q0: ─────[X]─[M]─
# Where $ represents the remote control of the gate
# ---------------------------
[circuit1, circuit2] = hsplit(circuit, 2)
data1, comm1 = circuit1.get_qubits()
print(f"{data1} and {comm1}")
data2, comm2 = circuit2.get_qubits()
print(f"{data2} and {comm2}")
qjobs = run([circuit1, circuit2], qpus_separated, shots=1024)
results = gather(qjobs)
for result in results:
print(f"Result after split: {result.counts}")
except Exception as error:
raise error
finally:
# ---------------------------
# Relinquishing resources
# ---------------------------
qdrop(family_original, family_separated, remove_logs=True)