Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Einen Evolution-Gate im fermionischen Raum synthetisieren

Wichtig

Die Konzepte in diesem Leitfaden sind derzeit nur in der Python-API verfügbar. Entsprechende Funktionalität wird in einer zukünftigen Version in der C-API bereitgestellt.

Ein Evolution-Gate trägt die Zeitentwicklung e−itHe^{-itH} eines ganzen Operators. Eine Fermion-zu-Fermion-Synthese schreibt dieses Gate in einen FermionicCircuit aus kleineren fermionischen Gates um. Beide Seiten der Umschreibung bleiben im fermionischen Raum, was sie von der anschließenden Fermion-zu-Qubit-Synthese unterscheidet: Die groups des Operators (siehe Operatorterme gruppieren: die Operatorstruktur nutzen) bleiben dabei erhalten, wohingegen eine Fermion-zu-Qubit-Abbildung eine SparseObservable zurückgibt, die keinen Begriff von Gruppierung kennt.

Zwei Stellschrauben wählen die Methode aus. Evolution.synthesis legt sie für ein einzelnes Gate fest, und der F2FSynthesis-Transpiler-Pass legt sie für einen ganzen Circuit fest. Beide nehmen ein F2FSynthesisPlugin entgegen, die Schnittstelle, die jede solche Umschreibung implementiert. Die Methoden, die dieses Paket derzeit für eine Evolution bereitstellt, sind Produktformeln, die im Rest dieses Leitfadens verwendet werden: FermionicLieTrotter für eine Formel erster Ordnung (der Standard) und FermionicSuzukiTrotter für eine höhere gerade Ordnung. Alles andere, was die Schnittstelle implementiert, wird auf dieselbe Weise ausgewählt, und die Abschnitte zu den beiden Stellschrauben gelten dafür unverändert.

Hinweis

Dieser Fermion-zu-Fermion-Schritt ist optional. Ein Evolution-Gate kann direkt in die Fermion-zu-Qubit-Phase gehen, die es als Ganzes abbildet und synthetisiert, egal wie viele Terme es enthält. Es vorher aufzuteilen ist eine Entscheidung, die du triffst, weil die entstehenden Gates einzeln günstiger sind oder weil die Aufteilung Struktur freilegt, die eine spätere Phase nutzen kann.

Die beiden Schritte lassen sich kombinieren, sodass eine Evolution auf der einen Ebene, auf beiden oder auf keiner genähert werden kann. Eine Produktformel ist eine Näherung, es sei denn, ihre Faktoren kommutieren; eine Synthesemethode muss gar nicht nähern.

Hinweis

Diesen Schritt zu überspringen muss nicht bedeuten, die Gruppierung vollständig zu verlieren. Wenn du den Mapper in group_wise() einpackst, wird der Operator Gruppe für Gruppe abgebildet und die Ergebnisse werden summiert, sodass die abgebildeten Pauli-Terme gruppiert statt verschachtelt ankommen. Eine Produktformel auf der Qubit-Seite synthetisiert sie in der Reihenfolge, in der sie sie erhält, sodass kommutierende Terme, die nebeneinander landen, gemeinsam eingeplant werden können.

Beim Hamiltonian unten gewinnt man so den gesamten Vorteil bei der Zwei-Qubit-Tiefe zurück, den das Aufteilen bringt: 18 statt 30, bei unveränderter Gate-Anzahl. Der Unterschied liegt darin, wo die Struktur ausgedrückt wird. Das Aufteilen im fermionischen Raum macht jede Gruppe zu einem eigenen Gate, das jede spätere Phase sehen und bearbeiten kann; group_wise belässt es bei einem Gate und ordnet nur die Terme darin um, was für die Einplanung genügt, aber einem fermionischen Pass nichts zum Arbeiten übrig lässt. Verwende es, wenn die Evolution ein einzelnes Gate bleiben muss, und greife andernfalls zu einer fermionischen Produktformel.

Einen gruppierten Hamiltonian erstellen​

Beide Produktformeln teilen einen Operator in Faktoren auf, und bei einem gruppierten Operator sind diese Faktoren seine Gruppen. Die Gruppierung bestimmt daher sowohl die Kosten als auch die Genauigkeit des Ergebnisses. Nimm eine Fermi-Hubbard-Kette mit sechs Plätzen, gruppiert danach, wie ihre Terme kommutieren: die Hopping-Terme auf geraden Bindungen, die Hopping-Terme auf ungeraden Bindungen und die diagonale Wechselwirkung.

>>> 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

Fünfzehn Terme in drei Gruppen. Jede Hopping-Gruppe paart jeden Term mit seinem konjugierten Partner, und das ist wichtig: Ein Faktor muss hermitesch sein, damit seine Exponentialfunktion unitär ist, und das folgt nicht daraus, dass die Summe hermitesch ist. groups_are_hermitian() prüft diese Konvention, statt sie der Inspektion zu überlassen:

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

