メインコンテンツへスキップ
このプロジェクトはベータ版です。バグを見つけましたか?GitHub Issueを開く。質問やアイデアがありますか?Discussionsで質問する — フィードバックをお待ちしています!
このページの翻訳品質はいかがですか?

Slater行列式準備の最適化

重要

このガイドの概念は、現在Python APIでのみ利用できます。同等の機能は、今後のリリースでC APIでも提供される予定です。

InitializeModes ゲートは参照となるモード占有を宣言し、OrbitalRotation ゲートは1粒子基底を回転させます。この2つのゲートを連続して配置するとSlater行列式が準備されます。モードは参照行列式から始まり、その後、目的の軌道へ回転されます。PrepareSlaterDeterminant ゲートはこの合成を表現したもので、参照占有を保持しているため、トランスパイル時には、OrbitalRotation 単独で必要となる完全な正方形の分解の代わりに、占有された軌道のみを実現する矩形の givens_decomposition_slater() を使えます。これは厳密により低コストな合成です(このガイドの最後にある効果を参照してください)。

MergeSlaterDeterminantPreparation トランスパイラーパスは、FermionicCircuit 内の InitializeModes の後に OrbitalRotation が続くパターンを検出し、PrepareSlaterDeterminant ゲートに書き換えるため、後続の合成ステージでは簡約された分解が自動的に使われます。シミュレーションでは、この書き換えは状態に影響しません。PrepareSlaterDeterminant は検証してから回転する(参照占有を検証した後に回転を適用する。2つの別々のゲートと同じです)ため、マージによって、準備される状態を変えずにより低コストな合成が可能になります。

このガイドでは、各回路をパスの前後で並べて描画します。左が入力、右がその入力にパスを実行した結果です。これには2つのヘルパーを使います。merge は回路のコピーに対して(その FermionicDAGCircuit を通して)パスを実行し、結果を描画可能な FermionicCircuit に変換し、draw_merge は両方を1つの前後比較の図に描画します。

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

マージされるもの​

このパスは3つのパターンを認識します。いずれも、InitializeModes と OrbitalRotation が使うブロックスピンのモード規約に基づいています。norb 個の空間軌道をもつ系では、モード 0..norb がスピンαセクター、norb..2*norb がスピンβセクターです。各セクターはそれぞれ独自の(電子数、軌道数)の形をもつため、Slater行列式の準備はスピンセクターごとに行われます。

全レジスタ(またはスピンなし)​

最も単純なパターンは、InitializeModes の直後に、同じモードに対する OrbitalRotation が続くものです。これはスピンなしの場合(全モードにわたる1つの行列式)であり、回転が両方のセクターにまたがるスピンありの回路でもあります。2つのゲートは、単一の PrepareSlaterDeterminant に融合されます。

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

スピンセクターごと​

これは同じ形ですが、片方のスピン半分に限られます。ここでは、セクターごとに1組ずつある、初期化してから回転する2つの独立したペアが、それぞれ単独で PrepareSlaterDeterminant に融合されます。

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

全体での初期化、スピンごとの回転​

これは、全レジスタ(2 * norb)の単一の InitializeModes の後に、連続する各スピン半分に1つずつ、2つの OrbitalRotation アクションが続くものです。パスはセクターの境界でグローバルな占有を分割し、2つの PrepareSlaterDeterminant ゲートを出力します。2つの回転はどちらの順序で現れてもかまいません。

これは、局所ユニタリークラスターJastrow(LUCJ)ワークフローによって生成される形です(LUCJガイドを参照)。ユーザーは、回路の先頭に InitializeModes(通常は from_hartree_fock() を使ったHartree-Fock参照)を置き、UCJ アンザッツを追加します。このアンザッツを分解すると、最初の2つのスピンごとの軌道回転が初期化の直後に続きます。

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

このパターンは、初期化の直後のゲートによって片方のスピン半分だけが回転される場合にも適用されます。これは、片方のスピンセクターの軌道回転がたまたま回路の先頭に置かれた場合に生じる形です。回転される半分は、その回転とともに PrepareSlaterDeterminant になり、もう一方の半分は恒等回転で準備されます。恒等による準備は、InitializeModes がその半分に対してどのみち出力するはずの参照Xゲートに合成されるため、パディングしても追加のゲートコストはかからず、回転される半分では簡約されたSlater合成が利用できます。

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

そのまま残るもの​

