Skip to main content
IBM Quantum Platform

フェルミオン空間におけるエボリューション・ゲートの合成

重要

このガイドで解説している機能は、現時点では Python APIでのみ利用可能です。 同等の機能は、今後のリリースにおいてC APIで提供される予定です。

アン・ Evolution ゲートは、演算子全体の時間発展 e−itHe^{-itH} を伝達する。 フェルミオン間合成により、そのゲートは、より小さなフェルミオンゲ FermionicCircuit ートの集合へと書き換えられる。 書き換え式の両辺はフェルミオン空間にとどまる。これが、後に続く「フェルミオンから量子ビットへの合成」との違いである。すなわち、演算子の( groups 「群演算子の項:演算子構造の利用 」を参照)は書き換え後も存続するが、フェルミオンから量子ビットへの写像は SparseObservable、群の概念を持たない を返す。

2つのノブで方式を選択します。 Evolution.synthesis あるゲートに設定し、 FermionicTrotterization トランスパイラ・パスでは回路全体に設定します。 どちらも FermionicEvolutionSynthesis、そのような書き換えが実装するインターフェースである `` を受け取ります。 このパッケージが現在提供しているメソッドは積の式であり、このガイドの残りの部分でもこれを使用しています。1次式(デフォルト)の FermionicLieTrotter 場合は、それより高い偶次の式の場合は FermionicSuzukiTrotter です。 このインターフェースを実装している他のものはすべて、同様の方法で選択され、2つのノブに関する説明はそのまま適用されます。

注

このフェルミオンからフェルミオンへのステップは任意です。 Anゲート Evolution は、フェルミオンからクビットへの段階に直接移行することができ、そこでは、そのゲートに含まれる項の数がいくら多くても、それをそのままマッピングして合成します。 最初に分割するのは一つの選択肢であり、その結果得られるゲートが個別に安くなるため、あるいは分割によって後の段階で活用できる構造が明らかになるために行われる。

これら2つの段階は相互に構成し合っているため、進化の結果は、いずれかの段階、あるいは両方の段階、あるいはどちらの段階でも近似されることになる。 因子が可換でない限り、製品の式は近似的なものとなるが、合成法では近似を必要としない。

注

この手順を省略しても、グループ化が完全に失われるとは限りません。 mapper をラップすることで、演算子は 1 グループずつマッピング group_wise() され、その結果が合計されるため、マッピングされたパウリ項は、交互に並んだ状態ではなく、グループ化された状態で得られることになる。 量子ビットの積の公式は、入力された順序通りにそれらを合成するため、最終的に隣接することになる交換可能な項をまとめて処理することができます。

以下のハミルトニアンでは、ゲート数を変えずに、2クビットの分割による深度の利点を完全に回復しています。つまり、ゲート数は30ではなく18で済みます。 違いは、その構造がどこで表現されるかという点にあります。 フェルミオン空間で分割すると、各グループが独立したゲートとなり、それ以降のすべての段階でそれを認識して処理を行うことができます。一方、1つのゲートを group_wise 残してその内部の項の順序のみを変更する場合、スケジューリングには十分ですが、フェルミオンパスが処理できる要素は何も残りません。 進化が単一のゲートにとどまる必要がある場合にこれを使用し、それ以外の場合はフェルミオン積の公式を用いる。


グループ化されたハミルトニアンの構築

どちらの式も、演算子を因数に分解しますが、グループ化された演算子の場合、それらの因数はそのグループとなります。 したがって、このグループ分けによって、コストと結果の精度の両方が決まることになる。 6つのサイトからなるフェルミ・ハバード鎖を取り、その項が可換になるようにグループ分けします。すなわち、偶数番目の結合におけるホッピング項、奇数番目の結合におけるホッピング項、そして対角線の相互作用です。

>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> def fermi_hubbard_chain(num_sites, tunneling, interaction):
...     """A 1D Fermi-Hubbard chain grouped into even bonds, odd bonds, and the interaction."""
...     terms, groups = [], []
...     for offset in (0, 1):
...         for j in range(offset, num_sites - 1, 2):
...             terms += [
...                 ([cre(j), ann(j + 1)], -tunneling),
...                 ([cre(j + 1), ann(j)], -tunneling),
...             ]
...             groups += [offset, offset]
...     for j in range(num_sites - 1):
...         terms.append(([cre(j), ann(j), cre(j + 1), ann(j + 1)], interaction))
...         groups.append(2)
...     operator = FermionOperator.from_terms(terms)
...     operator.groups = groups
...     return operator
>>>
>>> num_sites = 6
>>> hamiltonian = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> hamiltonian.num_groups()
3

