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?

Operatordarstellungen abbilden

Mapper sind Routinen, die Operatoren von einer Darstellung in eine andere überführen und dabei die mathematische Äquivalenz bewahren. Nutze sie, um flexibel über verschiedene Operatoralgebren hinweg zu arbeiten und Operatoren für die Ausführung von Quantenschaltkreisen vorzubereiten.

Eigene Mapper implementieren​

Das Mapper-Modul macht es einfach, eigene Abbildungen zu implementieren. Mapper arbeiten, indem sie die fundamentalen Aktionen (Erzeugung, Vernichtung, Majorana usw.), aus denen ein Operator besteht, transformieren und diese transformierten Aktionen dann nach den Regeln der Zieldarstellung kombinieren.

Als konkretes Beispiel betrachten wir eine Jordan-Wigner-Transformation, die einen MajoranaOperator mit map_majorana_action_generators() auf einen Qubit-Operator abbildet:

>>> from qiskit.quantum_info import SparseObservable
>>> from qiskit_fermions.mappers import map_majorana_action_generators
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Create a Majorana operator
>>> maj_op = MajoranaOperator.from_dict({(0, 1): 1.0, (2,): 0.5})
>>>
>>> # Define how each Majorana action maps to Pauli strings
>>> num_qubits = 2
>>> def map_action(mode: int) -> SparseObservable:
... idx = mode // 2
... qubits = list(range(idx + 1))
... pauli = "Y" if mode % 2 else "X"
... return SparseObservable.from_sparse_list(
... [("Z" * idx + pauli, qubits, 1.0)],
... num_qubits=num_qubits,
... )
>>>
>>> # Apply the mapping to transform the operator
>>> custom_observable = map_majorana_action_generators(
... maj_op,
... map_action,
... identity=lambda: SparseObservable.identity(num_qubits),
... compose=SparseObservable.compose,
... )
>>> print(custom_observable.simplify())
<SparseObservable with 2 terms on 2 qubits: (0+1j)(Z_0) + (0.5+0j)(X_1 Z_0)>

Die wichtigsten Schritte zur Implementierung eines beliebigen benutzerdefinierten Mappers sind:

  1. Aktionstransformation definieren: Lege fest, wie jede Aktion (indiziert nach Mode) auf die Zieldarstellung abgebildet wird. Im obigen Beispiel werden Majorana-Aktionen nach den Jordan-Wigner-Regeln auf Pauli-Strings abgebildet.

  2. Eine der bereitgestellten Mapping-Funktionen verwenden: Das Modul mappers stellt Hilfsfunktionen bereit, die das Iterieren über die Operatorterme und die anschließende Anwendung der benutzerdefinierten Aktionsabbildung für die vom Modul operators bereitgestellten Operatordarstellungen übernehmen. map_majorana_action_generators() funktioniert zum Beispiel mit Majorana-Operatoren.

  3. Ergebnis erhalten: Die Mapping-Funktion gibt einen Operator in der Zieldarstellung zurück, bei dem die Transformation konsistent auf alle Terme angewendet wurde.

Dieses Design macht es einfach, benutzerdefinierte Abbildungen zu prototypisieren, ohne die internen Operatorstrukturen genau zu kennen. Die Transformation wird automatisch auf jeden Term des Operators angewendet und erhält dabei die mathematische Äquivalenz.

Hinweis

Du kannst die Terme eines Operators auch manuell iterieren, statt eine der bereitgestellten Iterator-Funktionen zu verwenden. Details findest du im Abschnitt zur Term-Iteration im Operator-Leitfaden.

Bibliotheksimplementierungen​

Das Modul qiskit_fermions.mappers.library stellt effiziente, gründlich getestete Implementierungen gängiger Abbildungen bereit. Sie folgen demselben Grundmuster wie benutzerdefinierte Mapper, sind aber für den Produktiveinsatz optimiert und sowohl in der Python- als auch in der C-API verfügbar.

Das folgende Beispiel wendet einen Bibliotheks-Mapper auf denselben MajoranaOperator aus dem Abschnitt zum benutzerdefinierten Mapper an und zeigt äquivalente Ergebnisse:

>>> from qiskit_fermions.mappers.library import majorana_to_fermion, jordan_wigner
>>>
>>> # Convert MajoranaOperator to FermionOperator using library mapper
>>> ferm_op = majorana_to_fermion(maj_op)
>>>
>>> # Apply Jordan-Wigner mapper to get qubit operator
>>> sparse_observable = jordan_wigner(ferm_op, num_qubits=2)
>>> difference = sparse_observable - custom_observable
>>> print(difference.simplify() == SparseObservable.zero(num_qubits))
True
Hinweis

Der obige Zweischritt ist ausgeschrieben, um zu zeigen, wie zwei Bibliotheks-Mapper zusammenwirken. In der Praxis würdest du einen MajoranaOperator stattdessen in einem Schritt abbilden, mit majorana_jordan_wigner() (qf_maj_op_jordan_wigner() in der C-API), und ebenso für die anderen Operatortypen – edge_vertex_jordan_wigner() und transfer_vertex_jordan_wigner() (qf_edge_op_jordan_wigner(), qf_transfer_op_jordan_wigner()). Der Umweg über einen FermionOperator bläht außerdem das Zwischenergebnis auf: Jeder Generator dieser Algebren wird auf einen einzelnen Pauli-String abgebildet, während jede fermionische Aktion auf eine Summe aus zwei Termen abgebildet wird, sodass ein aus mehreren Generatoren aufgebauter Term erst expandiert und dann wieder zusammengeführt wird. Diese Ersparnis wächst mit der Länge der Terme; bei den Einzelgenerator-Termen eines Hopping-Hamiltonians kosten beide Wege etwa gleich viel.

In der Python-API sind einige dieser Bibliotheks-Mapper auch über typagnostische Komfort-Mapper erreichbar. fermion_operator() und majorana_operator() stehen vor majorana_to_fermion() und fermion_to_majorana() (unter anderem) und delegieren intern anhand des Typs des übergebenen Objekts an die passende Bibliotheksimplementierung, sodass du die richtige Funktion nicht selbst auswählen musst. Dasselbe gilt für jordan_wigner() und für ternary_tree() (weiter unten eingeführt); beide verteilen auf die direkte Implementierung, die zum übergebenen Operator passt. Wie diese Verteilung intern umgesetzt ist, steht unter qiskit_fermions.protocols.

Eine Kodierung mit ternären Bäumen wählen​

Jordan-Wigner ist nicht die einzige Fermion-zu-Qubit-Kodierung, und sie ist nicht die günstigste. Ihre ZZ-Strings wachsen mit dem Mode-Index, sodass ein Majorana-Operator auf der letzten von nn Moden auf einen Pauli-String mit Gewicht nn abgebildet wird. Kodierungen, die die Paritätsinformation statt über eine Kette über einen Baum verteilen, schneiden asymptotisch besser ab.

Ein ternärer Baum mit nn Knoten erfasst diese gesamte Familie. Jeder Knoten ist ein Qubit mit drei abwärts gerichteten Verbindungen, die mit X, Y und Z beschriftet sind; eine Verbindung führt entweder zu einem Kindknoten (eine Kante) oder endet (ein Blatt). Der Pfad jedes Blatts zurück zur Wurzel ergibt einen Pauli-String: Das Überqueren der mit PP beschrifteten Verbindung, die von Knoten uu abwärts führt, trägt PP auf Qubit uu bei. Jeder solche Baum liefert eine gültige Kodierung, und die bekannten Kodierungen sind einfach bestimmte Formen – eine Kette entlang Z ist Jordan-Wigner, während ein Baum, in dem jeder Knoten drei Kinder hat, soweit die Knotenzahl es erlaubt, balanciert ist.

>>> from qiskit_fermions.mappers import TernaryTree, TernaryTreeEncoding
>>>
>>> # Jordan-Wigner is a chain descending along Z, with X and Y as legs at every node.
>>> TernaryTree.chain(13, "Z").max_weight
13
>>> # The balanced tree spreads the same information over depth log_3(2n+1).
>>> TernaryTree.breadth_first(13, 3).max_weight
3

