Skip to main content
IBM Quantum Platform

Simule um campo inclinado de Ising 2D com a função QESEM

Note

Qiskit Functions são um recurso experimental disponível apenas para usuários dos planos IBM Quantum® Premium Plan, Flex Plan e On-Prem (via IBM Quantum Platform API). Eles estão no status de versão prévia e estão sujeitos a alterações.

Estimativa de uso: 20 minutos em um processador Heron r2. (OBSERVAÇÃO: essa é apenas uma estimativa. Seu tempo de execução pode variar)


Segundo plano

Este tutorial mostra como usar o QESEM, a função Qiskit da Qedma, para simular a dinâmica de um modelo de spin quântico canônico, o modelo 2D tilted-field Ising (TFI) com ângulos não-Clifford:

H=Ji,jZiZj+gxiXi+gziZi,H = J \sum_{\langle i,j \rangle} Z_i Z_j + g_x \sum_i X_i + g_z \sum_i Z_i ,

em que i,j\langle i,j \rangle denota os vizinhos mais próximos em uma rede. Simular a evolução temporal de sistemas quânticos de muitos corpos é uma tarefa computacionalmente difícil para computadores clássicos. Os computadores quânticos, por outro lado, são naturalmente projetados para realizar essa tarefa com eficiência. O modelo TFI, em particular, tornou-se uma referência popular em hardware quântico devido ao seu rico comportamento físico e implementação amigável ao hardware.

Em vez de simular a dinâmica de tempo contínuo, adotamos o modelo de Ising com chute intimamente relacionado. A dinâmica pode ser expressa exatamente como um circuito quântico periódico, em que cada etapa de evolução consiste em três camadas de portas fracionárias de dois qubits RZZ(αZZ)R_{ZZ} (\alpha_{ZZ}), intercaladas com camadas de portas de um único qubit RX(αX)R_X (\alpha_X) e RZ(αZ)R_Z (\alpha_Z).

Usaremos ângulos genéricos que são desafiadores tanto para a simulação clássica quanto para a atenuação de erros. Especificamente, escolhemos αZZ=1.0\alpha_{ZZ} = 1.0, αX=0.53\alpha_X = 0.53 e αZ=0.1\alpha_Z = 0.1, colocando o modelo longe de qualquer ponto integrável.

Neste tutorial, faremos o seguinte:

  • Estimar o tempo de execução esperado da QPU para mitigação total de erros usando os recursos de estimativa de tempo analítico e empírico do QESEM.
  • Construa e simule o circuito modelo de Ising de campo inclinado 2D usando layouts de qubit e camadas de porta inspirados em hardware.
  • Visualize a conectividade do qubit do dispositivo e os subgráficos selecionados para seu experimento.
  • Demonstre o uso da retropropagação do operador (OBP) para reduzir a profundidade do circuito. Essa técnica reduz as operações da extremidade do circuito, mas exige mais medições por parte do operador.
  • Execute a mitigação de erros (EM) imparcial para vários observáveis simultaneamente usando o QESEM, comparando resultados ideais, ruidosos e mitigados.
  • Analise e trace o impacto da atenuação de erros na magnetização em diferentes profundidades de circuito.

Observação: o OBP geralmente retorna um conjunto de observáveis possivelmente não comutáveis. O QESEM otimiza automaticamente as bases de medição quando os observáveis de destino contêm termos não comutáveis. Ele gera conjuntos de bases de medição candidatos usando vários algoritmos heurísticos e seleciona o conjunto que minimiza o número de bases distintas. Isso significa que o QESEM agrupa observáveis compatíveis em bases comuns para reduzir o número total de configurações de medição necessárias, melhorando a eficiência.


Sobre a QESEM

