LUCJ アンザッツのトランスパイル
このガイドの概念は Python API でのみ利用できます。
局所ユニタリー・クラスター・ジャストロウ(LUCJ)アンザッツは、相関のある電子波動関数のコンパクトでハードウェア効率の高いパラメーター化です。より一般的なユニタリー・クラスター・ジャストロウ(UCJ)ファミリーの一員であり、次の形をとります。
ここで は参照状態(通常はハートリー・フォック)、各 は軌道回転、各 は対角クーロン・オペレーターです。
は、スピン を持つ空間軌道 上の数演算子です。
ffsim がアンザッツ・オペレーターを構築し、このパッケージがそれを回路に変換して、その回路を量子ビット上へ下ろします。このガイドは、この文の後半部分についてのものです。この道筋をたどる理由は、下ろす処理そのものにあります。ffsim 独自の Qiskit ゲートはジョルダン・ウィグナー変換に固有ですが、FermionicCircuit はフェルミオン-量子ビット符号化について何も仮定しないため、同じアンザッツをターゲットデバイスに適した任意の符号化で合成できます。
したがって、アンザッツ・オペレーターはここに完成した状態で届き、以下はその後に起こること、つまりゲートが何に分解されるか、ジョルダン・ウィグナーを通じてどのように下ろされるか、デバイスのカップリングマップにどう対応付けられるか、そしてジョルダン・ウィグナー以外の下ろし方に何がまだ必要か、です。UCJ アンザッツの構築、パラメーター化、シミュレーションについては、ffsim の独自のガイドを参照してください。
2つのパッケージがどのように作業を分担しているかについては ffsim 関係ガイド、以下で使うパイプラインの各ステージについてはトランスパイルガイドを参照してください。
1. ffsim から LUCJ オペレーターを取得する
アンザッツは、結合クラスター一重・二重励起(CCSD)計算の振幅から初期化されます。量子化学には PySCF を使い、6-31g 基底の水素分子に対して制限ハートリー・フォックと、それに続く CCSD を実行し、その振幅を ffsim の from_t_amplitudes() に渡します。これは 振幅の二重因子分解を行って、層ごとの対角クーロン行列と軌道回転を求め、 振幅からオプションの最終軌道回転を導出します。繰り返し回数 はその因子分解が与える値で、n_reps はそれを切り詰め、精度と引き換えにより浅い回路にします。
>>> import ffsim
>>> import pyscf
>>> import pyscf.cc
>>>
>>> # build the molecule and run Hartree-Fock
>>> mol = pyscf.gto.Mole()
>>> mol.build(
... atom=[["H", (0, 0, 0)], ["H", (0, 0, 0.74)]],
... basis="6-31g",
... symmetry="Dooh",
... verbose=0,
... )
<pyscf.gto.mole.Mole object at ...>
>>> scf = pyscf.scf.RHF(mol).run()
>>>
>>> norb = scf.mo_coeff.shape[1]
>>> nelec = (mol.nelec[0], mol.nelec[1])
>>>
>>> # run CCSD for the t-amplitudes, then factorize them into an ansatz operator
>>> ccsd = pyscf.cc.CCSD(scf).run()
>>> t1, t2 = ccsd.t1, ccsd.t2
>>>
>>> ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(t2, t1=t1, n_reps=2)
ここが、アンザッツが ffsim の関心事でなくなる地点です。ffsim には、このパッケージが関知せず、知る必要もない選択肢があります。たとえば、optimize=True の背後にある変分的に最適化された(「圧縮」)因子分解や、変分オプティマイザーが操作するパラメーター・ベクトルのパッキングなどです。これらはすべて同じ種類のオペレーターを生成し、以下のすべてはそのどれに対してもそのまま動作します。
2. オペレーターをフェルミオン回路に変換する
UCJ ゲートはオペレーターをラップし、それをフェルミオン・モード上の回路として表現します。このゲートは参照を持たない純粋なユニタリーなので、アンザッツを適用する対象のハートリー・フォック参照を与えるために、InitializeModes ゲート(from_hartree_fock() で構築)を先頭に追加します。
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, UCJ
>>>
>>> ansatz = UCJ(ucj_op)
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(ansatz, circuit.modes)
回路を分解するとその構造が明らかになり、これがこのガイドの残りの部分が操作する表現です。InitializeModes ゲートが参照行列式を準備し、各アンザッツ層は OrbitalRotation 、続いて (対角クーロン・オペレーター の Evolution)、そして を加え、最後に末尾で最終的な OrbitalRotation が置かれます。軌道回転はスピン・セクターごとに作用するため、それぞれアルファ・モード 0..norb とベータ・モード norb..2*norb に独立に配置されます。
>>> circuit.decompose().draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

