Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Grouper les termes d'un opérateur : exploiter la structure de l'opérateur

Un BosonOperator peut étiqueter chacun de ses termes avec un indice de groupe. Les étiquettes n'ont pas de signification propre : elles indiquent seulement quels termes vont ensemble. Ce que les étiquettes signifient est décidé par ce qui les consomme. Dans ce paquet, les consommateurs sont la porte d'évolution temporelle, qui est scindée en un facteur par groupe, et la passe qDRIFT aléatoire, qui échantillonne des groupes entiers.

Ce guide montre comment attribuer des groupes, comment vérifier les propriétés sur lesquelles s'appuie un consommateur, et comment organiser l'opérateur pour que le choix de quelques groupes dans un grand opérateur reste peu coûteux.

Remarque

Aucune stratégie de groupement automatique n'est fournie avec ce paquet. Tu attribues toi-même les indices, à partir de ce que tu sais de ton modèle.

Attribuer des groupes​

L'exemple courant est une chaîne de Bose-Hubbard à trois sites. Chaque liaison apporte une paire de saut, et les interactions sur site forment un morceau diagonal. La façon la plus simple de construire un opérateur groupé est from_terms_with_groups(), qui prend chaque terme avec son indice de groupe sous forme de triplet (actions, coefficient, group). Tu décides du groupe au moment où tu écris le terme, quand tu sais encore pourquoi il y appartient :

>>> from qiskit_bosons.operators import BosonOperator, ann, cre
>>>
>>> bonds = [(0, 1), (1, 2)]
>>> terms = []
>>> for group, (left, right) in enumerate(bonds): # one group per bond
... terms.append(([cre(left), ann(right)], -1.0, group))
... terms.append(([cre(right), ann(left)], -1.0, group))
>>> for site in range(3): # the on-site interactions share one group
... terms.append(([cre(site), cre(site), ann(site), ann(site)], 1.0, len(bonds)))
>>> hamiltonian = BosonOperator.from_terms_with_groups(terms)
>>> len(hamiltonian), hamiltonian.num_groups()
(7, 3)

Les termes sont stockés dans l'ordre où tu les donnes, de sorte que les indices de groupe ne peuvent pas se retrouver attachés aux mauvais termes.

Un opérateur qui existe déjà doit être groupé après coup, en assignant l'attribut groups. C'est là qu'il faut faire attention, car from_dict() ne précise pas l'ordre dans lequel il stocke ses termes. Écrire la liste par position peut alors attacher un indice au mauvais terme. Dérive plutôt chaque indice du terme lui-même :

>>> existing = BosonOperator.from_dict({tuple(actions): coeff for actions, coeff, _ in terms})
>>> existing.groups = [
... len(bonds)
... if len({mode for _, mode in term}) == 1
... else bonds.index(tuple(sorted({mode for _, mode in term})))
... for term, _ in existing.iter_terms()
... ]
>>> existing.num_groups(), existing.equiv(hamiltonian)
(3, True)

Le nombre d'indices doit correspondre au nombre de termes. Tout autre cas est rejeté, car une liste trop courte ferait silencieusement disparaître les derniers termes partout où les termes sont parcourus avec leurs groupes :

>>> try:
... hamiltonian.groups = [0, 1]
... except ValueError as error:
... print(error)
expected one group index per term, but got 2 for 7 terms

Vérifier ce sur quoi s'appuie le consommateur​

Trois fonctions de qiskit_bosons.operators.terms répondent chacune à une question précise sur un groupement existant. Aucune ne dit qu'un groupement est « correct », car la correction est relative au consommateur.

Chaque groupe est-il hermitien ? C'est la propriété qui compte le plus. L'évolution temporelle d'un groupe n'est unitaire que si le groupe est hermitien, et un terme de saut isolé bi†bjb^\dagger_i b_j ne l'est pas. Seule la paire avec son adjoint est hermitienne :

