メインコンテンツまでスキップ

量子ゲートと量子回路

量子ゲートと量子回路は量子コンピューターにおいて重要な要素です。ここではQURI Partsでの標準的な扱いについて説明します。

前提条件

チュートリアルで使用するQURI Partsモジュール:quri-parts-circuit, quri-parts-core, quri-parts-braket, quri-parts-cirq, quri-parts-qiskit, quri-parts-qulacs, quri-parts-tket

インストールは以下のコマンドで行うことができます。

!pip install "quri-parts[braket,cirq,qiskit,qulacs,tket]"

量子ゲート

QURI Partsでは、量子ゲートをQuantumGateオブジェクト(正確にはNamedTuple)で表現します。QuantumGateはゲートだけではなく、ゲートのパラメータやゲートが作用する量子ビットなどのその他の情報も含んでいます。QuantumGateを使ってゲートオブジェクトを作ることができます:

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)
# output
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(), unitary_matrix=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

ファクトリ関数を使うとより簡単に作成できます。

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)
# output
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(), unitary_matrix=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())

QURI Partsでは単一量子ビットの回転ゲートは以下のように定義します。

RX(θ)=exp(iθ2X)RY(θ)=exp(iθ2Y)RZ(θ)=exp(iθ2Z)\begin{align*} R_X(\theta) &= \exp\left( -i\frac{\theta}{2} X\right) \\ R_Y(\theta) &= \exp\left( -i\frac{\theta}{2} Y\right) \\ R_Z(\theta) &= \exp\left( -i\frac{\theta}{2} Z\right) \\ \end{align*}

θ\thetaはゲートの回転角です。

ゲートオブジェクトのプロパティにアクセスすることができます(設定することはできません)。

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]}")
# output
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

量子回路オブジェクト

量子回路を構成するには、回路に使用する量子ビットの個数を次のように指定します:

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)

QuantumCircuitオブジェクトは複数のプロパティを持っています:

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)
# output
Qubit count: 3
Circuit depth: 3
# of gates in the circuit: 5
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='RX', target_indices=(1,), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(), unitary_matrix=())
QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=())
QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3), unitary_matrix=())

QuantumCircuitオブジェクトは結合したり拡張したりすることができます:

circuit2 = QuantumCircuit(3)
circuit2.add_Y_gate(1)
circuit2.add_H_gate(2)

combined = circuit + circuit2 # equivalent: combined = circuit.combine(circuit2)
print("Combined circuit:\n", combined.gates, "\n")

circuit2 += circuit # equivalent: circuit2.extend(circuit)
print("Extended circuit:\n", circuit2.gates, "\n")

# You can also embed a smaller circuit into a larger one
circuit_larger = QuantumCircuit(5)
circuit_larger.add_X_gate(3)
circuit_smaller = QuantumCircuit(3)
circuit_smaller.add_H_gate(0)
circuit_larger.extend(circuit_smaller)
print("Circuit extended by smaller one:\n", circuit_larger.gates)
# output
Combined circuit:
(QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='RX', target_indices=(1,), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(), unitary_matrix=()), QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3), unitary_matrix=()), QuantumGate(name='Y', target_indices=(1,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='H', target_indices=(2,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()))

Extended circuit:
(QuantumGate(name='Y', target_indices=(1,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='H', target_indices=(2,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='X', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='RX', target_indices=(1,), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(), unitary_matrix=()), QuantumGate(name='CNOT', target_indices=(1,), control_indices=(2,), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='PauliRotation', target_indices=(0, 1, 2), control_indices=(), classical_indices=(), params=(1.0471975511965976,), pauli_ids=(1, 2, 3), unitary_matrix=()))

Circuit extended by smaller one:
(QuantumGate(name='X', target_indices=(3,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()), QuantumGate(name='H', target_indices=(0,), control_indices=(), classical_indices=(), params=(), pauli_ids=(), unitary_matrix=()))

ミュータブルな回路オブジェクトとイミュータブルな回路オブジェクト

上記の例では、QuantumCircuitオブジェクトを最初に作成しその後ゲートを追加しています。QuantumCircuitの中身(ゲート列)は変更可能となっています。このようなオブジェクトのことをミュータブルオブジェクトと呼びます。ミュータブル回路は回路を段階的に構築する際に便利ですが、ミュータビリティがトラブルの原因になることも多くあります。例えば、回路に関数を渡した場合、関数が意図に反して回路の中身を変更することかもしれません:

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))
# output
# of gates: 2
Circuit depth: 1
# of gates: 3

上記の例では、get_depth()関数内でゲートが追加されているため、引数の回路深さと戻り値の回路深さが異なっています。

この例ではかなり明示的で避けるのは簡単ですが、トラブルの原因を探すのが困難なケースも存在します。このような問題を防ぐために、.freeze()を含む回路のイミュータブルなバージョン(凍結バージョンと呼びます)を使用することができます。

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))
# output
# of gates: 2
ERROR: 'ImmutableQuantumCircuit' object has no attribute 'add_X_gate'
# of gates: 2

