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

フェルミオン空間でEvolution・Gateを合成する

Important

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

EvolutionGateは、演算子全体の時間発展e−itHe^{-itH}を表します。フェルミオンからフェルミオンへの合成は、このGateをより小さなフェルミオン・GateからなるFermionicCircuitに書き換えます。書き換えの前後がどちらもフェルミオン空間にとどまることが、この後に続くフェルミオンからQubitへの合成との違いです。演算子のgroups(演算子の項のグループ化:演算子の構造を活用するを参照)はこの書き換えの後も保持されますが、フェルミオンからQubitへのマッピングはグルーピングの概念を持たないSparseObservableを返します。

手法を選択するノブは2つあります。Evolution.synthesisは1つのGateに対して手法を設定し、F2FSynthesisTranspilerパスは、Circuit全体に対して手法を設定します。どちらも、そのような書き換えが実装するインターフェースであるF2FSynthesisPluginを受け取ります。このパッケージが現在、時間発展に対して提供している手法は積公式であり、このガイドの以降の部分ではそれを使用します。1次の公式(デフォルト)にはFermionicLieTrotter、より高い偶数次にはFermionicSuzukiTrotterを使います。このインターフェースを実装するその他のものも同じ方法で選択でき、2つのノブに関するセクションの内容はそのまま当てはまります。

Note

このフェルミオンからフェルミオンへのステップは任意です。EvolutionGateは、そのままフェルミオンからQubitへの段階に進めます。この段階では、項がいくつあってもGate全体がマッピングされ、合成されます。先に分割するかどうかは選択です。分割するのは、結果として得られる個々のGateのコストが低くなる場合や、分割によって後の段階が利用できる構造が現れる場合です。

2つのステップは組み合わせられるため、時間発展は、どちらかのレベル、両方のレベル、またはどちらでもない形で近似されることがあります。積公式は、因子が可換でない限り近似になります。一方、合成手法は、まったく近似を行わないこともあります。

Note

このステップをスキップしても、グルーピングが完全に失われるとは限りません。マッパーをgroup_wise()でラップすると、演算子が一度に1グループずつマッピングされ、その結果が合計されるため、マッピング後のPauli項は入り乱れることなくグループ化されて得られます。Qubit側の積公式は、受け取った順序で項を合成するため、隣り合った可換な項は一緒にスケジュールできます。

下のハミルトニアンでは、これだけで分割による2Qubit Gateの深さの利点がすべて得られます。Gate数は変わらないまま、30ではなく18になります。違いは、構造がどこで表現されるかにあります。フェルミオン空間で分割すると、各グループが別々のGateになり、後続のすべての段階がそれを認識して処理できます。group_wiseはGateを1つのままにして、その内部で項を並べ替えるだけです。これはスケジューリングには十分ですが、フェルミオン・パスが作業できるものは何も残りません。時間発展を単一のGateのままにする必要がある場合にこれを使い、それ以外の場合はフェルミオンの積公式を利用してください。

グループ化されたハミルトニアンを構築する​

どちらの積公式も、演算子を因子に分割し、グループ化された演算子では、それらの因子がそのままグループになります。したがって、グルーピングが結果のコストと精度の両方を決定します。項が可換になるようにグループ化した、6サイトのFermi-Hubbardチェーンを考えます。偶数ボンドのホッピング項、奇数ボンドのホッピング項、そして対角な相互作用の3つです。

>>> 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つのGateに手法を設定する​

Gateに合成手法を渡し、それを分解します。出力される各Gateは、それぞれ独立したEvolutionGateとして返されます。

>>> 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))
17
>>> factors(FermionicSuzukiTrotter(order=2, reps=4, merge_adjacent=False))
20

1次では、グループごとに1つの因子が出力されます。2次では対称なパリンドロームが出力され、外側の2つのグループがそれぞれ半分の時間で2回ずつ現れて、合計5つになります。これを4回繰り返すと20になるはずですが、各繰り返しは、次の繰り返しの先頭と同じグループで終わります。これらの隣接するGateは同じ演算子のもとで時間発展するため、時間を合計した1つのGateに融合され、継ぎ目ごとにGateが1つ節約されます。教科書どおりの順序を維持するには、merge_adjacent=Falseを渡します。どの次数でも演算子の分割方法は同じです。変わるのは、因子の順序とその時間発展の時間であり、分割そのものではありません。

Note

因子にはatomicのマークが付いているため、再度分解しても、それ以上分割されずにそのまま残ります。この分解には不動点があるため、decompose()を繰り返し呼び出しても安全です。

パイプライン全体に手法を設定する​

Gateごとにsynthesisを設定すると、その選択を、Gateを構築するすべてのものに通す必要があります。これには、内部でGateを構築するUCCやUCJも含まれます。F2FSynthesisパスは、代わりにCircuit内のすべてのEvolutionに1つの手法を適用するため、選択はパイプラインに対して一度だけ行えば済みます。これは、F2QSynthesisと同様にGate名で設定され、名前を指定されていないGateはそのまま残されます。

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import F2FSynthesis
>>>
>>> circuit = FermionicCircuit(num_sites)
>>> circuit.append(Evolution(num_sites, hamiltonian, time=total_time), circuit.modes)
>>>
>>> expanded = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2, "reps": 4})})
... ).run(circuit)
>>> expanded.count_ops()["Evolution"]
17

