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?

Arbeiten mit fermionischen Circuits

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.

Dieser Leitfaden erklärt, wie du den FermionicCircuit verwendest, um Quantenalgorithmen direkt im fermionischen Raum zu implementieren, statt zuerst auf Qubits abzubilden.

Warum fermionische Circuits verwenden​

Betrachte den herkömmlichen Workflow zur Implementierung der Zeitentwicklung eines fermionischen Hamiltonians auf einer Qubit-basierten Architektur:

  1. Definiere den fermionischen Hamiltonian (zum Beispiel H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Bilde ihn auf einen Qubit-Operator ab (zum Beispiel mit der Jordan-Wigner-Abbildung).

  3. Implementiere die Zeitentwicklung im Qubit-Raum (mit Trotterisierung oder anderen Methoden).

Dieser Ansatz hat eine grundlegende Einschränkung: Die frühe Abbildung auf Qubits verwirft problemspezifisches Wissen über die fermionische Struktur. Standard-Optimierungs-Passes können fermionische Symmetrien oder erlaubte Umordnungen von Termen nicht ausnutzen, was zu suboptimalen Circuits mit unnötiger Tiefe und Gate-Anzahl führt.

Der FermionicCircuit ermöglicht einen besseren Workflow:

  1. Definiere den fermionischen Hamiltonian (zum Beispiel H=∑pqhpqcp†cqH = \sum_{pq} h_{pq} c^\dagger_p c_q).

  2. Implementiere die Zeitentwicklung direkt im fermionischen Raum mit fermionischen Gates.

  3. Bilde im Rahmen des Transpilationsprozesses auf den Qubit-Raum ab.

Dieser Ansatz behebt die Einschränkung, indem das problemspezifische Wissen im fermionischen Raum erhalten bleibt, wo Optimierungs-Passes die fermionische Struktur und Kommutationsrelationen ausnutzen können, um die Circuit-Tiefe zu verringern. Der fermionische Circuit beschreibt, welche Berechnung ausgeführt wird, während die Transpilation übernimmt, wie sie auf Qubits abgebildet wird. So entsteht eine sauberere Trennung der Zuständigkeiten.

Generische Mode-Indizierung​

Sowohl das Modul operators als auch der FermionicCircuit verwenden eine generische, Mode-basierte Indizierung, die keine Annahmen über die Natur der Moden trifft. Eine Mode ist einfach ein abstrakter Index, der einen fermionischen Freiheitsgrad bezeichnet, ohne inhärente Semantik. Diese Entwurfsentscheidung entspricht dem Operatoren-Modul und sorgt für maximale Flexibilität. Du kannst zum Beispiel die Zeitentwicklung für jeden Operator implementieren, der das Protokoll OperatorTrait umsetzt, unabhängig von seiner mathematischen Darstellung.

Einen fermionischen Circuit erstellen​

Das folgende Beispiel implementiert die im vorherigen Abschnitt beschriebene Zeitentwicklung. Es erstellt einen FermionicCircuit, indem es die Anzahl der fermionischen Moden angibt, und fügt dann fermionische Gates aus qiskit_fermions.circuit.library hinzu, um die Zeitentwicklung zu implementieren.

Das Beispiel zeigt außerdem, wie du mit der Gruppierung von Operatortermen Domänenwissen in einen Operator einbringst. Indem du den Termen des Hamiltonians Gruppenindizes zuweist, bewahrst du Strukturinformationen, die Optimierungs-Passes im gesamten Transpiler-Stack ausnutzen können.

>>> from qiskit_fermions.circuit import FermionicCircuit
>>> from qiskit_fermions.circuit.library import Evolution
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Create a circuit with 4 fermionic modes
>>> circuit = FermionicCircuit(4)
>>>
>>> # Define a simple fermionic Hamiltonian
>>> hamiltonian = FermionOperator.from_terms([
... ([cre(0), ann(2)], 0.5),
... ([cre(2), ann(0)], 0.5),
... ([cre(1), ann(3)], 0.5),
... ([cre(3), ann(1)], 0.5),
... ])
>>> # Add some problem structure by grouping our Hamiltonian terms
>>> hamiltonian.groups = [0, 0, 1, 1]
>>>
>>> # Add an evolution gate to implement exp(-i * t * H)
>>> evolution = Evolution(4, hamiltonian, time=1.0)
>>> circuit.append(evolution, circuit.register)

A simple `FermionicCircuit` with a single time evolution gate.

A decomposed `FermionicCircuit` with several time evolution gates.

Beachte, wie die Gruppierung der Operatorterme selbst bei einfachen Operationen wie der Dekomposition erhalten bleibt. Das zeigt, wie Strukturinformationen durch den Circuit-Stack fließen. Um die vollständige Transpilation auf Qubits zu verstehen, lies den Leitfaden Fermionische Circuits transpilieren.

Gates mit einer Barrier trennen​

Die Optimierungs-Passes verschmelzen benachbarte Gates, wo immer sie können. Zum Beispiel fasst MergeOrbitalRotations eine Folge aufeinanderfolgender OrbitalRotation-Gates zu einem zusammen, und MergeSlaterDeterminantPreparation faltet ein InitializeModes in die darauffolgende Rotation.

Verwende FermionicCircuit.barrier(), wenn du das verhindern möchtest. Eine Barrier hat keine unitäre Wirkung und verändert daher den Zustand nicht, den dein Circuit vorbereitet; sie markiert lediglich eine Stelle, über die die Passes nicht hinweg verschmelzen. Rufe sie ohne Argumente auf, um alle Moden abzudecken, oder übergib die Moden, auf die sie beschränkt werden soll:

>>> from qiskit_fermions.circuit.library import OrbitalRotation
>>> import numpy as np
>>>
>>> rotations = FermionicCircuit(4)
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)
>>> rotations.barrier()
>>> rotations.append(OrbitalRotation(np.eye(4, dtype=complex)), rotations.register)