凍結バージョンはその中身を変更するためのメソッドを持たないため、凍結回路オブジェクトはどのような場面でも安全に使用することができます。

.freeze()は凍結バージョンでも同様に呼ぶことができます。この場合では、コピーなしで凍結バージョンそのものが返却されます。

print(frozen_circuit)
frozen_circuit2 = frozen_circuit.freeze()
print(frozen_circuit2)
    <quri_parts.circuit.circuit.ImmutableQuantumCircuit object at 0x10cc88d90>
<quri_parts.circuit.circuit.ImmutableQuantumCircuit object at 0x10cc88d90>

回路をコピーしたい際には、さらなる変更がオリジナルに影響をあたえないように.get_mutable_copy()を呼びます:

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))
# output
# of gates in circuit: 2
# of gates in copied_circuit: 3
# of gates in frozen_circuit: 2
# of gates in copied_circuit2: 3

回路変換

QURI PartsにはプラットフォームやSDKなどによって設計された回路オブジェクトを生成する回路変換機が存在します。Qulacsの回路を得たいのであれば、quri_parts.qulacs.circuit.convert_circuit関数を使用することができます。

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)
# output
*** 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)

これらの変換関数の位置は使用するSDKやシミュレータによって異なりますが、通常はquri_parts.[SDK].circuit.convert_circuitを使用します。

現在、QURI Partsは以下の回路への変換をサポートしています:

  • Braket circuit: API
  • Cirq circuit: API
  • Qulacs circuit: API
  • Qiskit circuit: API
  • TKet circuit: API

回路の可視化

本当に求めている回路が得られているか確かめるため、回路の可視化が役に立つ場合があります。ここではいくつか方法を紹介します。

QURI Parsを使用

QURI Partsにはアスキーアートで回路を表示する関数draw_circuitがあります。

from quri_parts.algo.ansatz import HardwareEfficient
from quri_parts.circuit.utils.circuit_drawer import draw_circuit

hwe_ansatz = HardwareEfficient(4, 2)
draw_circuit(hwe_ansatz)
# output
___ ___ ___ ___ ___ ___
|PRY| |PRZ| |PRY| |PRZ| |PRY| |PRZ|
--|0 |---|1 |-------------●-----|11 |---|12 |-------------●-----|22 |---|23 |-
|___| |___| | |___| |___| | |___| |___|
___ ___ _|_ ___ ___ _|_ ___ ___
|PRY| |PRZ| |CZ | |PRY| |PRZ| |CZ | |PRY| |PRZ|
--|2 |---|3 |-----●-----|9 |---|13 |---|14 |-----●-----|20 |---|24 |---|25 |-
|___| |___| | |___| |___| |___| | |___| |___| |___|
___ ___ _|_ ___ ___ _|_ ___ ___
|PRY| |PRZ| |CZ | |PRY| |PRZ| |CZ | |PRY| |PRZ|
--|4 |---|5 |---|8 |-----●-----|15 |---|16 |---|19 |-----●-----|26 |---|27 |-
|___| |___| |___| | |___| |___| |___| | |___| |___|
___ ___ _|_ ___ ___ _|_ ___ ___
|PRY| |PRZ| |CZ | |PRY| |PRZ| |CZ | |PRY| |PRZ|
--|6 |---|7 |-----------|10 |---|17 |---|18 |-----------|21 |---|28 |---|29 |-
|___| |___| |___| |___| |___| |___| |___| |___|

