Designprinzipien der Operatordarstellungen
Dieser Leitfaden erläutert die gemeinsamen Designprinzipien und Kernkonzepte, die alle Operatordarstellungen im Modul operators teilen.
Überblick
Die von diesem Modul bereitgestellten Operatordarstellungen teilen mehrere grundlegende Designprinzipien:
-
Dünnbesetzte Datenstruktur: Operatoren kodieren nur Nicht-Identitätsoperationen. Das interne Datenlayout ist im Allgemeinen von dünnbesetzten Matrixformaten inspiriert und ermöglicht effiziente Speicherung und Berechnung für Systeme mit vielen Moden, aber relativ wenigen wesentlichen Beiträgen.
-
Term-Iteration und Rekonstruktion: Unabhängig vom internen dünnbesetzten Speicher bieten Operatoren eine konsistente Iterationsschnittstelle, sodass du Terme untersuchen, filtern und transformieren kannst, ohne die zugrunde liegende Datenstruktur zu verstehen, und anschließend aus den veränderten Termen neue Operatoren rekonstruierst.
-
Modenbasierte Indizierung: Operatoren verwenden abstrakte Mode-Indizes, um fermionische Freiheitsgrade zu benennen, was eine flexible Abbildung von physikalischen Systemen auf die Operatordarstellung ermöglicht.
-
Termgruppierung und Kommutationsbeziehungen: Operatoren unterstützen nativ Gruppierungsinformationen, die Terme mit Gruppenindizes verknüpfen. Das ermöglicht Optimierungen und den Erhalt der physikalischen Struktur, ohne separate Datenstrukturen zu benötigen. Die praktische Verwendung findest du im Gruppierungsleitfaden.
-
Arithmetische und mathematische Operationen: Alle Operatoren implementieren über das Protokoll
OperatorTraiteinen konsistenten Satz arithmetischer Operationen (wie Addition, Multiplikation und Komposition) und mathematischer Funktionen, was einheitlichen Code über verschiedene Operatortypen hinweg ermöglicht. -
Termreihenfolge und Normalformen von Operatoren: Mathematisch äquivalente Operatoren können in Quantenalgorithmen sehr unterschiedliche Darstellungen und ein sehr unterschiedliches Verhalten haben. Alle Operatordarstellungen unterstützen verschiedene Normalformen (basierend auf algebraspezifischen Kommutationsrelationen), um kanonische, vorhersagbare und optimierbare Operatordarstellungen zu erreichen.
Dünnbesetzte Datenstruktur
Alle Operatoren verwenden eine dünnbesetzte Darstellung, in der nur Nicht-Identitätsoperationen verfolgt werden. Jede Nicht-Identitätsoperation besteht aus einem Koeffizienten (komplexe Zahl) und einer Folge von Aktionen auf bestimmten Moden.
Dieser Ansatz reduziert Speicherbedarf und Rechenzeit drastisch, besonders bei Systemen mit vielen Moden, aber relativ wenigen wesentlichen Beiträgen. Indem nur Nicht-Identitätsoperationen kodiert werden, konzentrieren sich die Operationen nur auf das, was zählt, und ermöglichen die Arbeit mit großen Systemen, die mit dichten Darstellungen nicht machbar wären. Außerdem skalieren Operatoren natürlich auf beliebig viele Moden; ein Operator, der auf die Moden {0, 1} wirkt, funktioniert unverändert in Systemen mit viel mehr Moden, da nicht betroffene Moden implizit die Identität sind.
Identische Terme bleiben bei arithmetischen Operationen getrennt erhalten und müssen bei Bedarf explizit zusammengeführt werden.
Internes Speicherformat
Intern werden Operatoren in Arrays gespeichert, die von dünnbesetzten Matrixformaten inspiriert sind:
-
Koeffizienten-Array: Der komplexe Koeffizient jedes Terms
-
Mode-Index-Array: Die fermionischen Moden, auf die jede Aktion wirkt
-
Grenzen-Array: Indizes, die markieren, wo die Moden jedes Terms im Mode-Array beginnen und enden
Je nach Operatortyp können zusätzliche Arrays vorhanden sein. FermionOperator-Instanzen enthalten zum Beispiel ein Aktions-Array aus Booleans, das den Typ der fermionischen Aktion auf dem jeweiligen Mode-Index angibt. Die Klasse MajoranaOperator benötigt diese Unterscheidung dagegen nicht, da sie diese Information in der Parität des Mode-Index kodiert. Das vollständige Speicherformat findest du in der API-Dokumentation deines Operatortyps.
Die folgenden Beispiele zeigen, wie diese Arrays organisiert sind. Zuerst eine direkte Konstruktion mit den dünnbesetzten Arrays:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator
>>>
>>> # Construct operators directly using sparse arrays
>>> # First operator: 1.0 * +0 -1
>>> op1 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[0, 1],
... boundaries=[0, 2],
... )
>>>
>>> # Second operator: 1.0 * +2 -3
>>> op2 = FermionOperator(
... coeffs=[1.0],
... actions=[True, False],
... modes=[2, 3],
... boundaries=[0, 2],
... )
>>>
>>> # Combine the sparse operators
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * +0 -1
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {true, false};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
// Construct second operator: 1.0 * +2 -3
QkComplex64 coeff2[1] = {{1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, actions2, modes2, boundaries2);
// Combine the sparse operators. There is no in-place add in the C API, so
// the sum is a new operator that the caller owns.
QfFermionOperator *sum = qf_ferm_op_add(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(sum);
Praktische Konstruktionsmethoden
Für Python-Entwickler:innen stehen mehrere praktische Konstruktionsmethoden zur Verfügung, die die Details der dünnbesetzten Speicherung abstrahieren. Damit lassen sich Operatoren leichter aufbauen, ohne sich um die Verwaltung der Arrays für Koeffizienten, Moden und Grenzen kümmern zu müssen:
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct operators using operator algebra notation
>>> op1 = FermionOperator.from_dict({(cre(0), ann(1)): 1.0})
>>> op2 = FermionOperator.from_dict({(cre(2), ann(3)): 1.0})
>>>
>>> # The result is sparse even when combining them
>>> op1 += op2
>>> print(format(op1))
1.000000e0 +0.000000e0j * (+0 -1)
1.000000e0 +0.000000e0j * (+2 -3)
// The C API uses direct array construction; convenience methods are not available.
Einzelne Operator-Implementierungen unterstützen möglicherweise zusätzliche Konstruktionsmethoden, die auf ihren jeweiligen Anwendungsfall zugeschnitten sind. Sieh in der API-Dokumentation deines Operatortyps nach, um alle verfügbaren Konstruktionsoptionen zu sehen.
Iteration über Terme und Rekonstruktion
Operatoren bieten über OperatorTrait.iter_terms() unabhängig von ihrer internen dünnbesetzten Darstellung eine einheitliche Iterationsschnittstelle. Du kannst Terme daher untersuchen, filtern oder transformieren, ohne die zugrunde liegende Datenstruktur verstehen zu müssen. Aus den transformierten Termen kannst du anschließend mit OperatorTrait.from_terms() einen neuen Operator rekonstruieren.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct an operator with terms of different orders
>>> op = FermionOperator.from_dict({
... (): 0.5, # constant term (order 0)
... (cre(0), ann(1)): 1.0, # two-body term (order 2)
... (cre(0), cre(1), ann(1), ann(0)): 0.25 # four-body term (order 4)
... })
>>>
>>> # Filter to keep only terms of order 2
>>> order_two_terms = [
... (term, coeff) for term, coeff in op.iter_terms()
... if len(term) == 2
... ]
>>>
>>> # Reconstruct operator from filtered terms
>>> filtered_op = FermionOperator.from_terms(order_two_terms)
>>> print(f"Original operator has {len(op)} terms")
Original operator has 3 terms
>>> print(f"Filtered operator has {len(filtered_op)} term")
Filtered operator has 1 term
// WARNING: Term iteration and filtering are not yet available in the C API.
Modenbasierte Indizierung
Alle Operatordarstellungen bezeichnen die Indizes, auf die ihre Terme wirken, als Moden. Eine Mode ist schlicht ein Index, der einen fermionischen Freiheitsgrad in deinem System identifiziert. Die Abbildung von physikalischen Freiheitsgraden (etwa räumlichen Orbitalen, Spinzuständen oder anderen Quantenzahlen) auf Modenindizes bleibt dir überlassen, was maximale Flexibilität ermöglicht.
Diese Abstraktion findet sich auch im Modul qiskit_fermions.circuit, in dem FermionicCircuit auf einem Register fermionischer Moden arbeitet. Sowohl in der Operator- als auch in der Circuit-Darstellung bieten Moden eine einheitliche, algebraunabhängige Möglichkeit festzulegen, welche Freiheitsgrade an einer Operation beteiligt sind.
Aktuelle Implementierungen verwenden spinlose Moden: Alle derzeit von diesem Modul bereitgestellten Operatordarstellungen behandeln Moden als spinlose fermionische Freiheitsgrade. Das bedeutet: Wenn dein System sowohl Spin-up- als auch Spin-down-Elektronen oder -Fermionen enthält, musst du diese explizit auf unterschiedliche Moden abbilden (zum Beispiel Moden 0-3 für Spin-up von vier räumlichen Orbitalen, Moden 4-7 für Spin-down, oder eine beliebige andere Konvention deiner Wahl).
Dieser Entwurf hält die zentralen Darstellungen einfach und allgemein und vermeidet es, eine bestimmte Spin-Ordnungskonvention vorzuschreiben. Hilfsmodule wie qiskit_fermions.operators.library stellen Komfortfunktionen bereit (zum Beispiel FCIDump.from_file()), die solche Abbildungen für dich übernehmen, wenn du Daten zur Elektronenstruktur lädst.
Im Zuge der Weiterentwicklung des Pakets könnten spinbehaftete Operatordarstellungen hinzukommen, die Spinfreiheitsgrade nativ in ihrem Datenmodell unterstützen. Diese werden klar von den aktuellen spinlosen Implementierungen unterschieden und existieren neben ihnen im Modul.
Gruppierung von Termen und Kommutationsbeziehungen
Wie die Koeffizienten und Modenindizes können Operatoren optional ein Gruppen-Array speichern, das jedem Term einen Gruppenindex zuordnet. Da die Gruppierung direkt als Teil der dünnbesetzten Datenstruktur in die Operatordarstellung integriert ist, wandert die Gruppierungsinformation ganz natürlich mit dem Operator durch Transformationen. So lässt sich Struktur systematisch nutzen, sei sie aus physikalischen Eigenschaften, algebraischen Beziehungen oder problemspezifischen Symmetrien abgeleitet. Die strukturierte Information kann anschließend in nachgelagerten Operationen wie der Circuit-Synthese und -Zerlegung verwendet werden, etwa mit Methoden wie OperatorTrait.split_out_groups().
Eine ausführliche Anleitung zur Gruppierung von Operatortermen in deinen Workflows findest du im Leitfaden Gruppierung.
- PYTHON
- C
>>> from qiskit_fermions.operators import MajoranaOperator, gamma
>>> op = MajoranaOperator.from_dict({
... (gamma(0, False),): 1.0,
... (gamma(1, False),): 1.0,
... (gamma(2, False), gamma(3, False)): 1.0
... })
>>> # Assign group indices to terms
>>> op.groups = [0, 0, 1]
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 2, 3};
uint32_t boundaries[4] = {0, 1, 2, 4};
QfMajoranaOperator *op = qf_maj_op_new(3, 4, coeffs, modes, boundaries);
// Assign grouping information
uint32_t groups[3] = {0, 0, 1};
qf_maj_op_set_groups(op, groups, 3);
// Partition operator by groups
QfMajoranaOperator *grouped_ops[2];
qf_maj_op_split_out_groups(op, NULL, 0, grouped_ops);
Arithmetische und mathematische Operationen
Alle Operatoren implementieren das Protokoll OperatorTrait, das einen einheitlichen Satz von Operationen über verschiedene Operatortypen hinweg bereitstellt. Dadurch funktioniert Code, der für eine Operatordarstellung geschrieben wurde, einheitlich auch mit anderen. In qiskit_fermions.protocols erfährst du, wie dieses Protokoll mit den anderen Protokollen dieses Pakets zusammenhängt.
Das Protokoll umfasst arithmetische Operationen (wie Addition, Multiplikation und Komposition), strukturelle Operationen (Iteration über Terme, Analyse der Modenunterstützung, Umbenennung), mathematische Funktionen (Normalordnung, Vereinfachung, Äquivalenzprüfung) und mehr. Eine vollständige Referenz aller verfügbaren Operationen findest du in der Dokumentation zu OperatorTrait.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Construct a Hermitian operator: H = +0 -1 + +1 -0
>>> op = FermionOperator.from_dict({
... (cre(0), ann(1)): 1.0,
... (cre(1), ann(0)): 1.0
... })
>>>
>>> # Check if the operator is Hermitian
>>> print(f"Operator is Hermitian: {op.is_hermitian(atol=1e-10)}")
Operator is Hermitian: True
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex64 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
bool actions[4] = {true, false, true, false};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, actions, modes, boundaries);
// Check if the operator is Hermitian
bool is_hermitian = qf_ferm_op_is_hermitian(op, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
is_hermitian() prüft, ob gilt, indem die Differenz auf eine Normalform reduziert und mit zero() verglichen wird. In Form der anderen Protokolloperationen ausgeschrieben, führt sie Folgendes aus:
>>> difference = (op - op.adjoint()).normal_ordered().simplify(atol=1e-10)
>>> difference.equiv(FermionOperator.zero(), atol=1e-10)
True
Die Aussagekraft dieser Prüfung hängt vom Operatortyp ab, denn sie hängt davon ab, wie vollständig die Normalform des jeweiligen Typs benachbarte Generatoren kontrahiert. Ein Ergebnis True ist immer zuverlässig, aber bei manchen Typen ist ein Ergebnis False konservativ: Details siehe is_hermitian().
Das Beispiel verwendet atol=1e-10. Der Parameter atol (absolute Toleranz) legt einen Schwellenwert fest. Koeffizienten mit einem Betrag unterhalb von atol werden als null behandelt und verworfen. Das ist für die numerische Stabilität beim Vergleich von Operatoren wesentlich, da Gleitkommaarithmetik kleine Rundungsfehler einführen kann, die andernfalls verhindern würden, dass äquivalente Operatoren als solche erkannt werden.
Das Protokoll OperatorTrait stellt zwar eine gemeinsame Schnittstelle bereit, einzelne Operator-Implementierungen bieten aber möglicherweise zusätzliche Komfortmethoden, die nicht Teil des Protokolls sind. FermionOperator und MajoranaOperator stellen beispielsweise eine Methode max_rank() bereit, die die Länge ihres längsten Terms angibt. Sieh immer in der API-Dokumentation deines Operatortyps nach, um alle verfügbaren Funktionen zu entdecken.
Termordnung und Normalformen von Operatoren
Eine grundlegende Herausforderung der Quantenoperator-Algebra besteht darin, dass mathematisch äquivalente Operatoren auf viele verschiedene Arten dargestellt werden können, mit jeweils unterschiedlichen Folgen für Quantenalgorithmen. Derselbe Operator kann in algebraisch äquivalenten Formen geschrieben werden (zum Beispiel lässt sich als ausdrücken), doch diese Darstellungen führen zu unterschiedlichem Verhalten bei der Circuit-Synthese, der Vereinfachung und in numerischen Algorithmen.
Die Operatordarstellungen unterstützen Normalordnungs-Operationen, die Operatoren mithilfe von Kommutationsbeziehungen in algebraspezifische kanonische Formen überführen. Das ermöglicht verlässliche Vergleiche (zwei äquivalente Operatoren haben identische normalgeordnete Formen), macht Vereinfachungen sichtbar (Kommutationsbeziehungen lassen Terme sich aufheben oder zusammenfassen) und unterstützt Algorithmen, die für Korrektheit und Effizienz bestimmte Operatorformen benötigen.
Verschiedene Operatordarstellungen implementieren die Normalordnung möglicherweise auf Basis unterschiedlicher, zu ihrer Algebra passender Kommutationsbeziehungen. Die fermionische Normalordnung verwendet zum Beispiel Antikommutationsrelationen (), während die Majorana-Normalordnung andere algebraische Konventionen nutzt (). Sieh immer in der Dokumentation deines Operatortyps nach, um zu verstehen, wie die Normalordnung implementiert ist.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator, cre, ann
>>>
>>> # Two different representations of the same operator
>>> op1 = FermionOperator.from_dict({(ann(0), cre(0)): 1.0})
>>> op2 = FermionOperator.from_dict({(): 1.0, (cre(0), ann(0)): -1.0})
>>>
>>> # Direct comparison fails due to different forms
>>> op1.equiv(op2, atol=1e-10)
False
>>>
>>> # Normal-order both and compare again
>>> op1_normal = op1.normal_ordered()
>>> op2_normal = op2.normal_ordered()
>>> op1_normal.equiv(op2_normal, atol=1e-10)
True
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex64 coeff1[1] = {{1.0, 0.0}};
bool actions1[2] = {false, true};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, actions1, modes1, boundaries1);
QkComplex64 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
bool actions2[2] = {true, false};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, actions2, modes2, boundaries2);
// Direct comparison fails due to different forms
bool equiv_before = qf_ferm_op_equiv(op1, op2, 1e-10);
printf("Equivalent before normal ordering: %s\n", equiv_before ? "true" : "false");
// Normal-order both and compare again
QfFermionOperator *op1_normal = qf_ferm_op_normal_ordered(op1, NULL);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2, NULL);
bool equiv_after = qf_ferm_op_equiv(op1_normal, op2_normal, 1e-10);
printf("Equivalent after normal ordering: %s\n", equiv_after ? "true" : "false");
// Clean up
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);
qf_ferm_op_free(op1_normal);
qf_ferm_op_free(op2_normal);
Die Protokollmethode OperatorTrait.normal_ordered() lässt ihre Positions- und Schlüsselwortargumente bewusst unspezifiziert, sodass konkrete Operator-Implementierungen anpassbare Parameter definieren können, die die genaue erzeugte kanonische Form steuern. Das ermöglicht operatorspezifische Optimierungen und Normalform-Varianten, die auf ihre Algebra oder ihren Anwendungsfall zugeschnitten sind. Sieh in der API-Dokumentation deines Operatortyps nach, welche Parameter verfügbar sind.