Cómo enviar circuitos con formato específicos a Azure Quantum

Aprenda a usar el módulo qdk.azurePython para enviar circuitos en formatos específicos al servicio Azure Quantum. En este artículo se muestra cómo enviar circuitos en los siguientes formatos:

Para más información, consulte Circuitos cuánticos.

Requisitos previos

Para desarrollar y ejecutar los circuitos en Visual Studio Code (VS Code), debe tener lo siguiente:

  • Una cuenta de Azure con una suscripción activa. Si no tiene una cuenta de Azure, regístrese gratuitamente y regístrese para obtener una suscripción de pago por uso.

  • Un área de trabajo de Azure Quantum. Para obtener más información, consulte Create an Azure Quantum workspace.

  • Un entorno Python con Python y Pip instalados.

  • VS Code con las extensiones Microsoft Quantum Development Kit (QDK), Python y Jupyter instalado.

  • La biblioteca qdkPython con el azure extra y el paquete ipykernel.

    python -m pip install --upgrade "qdk[azure]" ipykernel
    

Creación de un cuaderno de Jupyter Notebook y conexión al área de trabajo de Quantum

Para conectarse al área de trabajo en un cuaderno de Jupyter Notebook en VS Code, siga estos pasos:

  1. En VS Code, abra el menú Ver y elija Paleta de comandos.

  2. Escriba Crear: Nuevo Cuaderno Jupyter. Se abre un archivo Jupyter Notebook vacío en una nueva pestaña.

  3. En la primera celda del cuaderno, ejecute el código siguiente. Puede encontrar el identificador de recurso en el panel Overview del área de trabajo en el portal de Azure.

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

Enviar circuitos con formato QIR

La representación intermedia cuántica (QIR) es una representación intermedia que actúa como una interfaz común entre los lenguajes de programación cuántica y las plataformas de cálculo cuántico de destino. Para obtener más información, consulte Quantum Intermediate Representation.

Para enviar un circuito con formato QIR, siga estos pasos:

  1. Cree el circuito QIR. Por ejemplo, ejecute el código siguiente en una nueva celda para crear un circuito de entrelazamiento simple.

    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. Cree una submit_qir_job función auxiliar para enviar el circuito QIR a un target. En este ejemplo, los formatos de datos de entrada y salida son qir.v1 y 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. Envíe el circuito QIR a un Azure Quantum target específico. Por ejemplo, para enviar el circuito QIR al simulador targetde IonQ, ejecute el código siguiente:

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

Enviar un circuito con un formato específico del proveedor para Azure Quantum

Cada proveedor de Azure Quantum tiene su propio formato para representar circuitos cuánticos. Puede enviar circuitos a Azure Quantum en formatos específicos del proveedor en lugar de idiomas QIR, como Q# o Qiskit.

Envío de un circuito a IonQ en formato JSON

IonQ usa el formato JSON para ejecutar circuitos en su targets. Para obtener más información, consulte la documentación de IonQ targets y ionQ API.

En el ejemplo siguiente se crea una superposición entre tres cúbits en formato JSON.

  1. En una nueva celda, cree un circuito cuántico en formato JSON.

    circuit = {
        "qubits": 3,
        "circuit": [
            {
            "gate": "h",
            "target": 0
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 1
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 2
            },
        ]
    }
    
  2. Envíe el circuito a IonQ target. En el ejemplo siguiente se usa el simulador de IonQ, que devuelve un objeto Job.

    target = workspace.get_targets(name="ionq.simulator")
    job = target.submit(circuit)
    
  3. Cuando finalice el trabajo, obtenga los resultados.

    results = job.get_results()
    print(results)
    

Envío de un circuito a Pasqal en formato sdk de Pulser

Puede usar el SDK de Pulser para crear secuencias de pulso y enviarlos a Pasqal targets.

Instalación del SDK de Pulser

Pulser es una plataforma que permite crear, simular y ejecutar secuencias de pulso para dispositivos cuánticos de átomos neutros. Pulser está diseñado por PASQAL como un paso a través para enviar experimentos cuánticos a sus procesadores cuánticos. Para obtener más información, consulte la documentación de Pulser.

Para enviar las secuencias de pulso, instale primero los paquetes del SDK de Pulser:

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

Creación de un registro cuántico

Defina un registro y un diseño. El registro especifica dónde organizar los átomos y el diseño especifica las posiciones de las trampas que estructuran los átomos dentro del registro.

Para obtener más información sobre los diseños, consulte la documentación de Pulser.

Cree un devices objeto para importar el equipo targetcuántico de Pasqal, FRESNEL_CAN1.

from pulser_pasqal import PasqalCloud

devices = PasqalCloud().fetch_available_devices()
QPU = devices["FRESNEL_CAN1"]
Diseños calibrados previamente

El dispositivo define una lista de diseños pre calibrados. Puede crear el registro a partir de uno de estos diseños.

Use diseños pre calibrados siempre que sea posible porque mejoran el rendimiento de la QPU.