O QESEM é um software confiável, de alta precisão e baseado em caracterização, que implementa uma mitigação de erros quase probabilística eficiente e imparcial. Ele foi projetado para atenuar erros em circuitos quânticos genéricos e é independente do aplicativo. Ele foi validado em diversas plataformas de hardware, incluindo experimentos em escala de utilidade em dispositivos IBM® Eagle e Heron. Os estágios do fluxo de trabalho do QESEM são os seguintes:

  1. Caracterização de dispositivos - mapeia fidelidades de porta e identifica erros coerentes, fornecendo dados de calibração em tempo real. Esse estágio garante que a mitigação aproveite as operações de maior fidelidade disponíveis.
  2. Transpilação com reconhecimento de ruído - gera e avalia mapeamentos alternativos de qubit, conjuntos de operações e bases de medição, selecionando a variante que minimiza o tempo de execução estimado da QPU, com paralelização opcional para acelerar a coleta de dados.
  3. Supressão de erros - redefine portas nativas, aplica o giro de Pauli e otimiza o controle de nível de pulso (em plataformas compatíveis) para melhorar a fidelidade.
  4. Caracterização de circuitos - cria um modelo de erro local personalizado e o ajusta às medições de QPU para quantificar o ruído residual.
  5. Atenuação de erros - constrói decomposições quase probabilísticas de vários tipos e coleta amostras delas em um processo adaptativo que minimiza o tempo de atenuação da QPU e a sensibilidade às flutuações do hardware, obtendo alta precisão em grandes volumes de circuitos.

Para obter mais informações sobre o QESEM e sobre um experimento em escala real desse modelo em um subgrafo de alta conectividade com 103 qubits da geometria nativa heavy-hex de ibm_marrakesh, consulte o artigo “Reliable high-accuracy error mitigation for utility-scale quantum circuits ”.

Fluxo de trabalho QESEM.

Requisitos