A `FermionicCircuit` with a barrier separating two orbital rotations.

Barriers bleiben bei der Fermion-zu-Qubit-Transpilation erhalten und schränken daher auch die anschließende Optimierung auf Qubit-Ebene ein. Der Leitfaden Fermionische Circuits transpilieren behandelt diese Phase.

Einen Circuit wiederholen​

Viele Algorithmen wenden denselben Gate-Block viele Male an. Die Zeitentwicklung ist der typische Fall: Ein Trotter-Schritt wird einmal erstellt und dann wiederholt, um die Zielzeit zu erreichen. Verwende repeat(), um den wiederholten Circuit zu erstellen, und compose(), um ihn mit einem Zustandspräparations-Präfix zu verbinden.

Das folgende Beispiel erstellt einen Trotter-Schritt zweiter Ordnung für ein Fermi-Hubbard-Modell. Der Schritt ist symmetrisch: Er teilt den Hopping-Term in zwei OrbitalRotation-Gates mit halber Dauer auf, die die On-Site-Wechselwirkung umschließen.

>>> import scipy.linalg
>>> from qiskit_fermions.circuit.library import InitializeModes, OrbitalRotation
>>>
>>> norb, reps, time = 2, 3, 1.0
>>> num_modes = 2 * norb
>>> dt = time / reps
>>>
>>> hopping = np.zeros((norb, norb))
>>> hopping[0, 1] = hopping[1, 0] = -1.0
>>> interaction = FermionOperator.from_dict(
... {(cre(p), ann(p), cre(p + norb), ann(p + norb)): 2.0 for p in range(norb)}
... )
>>>
>>> def rotation(duration):
... propagator = scipy.linalg.expm(-1j * duration * hopping)
... # the same rotation acts on the alpha and beta halves of the register
... return scipy.linalg.block_diag(propagator, propagator)
>>>
>>> trotter_step = FermionicCircuit(num_modes)
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)
>>> trotter_step.append(
... Evolution(num_modes, interaction, dt, atomic=True), trotter_step.modes
... )
>>> trotter_step.append(OrbitalRotation(rotation(dt / 2)), trotter_step.modes)

Den Schritt zu wiederholen und auf eine Hartree-Fock-Referenz zu komponieren ergibt die vollständige Zeitentwicklung:

>>> evolution = FermionicCircuit(num_modes)
>>> evolution.append(InitializeModes([True, False, True, False]), evolution.modes)
>>> evolution.compose(trotter_step.repeat(reps), inplace=True)
>>> evolution.count_ops()["OrbitalRotation"]
6