この分解に含まれるすべてのゲートは依然としてフェルミオン的です。OrbitalRotation と Evolution はモード上で定義されており、量子ビットやパウリ・オペレーターはまだ現れていません。だからこそ符号化が未決定のまま残り、合成ステージが1つを選ぶまで、アンザッツはこの状態にとどまります。
各層は で終わり、次の層は で始まるため、隣接する OrbitalRotation ゲートは1つの回転にマージできます。この融合を行うトランスパイル・パスは、今後の開発として計画されています。
3. フェルミオン-量子ビット符号化を選択する
回路は依然としてフェルミオン的であり、この時点でこのパッケージがワークフローにおける存在意義を発揮します。FermionicCircuit は、モードがどのように量子ビットになるかについて何も仮定しないため、符号化はアンザッツに組み込まれるものではなく、合成ステージでの選択になります。これが、ジョルダン・ウィグナー変換に固有の ffsim 独自の Qiskit ゲートに対して、このパッケージが加える価値です。
ジョルダン・ウィグナーだけならこの回り道は不要です。ffsim は UCJOpSpinBalancedJW とその兄弟クラスを提供しており、これらは UCJ オペレーターを単独で量子ビット回路に変換します。アンザッツをフェルミオン回路経由にする理由は、符号化がそれ以外にもたらしてくれるすべてのものにあります。たとえば、局所符号化は、各項のパウリ重みを抑えるために余分な量子ビットを使います。1D と 2D のフローセット・ガイドは、これを利用してトロッター・ステップの2量子ビット深さを系のサイズに依存しないようにしています。
UCJ アンザッツの場合、合成ステージは2種類のゲートを扱う必要があります。
-
対角クーロン発展。
Evolutionはすでに符号化に依存しません。MapperFnEvolutionSynthesisはマッパー関数を受け取るため、 の各層は、マッパー関数として表現できる任意の符号化を通じて下ろされます。フローセット・ガイドに、その書き方が示されています。 -
軌道回転とスレーター行列式の準備。
OrbitalRotationも、先頭のInitializeModesと回転が融合してできるPrepareSlaterDeterminant(後述)も、そのままでは量子ビットにマッピングされません。F2FSynthesisパスが、まずフェルミオン空間でこれらを、それぞれ1つまたは2つのモードに作用するEvolutionゲート(準備の場合は参照のInitializeModesも)に分解するので、Evolutionをマッピングできる符号化であれば、どれでもそれらを扱えます。ジョルダン・ウィグナーのもとでは、隣接するモード間の因子は、量子ビット・ステージでXXPlusYYGateオブジェクトになります。 -
初期化。
InitializeModesのプラグインは符号化によってパラメーター化されていません。モードと量子ビットが1対1に対応する占有基底符号化を前提とすることが文書化されており、その基底上で直接ゲートを出力します。別の符号化には専用のプラグインが必要ですが、まだ提供されていません。
ジョルダン・ウィグナー以外の符号化のもとで UCJ 回路をトランスパイルする方法は未解決の研究課題であり、このパッケージは、そのようなパイプラインを開発するための枠組みを提供します。このガイドの残りでは、例として、generate_preset_jw_pass_manager() が生成するものと同等の、ジョルダン・ウィグナー符号化用のトランスパイラー・パイプラインを構築します。
4. ジョルダン・ウィグナーを通じて下ろす
下ろす処理は F2QSynthesis で行われます。これは Qiskit の HighLevelSynthesis と同様に動作し、フェルミオン命令を順にたどって、各命令をそのゲート名に登録されたプラグインに渡します。その F2QSynthesis.methods 属性はゲート名ごとに1つの F2QSynthesisPlugin を保持するため、このステージを構築するには、回路に含まれるすべてのフェルミオン・ゲートに対してプラグインを選ぶことになります。
>>> from qiskit_fermions.mappers.library import jordan_wigner
>>> from qiskit_fermions.transpiler.passes import F2QSynthesis
>>> from qiskit_fermions.transpiler.passes.synthesis import (
... MapperFnEvolutionSynthesis,
... TrivialOccupationInitializeModesSynthesis,
... )
>>>
>>> synthesis = F2QSynthesis()
>>> synthesis.methods["Evolution"] = MapperFnEvolutionSynthesis(jordan_wigner)
>>> synthesis.methods["InitializeModes"] = TrivialOccupationInitializeModesSynthesis()
このようにインスタンスを代入することは、自分で書いたプラグインを、事前にどこかに登録することなくパイプラインに組み込む方法でもあります。
ここで符号化が確定します。セクション3で説明した2通りの方法で、明示的には jordan_wigner() を MapperFnEvolutionSynthesis に渡すことで、暗黙的には、すでにそれを前提としているもう一方のプラグインによって、決まります。
このステージは、ステージが固定された順序で実行される MultiStagePassManager に組み込まれます。
>>> from qiskit.passmanager import MultiStagePassManager
>>> from qiskit.transpiler import generate_preset_pass_manager
>>> from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
>>>
>>> from qiskit_fermions.transpiler import (
... FermionicCircuitToDAG,
... FermionicPassManager,
... QuantumDAGToCircuit,
... )
>>> from qiskit_fermions.transpiler.passes import (
... F2FSynthesis,
... HopEvolutionSynthesis,
... MergeOrbitalRotations,
... MergeSlaterDeterminantPreparation,
... TrivialF2QLayout,
... )
>>>
>>> pm = MultiStagePassManager(
... input=FermionicCircuitToDAG(),
... optimization=FermionicPassManager(
... [
... MergeOrbitalRotations(),
... MergeSlaterDeterminantPreparation(),
... F2FSynthesis(
... {"OrbitalRotation": "Givens", "PrepareSlaterDeterminant": "Givens"}
... ),
... ]
... ),
... layout=FermionicPassManager(TrivialF2QLayout()),
... synthesis=synthesis,
... qubit=generate_preset_pass_manager(
... hls_config=HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
... ),
... output=QuantumDAGToCircuit(),
... )
最適化ステージは合成の前に、フェルミオン回路上で実行されます。これが価値を持つ理由は、ゲートがまだフェルミオン的で符号化に依存しない間に書き換えを行うため、その後にどの符号化が続いても節約効果が引き継がれるからです。MergeOrbitalRotations は、連続する OrbitalRotation ゲートの各連なりを1つにまとめます。UCJ アンザッツでは、各層が で終わり、次の層が で始まるため、これが重要になります。次に MergeSlaterDeterminantPreparation が、先頭の InitializeModes とそれに続く回転を1つの PrepareSlaterDeterminant に融合し、合成はこれを、完全な正方回転ではなく、より低コストな縮約スレーター分解で下ろします。順序は重要です。回転を先にマージすると、初期化の直後に単一の回転が現れ、スレーター融合がそれを初期化に取り込みます。F2FSynthesis は最後に置かれます。残っている回転を分解してしまうため、これより早く行うと、2つのマージ・パスが融合する対象がなくなってしまうからです。
残りのステージは下回りの処理です。input と output は回路と DAG の間を変換し、layout はフェルミオン・モードを量子ビットに割り当て(TrivialF2QLayout はモード i を量子ビット i に対応付けます)、qubit は、フェルミオン的なものが何も残らなくなった時点で引き継ぐ通常の Qiskit パスマネージャーです。
実行には1つの準備ステップが必要です。最適化ステージが個々の回転と発展を認識できるように、複合の UCJ ゲートを、先にそのプリミティブ・ゲートへ分解しておく必要があります。
>>> transpiled = pm.run(circuit.decompose())
>>> print(dict(sorted(transpiled.count_ops().items())))
{'rz': 14, 'rzz': 12, 'x': 2, 'xx_plus_yy': 28}
ターゲットデバイスがない場合、これは全結合を仮定して 2 * norb 個の量子ビットにマッピングされます。軌道回転は XXPlusYYGateオブジェクトに、対角クーロン発展は RZZGateオブジェクトに合成されます。
>>> transpiled.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>
このパイプラインはまさに generate_preset_jw_pass_manager() が組み立てるものなので、実際にはプリセットを使い、いずれかのステージを変更する必要があるときに、ステージを手作業で構築してください。
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> preset = generate_preset_jw_pass_manager()
>>> preset.run(circuit.decompose()).count_ops() == transpiled.count_ops()
True
5. デバイスのトポロジーに合わせる
実デバイスの量子ビット・カップリングマップは制限されており、LUCJ アンザッツはそれに合うよう設計されています。同スピン(pairs_aa)の相互作用は2本の線形チェーンを形成し、アルファ-ベータ(pairs_ab)の相互作用がそれらを橋渡しします。ffsim の generate_lucj_pass_manager() は、この構造に対応するデバイス対応の量子ビット・パイプラインを構築し、ハードウェアが実際に収容できる pairs_ab の部分集合を返します。このパッケージ独自のフェルミオン-量子ビット合成を維持したまま、そのパイプラインをプリセットの qubit ステージに組み込みます。
>>> from ffsim.qiskit import generate_lucj_pass_manager
>>> from qiskit.providers.fake_provider import GenericBackendV2
>>> from qiskit.transpiler import CouplingMap
>>>
>>> # a heavy-hex device coupling map (any BackendV2 works, e.g. a real fake_provider backend)
>>> coupling_map = CouplingMap.from_heavy_hex(5)
>>> backend = GenericBackendV2(
... num_qubits=coupling_map.size(),
... basis_gates=["cp", "xx_plus_yy", "p", "x", "swap"],
... coupling_map=coupling_map,
... )
>>>
>>> # nearest-neighbor same-spin chain; let the pass manager choose the alpha-beta pairs
>>> pairs_aa = [(p, p + 1) for p in range(norb - 1)]
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> hls_config = HLSConfig(PauliEvolution=[HopEvolutionSynthesis(), "default"])
>>> pm.qubit, allowed_pairs_ab = generate_lucj_pass_manager(
... backend,
... norb,
... "heavy-hex",
... (pairs_aa, None),
... optimization_level=3,
... seed_transpiler=0,
... hls_config=hls_config,
... )
>>>
>>> # the alpha-beta interactions the heavy-hex connectivity can implement
>>> print(allowed_pairs_ab)
[(0, 0)]
この組み合わせこそが要点です。qubit ステージはデバイスを認識し、その前段のフェルミオン-量子ビット合成ステージはこのパッケージ独自のものであり続けるため、置き換え可能なままです。qubit ステージを置き換えると、プリセットがそこに登録しているもの、つまり軌道回転のギブンス因子を XXPlusYYGateオブジェクトに変換する HopEvolutionSynthesis が失われます。上記のように hls_config を通じてそれを渡すか、それなしでデバイスの基底に到達する必要があります。
pm.qubit がデバイス対応のパイプラインに設定された状態でパスマネージャーを実行すると、回路がバックエンドの量子ビット上にレイアウトされ、カップリングマップに合わせてルーティングされます。ステップ1の制限なしのアンザッツでは、対角クーロン・オペレーターに、ハードウェアが直接到達できないアルファ-ベータ項がまだ含まれているため、ルーターはそれらを橋渡しするために多数の SWAP ゲートを挿入しなければなりません。
>>> naive = pm.run(circuit.decompose())
>>> naive.num_qubits # laid out on the full heavy-hex device register
57
>>> naive_swaps = naive.count_ops()["swap"]
>>> naive_swaps # many SWAPs to bridge the unreachable alpha-beta interactions
33
アンザッツを、ハードウェアで実装可能な相互作用に制限する
対処法は、allowed_pairs_ab を、from_t_amplitudes() の interaction_pairs 引数を通じてアンザッツの構築にフィードバックすることです。これにより、対角クーロン・オペレーターには、カップリングマップが直接実装できるアルファ-ベータ項だけが含まれます。するとアンザッツはデバイスのトポロジーに一致し、ルーターはほとんど手を加える必要がなくなります。
>>> restricted = UCJ(
... ffsim.UCJOpSpinBalanced.from_t_amplitudes(
... t2, t1=t1, n_reps=2, interaction_pairs=(pairs_aa, allowed_pairs_ab)
... )
... )
>>>
>>> circuit = FermionicCircuit(2 * norb)
>>> circuit.append(InitializeModes.from_hartree_fock(norb, nelec), circuit.modes)
>>> circuit.append(restricted, circuit.modes)
>>>
>>> transpiled = pm.run(circuit.decompose())
>>> restricted_swaps = transpiled.count_ops()["swap"]
>>> restricted_swaps # far fewer routing SWAPs than the unrestricted ansatz
4
アクティブな量子ビットのみを描画する(idle_wires=False)と、2本のスピン・チェーンとアルファ-ベータの橋渡しに制限され、デバイスの基底ゲートで表現された回路が表示されます。レイアウトとルーティングにより論理モードはデバイスの物理量子ビットに散らばるため、回路の最終レイアウトから取得した wire_order(final_index_layout() は、各入力量子ビットが最終的に配置された物理量子ビットを入力量子ビットの順に列挙します)を渡して、ワイヤーを元のモード順に描き戻します。
>>> wire_order = transpiled.layout.final_index_layout(filter_ancillas=False)
>>> transpiled.draw("mpl", idle_wires=False, fold=-1, wire_order=wire_order)
<Figure size ... with 1 Axes>