Instale os seguintes pacotes Python antes de executar o notebook:

  • Qiskit SDK v2.0.0 ou posterior (pip install qiskit)
  • Qiskit Runtime v0.40.0 ou posterior (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.8.0 ou posterior ( pip install qiskit-ibm-catalog )
  • Complemento Operator Backpropagation Qiskit v0.3.0 ou posterior ( pip install qiskit-addon-obp )
  • Complemento do Qiskit Utils v0.1.1 ou posterior ( pip install qiskit-addon-utils )
  • Simulador Qiskit Aer v0.17.1 ou posterior ( pip install qiskit-aer )
  • Matplotlib v3.10.3 ou posterior ( pip install matplotlib )

Instalação

Primeiro, importe as bibliotecas relevantes:

%matplotlib inline

from typing import Sequence

import matplotlib.pyplot as plt
import numpy as np

import qiskit
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_catalog import QiskitFunctionsCatalog
from qiskit_aer import AerSimulator
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit.visualization import (
    plot_gate_map,
)

Em seguida, faça a autenticação usando sua chave de API do painel do IBM Quantum Platform. Em seguida, selecione a função do Qiskit da seguinte maneira. (Observe que, por motivos de segurança, é recomendável salvar as credenciais da sua conta no seu ambiente local, caso esteja em um computador confiável, para que você não precise digitar sua chave de API toda vez que for se autenticar.)

# Paste here your instance and token strings

instance = "YOUR_INSTANCE"
token = "YOUR_TOKEN"
channel = "ibm_quantum_platform"

catalog = QiskitFunctionsCatalog(
    channel=channel, token=token, instance=instance
)
qesem_function = catalog.load("qedma/qesem")

Passo 1: Mapear entradas clássicas para um problema quântico

Começamos definindo uma função que cria o circuito de Trotter:

def trotter_circuit_from_layers(
    steps: int,
    theta_x: float,
    theta_z: float,
    theta_zz: float,
    layers: Sequence[Sequence[tuple[int, int]]],
    init_state: str | None = None,
) -> qiskit.QuantumCircuit:
    """
    Generates an ising trotter circuit
    :param steps: trotter steps
    :param theta_x: RX angle
    :param theta_z: RZ angle
    :param theta_zz: RZZ angle
    :param layers: list of layers (can be list of layers in device)
    :param init_state: Initial state to prepare.
     If None, will not prepare any state. If "+", will
     add Hadamard gates to all qubits.
    :return: QuantumCircuit
    """
    qubits = sorted({i for layer in layers for edge in layer for i in edge})
    circ = qiskit.QuantumCircuit(max(qubits) + 1)

    if init_state == "+":
        print("init_state = +")
        for q in qubits:
            circ.h(q)

    for _ in range(steps):
        for q in qubits:
            circ.rx(theta_x, q)
            circ.rz(theta_z, q)

        for layer in layers:
            for edge in layer:
                circ.rzz(theta_zz, *edge)
        circ.barrier(qubits)

    return circ

Em seguida, criamos uma função para calcular os valores de expectativa ideal usando AerSimulator.

Observe que, para circuitos de grande porte (30 ou mais qubits), recomendamos o uso de valores pré-calculados a partir de simulações PEPS com propagação de crenças (BP). Este código inclui valores pré-calculados para 35 qubits a título de exemplo, com base na abordagem BP para a evolução de uma rede tensorial PEPS apresentada neste artigo (à qual nos referimos como PEPS-BP), utilizando o pacote quimb do Python para redes tensoriais.

def calculate_ideal_evs(circ, obs, num_qubits, step):
    # Predefined results for large circuits - calculated using
    # bppeps for 3, 5, 7, 9 trotter steps
    predefined_35 = [
        0.79537,
        0.78653,
        0.79699,
    ]

    if num_qubits == 35:
        print(
            "Using precalculated ideal values for large circuits calculated "
            "with belief propagation PEPS. Currently only for 35 qubits."
        )
        return predefined_35[step]

    else:
        simulator = AerSimulator()

        # Use Estimator primitive to get expectation value
        estimator = Estimator(simulator)
        sim_result = estimator.run([(circ, [obs])], precision=0.0001).result()

        # Extracting the result
        ideal_values = sim_result[0].data.evs[0]
        return ideal_values

Usamos um mapeamento de camada RZZR_{ZZ} baseado em hardware retirado do dispositivo Heron, do qual cortamos as camadas de acordo com o número de qubits que queremos simular. Definimos subgrafos para 10, 21, 28 e 35 qubits que mantêm uma estrutura 2D (sinta-se à vontade para mudar para seu subgrafo favorito):

LAYERS_HERON_R2 = [  # the full set of hardware layers for Heron r2
    [
        (2, 3),
        (6, 7),
        (10, 11),
        (14, 15),
        (20, 21),
        (16, 23),
        (24, 25),
        (17, 27),
        (28, 29),
        (18, 31),
        (32, 33),
        (19, 35),
        (36, 41),
        (42, 43),
        (37, 45),
        (46, 47),
        (38, 49),
        (50, 51),
        (39, 53),
        (60, 61),
        (56, 63),
        (64, 65),
        (57, 67),
        (68, 69),
        (58, 71),
        (72, 73),
        (59, 75),
        (76, 81),
        (82, 83),
        (77, 85),
        (86, 87),
        (78, 89),
        (90, 91),
        (79, 93),
        (94, 95),
        (100, 101),
        (96, 103),
        (104, 105),
        (97, 107),
        (108, 109),
        (98, 111),
        (112, 113),
        (99, 115),
        (116, 121),
        (122, 123),
        (117, 125),
        (126, 127),
        (118, 129),
        (130, 131),
        (119, 133),
        (134, 135),
        (140, 141),
        (136, 143),
        (144, 145),
        (137, 147),
        (148, 149),
        (138, 151),
        (152, 153),
        (139, 155),
    ],
    [
        (1, 2),
        (3, 4),
        (5, 6),
        (7, 8),
        (9, 10),
        (11, 12),
        (13, 14),
        (21, 22),
        (23, 24),
        (25, 26),
        (27, 28),
        (29, 30),
        (31, 32),
        (33, 34),
        (40, 41),
        (43, 44),
        (45, 46),
        (47, 48),
        (49, 50),
        (51, 52),
        (53, 54),
        (55, 59),
        (61, 62),
        (63, 64),
        (65, 66),
        (67, 68),
        (69, 70),
        (71, 72),
        (73, 74),
        (80, 81),
        (83, 84),
        (85, 86),
        (87, 88),
        (89, 90),
        (91, 92),
        (93, 94),
        (95, 99),
        (101, 102),
        (103, 104),
        (105, 106),
        (107, 108),
        (109, 110),
        (111, 112),
        (113, 114),
        (120, 121),
        (123, 124),
        (125, 126),
        (127, 128),
        (129, 130),
        (131, 132),
        (133, 134),
        (135, 139),
        (141, 142),
        (143, 144),
        (145, 146),
        (147, 148),
        (149, 150),
        (151, 152),
        (153, 154),
    ],
    [
        (3, 16),
        (7, 17),
        (11, 18),
        (22, 23),
        (26, 27),
        (30, 31),
        (34, 35),
        (21, 36),
        (25, 37),
        (29, 38),
        (33, 39),
        (41, 42),
        (44, 45),
        (48, 49),
        (52, 53),
        (43, 56),
        (47, 57),
        (51, 58),
        (62, 63),
        (66, 67),
        (70, 71),
        (74, 75),
        (61, 76),
        (65, 77),
        (69, 78),
        (73, 79),
        (81, 82),
        (84, 85),
        (88, 89),
        (92, 93),
        (83, 96),
        (87, 97),
        (91, 98),
        (102, 103),
        (106, 107),
        (110, 111),
        (114, 115),
        (101, 116),
        (105, 117),
        (109, 118),
        (113, 119),
        (121, 122),
        (124, 125),
        (128, 129),
        (132, 133),
        (123, 136),
        (127, 137),
        (131, 138),
        (142, 143),
        (146, 147),
        (150, 151),
        (154, 155),
        (0, 1),
        (4, 5),
        (8, 9),
        (12, 13),
        (54, 55),
        (15, 19),
    ],
]

subgraphs = {  # the subgraphs for the different qubit counts such that it's 2D
    10: list(range(22, 29)) + [16, 17, 37],
    21: list(range(3, 12)) + list(range(23, 32)) + [16, 17, 18],
    28: list(range(3, 12))
    + list(range(23, 32))
    + list(range(45, 50))
    + [16, 17, 18, 37, 38],
    35: list(range(3, 12))
    + list(range(21, 32))
    + list(range(41, 50))
    + [16, 17, 18, 36, 37, 38],
    42: list(range(3, 12))
    + list(range(21, 32))
    + list(range(41, 50))
    + list(range(63, 68))
    + [16, 17, 18, 36, 37, 38, 56, 57],
}

n_qubits = 35  # 21, 28, 35, 42
layers = [
    [
        edge
        for edge in layer
        if edge[0] in subgraphs[n_qubits] and edge[1] in subgraphs[n_qubits]
    ]
    for layer in LAYERS_HERON_R2
]

print(layers)

Output:

[[(6, 7), (10, 11), (16, 23), (24, 25), (17, 27), (28, 29), (18, 31), (36, 41), (42, 43), (37, 45), (46, 47), (38, 49)], [(3, 4), (5, 6), (7, 8), (9, 10), (21, 22), (23, 24), (25, 26), (27, 28), (29, 30), (43, 44), (45, 46), (47, 48)], [(3, 16), (7, 17), (11, 18), (22, 23), (26, 27), (30, 31), (21, 36), (25, 37), (29, 38), (41, 42), (44, 45), (48, 49), (4, 5), (8, 9)]]

Agora, visualizamos o layout do qubit no dispositivo Heron para o subgráfico selecionado:

catalog = QiskitFunctionsCatalog(
    channel=channel,
    token=token,
    instance=instance,
)
backend = catalog.backend("ibm_fez")  # or any available device

selected_qubits = subgraphs[n_qubits]
num_qubits = backend.configuration().num_qubits
qubit_color = [
    "#ff7f0e" if i in selected_qubits else "#d3d3d3"
    for i in range(num_qubits)
]

plot_gate_map(
    backend=backend,
    figsize=(15, 10),
    qubit_color=qubit_color,
)
plt.show()

Output:

Output of the previous code cell

Observe que a conectividade do layout de qubit escolhido não é necessariamente linear e pode abranger grandes regiões do dispositivo Heron, dependendo do número selecionado de qubits.

Agora, geramos o circuito de Trotter e a magnetização média observável para o número escolhido de qubits e parâmetros:

# Chosen parameters:
theta_x = 0.53
theta_z = 0.1
theta_zz = 1.0
steps = 9

circ = trotter_circuit_from_layers(steps, theta_x, theta_z, theta_zz, layers)
print(
    f"Circuit 2q layers: "
    f"{circ.depth(filter_function=lambda instr: len(instr.qubits) == 2)}"
)
print("\nCircuit structure:")

circ.draw("mpl", scale=0.8, fold=-1, idle_wires=False)
plt.show()

observable = qiskit.quantum_info.SparsePauliOp.from_sparse_list(
    [("Z", [q], 1 / n_qubits) for q in subgraphs[n_qubits]],
    np.max(subgraphs[n_qubits]) + 1,
)  # Average magnetization observable

print(observable)
obs_list = [observable]

Output:

Circuit 2q layers: 27

Circuit structure:
Output of the previous code cell
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
              coeffs=[0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j,
 0.02857143+0.j, 0.02857143+0.j, 0.02857143+0.j])

