Entwurfsprinzipien von Operatordarstellungen
Dieser Leitfaden erklärt, wie ein BosonOperator aufgebaut ist und wie du damit arbeitest: wie du einen erstellst, wie du Operatoren untersuchst und kombinierst und wie du zwei Operatoren in eine Form bringst, in der sie verglichen werden können. Beginne hier, wenn du einen Hamiltonoperator aufschreibst.
Ein BosonOperator ist eine exakte Summe von Produkten bosonischer Erzeugungs- und Vernichtungsoperatoren mit komplexen Koeffizienten. Er enthält keinen Besetzungs-Cutoff. Eine bosonische Mode kann beliebig viele Bosonen enthalten, ihr Fock-Raum ist also unendlichdimensional, und eine Abschneidung tritt erst auf, wenn der Operator auf Qubits abgebildet wird (siehe Den Fock-Raum abschneiden). Alles in diesem Leitfaden geschieht daher in der exakten Algebra der kanonischen Vertauschungsrelationen,
Zwei Folgerungen prägen die Schnittstelle. Ein Modenindex kann innerhalb eines einzelnen Terms wiederkehren, weil ein gültiger Operator ist, der zwei Bosonen erzeugt. Außerdem führt das Vertauschen zweier Faktoren nie ein Vorzeichen ein, obwohl das Verschieben eines Vernichtungsoperators an einem Erzeugungsoperator derselben Mode vorbei einen Term hinzufügt, .
Speicherung
Ein Operator hält seine Terme in vier flachen Arrays, ähnlich dem Compressed-Sparse-Row-Format einer dünnbesetzten Matrix:
coeffs | Ein komplexer Koeffizient pro Term. |
actions | Ein Boolescher Wert pro Faktor: True für einen Erzeugungs- und False für einen Vernichtungsoperator. |
modes | Ein Modenindex pro Faktor. |
boundaries | Die Positionen in actions und modes, an denen jeder Term beginnt und endet. |
Der Konstruktor nimmt diese vier Arrays direkt entgegen. Der folgende Operator hat drei Terme, nämlich , und die Konstante , und du kannst die Arrays mit den get_*-Methoden zurücklesen:
>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>> op = BosonOperator(
... coeffs=[2.0, -1.0j, 0.5],
... actions=[True, False, True, True],
... modes=[0, 1, 1, 1],
... boundaries=[0, 2, 4, 4],
... )
>>> op.get_coeffs()
[(2+0j), (-0-1j), (0.5+0j)]
>>> op.get_actions()
[True, False, True, True]
>>> op.get_modes()
[0, 1, 1, 1]
>>> op.get_boundaries()
[0, 2, 4, 4]
>>> len(op)
3
Der erste Term umfasst die Positionen 0 bis 2 von actions und modes, der zweite 2 bis 4 und der dritte 4 bis 4; so wird der konstante Term, ein Term ohne Faktoren, gespeichert. Die wiederholte Mode 1 im zweiten Term bleibt wie geschrieben erhalten; ein Term wird nie verworfen oder zusammengeführt, nur weil ein Modenindex doppelt vorkommt.
Ein mit from_dict() erstellter Operator speichert seine Terme nicht in der Reihenfolge des Dictionarys, daher können seine Arrays die Terme von Lauf zu Lauf in anderer Reihenfolge auflisten. Der Operator selbst ist derselbe, und nichts unten, was Operatoren vergleicht oder ausgibt, hängt von der gespeicherten Reihenfolge ab.
Das Layout macht Konstruktion und Arithmetik günstig, mit einer Folge: Ein Operator kann denselben Term mehrfach gleichzeitig enthalten. Summen und Produkte hängen Terme an und führen sie nicht zusammen. Rufe daher simplify() auf, wenn du eindeutige Terme brauchst (siehe Arithmetik durchführen).
Operatoren erstellen
Der direkteste Weg, einen Operator zu erstellen, ist from_dict(), das ein Dictionary von einem Tupel von Faktoren auf einen Koeffizienten entgegennimmt. Die Funktionen cre() und ann() erzeugen die Faktoren, und das leere Tupel ist der konstante Term:
>>> b = BosonOperator.from_dict({(ann(0),): 1.0})
>>> b_dag = BosonOperator.from_dict({(cre(0),): 1.0})
>>> number = BosonOperator.from_dict({(cre(0), ann(0)): 1.0})
>>> print(format(number))
1.000000e0 +0.000000e0j * (+0 -0)
In der ausgegebenen Form steht +0 für und -0 für . Die Konstruktoren zero() und one() liefern das additive bzw. das multiplikative neutrale Element, und from_terms() baut einen Operator aus dem Iterator von iter_terms() wieder auf.
Die Operatorbibliothek stellt die dimensionslosen Orts- und Impulsquadraturen bereit,
als position() und momentum(). Zusammen mit den Kommutatorfunktionen desselben Moduls kannst du die Algebra direkt prüfen:
>>> from qiskit_bosons.operators.library import commutator, momentum, position
>>> q, p = position(0), momentum(0)
>>> q.is_hermitian() and p.is_hermitian()
True
>>> commutator(q, p).equiv(1j * BosonOperator.one())
True
>>> commutator(b, b_dag).equiv(BosonOperator.one())
True
Das Modul stellt außerdem anti_commutator() und double_commutator() bereit. Alle drei nehmen Operatoren desselben Typs entgegen und liefern einen neuen Operator zurück.
Auf die Reihenfolge eines Produkts achten
Zwei Operatoren lassen sich auf zwei Arten multiplizieren, und man verwechselt sie leicht. Der Operator @ ist das mathematische Produkt, also ist a @ b gleich . Der Operator & ist die Komposition, die Qiskits Konvention folgt, von links nach rechts zu lesen: a & b wendet zuerst a und dann b an, ist also . Die Kommutatorfunktionen verwenden @, also ist commutator(a, b) gleich . Wenn du die Klammer selbst mit & baust, kehrst du stillschweigend ihr Vorzeichen um:
>>> (b @ b_dag).equiv(b_dag @ b + BosonOperator.one())
True
>>> (b & b_dag).equiv(b_dag @ b)
True
>>> ((b @ b_dag) - (b_dag @ b)).equiv(BosonOperator.one())
True
>>> ((b & b_dag) - (b_dag & b)).equiv(-BosonOperator.one())
True
Einen Operator untersuchen
Ein Operator ist nicht direkt iterierbar, weil seine Terme in den oben genannten flachen Arrays liegen. Verwende stattdessen iter_terms(), das für jeden Term eine Liste von (is_creation, mode)-Paaren und einen Koeffizienten liefert. Der Iterator gibt Kopien zurück, sodass eine Änderung daran den Operator nicht verändert.
>>> hopping = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(2), cre(2), ann(2), ann(2)): 0.5,
... }
... )
>>> len(hopping)
3
>>> sorted(hopping.iter_terms())
[([(True, 0), (False, 1)], (-1+0j)), ([(True, 1), (False, 0)], (-1+0j)), ([(True, 2), (True, 2), (False, 2), (False, 2)], (0.5+0j))]
Mehrere Methoden fassen die Struktur eines Operators zusammen:
>>> hopping.get_support()
{0, 1, 2}
>>> hopping.min_num_modes()
3
>>> hopping.max_rank()
4
get_support() ist die Menge der Moden, auf die der Operator wirkt, min_num_modes() ist das kleinste Register, das ihn aufnehmen kann, und max_rank() ist die Länge seines längsten Terms.
Eine Mode ist ein abstrakter Index, der einen bosonischen Freiheitsgrad kennzeichnet. Sie hat keine eigene Bedeutung: Du bestimmst, für welchen Gitterplatz, welche Schwingungskoordinate oder welchen optischen Port eine Mode steht. Der BosonicCircuit verwendet dieselben Indizes (siehe Mit bosonischen Circuits arbeiten), sodass ein Operator und die Gates, die ihn tragen, sich darüber einig sind, was Mode 2 ist, ohne dass einer von beiden weiß, was sie darstellt. Nichts im Operator legt fest, wie viele Niveaus eine Mode hat, weshalb die Registergröße auch nur eine untere Schranke ist; min_num_modes() ist das kleinste Register, das den Operator aufnimmt, und keine Eigenschaft von ihm.
Die Besetzungsmethoden geben an, wie viele Bosonen ein einzelner Term in einer Mode erzeugen kann. Das ist eine untere Schranke für den Cutoff, den eine Abbildung braucht. Sie ist kein ausreichender Cutoff, weil wiederholte Anwendungen des Operators, etwa bei einer Zeitentwicklung, höhere Niveaus besetzen können (siehe Den Fock-Raum abschneiden):
>>> hopping.mode_occupations()
[1, 1, 2]
>>> hopping.mode_occupations([2, 0])
[2, 1]
>>> hopping.max_occupation()
2
Zwei weitere Prüfungen betrachten Erhaltungsgrößen. Ein Operator erhält die Teilchenzahl, wenn jeder Term ebenso viele Erzeugungs- wie Vernichtungsoperatoren hat. Übergibt man eine Liste von Blockgrößen, wird dieselbe Bedingung innerhalb jedes Blocks aufeinanderfolgender Moden geprüft:
>>> hopping.conserves_particle_number()
True
>>> move = BosonOperator.from_dict({(cre(0), ann(2)): 1.0})
>>> move.conserves_sector([3])
True
>>> move.conserves_sector([2, 1])
False
Schließlich benennt relabel_modes() Moden um: Mode i wird zu permutation[i]. Die Liste muss lang genug sein, um jede Mode des Operators zu indizieren, und darf keinen Eintrag wiederholen:
>>> print(format(move.relabel_modes([5, 6, 7])))
1.000000e0 +0.000000e0j * (+5 -7)
Arithmetik durchführen
Operatoren unterstützen Addition, Subtraktion, Negation sowie Multiplikation und Division mit einer Zahl, in der üblichen Operatorform oder an Ort und Stelle:
>>> total = 2 * b_dag + b_dag - 3j * b
>>> len(total)
3
>>> len(total.simplify())
2
>>> total.simplify().equiv(3 * b_dag - 3j * b)
True
Die drei Terme von total wurden nie zusammengeführt, der Operator enthielt also b_dag zweimal, bis simplify() sie zusammenfasste. Die Methode summiert die Koeffizienten identischer Terme und verwirft diejenigen, deren Gesamtbetrag unter einer Toleranz liegt. Es ist eine strukturelle Operation: Sie vergleicht die geschriebenen Faktoren und wendet die Vertauschungsrelationen nicht an. Die verwandte Methode ichop() entfernt kleine Koeffizienten an Ort und Stelle, ohne vorher zusammenzuführen. Rufe daher simplify() vor ichop() auf, wenn ein Operator doppelte Terme enthalten kann.
Produkte verwenden @ oder & wie oben beschrieben, und ** potenziert einen Operator mit einer ganzen Zahl:
>>> x = b_dag + b
>>> print(format((x ** 2).simplify()))
1.000000e0 +0.000000e0j * (-0 -0)
1.000000e0 +0.000000e0j * (-0 +0)
1.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0)
Das Ergebnis listet die vier Anordnungen wie geschrieben auf. Sie auf eine einzige Form zu reduzieren, erfordert die Vertauschungsrelationen, die der nächste Abschnitt anwendet.
Die Adjungierte kehrt die Reihenfolge der Faktoren in jedem Term um, vertauscht Erzeugungs- und Vernichtungsoperatoren und konjugiert die Koeffizienten. Die Methode is_hermitian() prüft, ob ein Operator gleich seiner Adjungierten ist:
>>> hop = BosonOperator.from_dict({(cre(0), ann(1)): 1j})
>>> print(format(hop.adjoint()))
0.000000e0 -1.000000e0j * (+1 -0)
>>> hop.is_hermitian()
False
>>> (hop + hop.adjoint()).is_hermitian()
True
Ein einzelner Hopping-Term ist nicht hermitesch, was relevant ist, wenn du Terme für eine Zeitentwicklung gruppierst: Jede Gruppe muss für sich hermitesch sein (siehe Operatorterme gruppieren: die Operatorstruktur nutzen).
Normalordnung und Operatoren vergleichen
Zwei Produkte von Leiteroperatoren können derselbe Operator sein und trotzdem verschieden aussehen, weil und in der Algebra gleich sind. In diesem Sinne gleiche Operatoren müssen nicht gleich gespeichert sein, daher braucht ihr Vergleich die Vertauschungsrelationen. Die Methode normal_ordered() wendet sie an: Sie verschiebt jeden Erzeugungsoperator links von jeden Vernichtungsoperator und fügt jedes Mal einen Term hinzu, wenn ein Paar derselben Mode vertauscht wird.
>>> product = BosonOperator.from_dict({(ann(0), cre(0), ann(0), cre(0)): 1.0})
>>> ordered = product.normal_ordered()
>>> len(ordered)
5
>>> print(format(ordered.simplify()))
1.000000e0 +0.000000e0j * ()
3.000000e0 +0.000000e0j * (+0 -0)
1.000000e0 +0.000000e0j * (+0 +0 -0 -0)
Der geordnete Operator wird als nicht zusammengeführte Summe zurückgegeben, mit fünf gespeicherten Termen, die nach dem Zusammenführen durch simplify() auf drei schrumpfen. Die Normalordnung lässt nie einen Term verschwinden: und jeder andere wiederholte Faktor bleiben erhalten.
Das Argument sandwich legt fest, wie die Moden innerhalb der beiden Faktorgruppen angeordnet werden. Standardmäßig werden beide Gruppen absteigend sortiert. Mit sandwich=True sind die Erzeugungsoperatoren aufsteigend und die Vernichtungsoperatoren absteigend sortiert, wodurch die größeren Indizes zur Mitte hin liegen:
>>> pair = BosonOperator.from_dict({(cre(0), cre(1), ann(0), ann(1)): 1.0})
>>> list(pair.normal_ordered().iter_terms())
[([(True, 1), (True, 0), (False, 1), (False, 0)], (1+0j))]
>>> list(pair.normal_ordered(sandwich=True).iter_terms())
[([(True, 0), (True, 1), (False, 1), (False, 0)], (1+0j))]
Der Operator == prüft die exakte Gleichheit der gespeicherten Terme, ohne Toleranz und ohne Vereinfachung, sodass er einen Unterschied zwischen zwei mathematisch gleichen Operatoren melden kann. Die Methode equiv() bringt zuerst die Differenz der beiden Operatoren in Normalordnung und prüft dann, ob jeder Koeffizient unter einer Toleranz liegt. Es ist der Vergleich, den du fast immer willst:
>>> reordered = BosonOperator.from_dict({(cre(0), ann(0)): 1.0, (): 1.0})
>>> contracted = BosonOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> contracted == reordered
False
>>> contracted.simplify() == reordered.simplify()
False
>>> contracted.equiv(reordered)
True
Die Methode simplify allein hält die beiden Formen auseinander, weil sie nur identische geschriebene Terme zusammenführt. Ebenso vergleicht is_hermitian() einen Operator mit seiner Adjungierten in Normalordnung, erkennt also ohne weiteren Aufwand als hermitesch.
Die Normalordnung ist in der nicht abgeschnittenen Algebra eine exakte Identität, und equiv stützt sich darauf. Nach einer Abschneidung ist sie keine Identität. Ein abgebildeter Operator hängt von der Form ab, die du abbildest, sodass sich die abgebildeten Bilder von contracted und reordered oben unterscheiden, und zwar nur im höchsten beibehaltenen Niveau. Siehe Den Fock-Raum abschneiden für die Größe des Unterschieds und die Wahl einer Form.
Terme ordnen und filtern
Das Umordnen der Terme eines Operators, im Gegensatz zu den Faktoren innerhalb eines Terms, ändert den Operator nicht, kann aber das Verhalten von Algorithmen ändern, die die Terme der Reihe nach lesen, etwa eine randomisierte Produktformel. Die Funktionen von qiskit_bosons.operators.terms geben einen neuen Operator zurück und lassen alle Gruppenindizes an ihren Termen haften.
canonical_order() sortiert die Terme in eine feste Reihenfolge, die nur von den geschriebenen Faktoren abhängt, sodass ein Operator unabhängig davon, wie er zusammengesetzt wurde, gleich gelesen wird. order_terms() sortiert nach einem Schlüssel, den du angibst und der jeden Term als das Tupel erhält, das iter_terms() liefert:
>>> from qiskit_bosons.operators.terms import canonical_order, order_terms
>>> mixed = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> list(canonical_order(mixed).iter_terms())
[([], (2+0j)), ([(True, 0), (False, 1)], (3+0j)), ([(True, 1), (False, 0)], (1+0j))]
>>> by_weight = order_terms(mixed, key=lambda term: abs(term[1]), reverse=True)
>>> [coeff for _, coeff in by_weight.iter_terms()]
[(3+0j), (2+0j), (1+0j)]
Wenn ein Operator Gruppenindizes trägt (siehe Operatorterme gruppieren: die Operatorstruktur nutzen), macht die Funktion group_order() jede Gruppe zu einer zusammenhängenden Folge von Termen:
>>> from qiskit_bosons.operators.terms import group_order
>>> grouped = BosonOperator.from_dict({(cre(1), ann(0)): 1.0, (cre(0), ann(1)): 3.0, (): 2.0})
>>> grouped.groups = [1, 0, 1]
>>> group_order(grouped).groups
[0, 1, 1]
Der letzte Helfer entfernt Terme, statt sie umzuordnen. Ein Term, der ein Produkt von Teilchenzahloperatoren ist, multipliziert jeden Fock-Zustand nur mit einer Phase, sodass filter_diagonal_terms() die Konstante, die Teilchenzahloperatoren und ihre Produkte verwerfen kann, wenn diese Phase für das, was du berechnest, irrelevant ist. Es verändert den Operator an Ort und Stelle und setzt einen normalgeordneten Operator voraus, was es nicht prüft:
>>> from qiskit_bosons.operators.terms import filter_diagonal_terms
>>> model = BosonOperator.from_dict(
... {
... (cre(0), ann(1)): -1.0,
... (cre(1), ann(0)): -1.0,
... (cre(0), cre(0), ann(0), ann(0)): 0.5,
... (cre(0), ann(0)): 0.2,
... (): 0.1,
... }
... )
>>> filter_diagonal_terms(model)
>>> len(model)
2
>>> model.is_hermitian()
True
Nur die beiden Hopping-Terme bleiben übrig. Die Wechselwirkung am Gitterplatz und der Teilchenzahloperator sind in der Besetzungsbasis diagonal, daher werden sie zusammen mit der Konstante entfernt. Die wiederholte Mode in der Wechselwirkung wird korrekt behandelt: ist diagonal, während es nicht wäre.
Nächste Schritte
-
Operatorterme gruppieren: die Operatorstruktur nutzen zeigt, wie du Terme mit Gruppenindizes versiehst.
-
Operatordarstellungen abbilden überführt einen Operator in einen Qubit-Operator, einschließlich der Wahl der Kodierung und der Wirkung einer Abschneidung.
-
Mit bosonischen Circuits arbeiten platziert einen Operator als
Evolution-Gate in einem Circuit. -
Bose-Hubbard-Dynamik mit wählbarer Kodierung simulieren baut den Hamiltonoperator einer Bose-Hubbard-Kette und simuliert sie von Anfang bis Ende.
-
Die Module
qiskit_bosons.operatorsundqiskit_bosons.operators.termsdokumentieren die vollständige Schnittstelle.