量子コンピュータ
量子ゲート
NISQ

Googleの量子コンピュータNISQ向けフレームワーク「Cirq」チュートリアル(中級向け)

はじめに

Googleからオープンソースの量子コンピュータNISQ向けのフレームワークが発表されました。概要は見ていきましたが、ドキュメントの分量があまりないので、全部チュートリアルを和訳して見て見たいと思います。

概要はこちら、
「Googleが量子コンピュータNISQ向けオープンソースフレームワーク「Cirq」を発表」
https://qiita.com/YuichiroMinato/items/20ccfe9da8ad8a176093

チュートリアル

英語のページはこちら、
https://github.com/quantumlib/Cirq/blob/master/docs/tutorial.md

とりあえずのgoogle翻訳

このチュートリアルでは、Cirqについて何も知らないところから、量子変分アルゴリズムを作成するところまでを行います。このチュートリアルは、量子コンピューティングのチュートリアルではないことに注意してください。私たちは、NielsenとChuangのテキスト "Quantum Computation and Quantum Information"のレベルでの量子コンピューティングの精通を前提としています。概念的な概要については、概念的な文書を参照してください。 まず、Cirqのインストール手順に従ってください。

ということで、インストール手順は前回の記事で紹介しました。
https://qiita.com/YuichiroMinato/items/20ccfe9da8ad8a176093

また、基本的な量子コンピュータの知識は必要とするということで文章中で補足しながら進めたいと思います。

変分量子アルゴリズム

Variational quantum algorithmsというのが基本になります。これは現在のNISQの特徴である変分法と量子古典計算ハイブリッドを利用して基底状態を求めるという方向性に合致してるように思います。

早速google翻訳して見ます。

量子論における変分法は、量子系の低エネルギー状態を見出す古典的な方法である。この方法の大まかなアイデアは、いくつかのパラメータの関数としてトライアル波関数(ansatzとも呼ばれる)を定義し、次にこれらのパラメータに関するエネルギーの期待値を最小にするこれらのパラメータの値を見つけることです。この最小化されたanstazは、最も低いエネルギーの固有状態への近似であり、期待値は、基底状態のエネルギーの上限として働く。 ここ数年(例えばarXiv:1304.3061とarXiv:1507.08969を参照)、量子コンピュータは古典的な技術を模倣することができ、量子コンピュータは特定の利点を持っていると認識しています。特に、n個の量子ビットのシステムに古典的な変分法を適用する場合、システムの波動関数を総称的に表すために指数関数的な数(n)の複素数が必要である。しかし、量子コンピュータでは、パラメータ化された量子回路を用いてこの状態を直接生成することができ、その後、繰り返し測定することによって、エネルギーの期待値を推定することができる。 このアイデアは、変分量子アルゴリズムと呼ばれるアルゴリズムのクラスにつながっています。確かに、このアプローチは、低エネルギー固有状態を見つけることに限られるのではなく、量子観測として表現できる目的関数を最小限に抑えることです。これらの量子変分アルゴリズムがどのような条件で成功するかを明らかにすることは未解決の問題であり、このクラスのアルゴリズムを探索することは、ノイズの多い中間スケールの量子コンピュータの研究の重要な部分です。 私たちが焦点を当てる古典的な問題は、横電界(ISING)を伴う2D +/-イジングモデルです。この問題はNP完全であるため、量子コンピュータはすべてのインスタンスにわたって効率的にそれを解決することはほとんどありません。しかし、この種の問題は、Cirqが取り組むために設計された一般的な種類の問題の例です。

ansatzと呼ばれる波動関数を用意して、それをベースに繰り返し計算と変分法で解に近づくという方法です。Qiitaでも度々触れてきた横磁場のイジングモデルがベースになっていたりします。量子アニーリング学んどいてよかった。。。

横磁場イジング

横磁場イジングモデルと量子アニーリングの基本はこちらで紹介しています。

「量子アニーリング、イジングモデルとフレームワーク」
https://qiita.com/YuichiroMinato/items/e6952fec1a9965156873

2.png

引用:https://github.com/quantumlib/Cirq/blob/master/docs/tutorial.md

通常のイジングモデルの問題をパラメータ付きの波動関数に落とし込みます。

繰り返し計算

再度google翻訳、