Dieses maximale Gewicht ist optimal. Jiang et al. beweisen, dass die Majorana-Operatoren jeder Fermion-zu-Qubit-Abbildung von nn Moden ein durchschnittliches Pauli-Gewicht von mindestens log⁡3(2n)\log_3(2n) haben, sodass keine Abbildung das größte davon unter ⌈log⁡3(2n+1)⌉\lceil \log_3(2n+1) \rceil bringen kann. Beide Bäume verwenden genau nn Qubits, die Ersparnis liegt also im Pauli-Gewicht, nicht in der Qubit-Anzahl.

Um einen Operator abzubilden, kompilierst du einen Baum zu einer Kodierung und übergibst diese zusammen mit der Qubit-Anzahl für das Ergebnis an einen Mapper. Beim Kompilieren werden die Blätter des Baums zu Majorana-Operatoren gepaart und ihre Pauli-Strings einmalig aufgebaut, daher sollte dieselbe Kodierung für viele Operatoren wiederverwendet werden. Die Qubit-Anzahl gehört zum Mapping-Aufruf und nicht zur Kodierung, sodass eine Kodierung Register unterschiedlicher Breite bedienen kann; eine Anzahl oberhalb der Knotenzahl des Baums wird mit der Identität aufgefüllt.

>>> from qiskit_fermions.mappers.library import fermion_ternary_tree
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> hamiltonian = FermionOperator.from_dict(
... {((True, 0), (False, 3)): 1.0, ((True, 3), (False, 0)): 1.0}
... )
>>> encoding = TernaryTreeEncoding(TernaryTree.breadth_first(4, 3))
>>> fermion_ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>

Die Abbildung über eine Kette entlang Z reproduziert fermion_jordan_wigner() (qf_ferm_op_jordan_wigner() in der C-API) exakt, und in diesem Sinne verallgemeinert sie diese:

>>> from qiskit_fermions.mappers.library import fermion_jordan_wigner
>>> chain = TernaryTreeEncoding(TernaryTree.chain(4, "Z"))
>>> direct = fermion_jordan_wigner(hamiltonian, 4)
>>> (fermion_ternary_tree(hamiltonian, chain, 4) - direct).simplify().num_terms
0

Jede Operatordarstellung hat einen Ternary-Tree-Mapper, analog zur Jordan-Wigner-Gruppe; die vollständige Liste findest du unter qiskit_fermions.mappers.library und Mapper Library in der C-API. In der Python-API sind sie auch über ternary_tree() erreichbar, den typagnostischen Wrapper, der den richtigen für den übergebenen Operator auswählt, genau wie jordan_wigner():

>>> from qiskit_fermions.mappers.library import ternary_tree
>>> ternary_tree(hamiltonian, encoding, 4).simplify()
<SparseObservable with 2 terms on 4 qubits: (0.5+0j)(Y_3 Z_1 Y_0) + (0.5+0j)(X_3 Z_2 X_0)>
Hinweis

Einige Identitäten, die unter Jordan-Wigner gelten, sind Eigenschaften der Kette und nicht ternärer Bäume. Der Teilchenzahloperator ist nj=(1−Zj)/2n_j = (1 - Z_j)/2 nur auf der Kette; auf einem anderen Baum wird aus ZjZ_j ein ZZ-String über eine Menge von Qubits. Ebenso ist ein Vertex-Operator VjV_j nur auf der Kette das Pauli-Gewicht-1-Element ZjZ_j, wo sich die ZZ-Ketten der beiden Majorana-Strings aufheben.

Da jeder Teilchenzahloperator weiterhin auf einen einzelnen ZZ-String abgebildet wird, ist ein Fock-Zustand unter jedem ternären Baum weiterhin ein Zustand der Rechenbasis, aber Bit qq gibt nicht mehr die Besetzung qq wieder. Die Besetzung jj ist stattdessen die Parität der gemessenen Bits auf dem Träger des Bilds von njn_j. Lies diese Träger aus dem Mapper ab, statt sie aus dem Baum abzuleiten, damit die Dekodierung für jede Form oder Mode-Permutation korrekt bleibt:

>>> for mode in range(4):
... number = FermionOperator.from_dict({((True, mode), (False, mode)): 1.0})
... print(mode, ternary_tree(number, encoding, 4).simplify())
0 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2 Z_1 Z_0)>
1 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_1)>
2 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_2)>
3 <SparseObservable with 2 terms on 4 qubits: (0.5+0j)() + (-0.5+0j)(Z_3)>

Auf diesem balancierten Baum ist Mode 0 besetzt, wenn die Qubits 0, 1 und 2 eine ungerade Anzahl von Einsen enthalten. Das Präparieren bedeutet also, nur Qubit 0 zu flippen, während das Dekodieren eines gemessenen Bitstrings die Parität über alle drei Bits bildet. Ein einzelner Bitflip-Fehler auf Qubit 1 verfälscht dann die Besetzungen von Mode 0 und Mode 1, was für jeden Workflow wichtig ist, der Besetzungen aus verrauschten Samples ausliest.

Wenn eine Evolution über einen solchen Mapper heruntergebrochen wird, umhülle den Mapper mit group_wise() (siehe Synthesize an Evolution gate in fermionic space). Ein Hopping-Term wird auf zwei kommutierende Pauli-Strings abgebildet, von denen keiner für sich die Teilchenzahl erhält; nur ihr Produkt in der Evolution tut das. Unter Jordan-Wigner wirken beide Strings auf dieselben Qubits, sodass eine vereinfachte Termreihenfolge sie benachbart hält. Unter anderen Bäumen unterscheiden sich ihre Träger (wie bei Y_3 Z_1 Y_0 und X_3 Z_2 X_0 weiter oben), die rohe und die vereinfachte Reihenfolge verschachteln sie beide mit den Bildern anderer Terme, und eine Produktformel auf der Qubit-Seite lässt dann Teilchenzahl entweichen.

Einen beliebigen Baum aufbauen​

Die Konstruktoren für Ketten und Breitensuche decken die uniformen Familien ab, aber die allgemeine Schnittstelle nimmt ein (parent, label)-Paar pro Knoten entgegen, mit None für die Wurzel (die C-API nimmt stattdessen den Index der Wurzel). Das ist wichtig, weil die Bäume, nach denen sich zu suchen lohnt, meist weder eine Kette noch uniform sind – der Bonsai-Algorithmus zum Beispiel lässt einen Spannbaum des Kopplungsgraphen eines Geräts wachsen, sodass seine Verzweigung dieser Konnektivität folgt.

>>> # Node 0 branches three ways, node 1 twice, and node 4 carries a single child.
>>> tree = TernaryTree([None, (0, "X"), (0, "Y"), (0, "Z"), (1, "X"), (1, "Z"), (4, "Y")])
>>> tree.num_nodes, tree.num_legs, tree.max_weight
(7, 15, 4)
>>> tree.children(0)
{'X': 1, 'Y': 2, 'Z': 3}

Beachte die Blattanzahl: Sie beträgt immer 2n+12n+1, unabhängig von der Form. Ein Knoten mit weniger Kindern erzeugt nicht weniger Pauli-Strings – seine ungenutzten Verbindungen sind Blätter, und ein Blatt ergibt einen String ebenso wie die Nachkommen einer Kante. Von diesen 2n+12n+1 Strings werden 2n2n zu den Majorana-Operatoren der nn Moden gepaart, und genau einer bleibt übrig.

Gesamtparität​

Dieses übrig gebliebene Blatt ist die fermionische Gesamtparität ∏j(1−2nj)\prod_j (1 - 2 n_j). Sie ist eine Eigenschaft der Kodierung und keine Abbildung irgendeines Operators, daher ist sie eine Methode der Kodierung selbst:

>>> TernaryTreeEncoding(TernaryTree.chain(4, "Z")).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_2 Z_1 Z_0)>
>>> TernaryTreeEncoding(TernaryTree.breadth_first(4, 3)).total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3 Z_0)>

