Pular para o conteúdo principal

Primeiros passos com fórmulas multi-produto (MPF)

Primeiros passos com fórmulas multi-produto (MPFs)

Versões dos pacotes

O código desta página foi desenvolvido com os seguintes requisitos. Recomendamos usar essas versões ou mais recentes.

qiskit[all]~=2.3.0
qiskit-addon-utils~=0.3.0
qiskit-addon-mpf~=0.3.0
scipy~=1.16.3

Este guia demonstra como usar o pacote qiskit-addon-mpf, utilizando a evolução temporal de um modelo de Ising como exemplo. Com esse pacote, você pode construir uma Fórmula Multi-Produto (MPF) que alcança um erro de Trotter menor nas medições de observáveis. As ferramentas fornecidas permitem determinar os pesos de uma MPF escolhida, que podem ser usados para recombinar os valores esperados estimados a partir de vários circuitos de evolução temporal, cada um com um número diferente de passos de Trotter.

Comece considerando o Hamiltoniano de um modelo de Ising com 10 sítios:

HIsing=i=19Ji,(i+1)ZiZi+1+i=110hiXiH_{\text{Ising}} = \sum_{i=1}^9 J_{i,(i+1)}Z_iZ_{i+1} + \sum_{i=1}^{10} h_i X_i

onde Ji,(i+1)J_{i,(i+1)} é a intensidade do acoplamento e hih_i é a intensidade do campo magnético externo. Para configurar o problema, o observável a ser medido será a magnetização total do sistema

M=i=110Zi.\langle M \rangle = \sum_{i=1}^{10} \langle Z_i \rangle.

O trecho de código abaixo prepara o Hamiltoniano da cadeia de Ising usando o pacote qiskit-addon-utils e define o observável que será medido.

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-mpf qiskit-addon-utils scipy
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import SuzukiTrotter
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import StatevectorEstimator
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_addon_utils.problem_generators import (
generate_xyz_hamiltonian,
generate_time_evolution_circuit,
)
from qiskit_addon_mpf.costs import (
setup_exact_problem,
setup_sum_of_squares_problem,
)
from qiskit_addon_mpf.static import setup_static_lse

from scipy.linalg import expm
import numpy as np

# Generate some coupling map to use for this example
coupling_map = CouplingMap.from_line(10, bidirectional=False)

# Get a qubit operator describing the Ising field model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(0.0, 0.0, 1.0),
ext_magnetic_field=(0.4, 0.0, 0.0),
)
print(f"Hamiltonian:\n {hamiltonian}\n")

L = coupling_map.size()
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / L / 2) for i in range(L)], num_qubits=L
)
print(f"Observable:\n {observable}")
Hamiltonian:
SparsePauliOp(['IIIIIIIZZI', 'IIIIIZZIII', 'IIIZZIIIII', 'IZZIIIIIII', 'IIIIIIIIZZ', 'IIIIIIZZII', 'IIIIZZIIII', 'IIZZIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIXI', 'IIIIIIIXII', 'IIIIIIXIII', 'IIIIIXIIII', 'IIIIXIIIII', 'IIIXIIIIII', 'IIXIIIIIII', 'IXIIIIIIII', 'XIIIIIIIII'],
coeffs=[1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j, 1. +0.j,
1. +0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j, 0.4+0.j,
0.4+0.j, 0.4+0.j, 0.4+0.j])

Observable:
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j, 0.05+0.j,
0.05+0.j, 0.05+0.j, 0.05+0.j])

Em seguida, você prepara a MPF. A primeira escolha a ser feita é se os coeficientes serão estáticos (independentes do tempo) ou dinâmicos; este tutorial usa uma MPF estática. A próxima escolha é o conjunto de valores kjk_j. Isso determina quantos termos a MPF terá, bem como quantos passos de Trotter cada termo usa para simular a evolução temporal. A escolha dos valores kjk_j é heurística, portanto você precisa obter seu próprio conjunto de "bons" valores kjk_j. Leia as diretrizes para encontrar um bom conjunto de valores ao final da página de primeiros passos.

Em seguida, uma vez determinados os valores kjk_j, você pode preparar o sistema de equações Ax=bAx=b a resolver. A matriz AA também é determinada pela fórmula de produto a ser usada. As escolhas aqui são sua ordem, que é definida como 22 neste exemplo, e se a fórmula de produto deve ser simétrica, que é definida como True neste exemplo. O trecho de código abaixo seleciona um tempo total para evoluir o sistema, os valores kjk_j a serem usados e o conjunto de equações a resolver usando o método qiskit_addon_mpf.static.setup_static_lse.

time = 8.0
trotter_steps = (8, 12, 19)