Etapa 2: Otimizar o problema para execução em hardware quântico

Estimativa do tempo de QPU com e sem OBP

Os usuários geralmente querem saber quanto tempo de QPU é necessário para o seu experimento. No entanto, esse é considerado um problema difícil para os computadores clássicos.

O QESEM oferece dois modos de estimativa de tempo para informar os usuários sobre a viabilidade de seus experimentos:

  1. Estimativa analítica de tempo - fornece uma estimativa muito aproximada e não requer tempo de QPU. Isso pode ser usado para testar se uma passagem de transpilação reduziria potencialmente o tempo de QPU.
  2. Estimativa empírica de tempo (demonstrada aqui) - fornece uma estimativa muito boa e usa alguns minutos de tempo de QPU.

Em ambos os casos, o QESEM produz a estimativa de tempo para atingir a precisão necessária para todos os observáveis.

run_on_real_hardware = True

precision = 0.05
if run_on_real_hardware:
    backend_name = "ibm_fez"
else:
    backend_name = "fake_fez"

# Start a job for empirical time estimation
estimation_job_wo_obp = qesem_function.run(
    pubs=[(circ, obs_list)],
    instance=instance,
    backend_name=backend_name,  # E.g. "ibm_brisbane"
    options={
        # "empirical" - gets actual time estimates without running full mitigation
        "estimate_time_only": "empirical",
        "max_execution_time": 120,  # Limits the QPU time, specified in seconds.
        "default_precision": precision,
    },
)
print(estimation_job_wo_obp.job_id)
print(estimation_job_wo_obp.status())

