Truncar termos de Pauli durante a retropropagação
Este guia explica como configurar o mecanismo de truncamento de termos de Pauli fornecido pelo módulo qiskit_addon_obp.utils.truncating.
A retropropagação de operadores (OBP) pode ser usada para reduzir a profundidade de circuitos quânticos ao custo de um observável mais complexo. Para obter resultados significativos da OBP, normalmente é preciso truncar termos do observável para evitar que ele cresça demais. Uma maneira de permitir uma retropropagação mais profunda no circuito, evitando ao mesmo tempo que o operador cresça demais, é truncar termos com coeficientes pequenos em vez de adicioná-los ao operador. Truncar termos pode resultar em menos circuitos quânticos a executar, mas isso gera algum erro no cálculo final do valor esperado, proporcional à magnitude dos coeficientes dos termos truncados.
O método backpropagate aceita um TruncationErrorBudget opcional, que configura o truncamento de termos de Pauli de baixo peso de cada observável após a retropropagação bem-sucedida de cada fatia. A quantidade de termos truncados depende de vários parâmetros de configuração especificados pelo usuário. Por enquanto, apenas uma estratégia de truncamento está disponível - o método truncate_binary_search. Dado um observável e algum orçamento, ele realizará uma busca binária sobre os termos de Pauli e coeficientes desse observável para encontrar o limiar ótimo, de modo que a soma dos coeficientes truncados seja máxima, mas menor que o orçamento.
Nota: Por padrão, a norma L1 é usada para avaliar e limitar o erro de truncamento; no entanto, a configuração p_norm permite especificar a norma Lp a ser usada.
Para mais informações sobre como usar essa configuração, consulte o guia Use diferentes normas Lp para o truncamento de termos de Pauli.
Os exemplos a seguir ilustram várias formas de construir um TruncationErrorBudget usando a função auxiliar setup_budget.
Construa um circuito de exemplo
Este guia usa as seguintes fatias de circuito:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# Generate a linear chain of 10 qubits
linear_chain = rustworkx.generators.path_graph(10)
# Use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# Evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
# for visualization purposes, recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Vamos olhar para um único observável simples:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIIZIIII")
O caso mais simples: um orçamento de truncamento fixo para cada fatia
O orçamento disponível para o truncamento de termos de Pauli pode variar a cada etapa da retropropagação. Para entender como isso funciona, primeiro olhamos para o caso mais simples de um orçamento de truncamento fixo, conforme especificado pelo usuário.
A forma mais direta de especificar o orçamento de truncamento é usando o argumento max_error_per_slice.
Na verdade, é isso que é feito no tutorial Reduzindo a profundidade de circuitos com retropropagação de operadores. Definir max_error_per_slice como um float faz com que cada fatia receba um orçamento igual a esse valor.
No exemplo abaixo, definimos esse valor como 0.001, o que garante um erro de truncamento implícito de no máximo 0.018, se todas as 18 fatias forem retropropagadas.
Observe que qualquer orçamento de erro que sobrar após retropropagar uma fatia e truncar termos com coeficientes pequenos será sempre adicionado ao orçamento de erro da fatia seguinte.
from qiskit_addon_obp.utils.truncating import setup_budget
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 29 terms and 10 commuting groups.
Em seguida, usamos a instância de OBPMetadata e as ferramentas fornecidas pelo módulo visualization para visualizar o processo de retropropagação.
-
O gráfico superior esquerdo mostra que temos orçamento suficiente para começar a truncar termos do observável após retropropagar a terceira fatia. A partir da terceira fatia, sabemos que truncamos pelo menos um termo de cada fatia que retropropagamos, porque incorremos em algum erro de truncamento após cada fatia.
-
O gráfico superior direito mostra que o orçamento de erro cresce para
.003na terceira fatia. Vemos uma queda acentuada no orçamento restante, o que significa que termos foram truncados do observável. Isso é consistente com o que inferimos do gráfico superior esquerdo. -
O gráfico inferior esquerdo mostra que, à medida que truncamos termos do nosso observável, o erro acumulado total cresce monotonicamente. Esse gráfico também confirma que nenhum termo foi truncado até depois de a terceira fatia ter sido retropropagada.
-
O gráfico inferior direito mostra que o número de grupos de Pauli comutantes no nosso observável cresceu até o limite especificado de
10. Esse gráfico também mostra como retropropagar mais uma camada faria nosso observável ultrapassar o limite especificado, como você pode ver pelo cruzamento das linhas preta e vermelha.
Observe que, em todos esses gráficos, o eixo x enumera as fatias retropropagadas, mas, como a OBP atua no final do circuito, slice 1 é a última fatia, slice 2 é a anterior a ela, e assim por diante.
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_left_over_error_budget,
plot_num_qwc_groups,
plot_slice_errors,
)
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Especifique o orçamento de cada fatia explicitamente
Se você sabe como atribuir um orçamento a cada fatia de modo que o desempenho da retropropagação seja otimizado, pode querer atribuir explicitamente um orçamento a cada fatia. Para fins de demonstração, atribuiremos orçamento zero às três primeiras fatias e usaremos um orçamento de .001 por fatia para as demais.
# Zero out the first 3 slices' budgets
max_error_per_slice = [0.0] * 3 + [0.001] * (len(slices) - 3)
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 32 terms and 10 commuting groups.
Remover o orçamento das três primeiras camadas resultou em nenhum termo truncado até depois da quarta camada, como pode ser confirmado em três desses gráficos. O que pode ser um tanto surpreendente é que, embora a quarta fatia não tenha recebido nenhum orçamento restante, um termo ainda foi truncado usando o orçamento de .001 alocado. Isso é óbvio no gráfico superior esquerdo, mas também pode ser observado no gráfico superior direito, pois a curva do orçamento restante fica plana entre as fatias três e quatro. Outro detalhe notável é que pelo menos um termo foi truncado a partir desse ponto, assim como no exemplo anterior.
A principal conclusão é que, embora tenhamos incorrido em menos erro de truncamento no segundo exemplo por zerar o orçamento de algumas fatias, conseguimos retropropagar o mesmo número de fatias, e nosso observável contém o mesmo número de grupos de Pauli comutantes. Podemos confirmar que o limite do nosso erro é menor no segundo exemplo inspecionando o gráfico inferior esquerdo.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Especifique o orçamento de forma cíclica
Se você tem um circuito com algum padrão repetível, como um circuito de Trotter, pode querer especificar um orçamento para esse subconjunto repetido de fatias e fazer com que esse orçamento seja usado em todas as repetições seguintes dessas fatias.
Mais concretamente, o circuito de exemplo que estamos usando tem seis fatias que se repetem três vezes, totalizando 18 fatias. Atribuiremos arbitrariamente orçamento zero às camadas de um qubit e de RYY, e orçamento .003 a cada uma das camadas RXX. Observaremos como especificar o orçamento como uma sequência de comprimento 6 faz com que ele seja aplicado a todas as 18 fatias de forma cíclica.
Novamente, observe que as fatias são retropropagadas em ordem inversa (ou seja, começando pelo final). Portanto, a primeira entrada do nosso orçamento cíclico é, na verdade, usada para a última fatia, a segunda entrada para a fatia anterior, e assim por diante.
# Specify a length-6 per-slice budget.
# This will be cycled over three times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=inf, p_norm=1)
op_budget = OperatorBudget(max_qwc_groups=20)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 13 circuit slices.
New observable contains 49 terms and 14 commuting groups.
Como visto nas imagens superior esquerda e inferior esquerda, nenhum truncamento foi feito nas quatro primeiras fatias, já que elas não receberam orçamento de erro. As fatias cinco e seis tiveram alguns de seus termos truncados à medida que o orçamento ficou disponível.
Como visto na imagem superior esquerda, uma quantidade relativamente grande de erro foi incorrida após retropropagar a fatia sete, mesmo que essa fatia não tenha recebido orçamento. Isso ocorre porque apenas cerca de .002 dos .006 de orçamento total alocados às fatias cinco e seis foram usados, então o restante foi repassado à fatia sete e em grande parte consumido, como visto na imagem superior direita.
As imagens superior esquerda e superior direita mostram que a pequena quantidade de orçamento restante é consumida entre as fatias 8-10, e que um novo orçamento fica disponível na fatia 11, como esperado. As imagens superior esquerda, superior direita e inferior esquerda demonstram o comportamento cíclico do argumento max_error_per_slice quando seu comprimento é menor que o número de fatias. Esse comportamento cíclico teria continuado por todas as fatias, mas o critério de parada max_qwc_groups foi atingido após retropropagar 13 fatias, como visto na imagem inferior direita.
Também é interessante que o número de grupos de Pauli na verdade cai depois que a segunda rodada de orçamento fica disponível na fatia 11. Isso ocorre porque havia grupos com coeficientes pequenos que não puderam ser truncados até haver mais orçamento após retropropagar a fatia 11, então eles se acumularam no observável por várias iterações. Esse caso também destaca que max_qwc_groups precisa ser excedido para o algoritmo terminar.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Limite o erro total
Além de especificar o orçamento de erro por fatia, é possível especificar a quantidade máxima de erro a incorrer com o truncamento. Quando esse limite é atingido, nenhum truncamento adicional é feito; no entanto, a retropropagação continua até que o observável fique grande demais e um dos critérios de parada seja atendido.
Em seguida, executamos novamente o experimento acima, definindo um limite em max_error_total de modo que o orçamento de erro seja consumido após retropropagar a fatia sete.
# Specify a length-6 per-slice budget.
# This will be cycled over 3 times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice, max_error_total=0.006
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=0.006, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 10 circuit slices.
New observable contains 67 terms and 20 commuting groups.
Como esperado, nosso erro de truncamento chega ao limite de .006 (gráfico inferior esquerdo). É notável que, nesta execução, não conseguimos retropropagar a fatia 11. Isso ocorre porque não tínhamos orçamento suficiente para truncar termos e o número de grupos de Pauli comutantes ultrapassou o limite de 20, como visto na imagem inferior direita.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Pode ser desejável simplesmente limitar o orçamento total com max_error_total sem especificar max_error_per_slice.
truncation_error_budget = setup_budget(max_error_total=0.018)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.018], max_error_total=0.018, p_norm=1)
A saída da célula acima pode ser um pouco surpreendente porque o per_slice_budget é definido como o max_error_total. Isso indica que todo o orçamento disponível será consumido de forma gananciosa.
Pense da seguinte forma: todo o orçamento está disponível para cada fatia (porque percorremos o per_slice_budget em um laço). Mas qualquer orçamento que já tenha sido consumido será deduzido do orçamento disponível naquele ponto do algoritmo.
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 9 circuit slices.
New observable contains 25 terms and 9 commuting groups.
A imagem superior direita mostra como todo o orçamento de erro de .018 é disponibilizado para a primeira fatia. Nenhum truncamento é feito até a terceira fatia, então o orçamento restante permanece constante. O orçamento diminui monotonicamente, já que o orçamento completo é disponibilizado a cada fatia retropropagada até ser consumido.
É notável que este experimento resultou em duas fatias retropropagadas a menos em comparação com o primeiro experimento deste notebook, que é quase idêntico. Isso demonstra que, para alguns problemas, distribuir o orçamento uniformemente pode ser o ideal. Para outros problemas, permitir que as fatias consumam o orçamento total de forma gananciosa pode render melhor desempenho.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Limite juntos o número de fatias retropropagadas e o erro total
Se você não quer distribuir o orçamento de erro por todo o circuito, mas também não quer consumi-lo de forma gananciosa, pode especificar o número de fatias que pretende retropropagar (num_slices), junto com um orçamento de erro total (max_error_total). Isso distribuirá o orçamento de erro uniformemente (de acordo com p_norm) entre as fatias de entrada.
Aqui, limitaremos o número de fatias que podemos retropropagar a 12 e manteremos o mesmo orçamento de erro total.
num_slices = 12
truncation_error_budget = setup_budget(
max_error_total=0.018, num_slices=num_slices, p_norm=1
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0014999999999999998], max_error_total=0.018, p_norm=1)
Agora tentaremos retropropagar as 12 fatias para as quais alocamos algum orçamento na etapa anterior. Fazemos isso simplesmente passando as últimas 12 fatias do nosso circuito. Com p_norm=1, cada uma das 12 fatias tem um orçamento disponível de 0.018 / num_slices = 0.0015.
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices[-num_slices:],
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
Como passamos um subconjunto das nossas fatias (no código slices[-num_slices:]) para backpropagate, precisamos combinar as fatias restantes após a retropropagação com as fatias que nunca foram enviadas para retropropagação (no código slices[:-num_slices]).
Depois de combinar todas as fatias restantes, podemos usar combine_slices para gerar o QuantumCircuit de profundidade reduzida. Em seguida, inspecionamos quantas fatias foram retropropagadas em comparação com o tamanho que nosso observável atingiu.
# Recombine the slices remaining after backprop with the rest of the original circuit
reduced_circuit = combine_slices(slices[:-num_slices] + remaining_slices)
print(f"Backpropagated {num_slices - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 12 circuit slices.
New observable contains 29 terms and 9 commuting groups.
Os gráficos mostram que conseguimos retropropagar com sucesso todas as 12 fatias, mantendo nosso observável com menos de 10 grupos de Pauli comutantes. Também podemos ver que usar num_slices junto com max_error_total resulta em uma distribuição de orçamentos entre as fatias, e o orçamento não utilizado é novamente repassado para a próxima fatia. Isso fica mais evidente no gráfico superior direito, já que o orçamento é gasto e reposto ao longo da retropropagação.
Vale notar que esse método de distribuir o erro produziu os melhores resultados (mais fatias retropropagadas), em comparação com o primeiro experimento deste notebook e com o exemplo imediatamente anterior a este. Todos esses exemplos destinaram um orçamento de erro de .018, mas a retropropagação teve desempenhos diferentes dependendo de como esse orçamento foi distribuído.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Trabalhe com vários observáveis
O método qiskit_addon_obp.backpropagate permite passar uma sequência de observáveis. Isso simplifica o fluxo de trabalho ao lidar com vários observáveis-alvo.
Mencionamos isso explicitamente aqui para ensinar como a estratégia de truncamento lida com esse caso. Para este exemplo, adicionamos um observável extra àquele que temos usado até agora:
obs = [SparsePauliOp("IIIIIZIIII"), SparsePauliOp("IIIIIXIIII")]
Por fim, repetimos o primeiro experimento deste tutorial, mas com dois observáveis para os quais retropropagar o circuito.
Neste exemplo, isso não afeta o número de fatias que puderam ser retropropagadas. No entanto, podemos ver que os dois observáveis resultaram em números diferentes de termos de Pauli e de grupos comutantes.
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"The new first observable contains {len(bp_obs[0])} terms and {len(bp_obs[0].group_commuting(qubit_wise=True))} commuting groups."
)
print(
f"The new second observable contains {len(bp_obs[1])} terms and {len(bp_obs[1].group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
The new first observable contains 29 terms and 10 commuting groups.
The new second observable contains 23 terms and 8 commuting groups.
Os gráficos abaixo ajudam a ilustrar como o algoritmo de retropropagação lida com vários observáveis.
Primeiro, os gráficos superior esquerdo, superior direito e inferior esquerdo mostram que o orçamento para truncar termos é definido individualmente para cada observável. Em outras palavras, ambos os observáveis podem truncar termos assumindo um erro de 0.001 por fatia retropropagada.
Devido à natureza diferente dos observáveis, isso resulta em consumos diferentes do orçamento. Neste exemplo, eles apresentam muita sobreposição, o que nem sempre acontece.
O gráfico inferior direito ilustra que max_qwc_groups leva em conta todos os observáveis. Isso significa que os termos de todos os observáveis são agrupados para chegar a um número final de grupos comutantes qubit a qubit, que é comparado com max_qwc_paulis. O mesmo é feito para o limite max_paulis (não discutido neste notebook), que permite definir um limite para o número de termos de Pauli em todos os observáveis.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])
