Compartilhar via


Como enviar circuitos formatados específicos para Azure Quantum

Saiba como usar o módulo qdk.azurePython para enviar circuitos em formatos específicos para o serviço Azure Quantum. Este artigo mostra como enviar circuitos nos seguintes formatos:

Para mais informações, consulte Circuitos quânticos.

Pré-requisitos

Para desenvolver e executar seus circuitos em Visual Studio Code (VS Code), você deve ter o seguinte:

Criar um novo jupyter notebook e conectar-se ao workspace do Quantum

Para se conectar ao seu workspace em um Jupyter Notebook no VS Code, siga os passos:

  1. No VS Code, abra o menu Exibir e escolha Paleta de Comandos.

  2. Insira Criar: Novo Jupyter Notebook. Um arquivo de Jupyter Notebook vazio é aberto em uma nova guia.

  3. Na primeira célula do notebook, execute o código a seguir. Você pode encontrar a ID do recurso no painel Overview para seu workspace no portal Azure.

    from qdk.azure import Workspace
    
    workspace = Workspace (resource_id="") # Add your resource ID 
    

Enviar circuitos formatados em QIR

A QIR (Representação Intermediária Quântica) é uma representação intermediária que serve como uma interface comum entre linguagens de programação quântica e plataformas de computação quântica direcionadas. Para obter mais informações, veja Representação intermediária quântica.

Para enviar um circuito formatado por QIR, siga estas etapas:

  1. Crie o circuito QIR. Por exemplo, execute o código a seguir em uma nova célula para criar um circuito de emaranhamento simples.

    QIR_routine = """%Result = type opaque
    %Qubit = type opaque
    
    define void @ENTRYPOINT__main() #0 {
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*))
      call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
      call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Qubit* inttoptr (i64 0 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*))
      call void @__quantum__qis__cz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*))
      call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*))
      call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) #1
      call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) #1
      call void @__quantum__rt__tuple_record_output(i64 2, i8* null)
      call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null)
      call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null)
      ret void
    }
    
    declare void @__quantum__qis__ccx__body(%Qubit*, %Qubit*, %Qubit*)
    declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__cy__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__rx__body(double, %Qubit*)
    declare void @__quantum__qis__rxx__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__ry__body(double, %Qubit*)
    declare void @__quantum__qis__ryy__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__rz__body(double, %Qubit*)
    declare void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)
    declare void @__quantum__qis__h__body(%Qubit*)
    declare void @__quantum__qis__s__body(%Qubit*)
    declare void @__quantum__qis__s__adj(%Qubit*)
    declare void @__quantum__qis__t__body(%Qubit*)
    declare void @__quantum__qis__t__adj(%Qubit*)
    declare void @__quantum__qis__x__body(%Qubit*)
    declare void @__quantum__qis__y__body(%Qubit*)
    declare void @__quantum__qis__z__body(%Qubit*)
    declare void @__quantum__qis__swap__body(%Qubit*, %Qubit*)
    declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1
    declare void @__quantum__rt__result_record_output(%Result*, i8*)
    declare void @__quantum__rt__array_record_output(i64, i8*)
    declare void @__quantum__rt__tuple_record_output(i64, i8*)
    
    attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="4" "required_num_results"="2" }
    attributes #1 = { "irreversible" }
    
    ; module flags
    
    !llvm.module.flags = !{!0, !1, !2, !3}
    
    !0 = !{i32 1, !"qir_major_version", i32 1}
    !1 = !{i32 7, !"qir_minor_version", i32 0}
    !2 = !{i32 1, !"dynamic_qubit_management", i1 false}
    !3 = !{i32 1, !"dynamic_result_management", i1 false}
    """
    
  2. Crie uma submit_qir_job função auxiliar para enviar o circuito QIR para um target. Neste exemplo, os formatos de dados de entrada e saída são qir.v1 e microsoft.quantum-results.v1, respectivamente.

    # Submit the job with proper input and output data formats
    def submit_qir_job(target, input, name, count=100):
        job = target.submit(
            input_data=input, 
            input_data_format="qir.v1",
            output_data_format="microsoft.quantum-results.v1",
            name=name,
            input_params = {
                "entryPoint": "ENTRYPOINT__main",
                "arguments": [],
                "count": count
                }
        )
    
        print(f"Queued job: {job.id}")
        job.wait_until_completed()
        print(f"Job completed with state: {job.details.status}")
        #if job.details.status == "Succeeded":
        result = job.get_results()
    
        return result
    
  3. Envie o circuito de QIR para um Azure Quantum target específico. Por exemplo, para enviar o circuito QIR para o simulador targetIonQ, execute o seguinte código:

    target = workspace.get_targets(name="ionq.simulator") 
    result = submit_qir_job(target, QIR_routine, "QIR routine")
    result
    