Output:

17d3828e-9fdb-482e-8e9b-392f3eefe313
DONE
# Get the result object (blocking method).
# Use job.status() in a loop for non-blocking.
# This takes 1-3 minutes
result = estimation_job_wo_obp.result()
print(
    f"Empirical time estimation (sec): {result[0].metadata['time_estimation_sec']}"
)

Output:

Empirical time estimation (sec): 1200

Agora, utilizaremos a retropropagação do operador (OBP). (Consulte a documentação do OBP para obter mais detalhes sobre o complemento OBP Qiskit.) Criaremos uma função que gera as fatias do circuito para retropropagação:

def run_backpropagation(circ_vec, observable, steps_vec, max_qwc_groups=8):
    """
    Runs backpropagation for a list of circuits and observables.
    Returns lists of backpropagated circuits and observables.
    """
    op_budget = OperatorBudget(max_qwc_groups=max_qwc_groups)
    bp_circuit_vec = []
    bp_observable_vec = []

    for i, circ in enumerate(circ_vec):
        slices = slice_by_gate_types(circ)
        bp_observable, remaining_slices, metadata = backpropagate(
            observable,
            slices,
            operator_budget=op_budget,
        )
        bp_circuit = combine_slices(remaining_slices, include_barriers=True)
        bp_circuit_vec.append(bp_circuit)
        bp_observable_vec.append(bp_observable)
        print(f"n.o. steps: {steps_vec[i]}")
        print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
        print(
            f"New observable has {len(bp_observable.paulis)} terms, "
            f"which can be combined into "
            f"{len(bp_observable.group_commuting(qubit_wise=True))} groups.\n"
            f"After truncation, the error in our observable is bounded by "
            f"{metadata.accumulated_error(0):.3e}"
        )
        print("-----------------")
    return bp_circuit_vec, bp_observable_vec

