Principes de conception des représentations des opérateurs
Ce guide présente les principes de conception courants et les concepts fondamentaux communs à toutes les représentations d’opérateurs du module operators .
Présentation
Les représentations d'opérateurs fournies par ce module partagent plusieurs principes de conception fondamentaux :
- Structure de données clairsemée : les opérateurs n'encodent que les opérations autres que l'identité. La structure interne des données s'inspire généralement des formats de données des matrices creuses, ce qui permet un stockage et un traitement efficaces pour les systèmes comportant de nombreux modes mais dont un nombre relativement restreint seulement ont une contribution significative.
- Itération et reconstruction des termes : indépendamment du stockage interne en mode clairsemé, les opérateurs offrent une interface d’itération cohérente, ce qui vous permet d’inspecter, de filtrer et de transformer des termes sans avoir à comprendre la structure de données sous-jacente, puis de reconstruire de nouveaux opérateurs à partir des termes modifiés.
- Indexation basée sur les modes : les opérateurs utilisent des indices de mode abstraits pour désigner les degrés de liberté fermioniques, ce qui permet une mise en correspondance flexible entre les systèmes physiques et la représentation par opérateurs.
- Regroupement des termes et relations de commutation : les opérateurs prennent en charge nativement les informations de regroupement qui associent les termes à des indices de groupe. Cela permet de réaliser des optimisations et de préserver la structure physique sans avoir recours à des structures de données distinctes. Consultez le guide de regroupement pour une utilisation pratique.
- Opérations arithmétiques et mathématiques : tous les opérateurs implémentent un ensemble cohérent d'opérations arithmétiques (telles que l'addition, la multiplication et la composition) et de fonctions mathématiques à l'aide du protocole
OperatorTrait, ce qui permet d'uniformiser le code entre les différents types d'opérateurs. - Ordre des opérateurs et formes normales : des opérateurs mathématiquement équivalents peuvent présenter des représentations et des comportements très différents dans les algorithmes quantiques. Toutes les représentations d'opérateurs prennent en charge diverses formes normales (basées sur des relations de commutation propres à l'algèbre) afin d'obtenir des représentations d'opérateurs canoniques, prévisibles et optimisables.
Structure de données clairsemée
Tous les opérateurs utilisent une représentation clairsemée dans laquelle seules les opérations autres que l'identité sont prises en compte. Chaque opération non identitaire se compose d'un coefficient (nombre complexe) et d'une suite d'actions sur des modes spécifiques.
Cette approche réduit considérablement la consommation de mémoire et le temps de calcul, en particulier pour les systèmes comportant de nombreux modes mais dont un nombre relativement restreint seulement apporte une contribution significative. En n'encodant que les opérations autres que l'identité, les opérations se concentrent uniquement sur l'essentiel, ce qui permet de travailler avec des systèmes de grande taille, ce qui serait impossible avec des représentations denses. De plus, les opérateurs s'adaptent naturellement à n'importe quel nombre de modes; un opérateur agissant sur des modes fonctionne {0, 1} de la même manière dans des systèmes comportant un nombre bien plus élevé de modes, car les modes non affectés correspondent implicitement à l'identité.
Les termes identiques sont conservés séparément lors des opérations arithmétiques et doivent être combinés explicitement si nécessaire.
Format de stockage interne
En interne, les opérateurs sont stockés dans des tableaux inspirés des formats de données des matrices creuses :
- Tableau de coefficients : le coefficient complexe de chaque terme
- Tableau des indices de modes : les modes fermioniques sur lesquels agit chaque action
- Tableau « boundaries » : indices indiquant où commencent et où finissent les modes de chaque terme dans le tableau « mode »
D'autres tableaux peuvent être présents selon le type d'opérateur. Par exemple, les instances FermionOperator comportent un tableau d’actions de type booléen qui spécifie le type d’action fermionique agissant sur l’indice de mode correspondant. En revanche, la classe MajoranaOperator n'a pas besoin de cette distinction, car elle encode cette information dans la parité de l'index de mode. Consultez la documentation de l'API correspondant à votre type d'opérateur pour comprendre le format de stockage dans son intégralité.
Les exemples suivants montrent comment ces tableaux sont organisés. Voici tout d'abord une construction directe utilisant les tableaux creux :
[x] PYTHON
>>> 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)[] C
#include <qiskit_fermions.h>
// Construct first operator: 1.0 * c_0 a_1
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 1};
uint32_t boundaries1[2] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
// Construct second operator: 1.0 * c_2 a_3
QkComplex67 coeff2[1] = {{1.0, 0.0}};
uint32_t modes2[2] = {2, 3};
uint32_t boundaries2[2] = {0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(1, 2, coeff2, modes2, boundaries2);
// Add operators
qf_ferm_op_add_assign(op1, op2);
qf_ferm_op_free(op1);
qf_ferm_op_free(op2);Méthodes de construction pratiques
Pour les développeurs d' Python, plusieurs méthodes de construction pratiques sont disponibles, qui masquent les détails liés au stockage des données clairsemées. Cela facilite la création d'opérateurs sans avoir à se soucier de la gestion des tableaux de coefficients, de modes et de conditions aux limites :
[x] PYTHON
>>> 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)[] C
// The C API uses direct array construction; convenience methods are not available.Certaines implémentations d'opérateurs peuvent prendre en charge des méthodes de construction supplémentaires adaptées à leur cas d'utilisation spécifique. Consultez la documentation de l'API correspondant à votre type d'opérateur pour découvrir toutes les options de construction disponibles.
Itération et reconstruction des termes
Les opérateurs fournissent une interface d'itération cohérente, quelle OperatorTrait.iter_terms() que soit leur représentation clairsemée interne. Vous pouvez ainsi examiner, filtrer ou transformer des termes sans avoir besoin de comprendre la structure des données sous-jacente. Vous pouvez ensuite reconstruire un nouvel opérateur à partir des termes transformés en utilisant OperatorTrait.from_terms().
[x] PYTHON
>>> 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[] C
// WARNING: Term iteration and filtering are not yet available in the C API.Indexation par mode
Toutes les représentations d'opérateurs font référence aux indices sur lesquels leurs termes agissent en tant que modes. Un mode est simplement un indice identifiant un degré de liberté fermionique dans votre système. La mise en correspondance entre les degrés de liberté physiques (tels que les orbitales spatiales, les états de spin ou d'autres nombres quantiques) et les indices de mode est laissée à la discrétion de l'utilisateur, ce qui garantit une flexibilité maximale.
Cette abstraction est également présente dans le module qiskit_fermions.circuit , où l’opère FermionicCircuit sur un registre de modes fermioniques. Tant dans la représentation par opérateurs que dans celle par circuits, les modes offrent un moyen cohérent, indépendant de l'algèbre, de préciser quels degrés de liberté interviennent dans une opération donnée.
Les implémentations actuelles utilisent des modes sans spin : toutes les représentations d'opérateurs actuellement fournies par ce module traitent les modes comme des degrés de liberté fermioniques sans spin. Cela signifie que si votre système comporte à la fois des électrons ou des fermions à spin « up » et à spin « down », vous devez les associer explicitement à des modes distincts (par exemple, les modes 0 à 3 pour le spin « up » de quatre orbitales spatiales, les modes 4 à 7 pour le spin « down », ou toute autre convention de votre choix).
Cette conception permet de conserver des représentations de base simples et générales, tout en évitant d’imposer une convention spécifique d’ordre des spins. Les modules utilitaires tels que proposent qiskit_fermions.operators.library des fonctions pratiques (par exemple, FCIDump.from_file()) qui gèrent ces correspondances à votre place lors du chargement des données de structure électronique.
Au fur et à mesure de l'évolution du package, des représentations d'opérateurs à spin pourraient être ajoutées, qui prendraient en charge nativement les degrés de liberté de spin dans leur modèle de données. Celles-ci seront clairement distinguées des implémentations actuelles sans spin et coexisteront avec elles dans le module.
Regroupement des termes et relations de commutation
À l'instar des coefficients et des indices de mode, les opérateurs peuvent, s'ils le souhaitent, stocker un tableau de groupes qui associe chaque terme à un indice de groupe. En intégrant le regroupement directement dans la représentation de l'opérateur au sein de la structure de données clairsemée, les informations de regroupement sont naturellement transmises avec l'opérateur au fil des transformations. Cela permet d'exploiter systématiquement les structures, qu'elles découlent de propriétés physiques, de relations algébriques ou de symétries propres à un problème. Les informations structurées peuvent ensuite être utilisées dans des opérations en aval, telles que la synthèse et la décomposition de circuits, à l'aide de méthodes telles que OperatorTrait.split_out_groups().
Pour obtenir des instructions détaillées sur la manière de regrouper les termes d'opérateur dans vos flux de travail, consultez le guide de regroupement.
[x] PYTHON
>>> 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()[] C
#include <qiskit_fermions.h>
// Create operator with 3 terms
QkComplex67 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);Opérations arithmétiques et mathématiques
Tous les opérateurs implémentent le protocole OperatorTrait , qui fournit un ensemble unifié d'opérations valables pour différents types d'opérateurs. Cela garantit que le code écrit pour une représentation d'opérateur fonctionne de manière uniforme avec les autres. Consultez qiskit_fermions.protocols pour savoir comment ce protocole s'articule avec les autres définis dans ce paquet.
Le protocole comprend des opérations arithmétiques (telles que l'addition, la multiplication et la composition), des opérations structurelles (itération de termes, analyse de la prise en charge des modes, réétiquetage), des fonctions mathématiques (ordonnancement normal, simplification, vérification d'équivalence), et bien d'autres encore. Consultez la documentation OperatorTrait pour obtenir une liste complète de toutes les opérations disponibles.
[x] PYTHON
>>> 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 by verifying H - H† = 0
>>> adjoint = op.adjoint()
>>> difference = op - adjoint
>>> difference = difference.normal_ordered()
>>> difference = difference.simplify(atol=1e-10)
>>> is_hermitian = difference.equiv(FermionOperator.zero(), atol=1e-10)
>>> print(f"Operator is Hermitian: {is_hermitian}")
Operator is Hermitian: True[] C
#include <qiskit_fermions.h>
// Construct a Hermitian operator: H = +0 -1 + +1 -0
QkComplex67 coeffs[2] = {{1.0, 0.0}, {1.0, 0.0}};
uint32_t modes[4] = {0, 1, 1, 0};
uint32_t boundaries[3] = {0, 2, 4};
QfFermionOperator *op = qf_ferm_op_new(2, 4, coeffs, modes, boundaries);
// Check if Hermitian: compute H - H†, normal-order, and simplify
QfFermionOperator *adjoint = qf_ferm_op_adjoint(op);
QfFermionOperator *difference = qf_ferm_op_sub(op, adjoint);
QfFermionOperator *normal_ordered = qf_ferm_op_normal_ordered(difference);
qf_ferm_op_ichop(normal_ordered, 1e-10);
QfFermionOperator *zero = qf_ferm_op_zero();
bool is_hermitian = qf_ferm_op_equiv(normal_ordered, zero, 1e-10);
printf("Operator is Hermitian: %s\n", is_hermitian ? "true" : "false");
// Clean up
qf_ferm_op_free(op);
qf_ferm_op_free(adjoint);
qf_ferm_op_free(difference);
qf_ferm_op_free(normal_ordered);
qf_ferm_op_free(zero);Dans cet exemple, on utilise à la fois atol=1e-10 dans simplify() et dans equiv(). Le paramètre ( atol tolérance absolue) définit un seuil. Les coefficients dont la valeur absolue est inférieure à atol sont considérés comme nuls et sont écartés. C'est essentiel pour la stabilité numérique lors de la comparaison d'opérateurs, car l'arithmétique en virgule flottante peut introduire de petites erreurs d'arrondi qui, sans cela, empêcheraient de reconnaître comme équivalents des opérateurs qui le sont pourtant.
Bien que le protocole OperatorTrait fournisse une interface commune, les implémentations spécifiques des opérateurs peuvent proposer des méthodes pratiques supplémentaires qui ne font pas partie du protocole. Par exemple, certains opérateurs proposent une méthode is_hermitian() qui met en œuvre cette vérification. Consultez toujours la documentation de l'API correspondant à votre type d'opérateur pour découvrir toutes les fonctionnalités disponibles.
Ordre des termes d'opérateurs et formes normales
L'un des défis majeurs de l'algèbre des opérateurs quantiques réside dans le fait que des opérateurs mathématiquement équivalents peuvent être représentés de nombreuses façons différentes, chacune ayant des implications différentes pour les algorithmes quantiques. Un même opérateur peut s'écrire sous des formes algébriquement équivalentes (par exemple, l' e peut s'exprimer sous la forme ), mais ces représentations entraînent des comportements différents dans la synthèse de circuits, la simplification et les algorithmes numériques.
Les représentations des opérateurs prennent en charge les opérations d'ordonnancement normales qui transforment les opérateurs en formes canoniques spécifiques à l'algèbre à l'aide des relations de commutation. Cela permet d'effectuer des comparaisons fiables (deux opérateurs équivalents ont des formes ordonnées normales identiques), met en évidence des simplifications (les relations de commutation entraînent l'annulation ou la combinaison de termes) et facilite la mise en œuvre d'algorithmes qui nécessitent des formes d'opérateurs spécifiques pour garantir leur exactitude et leur efficacité.
Différentes représentations d'opérateurs peuvent mettre en œuvre l'ordre normal en se fondant sur différentes relations de commutation propres à leur algèbre. Par exemple, l'ordonnancement normal fermionique utilise des relations d'anticommutation ( ), tandis que l'ordonnancement normal de Majorana utilise des conventions algébriques différentes ( ). Consultez toujours la documentation relative à votre type d'opérateur pour comprendre comment l'ordonnancement normal est mis en œuvre.
[x] PYTHON
>>> 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[] C
#include <qiskit_fermions.h>
#include <stdbool.h>
// Two different representations of the same operator
QkComplex67 coeff1[1] = {{1.0, 0.0}};
uint32_t modes1[2] = {0, 0};
uint32_t boundaries1[3] = {0, 2};
QfFermionOperator *op1 = qf_ferm_op_new(1, 2, coeff1, modes1, boundaries1);
QkComplex67 coeff2[2] = {{1.0, 0.0}, {-1.0, 0.0}};
uint32_t modes2[2] = {0, 0};
uint32_t boundaries2[3] = {0, 0, 2};
QfFermionOperator *op2 = qf_ferm_op_new(2, 2, coeff2, 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);
QfFermionOperator *op2_normal = qf_ferm_op_normal_ordered(op2);
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);La méthode de protocole OperatorTrait.normal_ordered() laisse délibérément non spécifiés ses arguments de position et ses arguments-clés, ce qui permet aux implémentations concrètes des opérateurs de définir des paramètres ajustables qui contrôlent la forme canonique exacte produite. Cela permet des optimisations spécifiques à chaque opérateur et des variantes de forme normale adaptées à leur algèbre ou à leur cas d'utilisation. Consultez la documentation de l'API correspondant à votre type d'opérateur pour connaître les paramètres disponibles.