Würde man a0†a1a^\dagger_0 a_1 von seinem Partner a1†a0a^\dagger_1 a_0 trennen, wäre sie verletzt, und ein ungruppierter Operator hat dieselbe Wirkung, da beide Formeln ihn dann Term für Term aufteilen und ein einzelnes ai†aja^\dagger_i a_j nicht hermitesch ist:

>>> 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

Ein solcher Faktor ist ein gültiger fermionischer Operator, und dieses Paket baut das Gate dafür, sodass sich nichts beschwert, bis der abgebildete Operator PauliEvolutionGate erreicht, das reelle Koeffizienten verlangt und ValueError: Operator contains complex coefficients, which are not supported auslöst. Die Gruppierung vorab zu prüfen macht aus diesem späten Fehler eine Antwort, die verfügbar ist, bevor irgendein Circuit erstellt wird.

Die Methode für ein einzelnes Gate festlegen​

Übergib dem Gate eine Synthesemethode und dekomponiere es dann. Jedes Gate, das dabei entsteht, kommt als eigenes Evolution-Gate zurück:

>>> 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

Erste Ordnung erzeugt einen Faktor pro Gruppe. Zweite Ordnung erzeugt das symmetrische Palindrom, sodass die beiden äußeren Gruppen zweimal mit jeweils halber Zeit erscheinen, was fünf ergibt. Vier Wiederholungen davon würden zwanzig ergeben, aber jede Wiederholung endet mit derselben Gruppe, mit der die nächste beginnt. Diese benachbarten Gates entwickeln unter demselben Operator und werden daher zu einem Gate mit der summierten Zeit verschmolzen, was pro Naht ein Gate spart. Übergib merge_adjacent=False, um die Lehrbuchsequenz beizubehalten. Jede Ordnung teilt den Operator auf dieselbe Weise auf; was sich ändert, sind die Reihenfolge der Faktoren und ihre Evolutionszeiten, nicht die Partition.

Hinweis

Die Faktoren sind als atomic markiert, sodass erneutes Dekomponieren sie an Ort und Stelle lässt, statt sie weiter aufzuteilen. Die Dekomposition hat einen Fixpunkt, weshalb es sicher ist, decompose() wiederholt aufzurufen.

Die Methode für eine ganze Pipeline festlegen​

synthesis pro Gate zu setzen bedeutet, die Wahl durch alles hindurchzureichen, was eines erstellt, einschließlich UCC und UCJ, die ihre Gates intern konstruieren. Der F2FSynthesis-Pass wendet stattdessen eine Methode auf jedes Evolution in einem Circuit an, sodass die Wahl einmal für die Pipeline getroffen wird. Er wird über den Gate-Namen konfiguriert, genauso wie F2QSynthesis, und ein Gate, das nicht genannt wird, bleibt unverändert:

>>> 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

Das ist dieselbe Anzahl, die decompose() oben für diese Methode liefert. Der MergeEvolutions-Pass wendet dieselbe Verschmelzung auf benachbarte Gates an, die nicht aus einer Synthese stammen, etwa die Nähte zwischen aufeinanderfolgenden Circuits. Der Pass führt die Formel selbst aus, sodass kein separater Expansionsschritt nötig ist, und zweimaliges Ausführen ändert nichts, da die Faktoren, die er erzeugt, atomic sind und somit keine eigene Definition tragen.

Hinweis

Übergib apply=False, um die Methode nur auszuwählen und die Expansion etwas anderem zu überlassen, etwa Qiskits Decompose (die Angabe des Gates als Decompose("Evolution") beschränkt es auf die Evolutionen und überlässt die anderen fermionischen Gates ihren eigenen Synthese-Plugins).

Das ist nützlich, um die Methode für einen ganzen Circuit vorab zu wählen und die Expansion auf den Zeitpunkt zu verschieben, an dem sie gewünscht ist, zum Beispiel um den Circuit zuerst zu inspizieren:

>>> 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

Beachte aber, was ein nicht expandiertes Gate tut: Es erreicht die Fermion-zu-Qubit-Phase als Ganzes und wird dort abgebildet, ohne dass synthesis berücksichtigt wird. Das ist ein leicht zu machender Fehler beim Vergleich von Methoden, denn jede von ihnen liefert dann identische Ausgaben, und der Vergleich misst stillschweigend nichts. Beim obigen Hamiltonian schrumpft der viel größere Circuit unten, wenn das Gate zweiter Ordnung nicht expandiert wird, auf die Kosten einer einzelnen abgebildeten Evolution zusammen, egal welche Methode gewählt wurde.

Den Nutzen messen​

Eine höhere Ordnung kostet Tiefe, daher lautet die Frage nicht, ob sie genauer ist, sondern ob sie pro Gate genauer ist. Pro Methode werden zwei Zahlen benötigt: der Fehler des erzeugten Zustands und die Zwei-Qubit-Kosten des Circuits, der ihn erzeugt hat.