Chamamos a função:

bp_circ_vec, bp_obs_vec = run_backpropagation([circ], observable, [steps])

Output:

n.o. steps: 9
Backpropagated 11 slices.
New observable has 363 terms, which can be combined into 4 groups.
After truncation, the error in our observable is bounded by 0.000e+00
-----------------
print("The remaining circuit after backpropagation looks as follows:")
bp_circ_vec[-1].draw("mpl", scale=0.8, fold=-1, idle_wires=False)
None

Output:

The remaining circuit after backpropagation looks as follows:
Output of the previous code cell

Podemos ver que a retropropagação reduziu duas camadas do circuito. Agora que temos nosso circuito reduzido e observáveis expandidos, vamos fazer a estimativa de tempo para o circuito retropropagado:

# Start a job for empirical time estimation
estimation_job_obp = qesem_function.run(
    pubs=[(bp_circ_vec[-1], [bp_obs_vec[-1]])],
    instance=instance,
    backend_name=backend_name,
    options={
        "estimate_time_only": "empirical",
        "max_execution_time": 120,
        "default_precision": precision,
    },
)
print(estimation_job_obp.job_id)
print(estimation_job_obp.status())

Output:

8bae699d-a16b-4d39-bbd9-d123fbcce55d
DONE
result_obp = estimation_job_obp.result()
print(
    f"Empirical time estimation (sec): {result_obp[0].metadata['time_estimation_sec']}"
)

Output:

Empirical time estimation (sec): 900

Vemos que o OBP reduz o custo de tempo para a atenuação do circuito.


Passo 3: Execute usando Qiskit primitives

Execute com backend real

Agora, executamos o experimento completo em algumas etapas de Trotter. O número de qubits, a precisão necessária e o tempo máximo da QPU podem ser modificados de acordo com os recursos disponíveis da QPU. Observe que restringir o tempo máximo de QPU afetará a precisão final, como você verá no gráfico final abaixo.

Analisamos quatro circuitos com 5, 7 e 9 etapas de Trotter em uma precisão de 0.05, comparando seus valores de expectativa ideais, ruidosos e mitigados por erro:

steps_vec = [5, 7, 9]

circ_vec = []
for steps in steps_vec:
    circ = trotter_circuit_from_layers(
        steps, theta_x, theta_z, theta_zz, layers
    )
    circ_vec.append(circ)

Novamente, realizamos o OBP em cada circuito para reduzir o tempo de execução:

bp_circ_vec_35, bp_obs_vec_35 = run_backpropagation(
    circ_vec, observable, steps_vec
)

Output:

n.o. steps: 5
Backpropagated 11 slices.
New observable has 363 terms, which can be combined into 4 groups.
After truncation, the error in our observable is bounded by 0.000e+00
-----------------
n.o. steps: 7
Backpropagated 11 slices.
New observable has 363 terms, which can be combined into 4 groups.
After truncation, the error in our observable is bounded by 0.000e+00
-----------------
n.o. steps: 9
Backpropagated 11 slices.
New observable has 363 terms, which can be combined into 4 groups.
After truncation, the error in our observable is bounded by 0.000e+00
-----------------

