Pular para o conteúdo principal

Simular um modelo de Ising pulsado com a função TEM

O método de mitigação de erros por rede tensorial (TEM) da Algorithmiq é um algoritmo híbrido quântico-clássico projetado para realizar a mitigação de ruído inteiramente na etapa de pós-processamento clássico. Com o TEM, você pode calcular os valores esperados dos observáveis, mitigando os inevitáveis erros induzidos por ruído no hardware quântico com maior precisão e eficiência de custo, tornando-o uma opção muito atraente para pesquisadores quânticos e profissionais da indústria.

Este tutorial demonstra como o TEM pode obter resultados significativos para a dinâmica de um sistema quântico, que seria inacessível sem mitigação de erros e que requer substancialmente mais recursos quânticos se outros métodos de mitigação de erros como PEC e ZNE forem utilizados.

Estimativa de uso: este notebook utiliza aproximadamente 10 minutos de QPU em dispositivos Heron r3. O tempo de execução pode variar substancialmente dependendo do dispositivo escolhido. As estimativas de uso por seção podem ser encontradas abaixo.

Executar experimentos de física de muitos corpos com mitigação de erros via função TEM

Este tutorial é baseado na seguinte referência: L. E. Fischer et al., Nat. Phys. (2026). Esta referência discute uma simulação real em hardware quântico de até 91 qubits. Neste tutorial, recriamos uma simulação semelhante em um circuito de menor tamanho.

O modelo de Ising pulsado corresponde ao modelo de Ising usual:

H^I=Jn=0N2Z^nZ^n+1+hn=0N1Z^n\hat{H}_{\text{I}} = J \sum_{n=0}^{N-2} \hat{Z}_n \hat{Z}_{n+1} + h \sum_{n=0}^{N-1} \hat{Z}_n

ao qual é aplicado um pulso transversal:

H^K=bn=0N1X^n\hat{H}_{K} = b \sum_{n=0}^{N-1} \hat{X}_n

O objetivo é simular a dinâmica de um estado sob o Hamiltoniano de Ising pulsado transversal, cuja evolução temporal pode ser implementada por um unitário de Floquet U^KI=eiH^KeiH^I\hat{U}_{\text{KI}} = e^{-i \hat{H}_K} e^{-i \hat{H}_I} . O estado inicial a evoluir é aquele no qual o primeiro qubit está no estado +|+\rangle, enquanto os demais estão emparelhados e configurados no estado de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}.

A quantidade que queremos observar é a função de correlação. O artigo de referência discute como essa quantidade pode ser reescrita como um operador de Pauli X^\hat{X} no neˊsimon^{ésimo} qubit. Após um número de passos de tempo físicos tt, calculamos o valor do operador de Pauli X^n=t\hat{X}_{n=t}. Dependendo dos parâmetros do sistema, o valor deste observável é igual a um valor que pode ser calculado exatamente, ou apenas simulado por métodos aproximados. Especificamente, para J=b=π/4|J|=|b|=\pi/4, é igual a [cos(2h)]t[\cos(2h)]^t, que é o valor que usaremos para comparar os resultados deste tutorial. Além disso, em um dado passo de tempo tt, X^nt\langle\hat{X}_{n\neq t}\rangle é zero. Para detalhes sobre como obter esses valores, e para comparação com resultados de simulação clássica aproximada fora desses parâmetros, consulte L. E. Fischer et al., Nat. Phys. (2026).

O TEM funciona primeiro caracterizando o ruído de cada camada única de gates de dois qubits no circuito, bem como caracterizando o erro de leitura. Em seguida, o circuito é executado na máquina quântica. Por fim, a mitigação de erros por rede tensorial é realizada nos recursos clássicos no IBM Cloud® e o valor mitigado é retornado. Neste exemplo, o circuito tem duas camadas únicas para caracterizar.

Configuração

Como pré-requisito, certifique-se de que as dependências necessárias estejam instaladas.

%pip install numpy matplotlib qiskit qiskit-ibm-catalog qiskit-ibm-runtime pylatexenc qiskit_qasm3_import
import os
from matplotlib import pyplot as plt
import numpy as np

