Quantum gates and circuits#
Quantum gates and circuits are essential when working on quantum computing. Here we describe basic treatment of them in QURI Parts.
Prerequisite#
QURI Parts modules used in this tutorial: quri-parts-circuit
, quri-parts-core
and quri-parts-qulacs
. You can install them as follows:
[ ]:
!pip install "quri-parts[qulacs]"
QuantumGate object#
In QURI Parts, a quantum gate is represented by a QuantumGate
object (more precisely NamedTuple
). A QuantumGate
contains not only the kind of the gate but also some additional information such as gate parameters and qubits on which the gate acts. You can create gate objects using QuantumGate
:
[1]:
from math import pi
from quri_parts.circuit import QuantumGate
gates = [
# X gate acting on qubit 0
QuantumGate("X", target_indices=(0,)),
# Rotation gate acting on qubit 1 with angle pi/3
QuantumGate("RX", target_indices=(1,), params=(pi/3,)),
# CNOT gate on control qubit 2 and target qubit 1
QuantumGate("CNOT", target_indices=(1,), control_indices=(2,)),
]
for gate in gates:
print(gate)
QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), params=(1.0471975511965976,), pauli_ids=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), params=(), pauli_ids=())
However it is more convenient to use factory functions:
[2]:
from quri_parts.circuit import X, RX, CNOT
gates = [
# X gate acting on qubit 0
X(0),
# Rotation gate acting on qubit 1 with angle pi/3
RX(1, pi/3),
# CNOT gate on control qubit 2 and target qubit 1
CNOT(2, 1),
]
for gate in gates:
print(gate)
QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), params=(1.0471975511965976,), pauli_ids=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), params=(), pauli_ids=())
You can access (but not set) attributes of a gate object:
[3]:
from quri_parts.circuit import PauliRotation
x_gate = X(0)
print(f"name: {x_gate.name}, target: {x_gate.target_indices}")
rx_gate = RX(1, pi/3)
print(f"name: {rx_gate.name}, target: {rx_gate.target_indices}, angle: {rx_gate.params[0]}")
cnot_gate = CNOT(2, 1)
print(f"name: {cnot_gate.name}, control: {cnot_gate.control_indices}, target: {cnot_gate.target_indices}")
pauli_rot_gate = PauliRotation(target_indices=(0, 1, 2), pauli_ids=(1, 2, 3), angle=pi/3)
print(f"name: {pauli_rot_gate.name}, target: {pauli_rot_gate.target_indices}, pauli_ids: {pauli_rot_gate.pauli_ids}, angle: {pauli_rot_gate.params[0]}")
name: X, target: (0,)
name: RX, target: (1,), angle: 1.0471975511965976
name: CNOT, control: (2,), target: (1,)
name: PauliRotation, target: (0, 1, 2), pauli_ids: (1, 2, 3), angle: 1.0471975511965976
QuantumCircuit object#
You can construct a quantum circuit by specifying the number of qubits used in the circuit as follows:
[4]:
from quri_parts.circuit import QuantumCircuit
# Create a circuit for 3 qubits
circuit = QuantumCircuit(3)
# Add an already created QuantumGate object
circuit.add_gate(X(0))
# Or use methods to add gates
circuit.add_X_gate(0)
circuit.add_RX_gate(1, pi/3)
circuit.add_CNOT_gate(2, 1)
circuit.add_PauliRotation_gate(target_qubits=(0, 1, 2), pauli_id_list=(1, 2, 3), angle=pi/3)
A QuantumCircuit
object has several properties:
[5]:
print("Qubit count:", circuit.qubit_count)
print("Circuit depth:", circuit.depth)
gates = circuit.gates # .gates returns the gates in the circuit as a sequence
print("# of gates in the circuit:", len(gates))
for gate in gates:
print(gate)
Qubit count: 3
Circuit depth: 3
# of gates in the circuit: 5
QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=())
QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), params=(1.0471975511965976,), pauli_ids=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), params=(), pauli_ids=())
QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3))
QuantumCircuit
objects with the same number of qubits can be combined and extended:
[6]:
circuit2 = QuantumCircuit(3)
circuit2.add_Y_gate(1)
circuit2.add_H_gate(2)
combined = circuit + circuit2 # equivalent: combined = circuit.combine(circuit2)
print("Combined circuit:", combined.gates)
circuit2 += circuit # equivalent: circuit2.extend(circuit)
print("Extended circuit:", circuit2.gates)
Combined circuit: (QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='RX', target_indices=(1,), control_indices=(), params=(1.0471975511965976,), pauli_ids=()), QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), params=(), pauli_ids=()), QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3)), QuantumGate(name='Y', target_indices=(1,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='H', target_indices=(2,), control_indices=(), params=(), pauli_ids=()))
Extended circuit: (QuantumGate(name='Y', target_indices=(1,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='H', target_indices=(2,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=()), QuantumGate(name='RX', target_indices=(1,), control_indices=(), params=(1.0471975511965976,), pauli_ids=()), QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), params=(), pauli_ids=()), QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3)))
Mutable and immutable circuit objects#
In the above example, a QuantumCircuit
object is first created and then some gates are added to it. Contents (in this case a gate sequence) of the QuantumCircuit
object are mutated in-place. Such an object is called a mutable object. A mutable circuit is useful to construct a circuit step-by-step, but mutability is often a cause of a trouble. For example, you may pass a circuit to a function and the function may alter contents of the circuit against your intention:
[7]:
def get_depth(circuit):
# This function adds some gates despite its name!
depth = circuit.depth
circuit.add_X_gate(0)
return depth
circuit = QuantumCircuit(2)
circuit.add_Z_gate(0)
circuit.add_H_gate(1)
print("# of gates:", len(circuit.gates))
depth = get_depth(circuit)
print("Circuit depth:", depth)
print("# of gates:", len(circuit.gates))
# of gates: 2
Circuit depth: 1
# of gates: 3
This example is rather explicit and easy to avoid, but there are more subtle cases for which it is difficult to find the cause of the trouble. To prevent such a problem, you can use an immutable version (which we often call a frozen version) of the circuit obtained by .freeze()
:
[8]:
circuit = QuantumCircuit(2)
circuit.add_Z_gate(0)
circuit.add_H_gate(1)
print("# of gates:", len(circuit.gates))
frozen_circuit = circuit.freeze()
try:
depth = get_depth(frozen_circuit)
except Exception as e:
print("ERROR:", e)
print("# of gates:", len(circuit.gates))
# of gates: 2
ERROR: 'ImmutableQuantumCircuit' object has no attribute 'add_X_gate'
# of gates: 2
The frozen version does not have methods to alter its contents, so you can safely use one frozen circuit object in many places.
You can call .freeze()
on the frozen version too. In this case, the frozen version itself is returned without copy:
[9]:
print(frozen_circuit)
frozen_circuit2 = frozen_circuit.freeze()
print(frozen_circuit2)
<quri_parts.circuit.circuit.ImmutableQuantumCircuit object at 0x7fdd7bcc3160>
<quri_parts.circuit.circuit.ImmutableQuantumCircuit object at 0x7fdd7bcc3160>
When you want to copy a circuit so that further modification does not affect the original one, call .get_mutable_copy()
:
[10]:
copied_circuit = circuit.get_mutable_copy()
copied_circuit.add_X_gate(0)
print("# of gates in circuit:", len(circuit.gates))
print("# of gates in copied_circuit:", len(copied_circuit.gates))
# You can also copy a frozen circuit
copied_circuit2 = frozen_circuit.get_mutable_copy()
copied_circuit2.add_X_gate(0)
print("# of gates in frozen_circuit:", len(frozen_circuit.gates))
print("# of gates in copied_circuit2:", len(copied_circuit2.gates))
# of gates in circuit: 2
# of gates in copied_circuit: 3
# of gates in frozen_circuit: 2
# of gates in copied_circuit2: 3
State preparation with a quantum circuit#
A quantum state prepared by applying a quantum circuit to \(|00\ldots 0\rangle\) is represented by a CircuitQuantumState
interface. You can create a CircuitQuantumState
with a quantum circuit object by GeneralCircuitQuantumState
:
[11]:
from quri_parts.core.state import GeneralCircuitQuantumState
circuit = QuantumCircuit(2)
circuit.add_Z_gate(0)
circuit.add_H_gate(1)
# A quantum state of 2 qubits with an empty circuit (i.e. |00>)
circuit_state = GeneralCircuitQuantumState(2)
# A quantum state of 2 qubits with a given circuit (i.e. C|00> where C is the ciruict)
circuit_state = GeneralCircuitQuantumState(2, circuit)
Note that the ComputationalBasisState
we introduced in the previous section is also a CircuitQuantumState
, since such a state can always be constructed by applying a circuit to a zero state.
[12]:
from quri_parts.core.state import ComputationalBasisState
cb_state = ComputationalBasisState(2, bits=0b01)
CircuitQuantumState
has some properties and methods:
[13]:
print("(circuit_state)")
# Get how many qubits this state is for.
print("qubit_count:", circuit_state.qubit_count)
# Get the circuit of the state. This returns an immutable circuit.
print("circuit:", circuit_state.circuit)
# Create a new state with some new gates added.
gate_added_state = circuit_state.with_gates_applied([X(1), CNOT(1, 0)])
print("original circuit len:", len(circuit_state.circuit.gates))
print("new circuit len:", len(gate_added_state.circuit.gates))
(circuit_state)
qubit_count: 2
circuit: <quri_parts.circuit.circuit.ImmutableQuantumCircuit object at 0x7fdd7bcc31c0>
original circuit len: 2
new circuit len: 4
[14]:
print("(cb_state)")
# Get how many qubits this state is for.
print("qubit_count:", cb_state.qubit_count)
# Get the circuit of the state. This returns an immutable circuit.
cb_circuit = cb_state.circuit
for gate in cb_circuit.gates:
print(gate)
# Create a new state with some new gates added.
# Note that the new state is no longer a ComputationalBasisState.
gate_added_cb_state = cb_state.with_gates_applied([X(1), CNOT(1, 0)])
print("original circuit len:", len(cb_state.circuit.gates))
print("new circuit len:", len(gate_added_cb_state.circuit.gates))
(cb_state)
qubit_count: 2
QuantumGate(name='X', target_indices=(0,), control_indices=(), params=(), pauli_ids=())
original circuit len: 1
new circuit len: 3
Note that classes for quantum states in QURI Parts are always immutable; you cannot modify an already created quantum state object.
A CircuitQuantumState
prepared as above can be used to estimate an expectation value of an operator using an Estimator as described in the previous section. In an example below, we use an Estimator using Qulacs, which internally converts the given circuit into a Qulacs circuit, run it and compute the expectation value of the given operator.
[15]:
from quri_parts.core.operator import Operator, pauli_label
op = Operator({
pauli_label("X0 Y1"): 0.5 + 0.5j,
pauli_label("Z0 X1"): 0.2,
})
from quri_parts.qulacs.estimator import create_qulacs_vector_estimator
estimator = create_qulacs_vector_estimator()
estimate = estimator(op, circuit_state)
print(f"Estimated expectation value: {estimate.value}")
Estimated expectation value: (0.19999999999999996+0j)
Circuit conversion#
In the above example, the Estimator handles conversion of the QURI Parts circuit to a Qulacs circuit object internally, so you usually don’t need to do it yourself. If, however, you want to get the converted Qulacs circuit, you can use quri_parts.qulacs.circuit.convert_circuit
function.
[16]:
from quri_parts.qulacs.circuit import convert_circuit
qulacs_circuit = convert_circuit(circuit)
print(qulacs_circuit)
import qulacs
qulacs_state = qulacs.QuantumState(2)
qulacs_circuit.update_quantum_state(qulacs_state)
print(qulacs_state)
*** Quantum Circuit Info ***
# of qubit: 2
# of step : 1
# of gate : 2
# of 1 qubit gate: 2
Clifford : yes
Gaussian : no
*** Quantum State ***
* Qubit Count : 2
* Dimension : 4
* State vector :
(0.707107,0)
(-0,-0)
(0.707107,0)
(0,0)
The location of such a conversion function of course depends on the SDK/simulator you want to use, but it is typically located at quri_parts.[SDK].circuit.convert_circuit
.