3つのグループに分けられた15の用語。 各ホッピング群は、すべての項をその共役項と対に結びつけますが、これは重要な点です。すなわち、その指数関数がユニタリであるためには、因子がエルミートでなければならないのですが、これは単に和がエルミートであるということからは導かれません。 groups_are_hermitian() 検査に任せるのではなく、その規約を検証します:

>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

a0†a1a^\dagger_0 a_1 をそのパートナーである a1†a0a^\dagger_1 a_0 から分離すると、式が成り立たなくなります。また、演算子をグループ化せずに放置した場合も同様の結果となります。なぜなら、どちらの式も項ごとに分解され、単独の ai†aja^\dagger_i a_j はエルミート演算子ではないからです:

>>> ungrouped = fermi_hubbard_chain(num_sites, tunneling=1.0, interaction=2.0)
>>> ungrouped.groups = range(len(list(ungrouped.iter_terms())))  # one group per term
>>> all(groups_are_hermitian(ungrouped))
False

このような因子は有効なフェルミオン演算子であり、このパッケージはそれに対応するゲートを構築するため、マッピングされた演算子が に到達するまでは何の問題も発生しません PauliEvolutionGate。しかし、には実数の係数が必要であり、 が発生します ValueError: Operator contains complex coefficients, which are not supported。 事前にグループ分けを確認しておけば、その段階での失敗を、回路を構築する前に解決策を見出せるようにすることができます。


1つのゲートに対してメソッドを設定する

ゲートに合成法を渡し、それを分解します。 それが発する各ゲートは、それ自体と同じ Evolution ゲートとして戻ってきます:

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.circuit.library.synthesis import (
...     FermionicLieTrotter,
...     FermionicSuzukiTrotter,
... )
>>>
>>> total_time = 1.0
>>>
>>> def factors(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
...         circuit.modes,
...     )
...     return circuit.decompose().count_ops()["Evolution"]
>>>
>>> factors(FermionicLieTrotter())
3
>>> factors(FermionicSuzukiTrotter(order=2))
5
>>> factors(FermionicSuzukiTrotter(order=2, reps=4))
20

1次項は、各グループにつき1つの因子を放出する。 2番目の命令は対称的な回文を生成するため、2つの外側のグループがそれぞれ半分の時間で2回ずつ現れ、合計で5回となります。 それを4回繰り返すと、20になります。 どの命令でも、演算子の分割方法は同じです。変わるのは、因子の順序とその変化時間であり、分割方法そのものではありません。

注

これらの因子はマークされているため atomic、再度分解を行っても、それらはそのまま残され、それ以上分割されることはありません。 この分解には固定点が存在するため、これを繰り返し decompose() 呼び出しても安全です。


パイプライン全体に対する方法を設定する

「ゲート synthesis ごと」に設定するということは、ゲートを構成するすべての要素(内部でゲートを構築する や UCC を含む UCJ)に対してその選択を適用することを意味します。 このパス FermionicTrotterization では、代わりに回路 Evolution 内のすべての要素に対して1つのメソッドを適用するため、パイプラインごとに一度だけ選択が行われます:

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import FermionicTrotterization
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
...     FermionicTrotterization(FermionicSuzukiTrotter(order=2, reps=4))
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
20

20のゲート:2次探索ごとに5つの要素、これを4回繰り返す(上記の方法でも同じ数 decompose() になる)。 この展開は式そのものを展開するため、別途展開を行う必要はありません。また、これを2回実行しても結果は変わりません。なぜなら、この展開によって生成される因子は であり atomic 、それ自体に定義を持たないからです。

注

Pass を使用 apply=False すると、メソッドのみを選択し、展開処理は Qiskit などの他の機能に任せることができます( Decompose ゲートに という名前を付けることで、そのゲートは進化演算に限定 Decompose("Evolution") され、その他のフェルミオンゲートはそれぞれの合成プラグインに委ねられます)。

ただし、展開されていないゲートがどのような動作をするかには注意が必要です。このゲートは、フェルミオンからクビットへの段階にそのまま到達し、そこで確認 synthesis されることなくマッピングされてしまいます。 そのため、メソッドを比較する際に誤りを犯しやすくなります。というのも、どのメソッドもまったく同じ出力を生成してしまうため、比較を行っても実際には何も測定されていないことになるからです。 上記のハミルトニアンにおいて、2次のゲートを展開せずに残しておくと、どの手法を選択したかに関わらず、その下にあるはるかに大規模な回路が、単一のマッピングされた進化のコストに圧縮される。


見返りを測定する