このパスは意図的に保守的です。2つのゲートが実際にSlater行列式の準備として合成できる場合にのみ融合し、それ以外はすべて変更せずにそのままコピーします。「直後に続く」は、回路のデータフローグラフに基づいて解釈されます。OrbitalRotation が融合されるのは、すべてのモードにわたって InitializeModes がその唯一の先行ゲートである場合に限られます。以下の図では、前後が同一であり、何も融合されていません。

間に入る操作​

初期化と回転の間のモードにいずれかのゲートが作用すると、それらはもはや単純な準備として合成できないため、何もマージされません。

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

スピン半分ではない部分回転​

全レジスタの初期化のうち、ちょうど連続する片方のスピン半分を覆う回転は、融合されます(前のセクションを参照)。しかし、それ以外の部分的なモード集合(初期化より少ないモード数で、2つのスピン半分のどちらでもないもの。たとえばセクターの境界をまたぐ部分範囲)に対する回転は、セクターごとの準備ではありません。それをマージすると、除外されたモードに対する初期化の役割が黙って失われてしまうため、パスは両方のゲートをそのまま残します。

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

重要な理由: 合成による効果​

マージが有効なのは、PrepareSlaterDeterminant が、取り込む OrbitalRotation よりもはるかに少ないゲート数に合成されるためです。nn 個の軌道上の mm 電子の行列式は、2量子ビット回転が高々 m(n−m)m(n-m) 個で済み、対角位相ゲートは不要です。これに対し、完全な正方形の軌道回転では n(n−1)/2n(n-1)/2 個の回転と nn 個の位相が必要です。

プリセットのJordan-Wignerパスマネージャーは、最適化ステージで MergeSlaterDeterminantPreparation を実行するため、この削減は自動的に行われます。2電子・6軌道の準備を QuantumCircuit まで完全にトランスパイルして、ゲート数を数えてみましょう。

注

プリセットは、MergeSlaterDeterminantPreparation の直前に MergeOrbitalRotations を実行します。これは、初期化の後に連続する OrbitalRotation ゲートの列が続く場合に重要です。先に実行されるパスがまずその列を単一の回転にまとめ、MergeSlaterDeterminantPreparation がそれを初期化と縮約して、1つの PrepareSlaterDeterminant にします。この順序でないと、列の最初の回転だけが取り込まれ、残りは完全な正方形の分解で合成されてしまいます。

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

これを、同じ軌道回転を単独で合成した場合と比較してみましょう。先行する初期化がないとマージするものがないため、プリセットは完全な正方形の分解にフォールバックし、これはより深く、対角位相ゲートを含みます。

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

簡約された分解では、6つの位相ゲートがすべてなくなり、2量子ビット回転の数もほぼ半分になります。準備された行列式はグローバル位相を除いて正しく(状態準備では物理的に無関係です)、位相ゲートが固定していたのはこの位相です。

ゲートを分解する2つの方法​

PrepareSlaterDeterminant は2通りの方法で分解でき、両者は互換ではありません。

decompose()​

ゲートをその定義に展開します。参照の InitializeModes の後に、完全な OrbitalRotation が続く形です。これは、どの入力状態に対しても、演算子としてのゲートと等価です。

PrepareSlaterDeterminant.synthesis(F2FSynthesis によって適用)​

ゲートを、このガイドで説明した簡約された分解に置き換えます。これは、状態準備としてのみゲートと等価です。モードが真空から始まる場合に、グローバル位相を除いて同じ行列式を生成します。それを前提としているからこそ、より低コストになります。

ゲートが真空上で状態を準備する場合は、合成を使ってください。これはゲートの本来の用途であり、トランスパイラーが自動的に行うことでもあります。ゲートが演算子として作用し続ける必要がある場合(たとえば同じモード上で他のゲートが先行する場合)や、通常のゲートを調べたりシミュレートしたりしたい場合は、decompose() を使ってください。F2FSynthesis パスは、どちらの場合なのかを判別できないため、真空から行列式を準備するように回路を組むのは、渡す側の責任です。

ffsimで回路をシミュレートする場合は、どちらも必要ありません。すべてのゲートがそのユニタリー全体を適用するため、PrepareSlaterDeterminant を含む回路はそのままシミュレートされ、その周囲の OrbitalRotation ゲートに対する高速パスも維持されます。circuit.decompose("PrepareSlaterDeterminant") でSlaterゲートだけを分解した場合も、それらの回転は分解されずに残ります。

関連項目

PrepareSlaterDeterminant、InitializeModes、OrbitalRotation、MergeOrbitalRotations、F2FSynthesis、FermionicSlaterGivensDecomposition、およびグローバル初期化パターンを生成するワークフローについてはLUCJガイド。