>>> from qiskit_bosons.operators.terms import groups_are_hermitian
>>> groups_are_hermitian(hamiltonian)
[True, True, True]

Un groupement qui scinde une paire de saut est repéré immédiatement :

>>> split = BosonOperator.from_terms_with_groups(
... [(actions, coeff, index) for index, (actions, coeff, _) in enumerate(terms)]
... ) # one term per group
>>> [bool(flag) for flag in groups_are_hermitian(split)]
[False, False, False, False, True, True, True]

Les quatre termes de saut (hopping) sont détectés. Les trois entrées True sont les termes sur site bi†bi†bibib^\dagger_i b^\dagger_i b_i b_i, qui sont hermitiens par eux-mêmes. Un mode peut légitimement se répéter dans un terme, et rien n'est écarté pour cette raison. Les indicateurs suivent l'ordre de terms, ce qui permet de lire chacun par rapport au terme qu'il décrit.

Les coefficients sont-ils uniformes ? Une formule de produit aléatoire pondère un groupe par la magnitude moyenne de ses coefficients, ce qui ne décrit fidèlement le groupe que si les magnitudes concordent :

>>> from qiskit_bosons.operators.terms import group_coeff_means, groups_have_uniform_coeffs
>>> groups_have_uniform_coeffs(hamiltonian)
[True, True, True]
>>> group_coeff_means(hamiltonian)
[1.0, 1.0, 1.0]

Aucune des deux vérifications n'implique l'autre : des coefficients uniformes ne rendent pas un groupe hermitien, et un groupe hermitien peut mêler des magnitudes différentes. Par défaut, ce sont les magnitudes qui sont comparées, car c'est ce que group_coeff_means() moyenne. Une paire hermitienne avec des coefficients complexes a des magnitudes égales mais des coefficients inégaux, et ne satisfait donc que la forme par défaut :

>>> conjugate = BosonOperator.from_terms_with_groups(
... [([cre(0), ann(1)], 1.0j, 0), ([cre(1), ann(0)], -1.0j, 0)]
... )
>>> groups_have_uniform_coeffs(conjugate)
[True]
>>> groups_have_uniform_coeffs(conjugate, abs=False)
[False]
Note

groups_are_hermitian() est construite sur is_hermitian(), qui compare un opérateur à son adjoint après mise en ordre normal. C'est une identité de l'algèbre non tronquée : un groupe qui passe le test est donc hermitien en tant qu'opérateur bosonique. Consulte Principes de conception des représentations d'opérateurs pour savoir comment les produits d'opérateurs d'échelle sont ordonnés et comparés.

Ordonner les termes par groupe​

split_out_groups() renvoie un opérateur par groupe demandé. Quand les termes d'un groupe sont dispersés dans l'opérateur, elle doit parcourir tous les termes pour les trouver. Quand chaque groupe forme une plage contiguë, elle recherche à la place les frontières des groupes par dichotomie : le coût d'une recherche suit alors le nombre de groupes demandés plutôt que le nombre de termes conservés.

group_order() produit cette disposition. C'est un tri stable selon l'indice de groupe : la valeur de l'opérateur ne change pas, et les termes d'un même groupe conservent leur ordre relatif.

>>> from qiskit_bosons.operators.terms import group_order
>>> scattered = BosonOperator.from_terms_with_groups(
... [(actions, coeff, group) for (actions, coeff, _), group in zip(terms, [1, 0, 1, 0, 2, 2, 2])]
... )
>>> ordered = group_order(scattered)
>>> ordered.groups
[0, 0, 1, 1, 2, 2, 2]
>>> ordered.equiv(scattered)
True

Les deux dispositions renvoient les mêmes opérateurs ; seul le coût diffère :

>>> request = [2, 0]
>>> all(
... a.equiv(b)
... for a, b in zip(
... scattered.split_out_groups(request), ordered.split_out_groups(request), strict=True
... )
... )
True

Étapes suivantes​

Le regroupement est choisi ici, au niveau bosonique, tant que la structure du modèle reste visible.