piはこの状態を生成するパラメータです(ここでは純粋な状態を仮定していますが、混合状態ももちろん可能です)。 次に、変分アルゴリズムは、与えられたansatz状態の目的関数の値を

1、ansatz状態を準備します。
2、ハミルトニアンのいくつかのサンプルを測定する
3、1に戻る。

量子位相推定を使用せずに常にHを直接測定することはできないので、ステップ2でHの部分を測定するための期待値の線形性に依存することが多いことに注意してください。与えられた精度を達成するために必要な測定値はこのチュートリアルの範囲を超えていますが、Cirqはこの問題を調査するのに役立ちます。 上記は、量子コンピュータを使用して、ansatzの目的関数の推定値を得ることができることを示しています。これを外部ループで使用して、目的関数の最小値のパラメータを取得しようとします。これらの値に対して、最良のansatzを使用して、問題の解のサンプルを生成し、目的関数の可能な限り低い値に対する良好な近似を得ることができます。

ちょっとわかりづらい日本語になってしまいましたが、パラメータ付きの波動関数を用意してベクトルの固有値を計算し、それを元に新しいパラメータを割りふった波動関数を用意して繰り返します。

回路をグリッドで作成

早速google翻訳です。

Cirqを使用して上記の変分量子アルゴリズムを構築するには、適切な回路を構築することから始めます。 Cirq回路ではCircuitオブジェクトかScheduleオブジェクトのどちらかで表されます。スケジュールは量子ゲートと回路を、タイミング・レベルでより多くの制御を提供しますが、今回は必要ではありませんので、Circuitオブジェクトで作業します。 概念的には、サーキットはモーメントの集まりです。モーメントは、同じ抽象的なタイムスライスの間にすべて動作する一連の操作です。オペレーションは、Qubitsの特定のサブセットで動作するエフェクトです。最も一般的なタイプの操作は、いくつかのキュビット(ゲート操作)に適用されるゲートです。以下の図は、これらの概念を説明するのに役立ちます。

CircuitMomentOperation.png

つまりCirqにはcircuitオブジェクトかScheduleオブジェクトがあります。CircuitにはさらにMomentと呼ばれる概念があり、Operationの集まりのようです。Operationは単一のゲート論理回路の集まりのようです。

よりフレームワークのコンセプトに触れたい場合には下記のリンクがあるようです。このQiitaでも確認したいと思います。

「Circuits」
https://github.com/quantumlib/Cirq/blob/master/docs/circuits.md

Grid

こちらもGoogle翻訳、

これらのクラスの詳細については、概念的なドキュメントを参照してください。私たちが定義した問題は、グリッド上に自然な構造を持っているため、私たちはキュービットとしてGridQubitsで構築されたCirqを使用します。私たちはインタラクティブなPython環境でこれがどのように動作するかを説明します。次のコードは、CirqがインストールされているPython環境でシリーズで実行できます。 私たちのキュビットについて話すことから始めましょう。

どうやらGridという概念があり、サンプルコードを見ていきます。

>>> length = 3
>>> qubits = [cirq.GridQubit(i,j) for i in range(length) for j in range(length)]
>>> print(qubits)

実行してみると量子ビットがグリッド表現ででました。

[GridQubit(0, 0), GridQubit(0, 1), GridQubit(0, 2), GridQubit(1, 0), GridQubit(1, 1), GridQubit(1, 2), GridQubit(2, 0), GridQubit(2, 1), GridQubit(2, 2)]

どうやらこれは、3*3のグリッドに並んだ量子ビットを表現してるようです。

グリッド状の量子ビットを操作

懲りずにgoogle翻訳

ここで、我々はGridQubitsの束を作成したことがわかります。 GridQubitsはQubitIdクラスを実装しています。これは、それらが同等でハッシュ可能であることを意味します。 GridQubitsには、グリッド上の位置を示す行と列があります。 今、いくつかの量子ビットがあるので、これらの量子ビットについてサーキットを構築しましょう。例えば、行インデックス+列インデックスが偶数であるすべての量子ビットにアダマールゲートHを適用し、行インデックスと列インデックスが奇数であるすべての量子ビットに対してXゲートを適用すると仮定する。これを行うには、

とりあえず行と列がある量子ビットに対して操作をして見ます。

circuit = cirq.Circuit()
circuit.append(cirq.H.on(q) for q in qubits if (q.row + q.col) % 2 == 0)
circuit.append(cirq.X(q) for q in qubits if (q.row + q.col) % 2 == 1)
print(circuit)

