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:
- PYTHON
- C
>>> 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)>
// The C API provides direct mapping functions for common transformations.
// Custom mapper prototyping as shown in Python is primarily a Python feature.
// For custom mappings in C, you will need to perform the iteration and
// conversion logic entirely by yourself.
Die wichtigsten Schritte zur Implementierung eines beliebigen benutzerdefinierten Mappers sind:
-
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.
-
Eine der bereitgestellten Mapping-Funktionen verwenden: Das Modul
mappersstellt Hilfsfunktionen bereit, die das Iterieren über die Operatorterme und die anschließende Anwendung der benutzerdefinierten Aktionsabbildung für die vom Moduloperatorsbereitgestellten Operatordarstellungen übernehmen.map_majorana_action_generators()funktioniert zum Beispiel mit Majorana-Operatoren. -
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.
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:
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
// Erstelle einen MajoranaOperator mit denselben Termen wie maj_op oben
uint64_t num_terms = 2;
uint64_t num_modes = 3;
uint32_t modes[3] = {0, 1, 2};
QkComplex64 coeffs[2] = {{1.0, 0.0}, {0.5, 0.0}};
uint32_t boundaries[3] = {0, 2, 3};
QfMajoranaOperator *maj_op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
// Konvertiere den MajoranaOperator mit dem Bibliotheks-Mapper in einen FermionOperator
QfFermionOperator *ferm_op = qf_majorana_to_fermion(maj_op);
// Wende den Jordan-Wigner-Mapper an, um den Qubit-Operator zu erhalten
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_jordan_wigner(ferm_op, 2, &qubit_op);
assert(exit == QfExitCode_Success);
// Aufräumen
qf_ferm_op_free(ferm_op);
qf_maj_op_free(maj_op);
qk_obs_free(qubit_op);
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 -Strings wachsen mit dem Mode-Index, sodass ein Majorana-Operator auf der letzten von Moden auf einen Pauli-String mit Gewicht abgebildet wird. Kodierungen, die die Paritätsinformation statt über eine Kette über einen Baum verteilen, schneiden asymptotisch besser ab.
Ein ternärer Baum mit 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 beschrifteten Verbindung, die von Knoten abwärts führt, trägt auf Qubit 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.
- PYTHON
- C
>>> 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
#include <qiskit_fermions.h>
QfTernaryTree *chain;
qf_ternary_tree_chain(13, QfPauliLabel_Z, &chain);
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *balanced;
qf_ternary_tree_breadth_first(13, 3, order, &balanced);
assert(qf_ternary_tree_max_weight(chain) == 13);
assert(qf_ternary_tree_max_weight(balanced) == 3);
qf_ternary_tree_free(chain);
qf_ternary_tree_free(balanced);
Dieses maximale Gewicht ist optimal. Jiang et al. beweisen, dass die Majorana-Operatoren jeder Fermion-zu-Qubit-Abbildung von Moden ein durchschnittliches Pauli-Gewicht von mindestens haben, sodass keine Abbildung das größte davon unter bringen kann. Beide Bäume verwenden genau 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.
- PYTHON
- C
>>> 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)>
// Create the hopping Hamiltonian a^dagger_0 a_3 + a^dagger_3 a_0
QfFermionOperator *hamiltonian = qf_ferm_op_zero();
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t indices[4] = {0, 3, 3, 0};
for (int i = 0; i < 2; i++) {
qf_ferm_op_add_term(hamiltonian, 2, actions + 2 * i, indices + 2 * i, &coeffs[i]);
}
QfPauliLabel order[3] = {QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z};
QfTernaryTree *tree;
qf_ternary_tree_breadth_first(4, 3, order, &tree);
// Kompiliere den Baum einmal und verwende die Kodierung dann für jeden damit abgebildeten Operator wieder.
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
QkObs *qubit_op;
QfExitCode exit = qf_ferm_op_ternary_tree(hamiltonian, encoding, 4, &qubit_op);
assert(exit == QfExitCode_Success);
// Aufräumen
qf_ferm_op_free(hamiltonian);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(qubit_op);
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)>
Einige Identitäten, die unter Jordan-Wigner gelten, sind Eigenschaften der Kette und nicht ternärer Bäume. Der Teilchenzahloperator ist nur auf der Kette; auf einem anderen Baum wird aus ein -String über eine Menge von Qubits. Ebenso ist ein Vertex-Operator nur auf der Kette das Pauli-Gewicht-1-Element , wo sich die -Ketten der beiden Majorana-Strings aufheben.
Da jeder Teilchenzahloperator weiterhin auf einen einzelnen -String abgebildet wird, ist ein Fock-Zustand unter jedem ternären Baum weiterhin ein Zustand der Rechenbasis, aber Bit gibt nicht mehr die Besetzung wieder. Die Besetzung ist stattdessen die Parität der gemessenen Bits auf dem Träger des Bilds von . 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.
- PYTHON
- C
>>> # 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}
// Node 0, the root, branches three ways, node 1 twice, and node 4 carries a single child.
// The root has no parent and no incoming link, so its entries are never read: any value works.
#define IGNORED 0
uint32_t parents[7] = {IGNORED, 0, 0, 0, 1, 1, 4};
QfPauliLabel labels[7] = {
IGNORED, QfPauliLabel_X, QfPauliLabel_Y, QfPauliLabel_Z,
QfPauliLabel_X, QfPauliLabel_Z, QfPauliLabel_Y,
};
QfTernaryTree *tree;
QfExitCode exit = qf_ternary_tree_new(7, 0, parents, labels, &tree);
assert(exit == QfExitCode_Success);
assert(qf_ternary_tree_num_nodes(tree) == 7);
assert(qf_ternary_tree_num_legs(tree) == 15);
assert(qf_ternary_tree_max_weight(tree) == 4);
qf_ternary_tree_free(tree);
Beachte die Blattanzahl: Sie beträgt immer , 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 Strings werden zu den Majorana-Operatoren der Moden gepaart, und genau einer bleibt übrig.
Gesamtparität
Dieses übrig gebliebene Blatt ist die fermionische Gesamtparität . Sie ist eine Eigenschaft der Kodierung und keine Abbildung irgendeines Operators, daher ist sie eine Methode der Kodierung selbst:
- PYTHON
- C
>>> 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)>
QfTernaryTree *tree;
qf_ternary_tree_chain(4, QfPauliLabel_Z, &tree);
QfTernaryTreeEncoding *encoding;
qf_ternary_tree_encoding_new(tree, NULL, &encoding);
// Auf der Kette ist dies der vollständige Z-String; auf dem balancierten Baum nicht.
QkObs *parity;
qf_ternary_tree_encoding_total_parity(encoding, 4, &parity);
qf_ternary_tree_encoding_free(encoding);
qf_ternary_tree_free(tree);
qk_obs_free(parity);
Wie der balancierte Fall zeigt, ist dies im Allgemeinen nicht der reine -String: Es ist das Blatt, das über den reinen -Pfad erreicht wird, und es wirkt als Identität auf jedem Knoten, der nicht auf diesem Pfad liegt.
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 -Sektor: Eine Ternary-Tree-Kodierung verbraucht ein Qubit pro Mode und ist surjektiv auf den gesamten -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 die größte hineinpassende Zweierpotenz; der für diesen Bereich platzierte Knoten ist , 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 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 :
>>> 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 -Pfad erreicht wird, und hier endet dieser Pfad an der Wurzel, sodass die Observable das Einzel-Qubit- 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]