Agora, executamos um lote de trabalhos completos do QESEM. Limitamos o tempo máximo de execução da QPU para cada um dos pontos para melhor controle do orçamento da QPU.

run_on_real_hardware = True

precision = 0.05
if run_on_real_hardware:
    backend_name = "ibm_marrakesh"
else:
    backend_name = "fake_fez"
# Running full jobs for:
pubs_list = [
    [(bp_circ_vec_35[i], bp_obs_vec_35[i])] for i in range(len(bp_obs_vec_35))
]
# Initiating multiple jobs for different lengths
job_list = []
for pubs in pubs_list:
    job_obp = qesem_function.run(
        pubs=pubs,
        instance=instance,
        backend_name=backend_name,  # E.g. "ibm_brisbane"
        options={
            "max_execution_time": 300,  # Limits the QPU time, specified in seconds.
            "default_precision": 0.05,
        },
    )
    job_list.append(job_obp)

Aqui verificamos o status de cada trabalho:

for job in job_list:
    print(job.status())

Output:

DONE
DONE
DONE
DONE

Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado

Quando todos os trabalhos terminarem de ser executados, poderemos comparar seus valores de expectativa ruidosos e atenuados.

ideal_values = []
noisy_values = []
error_mitigated_values = []
error_mitigated_stds = []

for i in range(len(job_list)):
    job = job_list[i]
    result = job.result()  # Blocking - takes 3-5 minutes
    noisy_results = result[0].metadata["noisy_results"]

    ideal_val = calculate_ideal_evs(circ_vec[i], observable, n_qubits, i)
    print("---------------------------------")
    print(f"Ideal: {ideal_val}")
    print(f"Noisy: {noisy_results.evs}")
    print(f"QESEM: {result[0].data.evs} \u00b1 {result[0].data.stds}")

    ideal_values.append(ideal_val)
    noisy_values.append(noisy_results.evs)
    error_mitigated_values.append(result[0].data.evs)
    error_mitigated_stds.append(result[0].data.stds)

Output:

Using precalculated ideal values for large circuits calculated with belief propagation PEPS. Currently only for 35 qubits.
---------------------------------
Ideal: 0.79537
Noisy: 0.7039237951821501
QESEM: 0.7828018244130982 ± 0.013257266977728376
Using precalculated ideal values for large circuits calculated with belief propagation PEPS. Currently only for 35 qubits.
---------------------------------
Ideal: 0.78653
Noisy: 0.6478583812958806
QESEM: 0.7875259197423828 ± 0.02703045139248604
Using precalculated ideal values for large circuits calculated with belief propagation PEPS. Currently only for 35 qubits.
---------------------------------
Ideal: 0.79699
Noisy: 0.6171787879868142
QESEM: 0.6918791909168913 ± 0.0740873782039517

Por fim, podemos traçar um gráfico da magnetização em relação ao número de etapas. Isso resume o benefício de usar a função Qiskit do QESEM para a atenuação de erros sem polarização em dispositivos quânticos com ruído.

plt.plot(steps_vec, ideal_values, "--", label="ideal")
plt.scatter(steps_vec, noisy_values, label="noisy")
plt.errorbar(
    steps_vec,
    error_mitigated_values,
    yerr=error_mitigated_stds,
    fmt="o",
    capsize=5,
    label="QESEM mitigation",
)
plt.legend()
plt.xlabel("n.o. steps")
plt.ylabel("Magnetization")

Output:

Text(0, 0.5, 'Magnetization')
Output of the previous code cell
Note

A nona etapa tem uma grande barra de erro estatístico porque limitamos o tempo da QPU a 5 minutos. Se você executar essa etapa por 15 minutos (como sugere a estimativa empírica de tempo), obterá uma barra de erro menor. Portanto, o valor atenuado ficará mais próximo do valor ideal.

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.