偶数の行列にアダマール変換、奇数にXゲートを作用させています。
結果はこうなりました。

(0, 0): ───H───────

(0, 1): ───────X───

(0, 2): ───H───────

(1, 0): ───────X───

(1, 1): ───H───────

(1, 2): ───────X───

(2, 0): ───H───────

(2, 1): ───────X───

(2, 2): ───H───────

続いてまたgoogle翻訳

ここで気付くべきことが1つあります。最初のcirq.XはGateオブジェクトです。 Cirqは多くの異なるゲートをサポートしています。定義されているゲートを見るのに適した場所はcommon_gates.pyです。回避すべき1つの一般的な混乱は、ゲートクラスとゲートオブジェクトの違いです(これはクラスのインスタンス化です)。 2番目の方法は、(qubit)のメソッドか、Xゲートの場合のようにゲートをキュビット(qubit)に単純に適用することによって、ゲートオブジェクトがオペレーション(技術的にGateOperations)に変換されるということです。ここでは、単一量子ビットゲートのみを適用しますが、同様のパターンが複数のキュビットに適用されますが、今度は一連のキュビットをパラメータとして提供する必要があります。

ゲートクラスとゲートオブジェクトの違いに注意せよということです。

上記の回路についてもう一つ注目すべき点は、回路が互い違いのゲートを有することである。これは、ゲートを適用した方法によって2つのMomentが作成されたためです。

今度は、

for i, m in enumerate(circuit):
    print('Moment {}: {}'.format(i, m))

結果は、

Moment 0: H((0, 0)) and H((0, 2)) and H((1, 1)) and H((2, 0)) and H((2, 2))
Moment 1: X((0, 1)) and X((1, 0)) and X((1, 2)) and X((2, 1))

ここでは、モーメントと呼ばれるものがふたつできています。これには理由があるみたいで、モーメントを1つにまとめることもできるようです。

下記google翻訳

ここで、CircuitのMomentを繰り返すことができることがわかります。 2つのMomentが作成された理由は、appendメソッドがInsertStrategy NEW_THEN_INLINEを使用するためです。 InsertStrategysは、Circuitsへの新しい挿入がゲートをどのように配置するかを記述します。これらの戦略の詳細は、回路のマニュアルに記載されています。ゲートを挿入して1つのMomentを形成したい場合は、代わりにEARLIEST挿入戦略を使用できます。

ということで、

InsertStrategy of NEW_THEN_INLINE

を使用しているためにモーメントがふたつになっているようですので、明示的に処理を行うことで、これを改善できます。

circuit = cirq.Circuit()
circuit.append([cirq.H.on(q) for q in qubits if (q.row + q.col) % 2 == 0],
               strategy=cirq.InsertStrategy.EARLIEST)
circuit.append([cirq.X(q) for q in qubits if (q.row + q.col) % 2 == 1],
               strategy=cirq.InsertStrategy.EARLIEST)
print(circuit)

これを実行すると下記のようになり、モーメントを単一のものに揃えることができました。

(0, 0): ───H───

(0, 1): ───X───

(0, 2): ───H───

(1, 0): ───X───

(1, 1): ───H───

(1, 2): ───X───

(2, 0): ───H───

(2, 1): ───X───

(2, 2): ───H───

先ほどのモーメントを書き出す式再度実行してみると、

>>> for i, m in enumerate(circuit):
...     print('Moment {}: {}'.format(i, m))
... 
Moment 0: H((0, 0)) and H((0, 2)) and H((1, 1)) and H((2, 0)) and H((2, 2)) and X((0, 1)) and X((1, 0)) and X((1, 2)) and X((2, 1))

のようになり、モーメントが単一にまとめられているのが確認できます。

Ansatz波動関数を作成する

早速google翻訳してみます。

上記の回路作成コードを詳しく見てみると、生成メソッドとリストの両方にappendメソッドを適用したことがわかります(Pythonではメソッド呼び出しでジェネレータを使うことができます)。 appendメソッドが一般的にOP_TREE(またはモーメント)を受け取ることを、コードを検査して調べます。 OP_TREEとは何ですか?それはクラスではなくコントラクトです。おおまかに言うと、OP_TREEは、おそらく再帰的に操作のリストに、または単一の操作にフラット化できるものです。 OP_TREEの例は次のとおりです。