from qiskit.quantum_info import SparsePauliOp
from qiskit.qasm3 import load

from qiskit_ibm_catalog import QiskitFunctionsCatalog

Mitigação de erros com TEM

Fornecemos aqui um circuito que implementa o modelo de Ising pulsado descrito acima. O circuito é preparado da seguinte forma. Primeiro, há uma fase de preparação de estado, na qual o primeiro qubit está no estado +|+\rangle, enquanto os demais estão em pares de Bell (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2}. Isso é seguido pela estrutura em tijolos que implementa a evolução unitária U^KI\hat{U}_{\text{KI}}. O número de passos de tempo físicos corresponde a t/2t/2 camadas de circuito. O código a seguir baixa os dois arquivos QASM necessários para este tutorial.

# Download required QASM files
import urllib

urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/swy5jtq309b0xpzluzlmsmj908yphes8.qasm",
"ki_30q.qasm",
)
urllib.request.urlretrieve(
"https://ibm.box.com/shared/static/et3gkodonw6gsp2trs43lzaozrdtiu7s.qasm",
"ki_12q.qasm",
)

Podemos visualizar uma versão pequena do circuito, com 12 qubits e seis passos de tempo:

# Parameters of the kicked Ising model
h = 0.0
num_qubits = 12
t_steps = 6

# Load the circuit for the kicked Ising model
small_circuit = load("ki_12q.qasm")

# Draw the circuit
small_circuit.draw("mpl", scale=0.25, fold=-1)

Output of the previous code cell

Em seguida, construa o observável X^n=t\hat{X}_{n=t}. Ele é construído como uma simples cadeia de Pauli com a ordem correspondente à usada pelo Qiskit:

def xt_observable(n_qubits, t_steps):
pauli_str = "".join(["I" * t_steps, "X", "I" * (n_qubits - t_steps - 1)])
pauli_str = pauli_str[::-1] # Reverse the string to match qiskit order
return SparsePauliOp(data=pauli_str, coeffs=1.0)

Em nosso pequeno exemplo de 12 qubits, o observável se parece com isto:

# Build the observable for the kicked Ising model
small_observable = xt_observable(n_qubits=12, t_steps=6)
print(small_observable)
SparsePauliOp(['IIIIIXIIIIII'],
coeffs=[1.+0.j])

As funções Qiskit usam PUBs como forma de coletar as entradas. No nosso caso, vamos considerar um único circuito e observável como nosso PUB:

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(small_circuit, [small_observable])]

Em seguida, obtemos acesso à função TEM. Primeiro configuramos a autenticação necessária para o IBM Cloud e selecionamos um backend entre os dispositivos disponíveis. O token, os backends disponíveis e os nomes de recursos de nuvem correspondentes (CRN) podem ser obtidos fazendo login em sua conta no painel do IBM Quantum Platform.

# Set IBM Quantum credentials and backend configuration
personal_token = os.environ.get(
"QISKIT_IBM_TOKEN", "<API-KEY>"
) # Replace with your personal token or set the environment variable
channel = "ibm_quantum_platform"
crn = "your_crn" # Replace with the Cloud Resource Name (CRN)

# Select the QPU backend
backend_name = "ibm_qpu_name" # Replace with your desired backend's name

Carregue a função TEM do Qiskit Functions Catalog:

# Load the TEM function from the Qiskit Functions Catalog
catalog = QiskitFunctionsCatalog(
channel=channel,
token=personal_token,
instance=crn,
)
tem = catalog.load("algorithmiq/tem")

Agora podemos executar um experimento no circuito de Ising pulsado com mitigação de erros fornecida pelo TEM. Usando as configurações padrão, o TEM pode ser executado de forma simples com um tempo de execução QPU esperado de cerca de 2,5 minutos, dependendo do QPU:

tem_job = tem.run(pubs=pubs, backend_name=backend_name)

Com as opções padrão, a função TEM executa três jobs no computador quântico: aprendizado de ruído, mitigação de leitura e amostragem do circuito. O número de shots usados por cada um deles pode ser alterado nas opções passadas para a função. Por padrão, esses parâmetros são configurados para atingir uma precisão de 0,05 nos valores esperados mitigados. Você pode verificar o status do seu job no painel do IBM Quantum Platform ou com:

print(tem_job.status())
QUEUED

Quando o status for DONE, podemos verificar os resultados brutos e mitigados. Os tem_evs definidos abaixo são os valores esperados dos observáveis solicitados, neste caso apenas um observável, X^n=t\langle \hat X_{n=t}\rangle, e tem_std são os desvios padrão correspondentes.

# Get the results of the TEM job
tem_results = tem_job.result()[
0
] # Get the first and only result from the job
tem_evs = tem_results.data.evs[0]
tem_std = tem_results.data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 1.031 ± 0.046

Também podemos verificar quanto tempo de execução quântica foi usado para cada chamada no IBM Quantum Platform, ou inspecionando os metadados de resultados do código Python.

# Get the TEM job runtime
tem_runtime = tem_job.result().metadata["resource_usage"][
"RUNNING: EXECUTING_QPU"
]["QPU_TIME"]

print(f"TEM Runtime: {tem_runtime} seconds")
TEM Runtime: 155.0 seconds

Personalizar os parâmetros TEM e opções avançadas

A função TEM fornece várias opções avançadas para personalizar seu workflow de mitigação de erros. Essas opções permitem controlar a precisão, o número de shots, as estratégias de aprendizado de ruído e outros parâmetros para melhor atender às necessidades do seu experimento e aos recursos quânticos disponíveis.

As opções avançadas comuns são:

  • precision: Especifica a precisão alvo para os valores esperados mitigados.
  • default_shots: Em vez de precision, você pode especificar o número de shots usados pelo job de medição.
  • tem_max_bond_dimension: A dimensão de ligação máxima usada na rede tensorial.
  • tem_compression_cutoff: O valor de corte a ser usado para a rede tensorial.
  • opções de aprendizado de ruído: Configure como o ruído é caracterizado, como o número de repetições ou circuitos de calibração específicos.
  • private: Garante que os circuitos e resultados do experimento sejam privados para você e desativa os downloads múltiplos dos resultados do job.

Consulte a documentação do TEM ou o Qiskit Functions Catalog para uma lista completa de opções suportadas e suas descrições. Você pode ajustar esses parâmetros para equilibrar o tempo de execução, o uso de recursos e a precisão dos resultados. Você pode passar essas opções como um dicionário para o argumento options ao executar a função TEM:

options = {
"default_shots": 10_000,
"tem_max_bond_dimension": 512,
"tem_compression_cutoff": 1e-16,
# This option helps optimizing the measurement
# stage since the observable is strongly biased
# toward the X operator for all the qubits.
"compute_shadows_bias_from_observable": True,
# set to True to keep experiment results private,
# recommended for confidential circuits
"private": False,
}

Opções personalizadas para o noise learner também podem ser passadas. Elas seguem as definições usadas no Qiskit Runtime NoiseLearnerOptions:

nl_options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32],
}

# add noise learning options to the overall options
options |= nl_options

Execute o experimento novamente com essas opções personalizadas ajustadas ao nosso circuito. O tempo de execução esperado é de aproximadamente quatro minutos de QPU.

tem_job_custom = tem.run(
pubs=pubs, backend_name=backend_name, options=options
)

Se o job não estiver configurado como privado, podemos recuperar o resultado posteriormente. Para isso, salve o ID do job impresso aqui e use tem_job_custom = catalog.get_job_by_id("your-job-id").