Enviar um circuito com um formato específico do provedor para Azure Quantum

Cada provedor Azure Quantum tem seu próprio formato para representar circuitos quânticos. Você pode enviar circuitos para Azure Quantum em formatos específicos do provedor em vez de linguagens QIR, como Q# ou Qiskit.

Enviar um circuito para o IonQ no formato JSON

O IonQ usa o formato JSON para executar circuitos em seus targets. Para obter mais informações, consulte a documentação do IonQ targets e da API do IonQ.

O exemplo a seguir cria uma superposição entre três qubits no formato JSON.

  1. Em uma nova célula, crie um circuito quântico no formato JSON.

    circuit = {
        "qubits": 3,
        "circuit": [
            {
            "gate": "h",
            "target": 0
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 1
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 2
            },
        ]
    }
    
  2. Envie o circuito para o IonQ target. O exemplo a seguir usa o simulador do IonQ, que retorna um objeto Job.

    target = workspace.get_targets(name="ionq.simulator")
    job = target.submit(circuit)
    
  3. Quando o trabalho for concluído, obtenha os resultados.

    results = job.get_results()
    print(results)
    

Enviar um circuito para PASQAL no formato do SDK do Pulser

Você pode usar o SDK do Pulser para criar sequências de pulso e enviá-las para PASQAL targets.

Instalar o SDK do Pulsser

O Pulser é uma estrutura que permite criar, simular e executar sequências de pulso para dispositivos quânticos neutros. O Pulser foi projetado pela PASQAL como uma passagem para enviar experimentos quânticos para seus processadores quânticos. Para obter mais informações, consulte a documentação do Pulser.

Para enviar as sequências de pulso, primeiro instale os pacotes do SDK do Pulser:

try:
    import pulser
    import pulser_pasqal
except ImportError:
    !pip -q install pulser pulser-pasqal --index-url https://pypi.org/simple

Criar um registro quântico

Defina um registro e um layout. O registro especifica onde organizar os átomos e o layout especifica as posições das armadilhas que capturam e estruturam os átomos dentro do registro.

Para obter detalhes sobre layouts, consulte a documentação do Pulser.

Crie um devices objeto para importar o computador targetquântico PASQAL, Fresnel.

from pulser_pasqal import PasqalCloud

devices = PasqalCloud().fetch_available_devices()
QPU = devices["FRESNEL"]
Layouts pré-calibrados

O dispositivo define uma lista de layouts pré-calibrados. Você pode criar seu registro a partir de um desses layouts.

Use layouts pré-calibrados quando possível porque eles melhoram o desempenho da QPU.

O exemplo a seguir usa o primeiro layout pré-calibrado no dispositivo:

# Use the first layout available on the device
layout = QPU.pre_calibrated_layouts[0]

# Select traps 1, 3 and 5 of the layout to define the register
traps = [1,v3,v5]
reg = layout.define_register(*traps)

# Draw the register to verify that it matches your expectations
reg.draw()
Layouts arbitrários

Use um layout personalizado quando os layouts pré-calibrados não atenderem aos requisitos do experimento.

Para um determinado registro arbitrário, uma QPU de átomos neutros posiciona armadilhas conforme o layout previsto, que depois precisa ser calibrado. Como cada calibragem leva tempo, é uma prática recomendada reutilizar um layout calibrado existente quando possível.