高次演算は深度を消費するため、問題は「より正確か」ということではなく、「 ゲートあたりの精度が高いか」ということになる。 各メソッドにつき、2つの数値が必要です。それは、そのメソッドが生成する状態の誤差と、それを生成した回路の2量子ビットあたりのコストです。

このエラーは、 ffsim を使用して測定されます。ffsim はこれを**直接 **FermionicCircuit シミュレートします( 「このパッケージと ffsim の関係」 を参照)。 フェルミオンから量子ビットへの写像は一切関与していないため、この比較により、フェルミオンの積の公式が、写像や量子ビット側の合成がもたらす影響から切り離されて検討されることになる。 また、これは粒子数が固定されたセクターでも機能します。このセクターは、完全な 2N2^N 空間よりも小さく、かつ進化が実際に留まる空間よりも小さいものです。

>>> import ffsim
>>> import numpy as np
>>> from scipy.sparse.linalg import expm_multiply
>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> norb, nelec = num_sites, (3, 0)  # three spinless fermions on six modes
>>> initial = ffsim.hartree_fock_state(norb, nelec)
>>>
>>> hamiltonian_operator = ffsim.linear_operator(hamiltonian, norb=norb, nelec=nelec)
>>> exact = expm_multiply(
...     -1j * total_time * hamiltonian_operator,
...     initial,
...     traceA=-1j * total_time * ffsim.trace(hamiltonian, norb=norb, nelec=nelec),
... )
>>>
>>> def cost_and_error(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, hamiltonian, time=total_time, synthesis=synthesis),
...         circuit.modes,
...     )
...     # the method is set on the gate, so decompose() is what expands it here; a pipeline
...     # would reach for FermionicTrotterization instead (see the note above)
...     circuit = circuit.decompose()
...     evolved = ffsim.apply_unitary(initial, circuit, norb=norb, nelec=nelec)
...     error = 1 - abs(np.vdot(evolved, exact)) ** 2
...     qubit_circuit = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
...     two_qubit = lambda instruction: len(instruction.qubits) == 2
...     return error, qubit_circuit.count_ops()["cx"], qubit_circuit.depth(two_qubit)
>>>
>>> methods = [
...     ("order 1, reps 1 ", FermionicLieTrotter()),
...     ("order 2, reps 1 ", FermionicSuzukiTrotter(order=2)),
...     ("order 1, reps 10", FermionicLieTrotter(reps=10)),
...     ("order 2, reps 4 ", FermionicSuzukiTrotter(order=2, reps=4)),
... ]
>>> costs = {label: cost_and_error(synthesis) for label, synthesis in methods}
>>> labels, counts, depths = [], [], []
>>> for label, (error, gates, depth) in costs.items():
...     labels.append(label.strip().replace(", ", "\n"))
...     counts.append(gates)
...     depths.append(depth)
...     print(f"{label}  error {error:.6f}")
order 1, reps 1   error 0.515997
order 2, reps 1   error 0.180958
order 1, reps 10  error 0.004026
order 2, reps 4   error 0.000421

その表では、2量子ビットのコストは意図的に省略されています。 これは、Qiskitが選択する量子ビット側の合成方法によって異なりますが、この方法はリリースごとに改善されているため、ここに記載されている数値は、製品仕様の特性というよりは、あくまでその時点での状況を示すものです。 代わりに、以下の図にはそれらがプロットされており、この比較の根拠となるものは関係として示されている:

手法ごとのCXカウント数と深度をグループ別に棒グラフで示した。4回の反復を行った第2順序は、10回の反復を行った第1順序よりも明らかに短いことがわかる。

最後の2つの方法を併せて読んでください。これらが重要な比較ポイントだからです。 図に示すように、4回の反復を伴う順序2は、10回の反復を伴う順序1よりも精度がおよそ10倍高く、しかもそれを実現するために必要なエンタングルメントゲートの数は、増えるどころかむしろ少なくて済む。 ここでは、ゲート予算を上位のステップに割り当てる方が、より多くの第1次のステップに割り当てるよりも効率的です。

その結果は、すべての場合に当てはまるわけではありません。 「Increasing」と reps 「increasing」は、同じコストの場合でも、誤差を異なる速度で低減 order させますが、どちらが優れているかは、オペレーターや進化させる時間によって異なります。 上記の2行は、特定の問題についてその答えを導き出す方法です。

注

このフェルミオン式は、フェルミオンから量子ビットへの変換段階で採用された式と組み合わされる(参照 MapperFnEvolutionSynthesis.product_formula)。 両方が近似である場合、結果の精度はその2つのうち精度の低い方に左右されるため、クビット側の式を1次近似のままにしておきながら、ここで次数を上げてもほとんど意味がない。


