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?

Präparation von Slater-Determinanten optimieren

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 InitializeModes-Gate deklariert eine Referenz-Modenbesetzung; ein OrbitalRotation-Gate rotiert die Einteilchenbasis. Hintereinander platziert, präparieren die beiden Gates eine Slater-Determinante; die Moden starten in der Referenzdeterminante und werden dann in die Zielorbitale rotiert. Das PrepareSlaterDeterminant-Gate fasst diese Komposition zusammen, und weil es die Referenzbesetzung mitführt, kann sein Transpilieren die rechteckige givens_decomposition_slater() verwenden, die nur die besetzten Orbitale realisiert, statt der vollständigen quadratischen Zerlegung, die eine OrbitalRotation allein erfordert. Das ist eine streng günstigere Synthese (siehe den Nutzen am Ende dieses Leitfadens).

Der Transpiler-Pass MergeSlaterDeterminantPreparation erkennt das Muster InitializeModes gefolgt von OrbitalRotation in einem FermionicCircuit und schreibt es in PrepareSlaterDeterminant-Gates um, sodass eine spätere Synthesestufe die reduzierte Zerlegung automatisch verwendet. In der Simulation ist das Umschreiben für den Zustand ein No-op: PrepareSlaterDeterminant ist validate-then-rotate (es validiert die Referenzbesetzung und wendet dann die Rotation an, genau wie die zwei getrennten Gates), sodass das Zusammenführen die günstigere Synthese freischaltet, ohne den präparierten Zustand zu verändern.

In diesem Leitfaden wird jeder Circuit vor und nach dem Pass nebeneinander gezeichnet: die Eingabe links und das Ergebnis des Passes darauf rechts. Zwei Hilfsfunktionen übernehmen das. merge führt den Pass auf einer Kopie des Circuits aus (über dessen FermionicDAGCircuit) und wandelt das Ergebnis zurück in einen zeichenbaren FermionicCircuit, und draw_merge zeichnet beide Hälften in eine einzige Vorher-nachher-Abbildung:

>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>>
>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>> from qiskit_fermions.transpiler import FermionicCircuitToDAG
>>> from qiskit_fermions.transpiler.converters import FermionicDAGToCircuit
>>> from qiskit_fermions.transpiler.passes import MergeSlaterDeterminantPreparation
>>>
>>> def merge(circuit):
... dag = FermionicCircuitToDAG().run(circuit)
... merged = MergeSlaterDeterminantPreparation().run(dag)
... return FermionicDAGToCircuit().run(merged)
>>>
>>> def natural_size(circuit):
... # the figure size Qiskit picks for a circuit on its own (discard the probe figure)
... probe = circuit.draw("mpl")
... size = probe.get_size_inches()
... plt.close(probe)
... return size
>>>
>>> def draw_merge(circuit):
... merged = merge(circuit)
... # let Qiskit size each half naturally, then lay them out side by side
... (bw, bh), (aw, ah) = natural_size(circuit), natural_size(merged)
... fig, (before, after) = plt.subplots(1, 2, figsize=(bw + aw, max(bh, ah)))
... circuit.draw("mpl", ax=before)
... merged.draw("mpl", ax=after)
... before.set_title("before")
... after.set_title("after")
... return fig

Was zusammengeführt wird​

Der Pass erkennt drei Muster, die alle auf der Block-Spin-Mode-Konvention beruhen, die InitializeModes und OrbitalRotation verwenden. Bei einem System mit norb räumlichen Orbitalen bilden die Moden 0..norb den Spin-Alpha-Sektor und norb..2*norb den Spin-Beta-Sektor. Die Präparation von Slater-Determinanten erfolgt pro Spin-Sektor, da jeder Sektor seine eigene Form (Elektronen, Orbitale) hat.

Vollständiges Register (oder spinlos)​

Das einfachste Muster ist ein InitializeModes, unmittelbar gefolgt von einer OrbitalRotation auf denselben Moden. Das ist der spinlose Fall (eine Determinante über alle Moden) und auch ein spinbehafteter Circuit, dessen Rotation beide Sektoren auf einmal umfasst. Die beiden Gates verschmelzen zu einem einzigen PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A mode initialization and rotation fusing into one Slater determinant preparation.

Pro Spin-Sektor​

Das ist dieselbe Form, aber auf eine Spin-Hälfte beschränkt. Hier verschmelzen zwei unabhängige Initialisieren-dann-Rotieren-Paare, eines pro Sektor, jeweils für sich zu einem PrepareSlaterDeterminant:

>>> circuit = FermionicCircuit(6) # norb = 3: alpha modes 0..3, beta modes 3..6
>>> circuit.append(InitializeModes([1, 1, 0]), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(InitializeModes([1, 0, 0]), circuit.modes[3:])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

Two per-sector initialize-and-rotate pairs, each fusing into its own preparation.

Globale Initialisierung, Rotationen pro Spin​

Das umfasst ein einzelnes InitializeModes über das vollständige Register (2 * norb), gefolgt von zwei OrbitalRotation-Aktionen, je eine auf jeder zusammenhängenden Spin-Hälfte. Der Pass teilt die globale Besetzung an der Sektorgrenze auf und erzeugt zwei PrepareSlaterDeterminant-Gates. Die beiden Rotationen können in beliebiger Reihenfolge auftreten.

Das ist die Form, die der Workflow des Local Unitary Cluster Jastrow (LUCJ) erzeugt (siehe den LUCJ-Leitfaden); Eine Nutzerin oder ein Nutzer setzt ein InitializeModes (typischerweise die Hartree-Fock-Referenz mittels from_hartree_fock()) an den Anfang des Circuits und hängt ein UCJ-Ansatz an, dessen erste zwei Orbitalrotationen pro Spin unmittelbar auf die Initialisierung folgen, sobald der Ansatz zerlegt ist.

>>> circuit = FermionicCircuit(6) # norb = 3, nelec = (2, 1)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[3:])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with two per-spin rotations fusing into two preparations.

Dieses Muster greift auch, wenn nur eine Spin-Hälfte von einem Gate rotiert wird, das direkt auf die Initialisierung folgt – die Form, die entsteht, wenn zufällig nur die Orbitalrotation eines Spin-Sektors am Anfang des Circuits steht. Die rotierte Hälfte wird mit ihrer Rotation zu einem PrepareSlaterDeterminant, und die andere Hälfte wird mit einer Identitätsrotation präpariert. Die Identitätspräparation wird zu den Referenz-X-Gates synthetisiert, die das InitializeModes für diese Hälfte ohnehin erzeugt hätte, sodass das Auffüllen keine zusätzlichen Gates kostet und dennoch die reduzierte Slater-Synthese auf der rotierten Hälfte freischaltet:

>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes.from_hartree_fock(3, (2, 1)), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A global initialization with a single per-spin rotation fusing into two preparations.

Was unberührt bleibt​

Der Pass ist bewusst konservativ. Er verschmilzt nur, wenn die beiden Gates tatsächlich zu einer Slater-Determinanten-Präparation komponieren, und kopiert alles andere unverändert durch. „Unmittelbar gefolgt“ wird über den Datenflussgraphen des Circuits verstanden. Die OrbitalRotation verschmilzt nur, wenn das InitializeModes über alle ihre Moden hinweg ihr einziger Vorgänger ist. In den Abbildungen unten sind Vorher und Nachher identisch: Nichts wurde verschmolzen.

Eine Operation dazwischen​

Wenn irgendein Gate die Moden zwischen der Initialisierung und der Rotation berührt, komponieren sie nicht mehr zu einer reinen Präparation, sodass nichts zusammengeführt wird:

>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, ann, cre
>>>
>>> circuit = FermionicCircuit(4)
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> number_op = FermionOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> circuit.append(Evolution(4, number_op, time=0.5), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(4)), circuit.modes)
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

An operation between the initialization and rotation blocks the merge.

Eine partielle Rotation, die keine Spin-Hälfte ist​

Eine Rotation, die genau eine zusammenhängende Spin-Hälfte einer Initialisierung des vollständigen Registers abdeckt, verschmilzt tatsächlich (siehe den vorigen Abschnitt). Eine Rotation auf einer anderen partiellen Modenmenge (weniger Moden als die Initialisierung und keine ihrer beiden Spin-Hälften, etwa ein Teilbereich, der die Sektorgrenze überspannt) ist jedoch keine Präparation pro Sektor. Ein Zusammenführen würde stillschweigend die Rolle der Initialisierung auf den ausgelassenen Moden verwerfen, daher lässt der Pass beide Gates an ihrem Platz:

>>> circuit = FermionicCircuit(4) # norb = 2: the only spin halves are modes [0, 1] and [2, 3]
>>> circuit.append(InitializeModes([1, 1, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(np.eye(3)), circuit.modes[:3])
>>> draw_merge(circuit)
<Figure size ... with 2 Axes>

A partial rotation that is not a spin half is left unmerged.

Warum das wichtig ist: der Synthesegewinn​

Das Zusammenführen lohnt sich, weil PrepareSlaterDeterminant zu weit weniger Gates synthetisiert wird als die OrbitalRotation, die es aufnimmt. Eine mm-Elektronen-Determinante über nn Orbitale benötigt höchstens m(n−m)m(n-m) Zwei-Qubit-Rotationen und keine diagonalen Phasen-Gates, gegenüber den n(n−1)/2n(n-1)/2 Rotationen plus nn Phasen der vollständigen quadratischen Orbitalrotation.

Der voreingestellte Jordan-Wigner-Pass-Manager führt MergeSlaterDeterminantPreparation in seiner Optimierungsstufe aus, sodass diese Reduktion automatisch geschieht. Transpiliere eine Präparation mit zwei Elektronen und sechs Orbitalen bis zu einem QuantumCircuit und zähle seine Gates:

Hinweis

Die Voreinstellung führt MergeOrbitalRotations unmittelbar vor MergeSlaterDeterminantPreparation aus. Das ist wichtig, wenn eine Folge aufeinanderfolgender OrbitalRotation-Gates auf die Initialisierung folgt. Der frühere Pass fasst die Folge zunächst zu einer einzigen Rotation zusammen, die MergeSlaterDeterminantPreparation dann mit der Initialisierung zu einem PrepareSlaterDeterminant kontrahiert. Ohne diese Reihenfolge würde nur die erste Rotation der Folge aufgenommen, und der Rest würde mit der vollständigen quadratischen Zerlegung synthetisiert.

>>> from qiskit_fermions.transpiler.presets import generate_preset_jw_pass_manager
>>>
>>> # a reproducible random 6 x 6 orbital rotation
>>> rng = np.random.default_rng(1)
>>> z = rng.standard_normal((6, 6)) + 1j * rng.standard_normal((6, 6))
>>> q, r = np.linalg.qr(z)
>>> rotation = q * (np.diagonal(r) / np.abs(np.diagonal(r)))
>>>
>>> circuit = FermionicCircuit(6)
>>> circuit.append(InitializeModes([1, 1, 0, 0, 0, 0]), circuit.modes)
>>> circuit.append(OrbitalRotation(rotation), circuit.modes)
>>>
>>> pm = generate_preset_jw_pass_manager()
>>> prepared = pm.run(circuit)
>>> print(dict(sorted(prepared.count_ops().items())))
{'x': 2, 'xx_plus_yy': 8}
>>> prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized merged Slater determinant preparation.

Vergleiche das mit der Synthese derselben Orbitalrotation für sich allein. Ohne vorangehende Initialisierung gibt es nichts zusammenzuführen, sodass die Voreinstellung auf die vollständige quadratische Zerlegung zurückfällt, die tiefer ist und diagonale Phasen-Gates enthält:

>>> reference = FermionicCircuit(6)
>>> reference.append(OrbitalRotation(rotation), reference.modes)
>>> reference_prepared = pm.run(reference)
>>> print(dict(sorted(reference_prepared.count_ops().items())))
{'rz': 6, 'xx_plus_yy': 15}
>>> reference_prepared.draw("mpl", fold=-1)
<Figure size ... with 1 Axes>

The synthesized bare orbital rotation, with its extra phase gates.

Die reduzierte Zerlegung lässt alle sechs Phasen-Gates weg und halbiert die Anzahl der Zwei-Qubit-Rotationen nahezu. Die präparierte Determinante ist bis auf eine globale Phase korrekt (physikalisch irrelevant für die Zustandspräparation), die die Phasen-Gates festgelegt hätten.

Zwei Wege, das Gate zu zerlegen​

Ein PrepareSlaterDeterminant lässt sich auf zwei verschiedene Arten zerlegen, die nicht austauschbar sind:

decompose()​

Expandiert das Gate in seine Definition: das InitializeModes der Referenz gefolgt von der vollständigen OrbitalRotation. Das ist für jeden Eingangszustand äquivalent zum Gate als Operator.

PrepareSlaterDeterminant.synthesis, angewendet von F2FSynthesis​

Ersetzt das Gate durch die reduzierte Zerlegung aus diesem Leitfaden. Das ist nur als Zustandspräparation äquivalent zum Gate: Es erzeugt dieselbe Determinante, bis auf eine globale Phase, wenn die Moden im Vakuum starten. Es ist gerade deshalb günstiger, weil es das annimmt.

Verwende die Synthese, wenn das Gate einen Zustand auf dem Vakuum präpariert, wofür das Gate gedacht ist und was der Transpiler für dich übernimmt. Verwende decompose(), wenn das Gate weiterhin als Operator wirken muss, etwa weil andere Gates auf denselben Moden davor stehen, oder wenn du die einfachen Gates untersuchen oder simulieren möchtest. Der Pass F2FSynthesis kann nicht wissen, welcher Fall vorliegt, daher liegt es an dem Circuit, den du ihm gibst, die Determinante aus dem Vakuum zu präparieren.

Die Simulation eines Circuits mit ffsim braucht keines von beidem: Jedes Gate wendet seine gesamte Unitäre an, sodass ein Circuit mit einem PrepareSlaterDeterminant so simuliert wird, wie er ist, und für die umgebenden OrbitalRotation-Gates auf dem schnellen Pfad bleibt. Auch das Zerlegen nur des Slater-Gates mit circuit.decompose("PrepareSlaterDeterminant") lässt diese Rotationen ganz.

Siehe auch

PrepareSlaterDeterminant, InitializeModes, OrbitalRotation, MergeOrbitalRotations, F2FSynthesis, FermionicSlaterGivensDecomposition und der LUCJ-Leitfaden für den Workflow, der das Muster der globalen Initialisierung erzeugt.