A single Operation.
A list of Operations.
A tuple of Operations.
A list of a list of Operationss.
A generator yielding Operations.

この最後のケースでは、サブ回路/層を定義するための素敵なパターンが得られ、関連するパラメータを取り込んでサブ回路の演算を生成し、回路に追加することができます。

サブの回路を作るということもできるようです。

def rot_x_layer(length, half_turns):
    """Yields X rotations by half_turns on a square grid of given length."""
    rot = cirq.RotXGate(half_turns=half_turns)
    for i in range(length):
        for j in range(length):
            yield rot(cirq.GridQubit(i, j))

circuit = cirq.Circuit()
circuit.append(rot_x_layer(2, 0.1))
print(circuit)

結果は、

(0, 0): ───X^0.1───

(0, 1): ───X^0.1───

(1, 0): ───X^0.1───

(1, 1): ───X^0.1───

続いてgoogle翻訳

もう一つの重要な概念は、回転ゲートが「半回転」で指定されていることです。 Xについての回転の場合、これはゲートcos(half_turns * pi)I + i sin(half_turns * pi)Xです。 変化のあるansatzを定義する多くの自由があります。ここでは、QAOA戦略のバリエーションを行い、解決しようとしている問題に関連するansatzを定義します。 まず、問題のインスタンスがどのように表現されるかを選択する必要があります。これらはハミルトニアンの定義におけるJとhの値です。これらを2次元配列(リストのリスト)として表現します。 Jでは、行リンク用と列リンク用の2つのリストを使用します。 ランダムな問題インスタンスを生成するために使用できるコードは次のとおりです、

import random
def rand2d(rows, cols):
    return [[random.choice([+1, -1]) for _ in range(rows)] for _ in range(cols)]

def random_instance(length):
    # transverse field terms
    h = rand2d(length, length)
    # links within a row
    jr = rand2d(length, length - 1)
    # links within a column
    jc = rand2d(length - 1, length)
    return (h, jr, jc)

まずはランダムに二次元格子上に+1,-1のイジングスピンを配置し、あとは横磁場hとjr/jcという相互作用項を行と列でそれぞれ作ります。この辺りはイジングモデル参照、

「量子アニーリング、イジングモデルとフレームワーク」
https://qiita.com/YuichiroMinato/items/e6952fec1a9965156873

そして、関数が定義できたら、

h, jr, jc = random_instance(3)
print('transverse fields: {}'.format(h))
print('row j fields: {}'.format(jr))
print('column j fields: {}'.format(jc))

ランダム値を割り当てて、実際に横磁場hとそれぞれのJijのjrとjcをかきだしてみます。

>>> print('transverse fields: {}'.format(h))
transverse fields: [[1, -1, 1], [-1, -1, -1], [-1, 1, -1]]
>>> print('row j fields: {}'.format(jr))
row j fields: [[-1, -1, 1], [-1, 1, 1]]
>>> print('column j fields: {}'.format(jc))
column j fields: [[1, -1], [-1, 1], [1, -1]]

自分の実行ではこんな感じでした。ランダムなので毎回異なります。そしてgoogle翻訳、

random.choiceを使用しているため、実際の値は個々の実行で異なることに注意してください。 この問題インスタンスの定義を前提として、ansatzを導入することができます。私たちのansatzは、

1、すべてのキュビットに対して同じパラメータのRotXGateを適用します。これが上記の方法です。
2、横軸の項hが+1であるすべてのキュビットについて、同じパラメータに対してRotZGateを適用します。

def rot_z_layer(h, half_turns):
    """Yields Z rotations by half_turns conditioned on the field h."""
    gate = cirq.RotZGate(half_turns=half_turns)
    for i, h_row in enumerate(h):
        for j, h_ij in enumerate(h_row):
            if h_ij == 1:
                yield gate(cirq.GridQubit(i, j))

3、カップリングフィールド項Jが+1であるすべてのキュビット間で、同じパラメータのRot11Gateを適用します。フィールドが-1の場合、すべてのキュビットでXゲートによって結合されたRot11Gateを適用します。