Wie der balancierte Fall zeigt, ist dies im Allgemeinen nicht der reine ZZ-String: Es ist das Blatt, das über den reinen ZZ-Pfad erreicht wird, und es wirkt als Identität auf jedem Knoten, der nicht auf diesem Pfad liegt.

Wichtig

Die Gesamtparität ist hier eine zu messende Observable und keine aufzuerlegende Nebenbedingung. Beide ihrer Eigenwerte sind physikalisch, projiziere einen Zustand also nicht auf den +1+1-Sektor: Eine Ternary-Tree-Kodierung verbraucht ein Qubit pro Mode und ist surjektiv auf den gesamten 2n2^n-dimensionalen Raum, sodass es keinen unphysikalischen Unterraum gibt, der wegprojiziert werden müsste.

Durchgerechnetes Beispiel: ein Bravyi-Kitaev-Baum​

Spezifikationen werden häufiger berechnet als von Hand geschrieben, da der Baum einer realen Kodierung selten klein genug ist, um ihn von Hand aufzuschreiben. Bravyi-Kitaev ist ein gutes Beispiel: Sie gehört zu dieser Familie, aber ihr Baum ist nicht uniform, sodass kein Komfort-Konstruktor sie abdeckt.

Ihr Baum entsteht durch eine rekursive Aufteilung über die Knoten, einen pro Mode. Sei innerhalb eines Knotenbereichs [l,r][l, r] hh die größte hineinpassende Zweierpotenz; der für diesen Bereich platzierte Knoten ist l+h−1l + h - 1, der letzte in diesem Block. Seine X-Verbindung führt zu dem aus dem Block davor aufgebauten Baum und seine Z-Verbindung zu dem aus dem Rest danach aufgebauten Baum, wobei seine Y-Verbindung ein Blatt bleibt:

>>> def bravyi_kitaev_tree(num_modes: int) -> TernaryTree:
... """Builds the ternary tree of the Bravyi-Kitaev encoding on ``num_modes`` modes."""
... spec: list[tuple[int, str] | None] = [None] * num_modes
... def build(left: int, right: int) -> int | None:
... if left > right:
... return None # an empty range: the parent's link stays a leg
... block = 1
... while 2 * block <= right - left + 1:
... block *= 2
... node = left + block - 1
... for child, label in (
... (build(left, node - 1), "X"), # the block before this node
... (build(node + 1, right), "Z"), # the remainder after it
... ):
... if child is not None:
... spec[child] = (node, label)
... return node
... build(0, num_modes - 1)
... return TernaryTree(spec)

Bei vier Moden ist der gesamte Bereich ein einzelner Block, sodass der zuerst platzierte Knoten 3 ist, mit [0,2][0, 2] davor und nichts danach; dieser Teilbereich ergibt Knoten 1 über den einzelnen Knoten 0 und 2. Das Kompilieren des Baums bestätigt, dass die Kodierung Bravyi-Kitaev ist und nicht bloß ein Baum derselben Formklasse – ihr erstes Majorana-Bild ist X0X1X3X_0 X_1 X_3:

>>> tree = bravyi_kitaev_tree(4)
>>> tree.children(1)
{'X': 0, 'Z': 2}
>>> encoding = TernaryTreeEncoding(tree)
>>> encoding.majorana_image(0)
[(0, 'X'), (1, 'X'), (3, 'X')]

Ihre Gesamtparität macht den Punkt des vorherigen Abschnitts konkret: Das übrig gebliebene Blatt ist das, das über den reinen ZZ-Pfad erreicht wird, und hier endet dieser Pfad an der Wurzel, sodass die Observable das Einzel-Qubit-Z3Z_3 ist und kein String über alle vier Qubits:

>>> encoding.total_parity(4)
<SparseObservable with 1 term on 4 qubits: (1+0j)(Z_3)>

Das Hochskalieren erfordert nichts weiter als ein größeres Argument. Da kein Knoten je seine Y-Verbindung für eine Kante verwendet, verzweigt jeder höchstens zweifach, sodass das Pauli-Gewicht logarithmisch statt linear wächst:

>>> [bravyi_kitaev_tree(n).max_weight for n in (4, 8, 16, 32, 64)]
[3, 4, 5, 6, 7]