A `FermionicCircuit` with repeated steps.

Die Wiederholungen werden in den zurückgegebenen Circuit hineingeglättet, statt in einem einzelnen Gate verpackt zu werden, sodass count_ops() zwei Rotationen pro Schritt zählt. Dieses Glätten macht die Redundanz an den Schrittgrenzen für den Transpiler sichtbar: Die abschließende Rotation halber Dauer eines Schritts und die einleitende des nächsten liegen nebeneinander, sodass MergeOrbitalRotations sie zu einer einzigen Rotation zusammenfasst.

>>> from qiskit_fermions.transpiler import FermionicPassManager
>>> from qiskit_fermions.transpiler.passes import MergeOrbitalRotations
>>>
>>> merged = FermionicPassManager([MergeOrbitalRotations()]).run(evolution)
>>> merged.count_ops()["OrbitalRotation"]
4

A `FermionicCircuit` with repeated steps and merged orbital rotations.

Das Ergebnis kostet eine Rotation pro Schritt plus eine, was dem Ergebnis entspricht, wenn du diese halben Schritte von Hand verschmilzt. Wenn du den Schritt in seiner natürlichen symmetrischen Form schreibst und wiederholst, entsteht derselbe Circuit, sodass die Verschmelzung der Optimierungsphase überlassen bleibt.

Wenn du die Wiederholungen getrennt halten musst, übergib insert_barriers=True, um zwischen ihnen eine barrier() zu setzen. Die Optimierungs-Passes verschmelzen keine Gates über eine Barrier hinweg, sodass jede Rotation erhalten bleibt:

>>> separated = trotter_step.repeat(reps, insert_barriers=True)
>>> FermionicPassManager([MergeOrbitalRotations()]).run(separated).count_ops()[
... "OrbitalRotation"
... ]
6

A `FermionicCircuit` with repeated steps separated by barriers.

Hinweis

Das Argument reps von FermionicSuzukiTrotter ist ein anderes Konzept. Es unterteilt ein einzelnes Evolution-Gate in mehr, kürzere Faktoren, während repeat() ganze Circuits dupliziert. Das Wiederholen eines Circuits mit einem einzelnen Evolution vervielfacht lediglich dessen gesamte Evolutionszeit.

Fermionische Moden messen​

measure_all() liest jede Mode in ein eigenes klassisches Bit aus und ordnet Mode i dem Bit i zu. Verwende measure() mit deinem eigenen ClassicalRegister, um nur einige davon auszulesen.

>>> measured = trotter_step.measure_all(inplace=False)
>>> measured.count_ops()["measure"]
4

Die Übergabe von inplace=False gibt eine gemessene Kopie zurück und lässt das Original unverändert. Das ist wichtig, weil eine Messung nicht unitär ist: Ein Circuit mit einer Messung kann nicht mehr auf einen Zustandsvektor angewendet werden. Bewahre daher einen ungemessenen Circuit auf, wenn du ihn auch simulieren möchtest.

Hier zu messen, statt am transpilierten QuantumCircuit, hält die gesampelten Bitstrings gut lesbar. Eine fermionische Messung benennt die Mode, die sie ausliest, sodass jede Phase, die Moden verschiebt (RelabelModes auf fermionischer Ebene, Layout und Routing auf Qubit-Ebene), die Messung mitführt und der Bitstring nach der Mode indiziert bleibt, nach der du gefragt hast.

Hinweis

Anders als bei measure_all() wird vor den Messungen keine barrier() eingefügt. Rufe barrier() zuerst selbst auf, wenn du dort eine haben möchtest.

Ob ein gemessenes Bit das bedeutet, was du erwartest, hängt von der Fermion-zu-Qubit-Kodierung ab, die bei der Transpilation gewählt wird und nicht hier. Bei einer Kodierung in der Besetzungsbasis wie Jordan-Wigner wird jede Mode von einem Qubit getragen, und das Bit ist die Besetzung dieser Mode; siehe FermionicMeasure.

Fermionische Circuits transpilieren​

Um den Quantenalgorithmus umzusetzen, den dein FermionicCircuit darstellt, muss er in einen QuantumCircuit transpiliert werden. Wie das geht, erfährst du im Leitfaden Fermionische Circuits transpilieren.

Nächste Schritte​