Parametric circuit
Quantum circuits with variable parameters play an important role in some quantum algorithms, especially variational algorithms. QURI Parts treats such circuits in a special way so that such algorithms can be efficiently performed. In QURI Parts, we provide 2 types of parametric ciricuits. For either type of circuits, the parametric gates are all Pauli rotation gates, but they hold the parameters in different ways. They are
UnboundParametricQuantumCircuit
LinearMappedUnboundParametricQuantumCircuit
In this tutorial, we clearly distinguish two types of parameters: circuit parameter and gate parameter. Circuit parameters refer to independent parameters the circuit holds. Gate parameters refer to the parameter each parametric gate in the circuit hold. Depending on type of the circuit, the gate parameters have different relations with the circuit parameters. In the case of UnboundParametricQuantumCircuit
, gate parameters are trivial mapped as circuit parameters, i.e. they are the same. In the case of LinearMappedUnboundParametricQuantumCircuit
, circuit parameters are linearly mapped to gate parameters, so it is generally possible that the number of gate parameters are different from the number of circuit parameters. We will make this clearer with concrete formulae and examples in later sections.
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]"
Parameter
objects
An unbound parameter in a parametric circuit is represented by a quri_parts.circuit.Parameter
class. A Parameter
object works as a placeholder for the parameter and does not hold any specific value. Identity of a Parameter
object is determined by identity of it as a Python object. Even if two Parameter
objects have the same name, they are treated as different parameters:
from quri_parts.circuit import Parameter, CONST
phi = Parameter("phi")
psi1 = Parameter("psi")
psi2 = Parameter("psi2")
# CONST is a pre-defined parameter that represents a constant.
print(phi, psi1, psi2, CONST)
print("phi == psi1:", phi == psi1)
print("psi1 == psi2:", psi1 == psi2)
print("phi == CONST:", phi == CONST)
print("")
print("Parameters are treated as different even if they have the same name")
print(" Parameter('phi') == Parameter('phi'):", Parameter("phi") == Parameter("phi"))
#output
Parameter(name=phi) Parameter(name=psi) Parameter(name=psi2) Parameter(name=)
phi == psi1: False
psi1 == psi2: False
phi == CONST: False
Parameters are treated as different even if they have the same name
Parameter('phi') == Parameter('phi'): False
Parametric gates
The parametric gates are represented by the ParametricQuantumGate
object. It is an object containing only the attributes:
name
: name of the parametric gate.target_indices
: the qubit it acts on.control_indices
: the qubit that controls the gate on the target qubit.pauli_ids
: The sequence of Pauli matrix labels that represents the label of the Pauli rotation gates.
Like the QuantumGate
object, it is not to be constructed directly. In QURI Parts, we provide 4 factory functions for creating ParametricQuantumGates
. They are Pauli rotation gates with no concrete value of rotation angle bound to it.
from quri_parts.circuit import ParametricRX, ParametricRY, ParametricRZ, ParametricPauliRotation
print(ParametricRX(target_index=0))
print(ParametricRY(target_index=0))
print(ParametricRZ(target_index=0))
print(ParametricPauliRotation(target_indics=(0, 1, 2, 3), pauli_ids=(3, 2, 1, 2)))
#output
ParametricQuantumGate(name='ParametricRX', target_indices=(0,), control_indices=(), pauli_ids=())
ParametricQuantumGate(name='ParametricRY', target_indices=(0,), control_indices=(), pauli_ids=())
ParametricQuantumGate(name='ParametricRZ', target_indices=(0,), control_indices=(), pauli_ids=())
ParametricQuantumGate(name='ParametricPauliRotation', target_indices=(0, 1, 2, 3), control_indices=(), pauli_ids=(3, 2, 1, 2))
UnboundParametricQuantumCircuit
An unbound parametric circuit where each parametric gate in it has its own parameter independent from other parameters. The gates can only depend on a parameter via:
where is any Pauli string. In addition to parametric gates, you can also add all the non-parametric gates supported by the usual QuantumCircuit
object to a parametric circuit. As an example, let's create a circuit with gates: . Here, , and are rotation gates with parameters independent of each other.
from quri_parts.circuit import UnboundParametricQuantumCircuit
from quri_parts.circuit.utils.circuit_drawer import draw_circuit
parametric_circuit = UnboundParametricQuantumCircuit(2)
parametric_circuit.add_H_gate(0)
parametric_circuit.add_CNOT_gate(0, 1)
p_theta = parametric_circuit.add_ParametricRX_gate(0)
p_phi = parametric_circuit.add_ParametricRY_gate(0)
p_psi = parametric_circuit.add_ParametricRZ_gate(1)
draw_circuit(parametric_circuit)
#output
___ ___ ___
| H | |PRX| |PRY|
0 --|0 |-----●-----|2 |---|3 |-
|___| | |___| |___|
_|_ ___
|CX | |PRZ|
1 ----------|1 |---|4 |---------
|___| |___|
We may check whether or not we have 3 independent parameters added to to circuit and assign values to them. As an example, we assign , , to the parameters.
print("Number of parameters in the circuit:", parametric_circuit.parameter_count)
# bind parameters:
print("")
print("Bind parameters:")
parametric_circuit.bind_parameters([0.1, 0.2, 0.3]).gates
#output
Number of parameters in the circuit: 3
Bind parameters:
(QuantumGate(name='H', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(0,), controlled_on=(1,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='RX', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(0.1,), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='RY', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(0.2,), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='RZ', target_indices=(1,), control_indices=(), controlled_on=(), classical_indices=(), params=(0.3,), pauli_ids=(), unitary_matrix=()))
Looking at the gates, we indeed get the desired circuit with the rotation gates being
parametric_circuit.gates
#output
[QuantumGate(name='H', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(0,), controlled_on=(1,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
ParametricQuantumGate(name='ParametricRX', target_indices=(0,), control_indices=(), pauli_ids=()),
ParametricQuantumGate(name='ParametricRY', target_indices=(0,), control_indices=(), pauli_ids=()),
ParametricQuantumGate(name='ParametricRZ', target_indices=(1,), control_indices=(), pauli_ids=())]
Properties
We provide various properties for UnboundParametricQuantumCircuit
.
qubit_count
:
Number of qubits of the circuit
print("qubit_count:", parametric_circuit.qubit_count)
#output
qubit_count: 2
depth
:
Depth of the parametric circuit
print("Circuit depth:", parametric_circuit.depth)
#output
Circuit depth: 4
parameter_count
:
Number of circuit parameters.
print("Parameter count:", parametric_circuit.parameter_count)
#output
Parameter count: 3
gates
:
All the non-parametric and parametric gates.
print("Gates:")
parametric_circuit.gates
#output
Gates:
[QuantumGate(name='H', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(0,), controlled_on=(1,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
ParametricQuantumGate(name='ParametricRX', target_indices=(0,), control_indices=(), pauli_ids=()),
ParametricQuantumGate(name='ParametricRY', target_indices=(0,), control_indices=(), pauli_ids=()),
ParametricQuantumGate(name='ParametricRZ', target_indices=(1,), control_indices=(), pauli_ids=())]
gates_and_params
:
All the non-parametric and parametric gates and the associated circuit parameters. If the gate is non-parametric, the associated parameter would be None
.
print("Gates and parameters:")
parametric_circuit.gates_and_params
#output
Gates and parameters:
((QuantumGate(name='H', target_indices=(0,), control_indices=(), controlled_on=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
None),
(QuantumGate(name='CNOT', target_indices=(1,), control_indices=(0,), controlled_on=(1,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()),
None),
(ParametricQuantumGate(name='ParametricRX', target_indices=(0,), control_indices=(), pauli_ids=()),
Parameter(name=)),
(ParametricQuantumGate(name='ParametricRY', target_indices=(0,), control_indices=(), pauli_ids=()),
Parameter(name=)),
(ParametricQuantumGate(name='ParametricRZ', target_indices=(1,), control_indices=(), pauli_ids=()),
Parameter(name=)))
Mutability
An UnboundParametricQuantumCircuit
is a mutable object where we may freely add gates to. In the case where you want to freeze the parametric circuit like you freeze a QuantumCircuit
, you may use the freeze
method to create a new ImmutableUnboundParametricQuantumCircuit
where in-place gate additions are disabled.
frozen_parametric_circuit = parametric_circuit.freeze()
Note that if you freeze the frozen circuit again, you get the same frozen circuit back.
(frozen_parametric_circuit, frozen_parametric_circuit.freeze())
#output
(<quri_parts.circuit.circuit_parametric.ImmutableUnboundParametricQuantumCircuit at 0x10a8bbeb0>,
<quri_parts.circuit.circuit_parametric.ImmutableUnboundParametricQuantumCircuit at 0x10a8bbeb0>)
In the case where you want to unfreeze the circuit, you may use the get_mutable_copy
method.
frozen_parametric_circuit.get_mutable_copy()