Agrupar termos de operadores: use a estrutura do operador
O agrupamento é uma das operações em qiskit_fermions.operators.terms, que reúne rotinas que particionam os termos de um operador com base em sua estrutura.
Como descrito no guia de operadores, os operadores podem armazenar um array de grupos, uma parte opcional da estrutura de dados esparsa que associa cada termo a um índice de grupo. Termos que compartilham o mesmo índice de grupo formam um grupo. Isso permite aproveitar sistematicamente a estrutura no processamento posterior.
Ao agrupar termos relacionados (seja por propriedades físicas, relações algébricas ou simetrias específicas do problema), você pode obter vários benefícios:
-
Síntese de circuitos otimizada: Termos agrupados carregam informações específicas do problema ao longo da pilha, permitindo uma otimização mais simples dos circuitos sintetizados. Isso resulta em menor profundidade de circuito e menor quantidade de portas.
-
Preservação da estrutura física: Os grupos codificam estrutura significativa do problema original (por exemplo, padrões de interação ou simetrias) que pode ser usada para decomposições fisicamente mais significativas.
-
Seleção de conjuntos de fluxo: O agrupamento pode codificar naturalmente conjuntos de fluxo e outras estruturas da teoria dos grafos relevantes para a simulação quântica, como mostrado em [1].
-
Maior estabilidade algorítmica: Como demonstrado na rotina SqDRIFT, alguns algoritmos podem se beneficiar de maior estabilidade ou desempenho graças à preservação de propriedades físicas.
-
Fatores da fórmula de produto: Uma fórmula de produto fermiônica divide um operador em seus grupos, de modo que o agrupamento determina tanto o custo quanto o erro de Trotter de uma evolução temporal. Veja Sintetize uma porta de Evolution no espaço fermiônico.
Uso
A maneira mais simples de definir um agrupamento de operador é definir o atributo groups do operador de destino. Abaixo está um exemplo de agrupamento de termos de acordo com os conjuntos de fluxo de linha mostrados na Figura 1c de [1].
O módulo qiskit_fermions.operators.terms.grouping oferece métodos de conveniência para identificar automaticamente a estrutura em um operador e atribuir índices de grupo de acordo.
>>> 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>
Esta é uma rede simples de dois por três de modos de Majorana conectados por arestas direcionadas. Cada aresta representa um termo do operador que atua nos dois modos conectados. O objetivo é agrupar esses termos de acordo com os rótulos de suas arestas (mostrados no grafo). Termos conectados por arestas com o mesmo rótulo ficam no mesmo grupo. O código a seguir mostra como isso pode ser feito:
- 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);
Analise um agrupamento
Os grupos não prescrevem nenhum significado próprio: o array de grupos registra quais termos pertencem juntos, nunca o porquê. Nenhuma rotina desta biblioteca informa se um agrupamento está “correto”, porque não existe uma única noção de correção em relação à qual reportar. No entanto, os consumidores posteriores assumem propriedades de um agrupamento, e as funções desta seção permitem verificar essa suposição de antemão, em vez de pagar por ela a cada chamada.
A convenção mais comum é que cada grupo seja hermitiano separadamente. É isso que torna possíveis as fórmulas de produto fermiônicas aleatorizadas: um isolado com não é hermitiano, então sua evolução temporal não é unitária e ele não pode ser amostrado sozinho. Emparelhá-lo com resulta em um grupo que pode.
- 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);
Um índice de grupo por termo é obrigatório
O array de grupos contém um índice por termo, e atribuir um de comprimento diferente gera um erro. Nada no processamento posterior volta a verificar essa invariante, por isso ela é imposta onde os índices entram no operador: um array curto demais descartaria termos finais silenciosamente, e um longo demais inventaria grupos que nenhum termo possui.
>>> 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
Limpar os índices de grupo atribuindo None é sempre permitido.
As duas verificações são independentes
groups_have_uniform_coeffs() informa se os coeficientes de um grupo coincidem, que é a suposição que group_coeff_means() faz ao calcular a média das magnitudes. Ela não é uma forma mais fraca da verificação de hermiticidade: nenhuma das verificações implica a outra, em nenhuma direção.
>>> 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])
Por padrão, as magnitudes são comparadas, porque é isso que group_coeff_means() calcula a média. Um par conjugado hermitiano com coeficientes complexos tem magnitudes iguais, mas coeficientes diferentes, então satisfaz apenas a forma padrão.
>>> 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() herda a garantia unilateral de is_hermitian(): uma entrada True é sempre confiável, enquanto uma entrada False só é confiável para tipos de operador cuja forma normal seja uma verdadeira forma canônica. Consulte o tipo de operador específico para saber qual caso se aplica.
A. Gandon et al., Stabilizer-based quantum simulation of fermion dynamics with local qubit encodings, arXiv:2512.11418v2.