レイアウト後の正確なゲート数と深さは、ルーティング/最適化パスと選択したデバイスに依存するため、ここでは再現しません。重要な点は協調設計です。最近接の pairs_aa チェーンとハードウェアでフィルターされた pairs_ab の橋渡しでアンザッツを表現すると、合成された回路がデバイスのトポロジーに近いままとなり、ルーティングのオーバーヘッド(挿入される SWAP ゲート)が最小限に抑えられます。関連する合成の選択肢(minimize_2q_gate_count)については FermionicSlaterGivensDecomposition を参照してください。これは2量子ビット・ゲート数とルーティング後の深さのトレードオフになります。
次のステップ
-
個々のゲートの動作については、
qiskit_fermions.circuit.libraryのドキュメントとフェルミオン回路ガイドで学んでください。 -
フェルミオン・オペレーターとハミルトニアンを直接構築する方法を理解するには、オペレーターの解説ガイドを参照してください。
-
フェルミオン回路が量子ビットにマッピングされる様子は、トランスパイルガイドで確認してください。
-
合成ステージを構成するもう1つの方法、つまりインスタンスではなく、
qiskit_fermions.transpiler.synthesisエントリーポイントに登録された短い名前でプラグインを選択するconfig辞書については、F2QSynthesisのドキュメントを読んでください。 -
2つのパッケージがどのように作業を分担しているか、またどのプロトコルによって ffsim がこのパッケージの回路をシミュレートできるかについては、ffsim 関係ガイドを読んでください。