job_id = tem_job_custom.job_id
print(f"Job ID: {job_id}")
Job ID: 1ba10094-a541-457a-9287-dbd49306d12d
results_custom = tem_job_custom.result()
tem_evs = results_custom[0].data.evs[0]
tem_std = results_custom[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")
TEM Result: 0.956 ± 0.018

Agora podemos inspecionar os resultados e os metadados para obter informações sobre o experimento:

metadata_custom = results_custom[0].metadata

unmitigated_evs = metadata_custom["evs_non_mitigated"][0]
unmitigated_stds = metadata_custom["stds_non_mitigated"][0]
print(f"Unmitigated Result: {unmitigated_evs:.3f} ± {unmitigated_stds:.3f}")

# Exact result for the kicked Ising model from the reference paper
exact_evs = np.cos(2 * h) ** t_steps
print("Exact Result:", exact_evs)
Unmitigated Result: 0.894 ± 0.015
Exact Result: 1.0
# Plot comparing the different expectation values
plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell

Por fim, podemos verificar o impacto das opções personalizadas no tempo de execução do QPU e clássico:

# Get the metadata of the TEM job
job_metadata = results_custom.metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
QPU Runtime: 342.0 seconds
Classical Runtime: 107.632604 seconds

Escalar o TEM para circuitos grandes

Circuitos grandes podem, em princípio, ser executados com a função TEM. No entanto, é importante estar ciente das limitações dos recursos clássicos, pois o TEM é executado em runners do IBM Cloud com tempos de execução potencialmente muito longos. Para circuitos extremamente grandes, entre em contato com a equipe de suporte do TEM em qiskit_ibm@algorithmiq.fi.

Aqui executamos um exemplo com um circuito maior de 30 qubits em escala de utilidade, otimizando os parâmetros do TEM para velocidade em vez de precisão.

# Kicked Ising model parameters
n_qubits = 30
t_steps = 15
h = 0.0

# Load the circuit for the kicked Ising model
circuit = load("ki_30q.qasm")

# Build the observable for the kicked Ising model
observable = xt_observable(n_qubits=n_qubits, t_steps=t_steps)

# Collect the input PUBs, in this case composed of a
# single circuit and observable
pubs = [(circuit, [observable])]

Vamos definir algumas opções orientadas a desempenho:

options = {
"num_randomizations": 32,
"max_layers_to_learn": 2,
"shots_per_randomization": 128,
"layer_pair_depths": [0, 1, 2, 4, 16, 32, 64],
"default_shots": 5_000,
"tem_max_bond_dimension": 128,
"tem_compression_cutoff": 1e-10,
"compute_shadows_bias_from_observable": True,
"private": False,
}

Por fim, execute o experimento, obtenha o resultado e visualize-o. Isso levará aproximadamente 3,5 minutos de QPU.

tem_job_large = tem.run(pubs=pubs, backend_name=backend_name, options=options)
job_id = tem_job_large.job_id
print(f"Job ID: {job_id}")
Job ID: 9f3f190f-f4b0-4dcb-bb83-5f71f37d0d77
results_large = tem_job_large.result()
tem_evs = results_large[0].data.evs[0]
tem_std = results_large[0].data.stds[0]

print(f"TEM Result: {tem_evs:.3f} ± {tem_std:.3f}")

# Get the metadata of the TEM job
job_metadata = tem_job_large.result().metadata

# Get the runtime of the TEM job
qpu_runtime = job_metadata["resource_usage"]["RUNNING: EXECUTING_QPU"][
"QPU_TIME"
]
classical_runtime = (
job_metadata["resource_usage"]["RUNNING: OPTIMIZING_FOR_HARDWARE"][
"CPU_TIME"
]
+ job_metadata["resource_usage"]["RUNNING: POST_PROCESSING"]["CPU_TIME"]
)

print(f"QPU Runtime: {qpu_runtime} seconds")
print(f"Classical Runtime: {classical_runtime} seconds")
TEM Result: 0.794 ± 0.026
QPU Runtime: 203.0 seconds
Classical Runtime: 251.71805499999996 seconds
# Plot comparing the different expectation values
metadata_large = results_large[0].metadata
unmitigated_evs = metadata_large["evs_non_mitigated"][0]
unmitigated_stds = metadata_large["stds_non_mitigated"][0]

exact_evs = np.cos(2 * h) ** t_steps

plt.bar(
["Unmitigated", "TEM"],
[unmitigated_evs, tem_evs],
yerr=[unmitigated_stds, tem_std],
color=["grey", "c"],
)
plt.hlines(y=exact_evs, xmin=-0.5, xmax=1.5, colors="r", linestyles="dashed")
plt.ylabel("Expectation Value")
plt.ylim(0, 1.1)
plt.show()

Output of the previous code cell