Grouper les termes d'un opérateur : utiliser la structure de l'opérateur
Le regroupement est l'une des opérations de qiskit_fermions.operators.terms, qui rassemble des routines partitionnant les termes d'un opérateur selon leur structure.
Comme décrit dans le guide des opérateurs, les opérateurs peuvent stocker un tableau de groupes, partie facultative de la structure de données creuse qui associe à chaque terme un indice de groupe. Les termes partageant le même indice de groupe forment un groupe. Cela te permet d'exploiter systématiquement la structure dans les traitements en aval.
En regroupant des termes liés (que ce soit par propriétés physiques, relations algébriques ou symétries propres au problème), tu peux obtenir plusieurs avantages :
-
Synthèse de circuits optimisée : les termes regroupés transportent des informations propres au problème à travers la pile, ce qui permet une optimisation plus simple des circuits synthétisés. Il en résulte une profondeur de circuit et un nombre de portes réduits.
-
Préservation de la structure physique : les groupes encodent une structure significative du problème d'origine (par exemple des motifs d'interaction ou des symétries) qui peut servir à des décompositions plus pertinentes physiquement.
-
Sélection d'ensembles de flux : le regroupement peut encoder naturellement des ensembles de flux et d'autres structures de théorie des graphes pertinentes pour la simulation quantique, comme montré dans [1].
-
Meilleure stabilité algorithmique : comme démontré dans la routine SqDRIFT, certains algorithmes peuvent bénéficier d'une meilleure stabilité ou de meilleures performances grâce à la préservation des propriétés physiques.
-
Facteurs de formule de produit : une formule de produit fermionique découpe un opérateur selon ses groupes, de sorte que le regroupement détermine à la fois le coût et l'erreur de Trotter d'une évolution temporelle. Voir Synthétiser une porte d'évolution dans l'espace fermionique.
Utilisation
La façon la plus simple de définir un regroupement d'opérateur est de renseigner l'attribut groups de l'opérateur cible. Voici un exemple de regroupement des termes selon les ensembles de flux de lignes comme montré dans la figure 1c de [1].
Le module qiskit_fermions.operators.terms.grouping fournit des méthodes pratiques pour identifier automatiquement la structure d'un opérateur et attribuer les indices de groupe en conséquence.
>>> import rustworkx as rx
>>> graph = rx.PyDiGraph()
>>> _ = graph.add_nodes_from(range(6))
>>> edges = [(0, 1), (1, 2), (3, 0), (1, 4), (5, 2), (5, 4), (4, 3)]
>>> groups = [0, 0, 1, 2, 1, 3, 3]
>>> edges_with_group_as_payload = [(i, j, g) for (i, j), g in zip(edges, groups)]
>>> _ = graph.add_edges_from(edges_with_group_as_payload)
>>> from rustworkx.visualization import mpl_draw
>>> mpl_draw(
... graph,
... pos={i: (i % 3, i // 3) for i in range(6)},
... edge_labels=str,
... with_labels=True,
... )
<Figure size ... with 1 Axes>
Il s'agit d'un simple réseau de deux par trois de modes de Majorana reliés par des arêtes orientées. Chaque arête représente un terme de l'opérateur agissant sur les deux modes connectés. L'objectif est de regrouper ces termes selon leurs étiquettes d'arête (indiquées sur le graphe). Les termes reliés par des arêtes de même étiquette sont placés dans le même groupe. Le code suivant montre comment faire :
- PYTHON
- C
>>> from qiskit_fermions.operators import MajoranaOperator
>>>
>>> # Construct operator directly using sparse arrays
>>> # 7 terms, each with coefficient 1.0, modes from edge endpoints
>>> op = MajoranaOperator(
... coeffs=[1.0] * 7,
... modes=[node for edge in edges for node in edge],
... boundaries=[0, 2, 4, 6, 8, 10, 12, 14],
... )
>>>
>>> # Assign group indices to terms (groups is part of the sparse data structure)
>>> op.groups = groups
>>>
>>> # Partition operator by groups
>>> grouped_ops = op.split_out_groups()
>>>
>>> # Inspect each group
>>> for i, g in enumerate(grouped_ops):
... print(f"Group {i}: {list(sorted(g.iter_terms()))}")
Group 0: [([0, 1], (1+0j)), ([1, 2], (1+0j))]
Group 1: [([3, 0], (1+0j)), ([5, 2], (1+0j))]
Group 2: [([1, 4], (1+0j))]
Group 3: [([4, 3], (1+0j)), ([5, 4], (1+0j))]
#include <qiskit_fermions.h>
uint64_t num_terms = 7;
uint64_t num_modes = 14;
uint32_t modes[14] = {0, 1, 1, 2, 3, 0, 1, 4, 5, 2, 5, 4, 4, 3};
QkComplex64 coeffs[7] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0},
{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t boundaries[8] = {0, 2, 4, 6, 8, 10, 12, 14};
QfMajoranaOperator *op = qf_maj_op_new(num_terms, num_modes, coeffs, modes, boundaries);
uint32_t groups[7] = {0, 0, 1, 2, 1, 3, 3};
qf_maj_op_set_groups(op, groups, num_terms);
QfMajoranaOperator *group_ops[4];
qf_maj_op_split_out_groups(op, NULL, 0, group_ops);
Analyser un regroupement
Les groupes ne prescrivent aucune signification propre : le tableau de groupes indique quels termes vont ensemble, jamais pourquoi. Aucune routine de cette bibliothèque ne dit si un regroupement est « correct », car il n'existe pas de notion unique de correction par rapport à laquelle le dire. Les consommateurs en aval supposent cependant certaines propriétés d'un regroupement, et les fonctions de cette section te permettent de vérifier une telle hypothèse en amont plutôt que de la payer à chaque appel.
La convention la plus courante est que chaque groupe est hermitien séparément. C'est ce qui rend possibles les formules de produit fermioniques aléatoires : un isolé avec n'est pas hermitien, donc son évolution temporelle n'est pas unitaire et il ne peut pas être échantillonné seul. L'associer à donne un groupe qui peut l'être.
- PYTHON
- C
>>> from qiskit_fermions.operators import FermionOperator
>>> from qiskit_fermions.operators.terms.grouping import (
... group_coeff_means,
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>>
>>> # a conjugate pair, plus one unpaired term. The operator is built from the sparse
>>> # arrays rather than from a dictionary, because only the former fixes the term order
>>> # that the group indices below are paired with.
>>> pair_and_single = FermionOperator(
... [1.0, 1.0, 1.0],
... [True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3],
... [0, 2, 4, 6],
... )
>>> pair_and_single.groups = [0, 0, 1]
>>>
>>> groups_are_hermitian(pair_and_single) # group 1 is unpaired
[True, False]
>>> group_coeff_means(pair_and_single)
[1.0, 1.0]
#include <qiskit_fermions.h>
uint64_t num_terms = 3;
uint64_t num_actions = 6;
bool actions[6] = {true, false, true, false, true, false};
uint32_t modes[6] = {0, 1, 1, 0, 2, 3};
QkComplex64 coeffs[3] = {{1.0, 0.0}, {1.0, 0.0}, {1.0, 0.0}};
uint32_t boundaries[4] = {0, 2, 4, 6};
QfFermionOperator *op =
qf_ferm_op_new(num_terms, num_actions, coeffs, actions, modes, boundaries);
uint32_t groups[3] = {0, 0, 1};
qf_ferm_op_set_groups(op, groups, num_terms);
bool hermitian[2];
qf_ferm_op_groups_are_hermitian(op, 1e-8, hermitian);
double means[2];
qf_ferm_op_group_coeff_means(op, means);
Un indice de groupe par terme est imposé
Le tableau de groupes contient un indice par terme, et en affecter un de longueur différente lève une erreur. Rien en aval ne revérifie cet invariant, il est donc imposé à l'endroit où les indices entrent dans l'opérateur : un tableau trop court abandonnerait silencieusement les derniers termes, et un tableau trop long inventerait des groupes qu'aucun terme ne porte.
>>> pair_and_single.groups = [0, 0] # one index short, on a three-term operator
Traceback (most recent call last):
ValueError: expected one group index per term, but got 2 for 3 terms
Effacer les indices de groupe en affectant None est toujours autorisé.
Les deux vérifications sont indépendantes
groups_have_uniform_coeffs() indique si les coefficients d'un groupe concordent, ce qui est l'hypothèse que fait group_coeff_means() lorsqu'elle moyenne les modules. Ce n'est pas une forme affaiblie de la vérification d'hermiticité : aucune des deux vérifications n'implique l'autre, dans aucun sens.
>>> from qiskit_fermions.operators.terms.grouping import (
... groups_are_hermitian,
... groups_have_uniform_coeffs,
... )
>>> # uniform coefficients, but not Hermitian: the terms are not each other's adjoint
>>> uniform = FermionOperator(
... [1.0, 1.0],
... [True, False, True, False],
... [0, 1, 2, 3],
... [0, 2, 4],
... )
>>> uniform.groups = [0, 0]
>>> groups_have_uniform_coeffs(uniform), groups_are_hermitian(uniform)
([True], [False])
>>> # Hermitian, but with mixed magnitudes: two conjugate pairs at different scales
>>> mixed = FermionOperator(
... [1.0, 1.0, 5.0, 5.0],
... [True, False, True, False, True, False, True, False],
... [0, 1, 1, 0, 2, 3, 3, 2],
... [0, 2, 4, 6, 8],
... )
>>> mixed.groups = [0, 0, 0, 0]
>>> groups_are_hermitian(mixed), groups_have_uniform_coeffs(mixed)
([True], [False])
Par défaut, ce sont les modules qui sont comparés, car c'est ce que moyenne group_coeff_means(). Une paire de conjugués hermitiens à coefficients complexes a des modules égaux mais des coefficients inégaux, elle ne satisfait donc que la forme par défaut.
>>> conjugate = FermionOperator(
... [1.0j, -1.0j],
... [True, False, True, False],
... [0, 1, 1, 0],
... [0, 2, 4],
... )
>>> conjugate.groups = [0, 0]
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
groups_are_hermitian() hérite de la garantie unilatérale de is_hermitian() : une entrée True est toujours fiable, tandis qu'une entrée False n'est fiable que pour les types d'opérateurs dont la forme normale est une véritable forme canonique. Consulte le type d'opérateur concerné pour savoir lequel s'applique.
A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.