Partilhar via


Como submeter circuitos formatados específicos para o Azure Quantum

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

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

Pré-requisitos

Para desenvolver e executar os seus circuitos no Visual Studio Code (VS Code), deve ter o seguinte:

Crie um novo caderno Jupyter e ligue-se ao seu espaço de trabalho Quantum

Para se ligar ao seu espaço de trabalho num caderno Jupyter no VS Code, siga estes passos:

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

  2. Insira Create: New Jupyter Notebook. Um ficheiro vazio do Jupyter Notebook abre-se num novo separador.

  3. Na primeira célula do caderno, execute o seguinte código. Podes encontrar o ID do recurso no painel Overview do teu espaço de trabalho no portal Azure.

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

Enviar circuitos formatados em QIR

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

Para submeter um circuito com formato QIR, siga estes passos:

  1. Crie o circuito QIR. Por exemplo, execute o código seguinte numa nova célula para criar um circuito simples de emaranhamento.

    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 submeter o circuito QIR a um target. Neste exemplo, os formatos de dados de entrada e saída são qir.v1 e microsoft.quantum-results.v1, respetivamente.

    # 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. Submeta o circuito QIR a um Azure Quantum target específico. Por exemplo, para submeter o circuito QIR ao simulador targetIonQ, execute o seguinte código:

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

Submeter um circuito com formato específico de fornecedor ao Azure Quantum

Cada fornecedor Azure Quantum tem o seu próprio formato para representar circuitos quânticos. Pode submeter circuitos ao Azure Quantum em formatos específicos de fornecedores em vez de linguagens QIR, como Q# ou Qiskit.

Submeter um circuito ao IonQ em formato JSON

A IonQ utiliza o formato JSON para executar circuitos no seu targets. Para mais informações, consulte o IonQ targets e a documentação da API IonQ.

O exemplo seguinte cria uma superposição entre três qubits em formato JSON.

  1. Numa célula nova, crie um circuito quântico em formato JSON.

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

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

    results = job.get_results()
    print(results)
    

Submeter um circuito para PASQAL em formato Pulser SDK

Pode usar o Pulser SDK para criar sequências de pulsos e submetê-las ao PASQAL targets.

Instalar o SDK do Pulser

O Pulser é uma estrutura que permite criar, simular e executar sequências de pulsos para dispositivos quânticos de átomos neutros. O Pulser foi concebido pela PASQAL como uma passagem para submeter experiências quânticas aos seus processadores quânticos. Para mais informações, consulte a documentação do Pulser.

Para enviar as sequências de pulso, primeiro instale os pacotes 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 registo quântico

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

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. Pode construir o seu registo a partir de um destes layouts.

Use layouts pré-calibrados sempre que possível, pois melhoram o desempenho da QPU.

O exemplo seguinte utiliza o primeiro layout pré-calibrado do 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 satisfizerem os requisitos do seu experimento.

Para um dado registo arbitrário, uma QPU de átomo neutro coloca armadilhas de acordo com a disposição, que depois devem ser calibradas. Como cada calibração demora tempo, é uma boa prática reutilizar um layout calibrado existente sempre que possível.

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

  • Gerar automaticamente um layout com base num registo especificado. Para registos grandes, este processo pode produzir soluções subótimas. 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 o seu registo. Por exemplo, crie um layout arbitrário com 20 armadilhas posicionadas aleatoriamente num 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()
    

Escrever uma sequência de pulsos

Átomos neutros são controlados por pulsos laser. O SDK do Pulser permite criar sequências de pulso para aplicar ao registro quântico.

  1. Defina os atributos da sequência de pulsos declarando os canais que controlam os átomos. Para criar um Sequence, forneça uma Register instância juntamente com o dispositivo onde 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")
    

    Nota

    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 menos limitadas pelas especificações do dispositivo, o que permite correr num emulador. Para obter mais informações, consulte a documentação do Pulser.

  2. Adicione pulsos à sua sequência. Para isso, crie e adicione pulsos aos canais que declarou. Por exemplo, o seguinte código cria um pulso e adiciona-o 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 seguinte mostra a sequência de impulsos:

    Sequência de pulsos

Converter a sequência em uma cadeia de caracteres JSON

Para submeter as sequências de pulsos, converta os objetos Pulser numa string 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

Submeter a sequência de impulsos a um PASQAL target

  1. Defina os formatos corretos de dados de entrada e saída. Por exemplo, o código a seguir define o formato de dados de entrada como pasqal.pulser.v1 e o formato de dados de saída como 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
    

    Nota

    O tempo necessário para executar um trabalho na QPU depende dos tempos de fila atuais. Pode ver o tempo médio de espera para a target no painel de Fornecedores do seu espaço de trabalho.

  2. Submeta o programa ao PASQAL. Antes de submeteres o teu código para hardware quântico real, é uma boa prática testar o teu 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
    }
    

Submeter um circuito OpenQASM ao Quantinuum

  1. Crie um circuito quântico na representação OpenQASM. Por exemplo, o seguinte código 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, carregar o circuito a partir de um ficheiro OpenQASM:

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Submeter o circuito a um Quantinuum target. O exemplo seguinte submete o trabalho a um dos simuladores da Quantinuum targets.

    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, em seguida, obtenha os resultados.

    results = job.get_results()
    print(results)
    

Nota

Estes resultados devolvem 000 por cada disparo, o que não é aleatório. Isto porque o API Validator só verifica se o seu código pode correr em hardware Quantinuum, mas retorna 0 para cada medição quântica. Para um verdadeiro gerador de números aleatórios, você precisa executar seu circuito em hardware quântico.

Submeter um circuito Quil à Rigetti

Para submeter um trabalho Quil a 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 a que você quer submeter o 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 o seu programa seja aceite, deve definir a leitura para "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 Resultado com o nome da leitura. No código seguinte, data_per_shot é uma lista com um comprimento de num_shots, e cada item da lista é outra lista que armazena os dados para o registo dessa captura.

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

    Neste caso, como o tipo do registo é 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

Não podes submeter vários circuitos num único trabalho. Como solução alternativa, podes chamar o backend.run método para submeter cada circuito de forma assíncrona e depois obter os resultados de cada trabalho. Por exemplo:

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

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