lse = setup_static_lse(trotter_steps, order=2, symmetric=True)
print(lse)
LSE(A=array([[1.00000000e+00, 1.00000000e+00, 1.00000000e+00],
[1.56250000e-02, 6.94444444e-03, 2.77008310e-03],
[2.44140625e-04, 4.82253086e-05, 7.67336039e-06]]), b=array([1., 0., 0.]))

Uma vez instanciado o sistema linear de equações, ele pode ser resolvido de forma exata ou por meio de um modelo aproximado usando uma soma de quadrados (ou a norma de Frobenius para coeficientes dinâmicos; consulte a referência da API para mais informações). A escolha de usar um modelo aproximado geralmente surge quando a norma dos coeficientes para o conjunto escolhido de valores kjk_j é considerada muito alta e um conjunto diferente de valores kjk_j não pode ser escolhido. Este guia demonstra ambas as abordagens para comparar os resultados.

model_exact, coeffs_exact = setup_exact_problem(lse)
model_approx, coeffs_approx = setup_sum_of_squares_problem(
lse, max_l1_norm=3.0
)
model_exact.solve()
model_approx.solve()
print(f"Exact solution: {coeffs_exact.value}")
print(f"Approximate solution: {coeffs_approx.value}")
Exact solution: [ 0.17239057 -1.19447005  2.02207947]
Approximate solution: [-0.40454257 0.57553173 0.8290123 ]
nota

O objeto LSE também possui um método LSE.solve(), que resolve o sistema de equações de forma exata. O motivo pelo qual setup_exact_problem() é usado neste guia é para demonstrar a interface fornecida pelos demais métodos aproximados.

Configurar e executar circuitos de Trotter

Agora que os coeficientes xjx_j foram obtidos, o último passo é gerar os circuitos de evolução temporal para a ordem e o conjunto escolhido de passos kjk_j da MPF. O pacote qiskit-addon-utils pode acelerar esse processo.

circuits = []
for k in trotter_steps:
circ = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(order=2, reps=k),
time=time,
)
circuits.append(circ)
circuits[0].draw("mpl", fold=-1)

Output of the previous code cell

circuits[1].draw("mpl", fold=-1)

Output of the previous code cell

circuits[2].draw("mpl", fold=-1)

Output of the previous code cell

Uma vez construídos esses circuitos, você pode transpilar e executá-los usando um QPU. Para este exemplo, usaremos apenas um dos simuladores sem ruído para demonstrar como o erro de Trotter é reduzido.

backend = GenericBackendV2(num_qubits=10)
transpiler = generate_preset_pass_manager(
optimization_level=2, backend=backend
)

transpiled_circuits = [transpiler.run(circ) for circ in circuits]

estimator = StatevectorEstimator()
job = estimator.run([(circ, observable) for circ in transpiled_circuits])
result = job.result()

mpf_evs = [res.data.evs for res in result]
print(mpf_evs)
[array(0.23799162), array(0.35754312), array(0.38649906)]

Reconstruir os resultados

Agora que os circuitos foram executados, reconstruir os resultados é bastante simples. Conforme mencionado na página de visão geral das MPFs, nosso observável é reconstruído por meio da soma ponderada

M=jxjMj.\langle M \rangle = \sum_j x_j \langle M_j \rangle.

onde xjx_j são os coeficientes encontrados e Mj\langle M_j \rangle é a estimativa do observável iZi\sum_i \langle Z_i \rangle para cada circuito. Podemos então comparar os resultados obtidos com o valor exato usando o pacote scipy.linalg.

exp_H = expm(-1j * time * hamiltonian.to_matrix())
initial_state = np.zeros(exp_H.shape[0])
initial_state[0] = 1.0

time_evolved_state = exp_H @ initial_state
exact_obs = (
time_evolved_state.conj() @ observable.to_matrix() @ time_evolved_state
)

# Print out the different observable measurements
print(f"Exact value: {exact_obs.real}")
print(f"PF with 19 steps: {mpf_evs[-1]}")
print(f"MPF using exact solution: {mpf_evs @ coeffs_exact.value}")
print(f"MPF using approximate solution: {mpf_evs @ coeffs_approx.value}")
Exact value: 0.4006024248789992
PF with 19 steps: 0.3864990619977402
MPF using exact solution: 0.3954847855979902
MPF using approximate solution: 0.4299121425348959

Aqui você pode ver que a MPF reduziu o erro de Trotter em comparação com o obtido usando apenas uma única PF com kj=19k_j=19. No entanto, o modelo aproximado resultou em um valor esperado pior do que o modelo exato. Isso demonstra a importância de usar critérios de convergência rígidos no modelo aproximado e de encontrar um "bom" conjunto de valores kjk_j.