Para criar um layout arbitrário, escolha uma das seguintes opções:

  • Gere automaticamente um layout com base em um registro especificado. Para registros grandes, esse processo pode produzir soluções sub-ideais. Por exemplo:

    from pulser import Register
    qubits = {
        "q0": (0, 0),
        "q1": (0, 10),
        "q2": (8, 2),
        "q3": (1, 15),
        "q4": (-10, -3),
        "q5": (-8, 5),
    }
    
    reg = Register(qubits).with_automatic_layout(device) 
    
  • Defina manualmente um layout para criar seu registro. Por exemplo, crie um layout arbitrário com 20 armadilhas que são posicionadas aleatoriamente em um plano 2D:

    import numpy as np
    from pulser.register.register_layout import RegisterLayout
    
    # Generate random coordinates
    np.random.seed(301122)  # Keeps results consistent between runs
    traps = np.random.randint(0, 30, size=(20, 2))
    traps = traps - np.mean(traps, axis=0)
    
    # Create the layout
    layout = RegisterLayout(traps, slug="random_20")
    
    # Define your register with specific trap IDs
    trap_ids = [4, 8, 19, 0]
    reg = layout.define_register(*trap_ids, qubit_ids=["a", "b", "c", "d"])
    reg.draw()
    

Escreva uma sequência de pulso

Átomos neutros são controlados com pulsos laser. O SDK do Pulsser permite que você crie sequências de pulso para aplicar ao registro quântico.

  1. Defina os atributos de sequência de pulso declarando os canais que controlam os átomos. Para criar uma Sequence, forneça uma Register instância junto com o dispositivo em que a sequência será executada. Por exemplo, o código a seguir declara um canal: ch0.

    from pulser import Sequence
    
    seq = Sequence(reg, QPU)
    
    # Print the available channels for your sequence
    print(seq.available_channels)
    
    # Declare a channel. For example, `rydberg_global`
    seq.declare_channel("ch0", "rydberg_global")
    

    Observação

    Você pode usar o QPU = devices["FRESNEL"] dispositivo ou importar um dispositivo virtual do Pulser para obter mais flexibilidade. O uso de um VirtualDevice permite a criação de sequências com menos restrições das especificações do dispositivo, o que permite a execução em um emulador. Para obter mais informações, consulte a documentação do Pulser.

  2. Adicione pulsos à sua sequência. Para fazer isso, crie e adicione pulsos aos canais que você declarou. Por exemplo, o código a seguir cria um pulso e o adiciona ao canal ch0:

    from pulser import Pulse
    from pulser.waveforms import RampWaveform, BlackmanWaveform
    import numpy as np
    
    amp_wf = BlackmanWaveform(1000, np.pi)
    det_wf = RampWaveform(1000, -5, 5)
    pulse = Pulse(amp_wf, det_wf, 0)
    seq.add(pulse, "ch0")
    
    seq.draw()
    

    A imagem a seguir mostra a sequência de pulso:

    Sequência de pulso

Converter a sequência em uma string JSON

Para enviar as sequências de pulso, converta os objetos Pulser em uma cadeia de caracteres JSON que pode ser usada como dados de entrada.

import json

# Convert the sequence to a JSON string
def prepare_input_data(seq):
    input_data = {}
    input_data["sequence_builder"] = json.loads(seq.to_abstract_repr())
    to_send = json.dumps(input_data)
    return to_send

Enviar a sequência de pulso para um PASQAL target

  1. Defina os formatos de dados de entrada e saída adequados. Por exemplo, o código a seguir configura o formato de dados de entrada para pasqal.pulser.v1 e o formato de dados de saída para pasqal.pulser-results.v1.

    # Submit the job with proper input and output data formats
    def submit_job(target, seq, shots):
        job = target.submit(
            input_data=prepare_input_data(seq), # Take the JSON string previously defined as input data
            input_data_format="pasqal.pulser.v1",
            output_data_format="pasqal.pulser-results.v1",
            name="PASQAL sequence",
            shots=shots # Number of shots
        )
    
        print(f"Queued job: {job.id}")
        return job
    

    Observação

    O tempo necessário para executar um trabalho na QPU depende dos tempos de fila atuais. Você pode visualizar o tempo médio da fila para um target no painel Provedores do seu espaço de trabalho.

  2. Envie o programa para o PASQAL. Antes de enviar seu código para hardware quântico real, é uma prática recomendada testar seu código no emulador pasqal.sim.emu-tntarget.

    target = workspace.get_targets(name="pasqal.sim.emu-tn") # Change to "pasqal.qpu.fresnel" to use Fresnel QPU
    job = submit_job(target, seq, 10)
    
    job.wait_until_completed()
    print(f"Job completed with state: {job.details.status}")
    result = job.get_results()
    print(result)
    
    {
        "1000000": 3, 
        "0010000": 1, 
        "0010101": 1
    }
    

