この記事について
量子コンピュータの勉強をはじめて最近一通りAmazonBraketを触り終えました。
最近登場したサービスであることもあり、少し丁寧なドキュメントを残しておくと後続の方が勉強しやすくなったり、参照してわからないところを調べる際に便利かなと思ったので
自分自身の理解向上も兼ねて記事にまとめていっています。
また、量子コンピュータ関係の他の記事は、下記で紹介しています。
※本記事は2021年2月に更新している記事です。更新などにより内容が変わっている可能性もあるのでご注意ください。
概要
本記事では量子コンピュータの導入部分に関して解説をしていこうと思います。
参照するのはこちらの3_Deep_dive_into_the_anatomy_of_quantum_circuits.ipynb
というサンプルです。
こちらのサンプルではあまり前提とした知識を必要とせずAmazon Braketで使える仕様を説明しているサンプルコードとなりますので本記事でもそれに沿って仕様説明をしていこうと思います。
AmazonBraket
IMPORT STATEMENTS
こちらでは必要なモジュールをインポートするコードのみが記載されているため、本記事では省略させていただきます。
詳細を知りたい方は以下の二つの記事で紹介させていただいているのでそちらを見ていただけると良いかと思います。
CIRCUIT DEFINITION
こちらのセクションでは回路の定義、そして定義した回路の詳細を取得する方法を示しています。
回路詳細確認
サンプルとしてまずは4つの量子ビットを用いてそれぞれにアダマールゲートを適用し、CNOTを2か所に適用するような回路を作成しています。
# define circuit with 4 qubits
my_circuit = Circuit().h(range(4)).cnot(control=0, target=2).cnot(control=1, target=3)
print(my_circuit)
T : |0| 1 |
q0 : -H-C---
|
q1 : -H-|-C-
| |
q2 : -H-X-|-
|
q3 : -H---X-
T : |0| 1 |
上記の回路は二つの時間枠によって生成されます。
アダマールゲートを適用する時間とCNOTを適用する時間です。
アダマールゲートを適用する量子ビット同士はこの操作を排他的に行うことができるので同じ時間枠ですし、CNOTを適用する場合も同様に排他的であるので同時に行うことができます。
もちろんこの操作は一瞬の時間で切り取られますが、
時間0にアダマールゲートが適用され、時間1にCNOTが適用されるということがわかります。
また、それは実際にmomments
を取得することで以下のように詳細を見ることができます。
# show moments of our quantum circuit
my_moments = my_circuit.moments
for moment in my_moments:
print(moment)
MomentsKey(time=0, qubits=QubitSet([Qubit(0)]))
MomentsKey(time=0, qubits=QubitSet([Qubit(1)]))
MomentsKey(time=0, qubits=QubitSet([Qubit(2)]))
MomentsKey(time=0, qubits=QubitSet([Qubit(3)]))
MomentsKey(time=1, qubits=QubitSet([Qubit(0), Qubit(2)]))
MomentsKey(time=1, qubits=QubitSet([Qubit(1), Qubit(3)]))
time
に適用時間、qubits
に適用量子ビットが示されています。
もう少し詳しく見ようとするとinstructions
を見ることで見ることもできます。
# list all instructions/gates making up our circuit
my_instructions = my_circuit.instructions
for instruction in my_instructions:
print(instruction)
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(0)]))
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(1)]))
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(2)]))
Instruction('operator': H('qubit_count': 1), 'target': QubitSet([Qubit(3)]))
Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(0), Qubit(2)]))
Instruction('operator': CNot('qubit_count': 2), 'target': QubitSet([Qubit(1), Qubit(3)]))
先ほどの出力に加えて、アダマールゲートやCNOTゲートのような適用回路もわかるようになったかと思います。
パラメトリック回路
次にパラメトリック回路に数値を代入しながら定義する方法を示します。
おおむねやり方はfunction(qubit, parameter)
といった形になっています。
以下が例となります。
# define circuit with some parametrized gates
my_circuit = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).zz(1, 3, 0.15).x([1,3])
print(my_circuit)
q0 : -Rx(0.15)-C------------
|
q1 : -Ry(0.2)--|-ZZ(0.15)-X-
| |
q2 : -Rz(0.25)-X-|----------
|
q3 : -H----------ZZ(0.15)-X-
T : | 0 | 1 |2|
rxやry, rzでは前述した通り、function(qubit, parameter)
といった形になっているかと思います。
- cnotでは制御ビット、ターゲットビットを定義しないといけない。
- ZZでは適用量子ビットを二つ定義しないといけない。
といったことがあるため、cnotやZZ回路では変数の定義の仕方が異なります。
また、複数ビットに同じ回路を適用する際にx([1,3)]
といったように配列
を定義できることもわかるかと思います。
回路関数
回路を返却してくれる関数を定義できます。
やり方はいくつかあるのですが、今回は以下の二つをやってみます。
-
unitary()
関数を使う -
circuit.subroutine
を使う
unitary()
関数を使う
以下のように、変数を受け取り、np.arrayで2×2の行列を返すような関数を定義します。
今回定義しているものはU3回転行列で任意の回転を表す量子ゲートを示していますが、組み込みで定義されているものではなく、自ら定義しています。
# helper function to build custom gate
def u3(alpha, theta, phi):
"""
function to return matrix for general single qubit rotation
rotation is given by exp(-i sigma*n/2*alpha) where alpha is rotation angle
and n defines rotation axis as n=(sin(theta)cos(phi), sin(theta)sin(phi), cos(theta))
sigma is vector of Pauli matrices
"""
u11 = np.cos(alpha/2)-1j*np.sin(alpha/2)*np.cos(theta)
u12 = -1j*(np.exp(-1j*phi))*np.sin(theta)*np.sin(alpha/2)
u21 = -1j*(np.exp(1j*phi))*np.sin(theta)*np.sin(alpha/2)
u22 = np.cos(alpha/2)+1j*np.sin(alpha/2)*np.cos(theta)
return np.array([[u11, u12], [u21, u22]])
こちらの関数を使うには、回路定義のメソッドチェーンに unitary(matrix=u3(alpha, theta, phi), targets=[n])
を繋げてあげることにより使うことができます。
もちろん、引数部分には適切な変数を与えてあげてください。
具体的には以下の通りに定義すれば良いです。
# define and print custom unitary
my_u3 = u3(np.pi/2, 0, 0)
# print(my_u3)
# define example circuit applying custom U to the first qubit
circ = Circuit().unitary(matrix=my_u3, targets=[0]).h(1).cnot(control=0, target=1)
print(circ)
circuit.subroutine
を使う
次にcircuit.subroutine
を使う方法を紹介します。
こちらの機能は簡単に言うと定義した回路を他の組み込みゲートと同じように使う機能となります。
上記と同じU3を定義しようとすると以下のように定義されます。
# helper function to build custom gate
@circuit.subroutine(register=True)
def u3(target, angles):
"""
Function to return the matrix for a general single qubit rotation,
given by exp(-i sigma*n/2*alpha), where alpha is the rotation angle,
n defines the rotation axis via n=(sin(theta)cos(phi), sin(theta)sin(phi), cos(theta)),
and sigma is the vector of Pauli matrices
"""
# get angles
alpha = angles[0]
theta = angles[1]
phi = angles[2]
# set 2x2 matrix entries
u11 = np.cos(alpha/2)-1j*np.sin(alpha/2)*np.cos(theta)
u12 = -1j*(np.exp(-1j*phi))*np.sin(theta)*np.sin(alpha/2)
u21 = -1j*(np.exp(1j*phi))*np.sin(theta)*np.sin(alpha/2)
u22 = np.cos(alpha/2)+1j*np.sin(alpha/2)*np.cos(theta)
# define unitary as numpy matrix
u = np.array([[u11, u12], [u21, u22]])
# print('Unitary:', u)
# define custom Braket gate
circ = Circuit()
circ.unitary(matrix=u, targets=target)
return circ
使い方は以下となります。
# define example circuit applying custom single-qubit gate U to the first qubit
angles = [np.pi/2, np.pi/2, np.pi/2]
angles = [np.pi/4, 0, 0]
# build circuit using custom u3 gate
circ2 = Circuit().u3([0], angles).cnot(control=0, target=1)
print(circ2)
CIRCUIT DEPTH AND CIRCUIT SIZE
まずdepth
を用いて回路の深さを取得してみます。
# define circuit with parametrized gates
my_circuit = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).zz(1, 3, 0.15).x(0)
circuit_depth = my_circuit.depth
print(my_circuit)
print()
print('Total circuit depth:', circuit_depth)
T : | 0 | 1 |2|
q0 : -Rx(0.15)-C----------X-
|
q1 : -Ry(0.2)--|-ZZ(0.15)---
| |
q2 : -Rz(0.25)-X-|----------
|
q3 : -H----------ZZ(0.15)---
T : | 0 | 1 |2|
Total circuit depth: 3
回路の深さが3であることがわかったかと思います。
注意していただきたいのは以下のサンプルのようにx(4)
を最後の方に適用したとしても第4量子ビットはXしか適用しないため、最初の方に適用されます。
このように回路の実行順序はなるべく左詰めで実行されることに注意してください。
# define circuit with parameterized gates
my_circuit = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).zz(1, 3, 0.15).x(4)
# get circuit depth
circuit_depth = my_circuit.depth
# get qubit number
qubit_count = my_circuit.qubit_count
# get approx. estimate of circuit size
circuit_size = circuit_depth*qubit_count
# print circuit
print(my_circuit)
print()
# print characteristics of our circuit
print('Total circuit depth:', circuit_depth)
print('Number of qubits:', qubit_count)
print('Circuit size:', circuit_size)
T : | 0 | 1 |
q0 : -Rx(0.15)-C----------
|
q1 : -Ry(0.2)--|-ZZ(0.15)-
| |
q2 : -Rz(0.25)-X-|--------
|
q3 : -H----------ZZ(0.15)-
q4 : -X-------------------
T : | 0 | 1 |
Total circuit depth: 2
Number of qubits: 5
Circuit size: 10
また、上記では回路サイズも取得しています。
回路サイズは回路の複雑さを考える指標となります。
もちろん大きいほど必ず難しいというわけでも、小さいからといって簡単というわけでもありません。
回路サイズは、量(qubit数)と質(回路の深さ)の両方を考慮します。
ここでは、qubit数と回路の深さ(図の面積)を掛け合わせた非常に単純な定義を使用してているといった具合になります。
APPENDING CIRCUITS
ここからは回路の追加方法を3つのやり方で示します。
上記までで定義した my_circuit
の回路を用います。
つまり以下の図で示すものです。
T : | 0 | 1 |
q0 : -Rx(0.15)-C----------
|
q1 : -Ry(0.2)--|-ZZ(0.15)-
| |
q2 : -Rz(0.25)-X-|--------
|
q3 : -H----------ZZ(0.15)-
q4 : -X-------------------
T : | 0 | 1 |
シンプルな手法
まず最も簡単な手法で回路を追加してみます。
第4qubitにYゲートを追加します。
my_circuit = my_circuit.y(4)
とするだけでYゲートを第4qubitに追加できます。
以下のような結果となるはずです。
T : | 0 | 1 |
q0 : -Rx(0.15)-C----------
|
q1 : -Ry(0.2)--|-ZZ(0.15)-
| |
q2 : -Rz(0.25)-X-|--------
|
q3 : -H----------ZZ(0.15)-
q4 : -X--------Y----------
T : | 0 | 1 |
add_instruction()メソッドで追加
次に add_instruction()メソッドを活用して追加する方法を示します。
以下のようにCNOTゲートをInstructionとして定義し、add_instruction()メソッドを使用することで既存の回路オブジェクトに追加できます。
gate_instr = Instruction(Gate.CNot(), [0, 1])
my_circuit = my_circuit.add_instruction(gate_instr)
以下のような結果となります。
T : | 0 | 1 |2|
q0 : -Rx(0.15)-C----------C-
| |
q1 : -Ry(0.2)--|-ZZ(0.15)-X-
| |
q2 : -Rz(0.25)-X-|----------
|
q3 : -H----------ZZ(0.15)---
q4 : -X--------Y------------
T : | 0 | 1 |2|
add_circuit()メソッドで追加
最後はadd_circuit()メソッドでの追加です。
個人的には長い回路も簡単に定義して追加しやすいのでこれが一番使われるような気がします。
Circuitオブジェクトを定義し、add_circuit()で追加するイメージです。
以下のように書きます。
my_circuit2 = Circuit().rz(0, 0.1).rz(1, 0.2).rz(3, 0.3).rz(4, 0.4)
my_circuit.add_circuit(my_circuit2)
T : | 0 | 1 | 2 | 3 |
q0 : -Rx(0.15)-C----------C-------Rz(0.1)-
| |
q1 : -Ry(0.2)--|-ZZ(0.15)-X-------Rz(0.2)-
| |
q2 : -Rz(0.25)-X-|------------------------
|
q3 : -H----------ZZ(0.15)-Rz(0.3)---------
q4 : -X--------Y----------Rz(0.4)---------
T : | 0 | 1 | 2 | 3 |
是非使いこなしてみてください。
CIRCUIT EXECUTION AND TASK TRACKING
こちらは実機で使える
- 回路の出力方法
- S3の登録方法
- タスクの取得
- タスクの復元方法
等が記載されているおり、こちらの内容はAmazonBraketで学ぶ量子コンピュータ③で紹介させていただいているので省略させていただきます。
一点だけ、タスクのキャンセル方法だけこちらで初出なので補足させていただきます。
Amazon Braketでは実機で計算する際は様々な方が使っている中で実機を使うのでキュー待ちが発生します。
あまり待ちたくなかったりした場合にやっぱりタスクを打ち切りたくなることもあるのでこういった機能があります。(たぶん)
やり方としてはタスクのIDを取得するか、実行時のタスクを保存しておき、task.cancel()
といったようにcancel()関数を呼び出すだけでできます。
例としては以下のようになります。
# define task
task = device.run(my_circuit, s3_folder, shots=1000)
# get id and status of submitted task
task_id = task.id
status = task.state()
# print('ID of task:', task_id)
print('Status of task:', status)
# cancel task
task.cancel()
status = task.state()
print('Status of task:', status)
キャンセル中は以下の通り、CANCELLING
ステータスになります。
Status of task: QUEUED
Status of task: CANCELLING
DEMONSTRATION OF RESULT-TYPES: Expectation Values and Observables
シミュレータにおいて前述したまでの例では、shots>0と設定し、実際の量子ハードウェアの動作を模倣したような動作を行いました。
しかし、古典的なシミュレータでは、sshot=0の場合には完全な状態ベクトルにアクセスすることができます。
特に量子ハードウェアでは観測した時点で状態が壊れてしまうため、こういった完全な状態ベクトルにアクセスできるのは古典的なシミュレータの大きな強みであるといえます。
このセクションでこの機能について詳しく説明します。
ResultTypesの機能を使って、完全な状態ベクトル、振幅、観測確率、単一および複数量子ビットの観測値にアクセスし、最終的な状態ベクトルの値、期待値、状態の振幅を出力することができます。
RESULT TYPES FOR shots=0
完全な状態ベクトルと振幅は古典的なシミュレータでshots=0の時にのみ取得できることに注意してください。
シミュレータでshots=0としたとき、確率、期待値は正確な値となります。
以下は
① 第2量子ビットをX基底で観測した際の期待値
② 第0量子ビット、第1量子ビットをZ基底で観測した際の期待値
③ $|00000 \rangle$の確率振幅
④ 第3量子ビットの0, 1の観測確率
を取得するコードとなっています。
# add result types
circ = my_circuit
# add the state_vector ResultType available for shots=0
circ.state_vector()
# add single qubit expectation value ① 第2量子ビットをX基底で観測した際の期待値
obs1 = Observable.X()
circ.expectation(obs1, target=[2])
# add the two-qubit Z0*Z1 expectation value ② 第0量子ビット、第1量子ビットをZ基底で観測した際の期待値
obs2 = Observable.Z() @ Observable.Z()
circ.expectation(obs2, target=[0,1])
# add the amplitude for |0...0> ③ |00000>の確率振幅
bitstring = '0'*qubit_count
circ.amplitude(state=[bitstring])
# add marginal probability ④ 第3量子ビットの0, 1の観測確率
circ.probability(target=[3])
print(circ)
回路図を示すと以下のようになります。
Result Type部分でどういった観測のやり方で観測をしようとしているかを見ることができ、少し便利かと思います。
T : | 0 | 1 | 2 | 3 | Result Types |
q0 : -Rx(0.15)-C----------C-------Rz(0.1)-Expectation(Z@Z)-
| | |
q1 : -Ry(0.2)--|-ZZ(0.15)-X-------Rz(0.2)-Expectation(Z@Z)-
| |
q2 : -Rz(0.25)-X-|------------------------Expectation(X)---
|
q3 : -H----------ZZ(0.15)-Rz(0.3)---------Probability------
q4 : -X--------Y----------Rz(0.4)--------------------------
T : | 0 | 1 | 2 | 3 | Result Types |
出力してみると以下のようになります。
量子コンピュータを模倣しているとはいえ、shots=0の設定で動かしたシミュレータですので何度実行してもこの結果から変わることのない不変な結果となることに注意してください。
# set up device
device = LocalSimulator()
# run the circuit and output the results specified above
task = device.run(circ, shots=0)
result = task.result()
print("Final state vector:\n", result.values[0])
print("Expectation value <X2>", result.values[1])
print("Expectation value <Z0Z1>:", result.values[2])
print("Amplitude <00000|Final state>:", result.values[3])
print("Marginal probability for target qubit 3 in computational basis:", result.values[4])
Final state vector:
[-0.45198076-0.53661046j 0. +0.j -0.17357771-0.67978539j
0. +0.j 0. +0.j 0. +0.j
0. +0.j 0. +0.j -0.0241381 -0.06612661j
0. +0.j -0.01398522-0.06899123j 0. +0.j
0. +0.j 0. +0.j 0. +0.j
0. +0.j 0. +0.j 0. +0.j
0. +0.j 0. +0.j -0.00476292+0.00230075j
0. +0.j -0.00505326+0.00156316j 0. +0.j
0. +0.j 0. +0.j 0. +0.j
0. +0.j -0.04855705+0.02052959j 0. +0.j
-0.05265272-0.00263483j 0. +0.j ]
Expectation value <X2> 0.0
Expectation value <Z0Z1>: 0.9800665778412411
Amplitude <00000|Final state>: {'00000': (-0.45198075706658136-0.5366104621057316j)}
Marginal probability for target qubit 3 in computational basis: [0.5 0.5]
RESULT TYPES FOR shots > 0
shots>0 のとき、完全な状態ベクトルにはアクセスできませんが、測定サンプルから取得した近似的な期待値を得ることはできます。
確率、サンプル、期待値、分散は量子コンピュータの実機でもサポートされています。
# define example circuit
circ2 = Circuit().rx(0, 0.15).ry(1, 0.2).rz(2, 0.25).h(3).cnot(control=0, target=2).zz(1, 3, 0.15).x(4)
# add expectation value
obs = Observable.X() @ Observable.Y()
target_qubits = [0, 1]
circ2.expectation(obs, target=target_qubits)
# add variance
circ2.variance(obs, target=target_qubits)
# add samples
circ2.sample(obs, target=target_qubits)
print(circ2)
T : | 0 | 1 | Result Types |
q0 : -Rx(0.15)-C----------Expectation(X@Y)-Variance(X@Y)-Sample(X@Y)-
| | | |
q1 : -Ry(0.2)--|-ZZ(0.15)-Expectation(X@Y)-Variance(X@Y)-Sample(X@Y)-
| |
q2 : -Rz(0.25)-X-|---------------------------------------------------
|
q3 : -H----------ZZ(0.15)--------------------------------------------
q4 : -X--------------------------------------------------------------
T : | 0 | 1 | Result Types |
# run the circuit and output the results specified above
task = device.run(circ2, shots=100)
result = task.result()
print("Expectation value for <X0*Y1>:", result.values[0])
print("Variance for <X0*Y1>:", result.values[1])
print("Measurement samples for X0*Y1:", result.values[2])
Expectation value for <X0*Y1>: -0.02
Variance for <X0*Y1>: 0.9995999999999998
Measurement samples for X0*Y1: [ 1 1 -1 -1 -1 1 1 1 -1 1 -1 -1 1 -1 1 -1 1 1 -1 1 1 1 1 -1
-1 -1 1 -1 -1 1 -1 -1 -1 -1 1 1 -1 1 -1 1 -1 1 1 1 -1 1 1 -1
-1 -1 1 1 -1 -1 1 1 -1 -1 -1 -1 -1 1 1 1 1 -1 1 -1 1 -1 1 1
1 -1 1 -1 -1 1 1 -1 -1 1 -1 1 -1 -1 -1 -1 1 1 1 -1 1 1 -1 -1
-1 -1 -1 1]
上記結果は shots>0
であるときの観測結果に基づく近似的な値です。
なので実行毎に結果が変わります。
実際に何度も実行するとわかるかと思います。
ADVANCED LOGGING
量子コンピュータ的な特別説明が必要そうな部分はありませんので省略します。
ログの出力の仕方などが書いてありますので是非見てみてください。
最後に
以上で「AmazonBraketで学ぶ量子コンピュータ④」を終わります。
今回はAmazon Braketの使い方を少し詳しめに行いました。
また別記事で他のBraketサンプルの解説を行ないながら量子コンピュータを学んでいける記事を書いていくので是非見てみてください。