def rot_11_layer(jr, jc, half_turns):
    """Yields rotations about |11> conditioned on the jr and jc fields."""
    gate = cirq.Rot11Gate(half_turns=half_turns)    
    for i, jr_row in enumerate(jr):
        for j, jr_ij in enumerate(jr_row):
            if jr_ij == -1:
                yield cirq.X(cirq.GridQubit(i, j))
                yield cirq.X(cirq.GridQubit(i + 1, j))
            yield gate(cirq.GridQubit(i, j),
                       cirq.GridQubit(i + 1, j))
            if jr_ij == -1:
                yield cirq.X(cirq.GridQubit(i, j))
                yield cirq.X(cirq.GridQubit(i + 1, j))

    for i, jc_row in enumerate(jc):
        for j, jc_ij in enumerate(jc_row):
            if jc_ij == 1:
                yield gate(cirq.GridQubit(i, j),
                           cirq.GridQubit(i, j + 1))

これをまとめると、3つのパラメータだけを使用するステップを作成できます。これを行うコードは、各レイヤーのジェネレータを使用します。

def one_step(h, jr, jc, x_half_turns, h_half_turns, j_half_turns):
    length = len(h)
    yield rot_x_layer(length, x_half_turns)
    yield rot_z_layer(h, h_half_turns)
    yield rot_11_layer(jr, jc, j_half_turns)

h, jr, jc = random_instance(3)

circuit = cirq.Circuit()    
circuit.append(one_step(h, jr, jc, 0.1, 0.2, 0.3))
print(circuit)

実行結果は、

(0, 0): ───X^0.1───Z^0.2───X───@───────X───────────────────────────────────────────@───────────────
                               │                                                   │
(0, 1): ───X^0.1───Z^0.2───────┼───────X───@───────X───────────────────────────────@^0.3───────────
                               │           │
(0, 2): ───X^0.1───Z^0.2───────┼───────────┼───────X───@───────X───────────────────────────────────
                               │           │           │
(1, 0): ───X^0.1───Z^0.2───X───@^0.3───X───┼───────────┼───────────@───────────────@───────────────
                                           │           │           │               │
(1, 1): ───X^0.1───────────────────────X───@^0.3───X───┼───────────┼───────@───────@^0.3───────────
                                                       │           │       │
(1, 2): ───X^0.1───Z^0.2───────────────────────────X───@^0.3───X───┼───────┼───────@───────────────
                                                                   │       │       │
(2, 0): ───X^0.1───Z^0.2───────────────────────────────────────────@^0.3───┼───────┼───────@───────
                                                                           │       │       │
(2, 1): ───X^0.1───────────────────────────────────────────────────────────@^0.3───┼───────@^0.3───
                                                                                   │
(2, 2): ───X^0.1───────────────────────────────────────────────────────────────────@^0.3───────────

こんな感じで。ここで、パラメータ(0.1,0.2,0.3)を使用した回路が作成できました。

シミュレーション

google翻訳。

ここで、ansatzの作成に対応する回路のシミュレーション方法を見てみましょう。 Cirqでは、シミュレータは「実行」と「シミュレーション」を区別します。 「実行」は、実際の量子ハードウェアを模倣するシミュレーションのみを可能にします。例えば、システムの波動関数の振幅にアクセスすることはできません。これは、実験的にアクセス可能ではないためです。しかし、「シミュレート」コマンドはより広範であり、さまざまな形態のシミュレーションを可能にします。小さな回路を試作するときは、「シミュレート」メソッドを実行すると便利ですが、実際のハードウェアに対して実行する場合は、その方法に頼らないように注意してください。

こちらはお決まりです。シミュレーションは波動関数と呼ばれるものが得られますが、実機は結果しか得られません。注意が必要です。

続いてgoogle翻訳

現在、CirqはシミュレータをGoogle xmonアーキテクチャのゲートセットに強く結びつけて出荷しています。しかし、便宜上、シミュレータは未知の演算をXmonGateに自動的に変換しようとします(演算が行列またはXmonGatesへの分解を指定している限り)。これは原則として、1つと2つのキュビットKnownMatrixゲートを実装するゲートを持つ回路をシミュレートすることができます。 Cirqの今後のリリースでは、これらのシミュレータが拡張されます。

googleはXモンと呼ばれるX型の量子ビットを開発しているため、Xモン型に変換されるということを言っています。
シミュレーションを早速実行して見ます。

simulator = cirq.google.XmonSimulator()
circuit = cirq.Circuit()    
circuit.append(one_step(h, jr, jc, 0.1, 0.2, 0.3))
circuit.append(cirq.measure(*qubits, key='x'))
results = simulator.run(circuit, repetitions=100, qubit_order=qubits)
print(results.histogram(key='x'))

