Synthese eines Evolutionsgates im fermionischen Raum
Die in diesem Leitfaden beschriebenen Konzepte sind derzeit nur in der „ Python “-API verfügbar. Eine entsprechende Funktionalität wird in einer zukünftigen Version in der C-API zur Verfügung stehen.
Ein Gatter Evolution überträgt die Zeitentwicklung eines gesamten Operators. Bei einer Fermion-zu-Fermion-Synthese wird dieses Gatter in eine FermionicCircuit Reihe kleinerer Fermionengatter umgeschrieben. Beide Seiten der Umformulierung verbleiben im Fermionenraum, was sie von der nachfolgenden Fermion-zu-Qubit-Synthese unterscheidet: Die Operatoren ( groups siehe „Gruppenoperatorterme: Verwendung der Operatorstruktur“ ) bleiben dabei erhalten, während eine Fermion-zu-Qubit-Abbildung ein Ergebnis liefert SparseObservable, bei dem es kein Konzept der Gruppierung gibt.
Die Methode wird über zwei Drehregler ausgewählt. Evolution.synthesis stellt es auf ein Gatter ein, und der FermionicTrotterization Transpiler-Durchlauf wendet es auf eine gesamte Schaltung an. Beide nehmen eine FermionicEvolutionSynthesis, die Schnittstelle, die eine solche Umschreibung implementiert. Die Methoden, die dieses Paket derzeit bereitstellt, sind Produktformeln, auf die im weiteren Verlauf dieses Leitfadens zurückgegriffen wird: FermionicLieTrotter für eine Formel erster Ordnung (Standard) und FermionicSuzukiTrotter für eine Formel höherer gerader Ordnung. Alles andere, was diese Schnittstelle implementiert, wird auf dieselbe Weise ausgewählt, und die Erläuterungen zu den beiden Reglern gelten unverändert auch hierfür.
Dieser Schritt von Fermion zu Fermion ist optional. Ein -Gatter Evolution kann direkt in die Fermion-zu-Qubit-Stufe übergehen, die es vollständig abbildet und synthetisiert, unabhängig davon, wie viele Terme es enthält. Die Aufteilung in einen ersten Schritt ist eine Entscheidung, die getroffen wird, weil die daraus resultierenden Gatter einzeln kostengünstiger sind oder weil durch die Aufteilung eine Struktur sichtbar wird, die in einer späteren Phase genutzt werden kann.
Die beiden Schritte ergänzen sich, sodass eine Entwicklung entweder auf einer der beiden Ebenen, auf beiden oder auf keiner der beiden annähernd dargestellt werden kann. Eine Produktformel liefert nur eine Näherung, sofern ihre Faktoren nicht kommutieren; eine Synthesemethode muss hingegen überhaupt keine Näherung liefern.
Wenn man diesen Schritt überspringt, muss das nicht zwangsläufig bedeuten, dass die Gruppierung komplett verloren geht. Durch das Einbetten des Mappers in wird der Operator gruppenweise abgebildet group_wise() und die Ergebnisse summiert, sodass die abgebildeten Pauli-Terme gruppiert und nicht verschachtelt vorliegen. Eine Qubit-Seitenproduktformel synthetisiert sie in der Reihenfolge, in der sie empfangen werden, sodass kommutierende Terme, die nebeneinander liegen, gemeinsam verarbeitet werden können.
Zum folgenden Hamilton-Operator, der den gesamten Vorteil der Aufteilung in Bezug auf die Tiefe bei zwei Qubits wiederherstellt: 18 statt 30 bei unveränderter Gate-Anzahl. Der Unterschied liegt darin, wo die Struktur zum Ausdruck kommt. Durch die Aufteilung im fermionischen Raum wird jede Gruppe zu einem separaten Gatter, das jede nachfolgende Stufe erkennen und bearbeiten kann; es group_wise bleibt ein Gatter übrig, und es werden lediglich die Terme darin neu angeordnet, was für die Planung ausreicht, aber nichts übrig lässt, womit ein fermionischer Durchlauf arbeiten könnte. Verwende sie, wenn die Entwicklung auf ein einziges Tor beschränkt bleiben muss; andernfalls greife auf eine fermionische Produktformel zurück.
Erstellen Sie einen gruppierten Hamilton-Operator
Beide Produktformeln zerlegen einen Operator in Faktoren, und bei einem gruppierten Operator sind diese Faktoren seine Gruppen. Die Gruppierung ist daher ausschlaggebend sowohl für die Kosten als auch für die Genauigkeit des Ergebnisses. Betrachten wir eine Fermi-Hubbard-Kette mit sechs Knoten, deren Terme entsprechend ihrer Kommutationsbeziehung gruppiert sind: die Sprungterme auf den geraden Bindungen, die Sprungterme auf den 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()
3Fünfzehn Begriffe in drei Gruppen. Jede Hopping-Gruppe ordnet jedem Term seinen konjugierten Partner zu, was von Bedeutung ist: Ein Faktor muss hermitisch sein, damit sein Exponential unitar ist, und das folgt nicht daraus, dass die Summe hermitisch ist. groups_are_hermitian() überprüft diese Konvention, anstatt sie der Inspektion zu überlassen:
>>> from qiskit_fermions.operators.terms.grouping import groups_are_hermitian
>>>
>>> groups_are_hermitian(hamiltonian)
[True, True, True]Die Trennung von „ “ von seinem Partner „ “ würde die Gleichung zerstören, und wenn man den Operator ungruppiert lässt, hat dies denselben Effekt, da beide Formeln die Gleichung dann Term für Term aufspalten und ein einzelnes „ “ nicht hermitisch 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))
FalseEin solcher Faktor ist ein gültiger fermionischer Operator, und dieses Paket erstellt das entsprechende Gatter dafür, sodass keine Fehlermeldung auftritt, bis der abgebildete Operator den Wert erreicht PauliEvolutionGate, der reelle Koeffizienten erfordert und den Wert erhöht ValueError: Operator contains complex coefficients, which are not supported. Durch die Überprüfung der Gruppierung im Vorfeld wird dieser späte Fehler zu einer Lösung, die bereits vor dem Aufbau einer Schaltung verfügbar ist.
Stellen Sie die Methode an einem Tor ein
Übergebe dem Gate eine Synthesemethode und zerlege sie anschließend. Jedes von ihm ausgesendete Gate kehrt als eigenes Gate Evolution 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))
20Bei der ersten Ordnung wird pro Gruppe ein Faktor ausgegeben. Die Anordnung „zwei“ ergibt das symmetrische Palindrom, sodass die beiden äußeren Gruppen jeweils zweimal in der Hälfte der Zeit vorkommen, was insgesamt fünf ergibt. Viermal wiederholt ergibt das zwanzig. Jeder Befehl teilt den Operator auf dieselbe Weise auf; was sich ändert, ist die Reihenfolge der Faktoren und ihre Auswertungszeiten, nicht die Aufteilung.
Da die Faktoren markiert sind atomic, bleiben sie bei einer erneuten Zerlegung an ihrer Stelle, anstatt weiter aufgeteilt zu werden. Die Zerlegung hat einen Fixpunkt, weshalb sie bedenkenlos wiederholt decompose() aufgerufen werden kann.
Die Methode für eine gesamte Pipeline festlegen
Die Einstellung „pro synthesis Gate“ bedeutet, dass die Auswahl auf alle Elemente angewendet wird, aus denen ein Gate besteht, einschließlich und UCC UCJ, die ihre Gates intern aufbauen. Der Durchlauf FermionicTrotterization wendet stattdessen eine Methode auf jedes Element in Evolution einem Schaltkreis an, sodass die Auswahl für die Pipeline nur einmal getroffen wird:
>>> 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"]
20Zwanzig Tore: fünf Faktoren pro Durchgang zweiter Ordnung, mal vier Wiederholungen (dies ergibt decompose() dieselbe Anzahl wie bei der oben beschriebenen Methode). Der Durchlauf führt die Formel selbst aus, sodass kein separater Entwicklungsschritt erforderlich ist, und eine zweimalige Ausführung ändert nichts, da die dabei erzeugten Faktoren sind atomic und somit keine eigene Definition besitzen.
Verwenden Sie „Pass“, um apply=False lediglich die Methode auszuwählen und die Erweiterung einer anderen Komponente zu überlassen, beispielsweise Qiskit ( Decompose durch die Benennung des Gatters als „restricts Decompose("Evolution") it to the evolutions“ werden die anderen fermionischen Gatter ihren eigenen Synthese-Plugins überlassen).
Man sollte sich jedoch bewusst sein, was ein nicht expandiertes Gate bewirkt: Es gelangt vollständig in die „Fermion-zu-Qubit“-Phase, wo es abgebildet wird, ohne dass es abgefragt synthesis wird. Das führt leicht zu Fehlern beim Vergleich von Methoden, da jede einzelne von ihnen dann identische Ergebnisse liefert und der Vergleich stillschweigend nichts misst. Bei dem oben dargestellten Hamilton-Operator führt das Nicht-Entfalten des Gatters zweiter Ordnung dazu, dass die weitaus umfangreichere Schaltung unten auf die Kosten einer einzigen abgebildeten Entwicklung reduziert wird, unabhängig davon, welche Methode gewählt wurde.
Den Nutzen messen
Eine höhere Ordnung erfordert eine größere Tiefe; daher geht es nicht darum, ob sie genauer ist, sondern ob sie pro Gatter genauer ist. Pro Methode werden zwei Zahlen benötigt: der Fehler des dadurch erzeugten Zustands und die Zwei-Qubit-Kosten des Schaltkreises, der ihn erzeugt hat.
Der Fehler wird mit ffsim gemessen, das einen **direkt **FermionicCircuit simuliert (siehe „Wie dieses Paket mit ffsim zusammenhängt “). Da keine Abbildung von Fermionen auf Qubits stattfindet, isoliert der Vergleich die Fermionen-Produktformel von allen möglichen Einflüssen, die durch die Abbildung oder die Synthese auf der Qubit-Seite entstehen könnten. Es funktioniert auch im Bereich mit fester Teilchenzahl, der sowohl kleiner ist als der gesamte Raum „ “ als auch als der Raum, in dem sich die Entwicklung tatsächlich abspielt.
>>> 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.000421Die Kosten für zwei Qubits wurden in dieser Tabelle bewusst nicht aufgeführt. Das hängt von der von Qiskit gewählten Qubit-seitigen Synthese ab, die von Version zu Version verbessert wird, sodass die hier angegebenen Zahlen eher eine Momentaufnahme darstellen als eine Eigenschaft der Produktformel. In der folgenden Abbildung sind sie stattdessen dargestellt, und worauf der Vergleich beruht, wird als Beziehung angegeben:
Lies dir die letzten beiden Methoden gemeinsam durch, denn sie bilden den entscheidenden Vergleich. Die Ordnung 2 mit vier Wiederholungen ist etwa zehnmal genauer als die Ordnung 1 mit zehn, und dies wird, wie die Abbildung zeigt, mit weniger statt mit mehr Verschränkungsgattern erreicht. Hier ist es sinnvoller, das Gate-Budget für eine höhere Ordnung auszugeben, als es für weitere Schritte erster Ordnung zu verwenden.
Dieses Ergebnis gilt nicht in allen Fällen. „Increasing“ und reps „Increasing“ order reduzieren den Fehler bei gleichen Kosten in unterschiedlichem Maße, und welche Variante sich durchsetzt, hängt vom Betreiber und vom jeweiligen Entwicklungszeitpunkt ab. In den beiden Zeilen oben wird beschrieben, wie man dies bei einem bestimmten Problem herausfindet.
Die Fermionenformel lässt sich mit derjenigen kombinieren, die für die Fermion-zu-Qubit-Stufe gewählt wurde (siehe MapperFnEvolutionSynthesis.product_formula). Wenn beide Näherungen zum Tragen kommen, wird die Genauigkeit des Ergebnisses durch die schwächere der beiden bestimmt, sodass eine Erhöhung der Ordnung hier – während die qubit-seitige Formel in erster Ordnung bleibt – kaum einen Vorteil bringt.
Den Zugang auf die Tore beschränken, die davon profitieren
Eine höhere Ordnung ist nur dann hilfreich, wenn die Faktoren nicht kommutieren. Wenn sie alle pendeln, ist die Formel erster Ordnung bereits exakt, und die zusätzlichen Faktoren stellen reinen Mehraufwand dar. Ein rein diagonaler Operator verdeutlicht dies, da sich Zahlenoperatoren 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))
18Beide Schaltungen sind exakt, daher verbraucht die zweite 80 % mehr Verschränkungsgatter, ohne dass dies einen Nutzen bringt. Die diagonalen Coulomb-Operatoren von haben UCJ diese Form, weshalb FermionicTrotterization.filter existiert: Es wendet ein Prädikat auf die an und DAGOpNode lässt die abgelehnten Gatter unberührt, sodass durch einen Durchlauf die Ordnung der Gatter, die davon profitieren, erhöht werden kann, während die übrigen in erster Ordnung bleiben.
Ein Schaltkreis, der beide Arten der Entwicklung vereint, zeigt, was dadurch eingespart wird. Das folgende Prädikat akzeptiert ein Gatter nur dann, wenn sein Operator Teilchen zwischen Moden verschiebt; genau dann sind die Gruppen nicht kommutativ und eine höhere Ordnung hat etwas zu sagen:
>>> 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]
TrueDie Gates, die der Filter einspart, sind genau diejenigen, für die „Order Two“ bei der diagonalen Entwicklung aufgewendet hat – was der obige diagonale Vergleich mit acht bewertet (18 gegenüber 10). Sie haben dort nichts gekauft, da diese Entwicklung bereits in erster Näherung exakt ist, sodass der Verzicht darauf keine Genauigkeitseinbußen mit sich bringt.
Vergleiche die beiden Schaltungen zweiter Ordnung miteinander und nicht mit der nicht erweiterten Schaltung. Wenn beide Gatter nicht erweitert werden, gelangt jedes Gatter vollständig in die Fermion-zu-Qubit-Stufe – genau das ist der Fehler, der in der obigen Anmerkung apply=False beschrieben wird: Es kann zwar dieselbe Anzahl an Verschränkungsgattern ergeben wie der gefilterte Schaltkreis, ist aber ein anderer Schaltkreis mit einer anderen Tiefe, sodass die Ableitung des Nutzens des Filters aus dieser Zahl keine Aussage liefert.
Keiner der beiden Regler überprüft, ob die Faktoren hermitisch sind, und ein falsch gruppierter Operator kann bei einer geraden Ordnung besser aussehen als in erster Ordnung, obwohl er immer noch falsch ist, da die Symmetrisierung den Fehler teilweise aufhebt. Fassen Sie konjugierte Partner zu Gruppen zusammen, wie es oben fermi_hubbard_chain geschehen ist.
Nächste Schritte
- Lesen Sie die Bedingungen für Gruppenoperatoren: Verwenden Sie die Operatorstruktur, um festzulegen, wie die Gruppen, nach denen diese Produktformeln aufgeteilt werden, zugewiesen werden.
- Lesen Sie den Artikel „Transpile fermionische Schaltungen“ zu den Phasen, die a
FermionicCircuitdurchläuft. - Siehe „Simulation der Fermi-Hubbard-Dynamik von 1D mit Flow-Sets“ für ein Beispiel, bei dem die Fermionenformel die Anzahl der Schritte liefert, die eine benutzerdefinierte Synthese auf Qubit-Ebene nicht bereitstellen kann.
- Weitere Informationen zu
synthesisden Synthesemethoden selbst finden Sie hier, und hierQDriftTrotterizationfinden Sie eine randomisierte Alternative.