En el ejemplo siguiente se usa el primer diseño calibrado previamente en el 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()
Diseños arbitrarios

Use un diseño personalizado cuando los diseños pre calibrados no cumplan los requisitos del experimento.

Para un registro arbitrario determinado, una QPU de átomo neutro coloca trampas según la disposición, que luego deben calibrarse. Dado que cada calibración tarda tiempo, es un procedimiento recomendado reutilizar un diseño calibrado existente siempre que sea posible.

Para crear un diseño arbitrario, elija una de las siguientes opciones:

  • Genere automáticamente un diseño basado en un registro especificado. En el caso de los registros grandes, este proceso puede producir soluciones sub óptimas. Por ejemplo:

    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 un diseño para crear el registro. Por ejemplo, cree un diseño arbitrario con 20 trampas que se colocan aleatoriamente en un 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()
    

Escribir una secuencia de pulsos

Los átomos neutros se controlan con pulsos láser. El SDK de Pulser permite crear secuencias de pulso para aplicar al registro cuántico.

  1. Defina los atributos de secuencia de pulso declarando los canales que controlan los átomos. Para crear un Sequence, proporcione una Register instancia junto con el dispositivo donde se ejecutará la secuencia. Por ejemplo, el código siguiente declara un 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:

    Puede usar el QPU = devices["FRESNEL_CAN1"] dispositivo o importar un dispositivo virtual desde Pulser para obtener más flexibilidad. El uso de VirtualDevice permite la creación de secuencias menos restringidas por las especificaciones del dispositivo, lo que le permite ejecutarse en un emulador. Para obtener más información, consulte la documentación de Pulser.

  2. Agregue pulsos a la secuencia. Para ello, cree y agregue pulsos a los canales que haya declarado. Por ejemplo, el código siguiente crea un pulso y lo agrega al 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()
    

    En la imagen siguiente se muestra la secuencia de pulsos:

    Secuencia de pulsos

Conversión de la secuencia en una cadena JSON

Para enviar las secuencias de pulso, convierta los objetos Pulser en una cadena JSON que se pueda usar como datos 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 la secuencia de pulso a Pasqal target

  1. Establezca los formatos de datos de entrada y salida adecuados. Por ejemplo, el código siguiente establece el formato pasqal.pulser.v1 de datos de entrada en y el formato de datos de salida en 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:

    El tiempo necesario para ejecutar un trabajo en la QPU depende de los tiempos de cola actuales. Puede ver el tiempo medio de cola de un target en el panel Proveedores del área de trabajo.

  2. Envíe el programa a Pasqal. Antes de enviar el código al hardware cuántico real, se recomienda probar el código en el emulador pasqal.sim.emu-mpstarget.

    target = workspace.get_targets(name="pasqal.sim.emu-mps") # Change to "pasqal.qpu.fresnel-can1" to use FRESNEL_CAN1 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 un circuito OpenQASM a Quantinuum

  1. Cree un circuito cuántico en la representación de OpenQASM. Por ejemplo, el código siguiente crea un circuito de teletransportación:

    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];
    """
    

    O bien, cargue el circuito desde un archivo OpenQASM:

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Envíe el circuito a quantinuum target. En el ejemplo siguiente se envía el trabajo a uno de los simuladores targetsde Quantinuum .

    target = workspace.get_targets(name="quantinuum.sim.h2-1sc")
    job = target.submit(circuit, shots=500)
    
  3. Espere hasta que se complete el trabajo y obtenga los resultados.

    results = job.get_results()
    print(results)
    

Nota:

Estos resultados devuelven 000 para cada disparo, los cuales no son aleatorios. Esto se debe a que el validador de API solo comprueba si el código se puede ejecutar en hardware Quantinuum, pero devuelve 0 para cada medida cuántica. Para que el generador de números aleatorios sea verdadero, debe ejecutar el circuito en hardware cuántico.

Enviar un circuito Quil a Rigetti

Para enviar un trabajo de Quil a Rigetti target, use el módulo qdk.azurePython.

  1. Cargue las importaciones necesarias.

    from azure.quantum import Workspace
    from azure.quantum.target.rigetti import Result, Rigetti, RigettiTarget, InputParams
    
  2. Cree un target objeto y pase el nombre de Rigetti target al que desea enviar el trabajo. Por ejemplo, el siguiente código selecciona QVMtarget.

    target = Rigetti(workspace=workspace, name=RigettiTarget.QVM)
    
  3. Cree un programa Quil. Para que el programa se acepte, debe establecer la lectura en "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. Puede indexar un resultado con el nombre de la lectura. En el código siguiente, data_per_shot es una lista de longitud num_shots, y cada elemento de la lista es otra lista que contiene los datos del registro de esa captura.

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

    En este caso, dado que el tipo del registro es BIT, el tipo es entero y el valor 0 o 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 los datos.

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

Importante

No se pueden enviar varios circuitos en un solo trabajo. Como solución alternativa, puede llamar al backend.run método para enviar cada circuito de forma asincrónica y, a continuación, capturar los resultados de cada trabajo. Por ejemplo:

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

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