結果は、

Counter({0: 82, 256: 4, 64: 3, 32: 2, 1: 2, 192: 1, 128: 1, 292: 1, 68: 1, 16: 1, 48: 1, 8: 1})

このようになりました。今回は100回の試行を行い、結果はヒストグラムで戻りました。

続いて2段落google翻訳

シミュレーションを100回実行し、測定結果の数のヒストグラムを作成したことに注意してください。ヒストグラムカウンタのキーは何ですか?我々がキュビットの順に合格したことに注意してください。この順序付けを使用して、ビッグエンディアン表現を使用して測定結果の順序をレジスタに変換します。

私たちの最適化問題については、与えられた結果実行の目的関数の値を計算したいと思うでしょう。これを行う1つの方法は、simulator.runの結果から生の測定データを使用することです。これを行うもう1つの方法は、ヒストグラムに目的を計算する方法を提供することです。これは返されたカウンタのキーとして使用されます。

import numpy as np

def energy_func(length, h, jr, jc):
    def energy(measurements):
        meas_list_of_lists = [measurements[i:i + length] for i in range(length)]
        pm_meas = 1 - 2 * np.array(meas_list_of_lists).astype(np.int32)
        tot_energy = np.sum(pm_meas * h)
        for i, jr_row in enumerate(jr):
            for j, jr_ij in enumerate(jr_row):
                tot_energy += jr_ij * pm_meas[i, j] * pm_meas[i + 1, j]
        for i, jc_row in enumerate(jc):
            for j, jc_ij in enumerate(jc_row):
                tot_energy += jc_ij * pm_meas[i, j] * pm_meas[i, j + 1]
        return tot_energy
    return energy
print(results.histogram(key='x', fold_func=energy_func(3, h, jr, jc)))

結果は、

Counter({3: 87, 1: 9, 5: 2, -3: 1, -1: 1})

そして、全ての繰り返しに対して期待値も計算できます。

def obj_func(result):
    energy_hist = result.histogram(key='x', fold_func=energy_func(3, h, jr, jc))
    return np.sum(k * v for k,v in energy_hist.items()) / result.repetitions
print('Value of the objective function {}'.format(obj_func(results)))

結果は、

Value of the objective function 2.76

こんな感じでした。

Ansatzにパラメータを割り振る

ここでは、variational ansatzを構築し、Cirqを使用してシミュレーションする方法を示したので、ここで値の最適化について考えることができます。量子ハードウェアでは、可能な限りハードウェアの近くに最適化コードを置くことが最も望ましいでしょう。量子ハードウェアとの相互運用が可能な古典的なハードウェアがより明確に指定されるにつれて、この言語はよりよく定義されます。しかし、この仕様がなければ、Cirqは多くの最適化アルゴリズムでループを最適化するための便利なコンセプトを提供します。これは、ゲートセットの値の多くが、浮動小数点数で指定される代わりにシンボルで指定され、このシンボルを実行時に指定された値に置き換えることができるという事実です。 幸いなことに、パラメータ化された値を使用すると、以前に浮動小数点値を渡したSymbolオブジェクトを渡すのと同じくらい単純なので、コードを記述しました。

circuit = cirq.Circuit()
alpha = cirq.Symbol('alpha')
beta = cirq.Symbol('beta')
gamma = cirq.Symbol('gamma')
circuit.append(one_step(h, jr, jc, alpha, beta, gamma))
circuit.append(cirq.measure(*qubits, key='x'))
print(circuit)

結果は、パラメータで表示されました。

(0, 0): ───X^alpha───Z^beta───X───@─────────X───────────────────────────────────────────────────@───────────────────M('x')───
                                  │                                                             │                   │
(0, 1): ───X^alpha───Z^beta───────┼─────────X───@─────────X─────────────────────────────────────@^gamma─────────────M────────
                                  │             │                                                                   │
(0, 2): ───X^alpha───Z^beta───────┼─────────────┼─────────X───@─────────X───────────────────────────────────────────M────────
                                  │             │             │                                                     │
(1, 0): ───X^alpha───Z^beta───X───@^gamma───X───┼─────────────┼─────────────@───────────────────@───────────────────M────────
                                                │             │             │                   │                   │
