Termini relativi agli operatori di gruppo: utilizzare la struttura degli operatori
Il raggruppamento è una delle operazioni previste dalla sezione qiskit_fermions.operators.terms, che raccoglie le routine che suddividono i termini di un operatore in base alla loro struttura.
Come descritto nella guida per gli operatori, questi ultimi possono memorizzare un array di gruppi, una parte facoltativa della struttura dati sparsa che associa ciascun termine a un indice di gruppo. I termini che condividono lo stesso indice di gruppo formano un gruppo. Ciò consente di avvalersi in modo sistematico della struttura nelle fasi successive dell'elaborazione.
Raggruppando i termini correlati (sia in base alle proprietà fisiche, alle relazioni algebriche o alle simmetrie specifiche di un problema), è possibile ottenere numerosi vantaggi:
- Sintesi ottimizzata dei circuiti : i termini raggruppati trasmettono informazioni specifiche del problema attraverso lo stack, consentendo un'ottimizzazione più semplice dei circuiti sintetizzati. Ciò comporta una riduzione della profondità del circuito e del numero di gate.
- Conservazione della struttura fisica : i gruppi codificano la struttura significativa del problema originale (ad esempio, modelli di interazione o simmetrie) che può essere utilizzata per decomposizioni più significative dal punto di vista fisico.
- Selezione degli insiemi di flussi : il raggruppamento può naturalmente codificare insiemi di flussi e altre strutture della teoria dei grafi rilevanti per la simulazione quantistica, come illustrato in [1].
- Maggiore stabilità algoritmica : come dimostrato nella routine " SqDRIFT ", alcuni algoritmi possono trarre vantaggio da una maggiore stabilità o da prestazioni migliori grazie alla conservazione delle proprietà fisiche.
- Fattori legati alla formula del prodotto : una formula del prodotto fermionica suddivide un operatore nei suoi gruppi, pertanto il raggruppamento determina sia il costo che l'errore di Trotter di un'evoluzione temporale. Vedi “Sintesi di un gate Evolution nello spazio fermionico ”.
Utilizzo
Il modo più semplice per definire un raggruppamento di operatori consiste nell'impostare l'attributo groups dell'operatore di destinazione. Di seguito è riportato un esempio di raggruppamento dei termini in base agli insiemi di flusso delle linee, come illustrato nella Figura 1c di [1].
Il modulo qiskit_fermions.operators.terms.grouping fornisce metodi di comodità per identificare automaticamente la struttura di un operatore e assegnare di conseguenza gli indici di gruppo.
>>> 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>
Si tratta di un semplice reticolo 2×3 di modi di Majorana collegati da spigoli orientati. Ogni spigolo rappresenta un termine dell'operatore che agisce sulle due modalità collegate. L'obiettivo è raggruppare questi termini in base alle etichette dei loro bordi (visibili sul grafico). I termini collegati da bordi con la stessa etichetta vengono inseriti nello stesso gruppo. Il codice seguente mostra come farlo:
>>> 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);Analizzare un raggruppamento
I gruppi non attribuiscono alcun significato proprio: l’insieme dei gruppi registra quali termini vanno accoppiati, ma non ne spiega mai il motivo. Nessuna routine di questa libreria indica se un raggruppamento sia “corretto”, poiché non esiste un unico criterio di correttezza in base al quale effettuare tale valutazione. I consumatori a valle, tuttavia, assumono le proprietà di un raggruppamento, e le funzioni descritte in questa sezione consentono di verificare tale ipotesi in anticipo, anziché doverne sostenere il costo ad ogni chiamata.
La convenzione più diffusa prevede che ciascun gruppo sia separatamente ermitiano. È proprio questo che rende possibili le formule del prodotto randomizzato fermionico: un singolo con non è hermitiano, quindi la sua evoluzione temporale non è unitaria e non può essere campionato da solo. Abbinandolo a si ottiene un gruppo che può essere.
>>> 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);È obbligatorio un indice di gruppo per ogni termine
L'array dei gruppi contiene un indice per ogni termine, e l'assegnazione di un indice di lunghezza diversa provoca un errore. Nessuna operazione successiva ricontrolla l'invariante, quindi questa viene applicata nel punto in cui gli indici entrano nell'operatore: un array troppo corto ometterebbe silenziosamente i termini finali, mentre uno troppo lungo creerebbe gruppi che nessun termine contiene.
>>> 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 termsNone È sempre consentito azzerare gli indici di gruppo assegnando un valore.
I due controlli sono indipendenti
groups_have_uniform_coeffs() indica se i coefficienti di un gruppo sono concordanti, il che è l’ipotesi su cui si basa group_coeff_means() quando calcola la media dei valori. Non si tratta di una forma più debole del test di ermiticità: nessuno dei due test implica l'altro, in nessuna delle due direzioni.
>>> 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])Per impostazione predefinita vengono confrontati i valori assoluti, poiché è questo che si intende per group_coeff_means() “media”. Una coppia di coniugati ermitiani con coefficienti complessi ha modulazioni uguali ma coefficienti disuguali, quindi soddisfa solo la forma predefinita.
>>> 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() eredita la garanzia unilaterale secondo cui is_hermitian(): una voce True è sempre affidabile, mentre una False voce è affidabile solo per i tipi di operatori la cui forma normale è una forma canonica autentica. Consultare la tipologia specifica di operatore per verificare quale sia quella applicabile.
R. Gandon et al., Simulazione quantistica basata su stabilizzatori della dinamica dei fermioni con codifiche locali dei qubit, arXiv:2512.11418v2.