他のライブラリを使用

違う形式でイメージを得たいのであれば、回路をqiskitのQuantumCircuitなど他の形式に変換して、対応する描画メソッドを使うこともできます。

ほとんどのライブラリがパラメータ付き回路パラメータ付き回路の概念を持たないため、多くの場合、回路のパラメータを変換の前にbindしなければいけないことに注意してください。

qiskit

import random

from quri_parts.algo.ansatz import HardwareEfficient
from quri_parts.qiskit.circuit import convert_circuit

hwe_ansatz = HardwareEfficient(4, 2)

# bind random parameters
circuit = hwe_ansatz.bind_parameters(
[random.random() for _ in range(hwe_ansatz.parameter_count)]
)

qiskit_circuit = convert_circuit(circuit)
# qiskit_circuit.draw() # default: ASCII art
qiskit_circuit.draw("mpl")

qiskit

braket

import random

from quri_parts.algo.ansatz import HardwareEfficient
from quri_parts.braket.circuit import convert_circuit

hwe_ansatz = HardwareEfficient(4, 2)

# bind random parameters
circuit = hwe_ansatz.bind_parameters(
[random.random() for _ in range(hwe_ansatz.parameter_count)]
)

braket_circuit = convert_circuit(circuit)
print(braket_circuit)
# output
T : | 0 | 1 |2|3| 4 | 5 |6|7| 8 | 9 |

q0 : -Ry(0.12)-Rz(0.18)---C-Ry(0.21)-Rz(0.86)---C-Ry(0.65)-Rz(0.07)-
| |
q1 : -Ry(0.23)-Rz(0.75)-C-Z-Ry(0.25)-Rz(0.67)-C-Z-Ry(0.02)-Rz(0.73)-
| |
q2 : -Ry(0.26)-Rz(0.26)-Z-C-Ry(0.32)-Rz(0.02)-Z-C-Ry(0.24)-Rz(0.86)-
| |
q3 : -Ry(0.72)-Rz(0.20)---Z-Ry(0.09)-Rz(0.55)---Z-Ry(0.91)-Rz(0.06)-

T : | 0 | 1 |2|3| 4 | 5 |6|7| 8 | 9 |

cirq

import random

from quri_parts.algo.ansatz import HardwareEfficient
from quri_parts.cirq.circuit import convert_circuit

from cirq.contrib.svg import SVGCircuit

hwe_ansatz = HardwareEfficient(4, 2)

# bind random parameters
circuit = hwe_ansatz.bind_parameters(
[random.random() for _ in range(hwe_ansatz.parameter_count)]
)

cirq_circuit = convert_circuit(circuit)
# print(cirq_circuit) # ASCII art
SVGCircuit(cirq_circuit)

cirq

tket

import random

from pytket.circuit.display import render_circuit_jupyter
from quri_parts.algo.ansatz import HardwareEfficient
from quri_parts.tket.circuit import convert_circuit

hwe_ansatz = HardwareEfficient(4, 2)

# bind random parameters
circuit = hwe_ansatz.bind_parameters(
[random.random() for _ in range(hwe_ansatz.parameter_count)]
)

tket_circuit = convert_circuit(circuit)

render_circuit_jupyter(tket_circuit) # Render interactive circuit diagram

tket