(1, 1): ───X^alpha──────────────────────────X───@^gamma───X───┼─────────────┼─────────@─────────@^gamma─────────────M────────
                                                              │             │         │                             │
(1, 2): ───X^alpha───Z^beta───────────────────────────────X───@^gamma───X───┼─────────┼─────────@───────────────────M────────
                                                                            │         │         │                   │
(2, 0): ───X^alpha───Z^beta─────────────────────────────────────────────────@^gamma───┼─────────┼─────────@─────────M────────
                                                                                      │         │         │         │
(2, 1): ───X^alpha────────────────────────────────────────────────────────────────────@^gamma───┼─────────@^gamma───M────────
                                                                                                │                   │
(2, 2): ───X^alpha──────────────────────────────────────────────────────────────────────────────@^gamma─────────────M────────

再度google翻訳

ここで、回路のゲートはパラメータ化されていることに注意してください。 パラメタは実行時にParamResolverを使用して指定されます。ParamResolverはSymbolキーからランタイム値までの辞書です。

ゲート回路をパラメータで記述できたので、パラメータに値を代入して実行できます。

resolver = cirq.ParamResolver({'alpha': 0.1, 'beta': 0.3, 'gamma': 0.7})
resolved_circuit = circuit.with_parameters_resolved_by(resolver)

上記の回路のパラメータを実際の値に変換します。

続いてgoogle翻訳

より便利には、Cirqには「スイープ」という概念もあります。本質的にスイープとは、パラメータリゾルバの集合です。このランタイム情報は、多くの異なるパラメータ値に対して多数の回路を実行する場合に非常に便利です。スウィープを作成して値を直接指定することもできます(これは、古典的な情報を回路に取り込む1つの方法です)、またはさまざまなヘルパーメソッドです。例えば、等間隔に配置されたパラメータ値のグリッド上で回路を評価したいとします。 LinSpaceを使って簡単に作成できます。

sweep = (cirq.Linspace(key='alpha', start=0.1, stop=0.9, length=5)
         * cirq.Linspace(key='beta', start=0.1, stop=0.9, length=5)
         * cirq.Linspace(key='gamma', start=0.1, stop=0.9, length=5))
results = simulator.run_sweep(circuit, params=sweep, repetitions=100)
for result in results:
    print(result.params.param_dict, obj_func(result))

実行結果は、

OrderedDict([('alpha', 0.1), ('beta', 0.1), ('gamma', 0.1)]) 2.76
OrderedDict([('alpha', 0.1), ('beta', 0.1), ('gamma', 0.30000000000000004)]) 2.88
(中略)
OrderedDict([('alpha', 0.9), ('beta', 0.9), ('gamma', 0.9)]) -2.66

基底状態を探す

今度は最小値を見つけるために値を単純なグリッドで検索するために必要なすべてのコードがあります。グリッド検索は、最適な最適化アルゴリズムではありませんが、ここでは簡単に説明しています。

sweep_size = 10
sweep = (cirq.Linspace(key='alpha', start=0.0, stop=1.0, length=10)
         * cirq.Linspace(key='beta', start=0.0, stop=1.0, length=10)
         * cirq.Linspace(key='gamma', start=0.0, stop=1.0, length=10))
results = simulator.run_sweep(circuit, params=sweep, repetitions=100)

min = None
min_params = None
for result in results:
    value = obj_func(result)
    if min is None or value < min:
        min = value
        min_params = result.params
print('Minimum objective value is {}.'.format(min))

実行結果

Minimum objective value is -3.0.

このようになりました。

google翻訳、

Cirqを使って簡単な変分量子アルゴリズムを作成しました。次は何をする?おそらく上記のコードを使い、アルゴリズムのパフォーマンスを分析することができます。新しいパラメータ化された回路を追加し、これらの回路を解析するためのエンドツーエンドプログラムを構築します。最後に、Cirqの機能の詳細を知るには、概念的な文書を読むことが大切です。

考察

google翻訳で無理やり進めて全体概要をざっくりと確認しました。イジングモデルの問題をゲートに実装し、そこからシミュレーションを通じて基底状態の値をとることに成功しました。これらの手順は古典量子ハイブリッド計算の基本なので、今回のフレームワークのコンセプト文章と合わせて確認したいと思います。フレームワークだけあって、理論的なところから独自の実装や概念もあったので、理論と分けて学ぶことをお勧めします。以上です。