Der Fehler wird mit ffsim gemessen, das einen FermionicCircuit direkt simuliert (siehe Wie dieses Paket zu ffsim steht). Es ist keine Fermion-zu-Qubit-Abbildung beteiligt, sodass der Vergleich die fermionische Produktformel von allem isoliert, was die Abbildung oder die Synthese auf Qubit-Seite beitragen könnte. Außerdem arbeitet es im Sektor fester Teilchenzahl, der sowohl kleiner ist als der volle 2N2^N-Raum als auch der Raum, in dem die Evolution tatsächlich bleibt.

>>> 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

Die Zwei-Qubit-Kosten sind absichtlich aus dieser Tabelle ausgelassen. Sie hängen von der Synthese auf Qubit-Seite ab, die Qiskit auswählt und die sich zwischen Versionen verbessert, sodass die hier ausgegebenen Zahlen eine Momentaufnahme wären und keine Eigenschaft der Produktformel. Stattdessen stellt die Abbildung unten sie dar, und worauf der Vergleich beruht, wird als Relation geprüft:

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

Lies die letzten beiden Methoden zusammen, denn sie sind der entscheidende Vergleich. Zweite Ordnung mit vier Wiederholungen ist etwa zehnmal genauer als erste Ordnung mit zehn und erreicht das mit weniger statt mehr verschränkenden Gates, wie die Abbildung zeigt. Ein Gate-Budget in eine höhere Ordnung zu investieren ist hier besser, als es in mehr Schritte erster Ordnung zu investieren.

Dieses Ergebnis ist nicht allgemeingültig. Eine Erhöhung von reps und eine Erhöhung von order verringern den Fehler bei gleichen Kosten unterschiedlich schnell, und was gewinnt, hängt vom Operator und von der entwickelten Zeit ab. Die beiden Zeilen oben zeigen, wie du es für ein gegebenes Problem herausfindest.

Hinweis

Die fermionische Formel lässt sich mit der kombinieren, die für die Fermion-zu-Qubit-Phase gewählt wurde (siehe MapperFnEvolutionSynthesis.product_formula). Wo beide nähern, wird die Genauigkeit des Ergebnisses von der schwächeren der beiden bestimmt, sodass eine höhere Ordnung hier bei einer Formel erster Ordnung auf Qubit-Seite wenig bringt.

Den Pass auf die Gates beschränken, die profitieren​

Eine höhere Ordnung hilft nur, wenn die Faktoren nicht kommutieren. Kommutieren alle, ist die Formel erster Ordnung bereits exakt, und die zusätzlichen Faktoren sind reiner Overhead. Ein rein diagonaler Operator macht das konkret, da Teilchenzahloperatoren miteinander kommutieren:

>>> 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

Beide Circuits sind exakt, sodass der zweite 80 % mehr verschränkende Gates umsonst aufwendet. Die diagonalen Coulomb-Operatoren eines UCJ haben diese Form, weshalb es F2FSynthesis.filter gibt: Es nimmt ein Prädikat über den DAGOpNode entgegen und lässt abgelehnte Gates unangetastet, sodass ein Pass die Ordnung bei den Gates erhöhen kann, die profitieren, und den Rest bei erster Ordnung belässt.

Ein Circuit mit beiden Arten von Evolution zeigt, was das einspart. Das folgende Prädikat akzeptiert ein Gate nur, wenn sein Operator Teilchen zwischen Moden bewegt, also genau dann, wenn die Gruppen nicht kommutieren und eine höhere Ordnung etwas bringen kann:

>>> 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

Die Gates, die der Filter einspart, sind genau die, die zweite Ordnung für die diagonale Evolution aufgewendet hat und die der obige diagonale Vergleich mit acht beziffert (18 gegenüber 10). Sie brachten dort nichts, weil diese Evolution bereits bei erster Ordnung exakt ist, sodass ihr Wegfall keine Genauigkeit kostet.

Hinweis

Vergleiche die beiden Circuits zweiter Ordnung miteinander, nicht mit dem nicht expandierten. Wenn beide Gates nicht expandiert bleiben, geht jedes als Ganzes in die Fermion-zu-Qubit-Phase, was der Fehler ist, den die obige Anmerkung zu apply=False beschreibt: Er kann bei derselben Anzahl verschränkender Gates wie der gefilterte Circuit landen und dabei ein anderer Circuit mit anderer Tiefe sein, sodass der Nutzen des Filters an dieser Zahl abzulesen nichts misst.

Vorsicht

Keine der beiden Stellschrauben prüft, ob die Faktoren hermitesch sind, und ein falsch gruppierter Operator kann bei gerader Ordnung besser aussehen als bei erster Ordnung und trotzdem falsch sein, weil die Symmetrisierung den Fehler teilweise aufhebt. Gruppiere konjugierte Partner zusammen, wie es fermi_hubbard_chain oben tut.

Nächste Schritte​