これは、上記でその手法についてdecompose()が返すものと同じ数です。MergeEvolutionsパスは、連続するCircuit間の継ぎ目など、1回の合成から生じたものではない隣接Gateに対して、同じ融合を適用します。このパスは公式自体を実行するため、別途展開するステップは不要です。また、出力される因子はatomicであり、それ自体の定義を持たないため、2回実行しても何も変わりません。

Note

apply=Falseを渡すと、手法を選択するだけで、展開は別のものに任せられます。たとえばQiskitのDecomposeなどです(GateをDecompose("Evolution")のように指定すると、時間発展のみに限定され、その他のフェルミオン・Gateはそれぞれの合成プラグインに任されます)。

これは、Circuit全体の手法を先に選択し、展開は必要になる時点まで延期する場合に便利です。たとえば、先にCircuitを確認したい場合などです。

>>> selected = FermionicPassManager(
... F2FSynthesis({"Evolution": ("SuzukiTrotter", (), {"order": 2})}, apply=False)
... ).run(circuit)
>>> selected.count_ops()["Evolution"] # selected, not yet expanded
1
>>> selected.decompose().count_ops()["Evolution"] # expanded on demand
5

ただし、展開されていないGateがどうなるかには注意してください。そのGateはそのままフェルミオンからQubitへの段階に到達し、synthesisが参照されないままマッピングされます。手法を比較するときに間違えやすい点で、どの手法でも出力が同一になり、比較は何も測定していないことになります。上記のハミルトニアンでは、2次のGateを展開しないままにすると、下のはるかに大きなCircuitが、選択した手法に関係なく、マッピングされた1回の時間発展のコストにまで縮んでしまいます。

効果を測定する​

次数を上げると深さが増えるため、問題は、精度が高いかどうかではなく、Gateあたりの精度が高いかどうかです。手法ごとに2つの数値が必要です。生成された状態の誤差と、それを生成したCircuitの2Qubitコストです。

誤差は、FermionicCircuitを直接シミュレーションするffsimで測定します(このパッケージとffsimの関係を参照)。フェルミオンからQubitへのマッピングが関与しないため、この比較では、マッピングやQubit側の合成が及ぼしうる影響から切り離して、フェルミオンの積公式だけを調べられます。また、粒子数固定のセクターで動作するため、完全な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 F2FSynthesis 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

2Qubitコストは、意図的にその表から除外しています。これはQiskitが選択するQubit側の合成に依存しており、リリースごとに改善されるため、ここに表示される数値は、積公式の性質ではなくスナップショットになってしまいます。代わりに下の図にそれらをプロットし、比較の根拠となるものは関係式として主張されます。

Grouped bars of CX count and depth per method, with order two at four repetitions clearly shorter than order one at ten.

最後の2つの手法を合わせて読んでください。これらが重要な比較だからです。4回繰り返した2次は、10回繰り返した1次よりも約10倍正確で、図に示されるように、より多くではなく、より少ないエンタングルメントGateでそこに到達します。ここでは、Gateの予算を、1次のステップを増やすことよりも高い次数に費やすほうが優れています。

この結果は普遍的ではありません。repsを増やす場合とorderを増やす場合とでは、同じ種類のコストに対して誤差の減少率が異なり、どちらが優れているかは、演算子と時間発展させる時間によって決まります。上の2つの行は、特定の問題についてそれを調べる方法です。

Note

フェルミオンの公式は、フェルミオンからQubitへの段階で選択された公式と組み合わせられます(MapperFnEvolutionSynthesis.product_formulaを参照)。両方が近似を行う場合、結果の精度は2つのうち弱いほうで決まるため、Qubit側の公式が1次のままでここだけ次数を上げても、得られるものはほとんどありません。

効果のあるGateにパスを限定する​

高い次数が役立つのは、因子が可換でない場合だけです。すべてが可換であれば、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

どちらのCircuitも厳密であるため、2つ目は何の利益もなく80%多くのエンタングルメントGateを消費しています。UCJの対角クーロン演算子はこの形をしており、これがF2FSynthesis.filterが存在する理由です。これはDAGOpNodeに対する述語を受け取り、拒否されたGateには手を加えないため、1つのパスで、効果のあるGateの次数を上げ、それ以外は1次のままにできます。

両方の種類の時間発展を含むCircuitを使うと、これで何が節約されるかが分かります。以下の述語は、演算子がモード間で粒子を移動させる場合にのみGateを受け入れます。これはまさに、グループが可換でなくなり、高い次数が効果を持つ場合です。

>>> 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(F2FSynthesis({"Evolution": order_2})).run(mixed_circuit())
... )
>>> restricted = two_qubit_cost(
... FermionicPassManager(
... F2FSynthesis(
... {"Evolution": 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

フィルターが節約するGateは、2次が対角の時間発展に費やしたGateとまったく同じで、上の対角の比較ではその数を8(18対10)としています。その時間発展は1次ですでに厳密なため、そこでは何の利益ももたらしておらず、したがってそれらを省いても精度は下がりません。

Note

2つの2次のCircuit同士を比較し、展開されていないものとは比較しないでください。両方のGateを展開しないままにすると、それぞれが丸ごとフェルミオンからQubitへの段階に送られます。これは、上記のapply=Falseの注意事項で説明した間違いです。フィルター済みCircuitと同じエンタングルメントGate数になることもありますが、深さの異なる別のCircuitであるため、その数からフィルターの効果を読み取っても、何も測定していないことになります。

Caution

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

次のステップ​