Enviar um circuito do OpenQASM para o Quantinuum

  1. Crie um circuito quântico na representação OpenQASM. Por exemplo, o código a seguir cria um circuito de Teletransporte:

    circuit = """OPENQASM 2.0;
    include "qelib1.inc";
    qreg q[3];
    creg c0[3];
    h q[0];
    cx q[0], q[1];
    cx q[1], q[2];
    measure q[0] -> c0[0];
    measure q[1] -> c0[1];
    measure q[2] -> c0[2];
    """
    

    Ou carregue o circuito de um arquivo OpenQASM:

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Envie o circuito para um Quantinuum target. O exemplo a seguir envia o trabalho para um dos simuladores targetsde Quantinuum.

    target = workspace.get_targets(name="quantinuum.sim.h2-1sc")
    job = target.submit(circuit, shots=500)
    
  3. Aguarde até que o trabalho seja concluído e obtenha os resultados.

    results = job.get_results()
    print(results)
    

Observação

Esses resultados retornam 000 para cada tiro, o que não é aleatório. Isso ocorre porque o Validador de API verifica apenas se o código pode ser executado no hardware Quantinuum, mas retorna 0 para cada medida quântica. Para um gerador de número aleatório verdadeiro, você precisa executar o circuito no hardware quântico.

Enviar um circuito Quil para a Rigetti

Para enviar um trabalho Quil para um Rigetti target, use o módulo qdk.azurePython.

  1. Carregue as importações necessárias.

    from azure.quantum import Workspace
    from azure.quantum.target.rigetti import Result, Rigetti, RigettiTarget, InputParams
    
  2. Crie um target objeto e passe o nome do Rigetti target para o qual você deseja enviar seu trabalho. Por exemplo, o código a seguir seleciona o QVMtarget.

    target = Rigetti(workspace=workspace, name=RigettiTarget.QVM)
    
  3. Crie um programa Quil. Para que seu programa seja aceito, você deve definir o parâmetro como "ro".

    readout = "ro"
    bell_state_quil = f"""
    DECLARE {readout} BIT[2]
    
    H 0
    CNOT 0 1
    
    MEASURE 0 {readout}[0]
    MEASURE 1 {readout}[1]
    """
    
    num_shots = 5
    job = target.submit(
        input_data=bell_state_quil, 
        name="bell state", 
        shots=100, 
        input_params=InputParams(skip_quilc=False)
    )
    
    print(f"Job completed with state: {job.details.status}")
    result = Result(job)  # This throws an exception if the job failed
    
  4. Você pode indexar um Result com o nome do resultado. No código a seguir, data_per_shot há uma lista de comprimento num_shotse cada item na lista é outra lista que contém os dados do registro dessa captura.

    data_per_shot = result[readout]
    
    ro_data_first_shot = data_per_shot[0]
    

    Nesse caso, como o tipo do registro é BIT, o tipo é inteiro e o valor 0 ou 1.

    assert isinstance(ro_data_first_shot[0], int)
    assert ro_data_first_shot[0] == 1 or ro_data_first_shot[0] == 0
    
  5. Imprima todos os dados.

    print(f"Data from '{readout}' register:")
    for i, shot in enumerate(data_per_shot):
        print(f"Shot {i}: {shot}")
    

Importante

Você não pode enviar vários circuitos em um único trabalho. Como solução alternativa, você pode chamar o método backend.run para enviar cada circuito de forma assíncrona e buscar os resultados de cada tarefa. Por exemplo:

jobs = []
for circuit in circuits:
    jobs.append(backend.run(circuit, shots=N))

results = []
for job in jobs:
    results.append(job.result())