その通行を、メリットのあるゲートに限定する

高次項が役立つのは、因子が可換でない場合に限られる。 もし全員が通勤する場合、一階の式はすでに正確であり、余分な因子は単なるオーバーヘッドに過ぎない。 純粋に対角線上の演算子を用いると、それが具体的に理解できます。なぜなら、数演算子は互いに可換だからです:

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> diagonal = FermionOperator.from_terms(
...     [([cre(j), ann(j), cre(j + 1), ann(j + 1)], 1.0 + 0.1 * j) for j in range(num_sites - 1)]
... )
>>> diagonal.groups = list(range(num_sites - 1))
>>>
>>> def diagonal_cost(synthesis):
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(
...         Evolution(num_sites, diagonal, time=total_time, synthesis=synthesis), circuit.modes
...     )
...     transpiled = generate_preset_jw_pass_manager().run(circuit.decompose())
...     return transpiled.decompose(reps=6).count_ops()["cx"]
>>>
>>> diagonal_cost(FermionicLieTrotter())
10
>>> diagonal_cost(FermionicSuzukiTrotter(order=2))
18

どちらの回路も正確であるため、2番目の回路は80%も多くエンタングルメントゲートを無駄に消費している。 の対角クーロン演算子はこのような形を UCJ しており、これが が存在 FermionicTrotterization.filter する理由である。すなわち DAGOpNode 、 は 上の述語を取り、拒否されたゲートには手を加えずに残すため、1回の処理で恩恵を受けるゲートの順序を高くし、残りのゲートは第1階のままで残すことができる。

両方の進化形態を併せ持つ回路は、それによってどれだけの節約になるかを示しています。 以下の述語は、その演算子がモード間で粒子を移動させる場合にのみゲートを受け入れる。これはまさに、群が可換性を失い、高次項が意味を持つ場合である:

>>> def is_diagonal(operator):
...     """Whether every term only counts particles, rather than moving them between modes."""
...     return all(
...         sorted(mode for created, mode in actions if created)
...         == sorted(mode for created, mode in actions if not created)
...         for actions, _ in operator.iter_terms()
...     )
>>>
>>> def mixed_circuit():
...     circuit = FermionicCircuit(num_sites)
...     circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
...     circuit.append(Evolution(num_sites, diagonal, time=total_time), circuit.modes)
...     return circuit
>>>
>>> def two_qubit_cost(circuit):
...     transpiled = generate_preset_jw_pass_manager().run(circuit).decompose(reps=6)
...     two_qubit = lambda instruction: len(instruction.qubits) == 2
...     return transpiled.count_ops()["cx"], transpiled.depth(two_qubit)
>>>
>>> order_2 = FermionicSuzukiTrotter(order=2)
>>>
>>> everywhere = two_qubit_cost(
...     FermionicPassManager(FermionicTrotterization(order_2)).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
...     FermionicPassManager(
...         FermionicTrotterization(
...             order_2, filter=lambda node: not is_diagonal(node.op.operator)
...         )
...     ).run(mixed_circuit())
... )
>>> everywhere[0] - restricted[0]  # the gates the filter saved
8
>>> restricted[0] < everywhere[0] and restricted[1] < everywhere[1]
True

このフィルターによって節約されるゲート数は、オーダー2が対角線進化に費やしたゲート数とまったく同じであり、上記の対角線比較では、その数は8と評価されている(18対10)。 彼らはそこで何も購入しなかった。なぜなら、その展開はすでに一階で正確であるため、それらを省いても精度が損なわれることはないからだ。

注

2次の2つの回路を、展開されていない回路と比較するのではなく、互いに比較してください。 両方のゲートを展開せずに残すと、それぞれがフェルミオンから量子ビットへの変換ステージにそのまま送られてしまいます。これが、上記の注記 apply=False で説明されている間違いです。つまり、深さは異なるものの、フィルタリングされた回路と同じエンタングルメントゲート数になる可能性があるため、その数値からフィルタの利点を読み取っても、何も測定できないのです。

警告

どちらのノブも、因子がエルミートであるかどうかを検証するものではありません。また、誤ってグループ化された演算子は、対称化によって誤差が部分的に打ち消されるため、1次では間違っているにもかかわらず、偶次数では1次よりも見栄えが良くなる場合があります。 上記 fermi_hubbard_chain のように、共役パートナーをグループごとにまとめます。


次のステップ

このページは役に立ちましたか?
バグや誤字の報告、またはコンテンツの要求はGitHubで行ってください。