Comment envoyer des circuits mis en forme spécifiques à Azure Quantum

Découvrez comment utiliser le module qdk.azurePython pour envoyer des circuits dans des formats spécifiques au service Azure Quantum. Cet article vous montre comment envoyer des circuits dans les formats suivants :

Pour plus d’informations, consultez la page Circuits quantiques.

Prérequis

Pour développer et exécuter vos circuits dans Visual Studio Code (VS Code), vous devez disposer des éléments suivants :

Créez un notebook Jupyter et connectez-vous à votre espace de travail Quantum

Pour vous connecter à votre espace de travail dans un notebook Jupyter dans VS Code, procédez comme suit :

  1. Dans VS Code, ouvrez le menu Affichage et choisissez Palette de commandes.

  2. Entrez Créer : Nouveau cahier Jupyter. Un fichier Jupyter Notebook vide s’ouvre dans un nouvel onglet.

  3. Dans la première cellule du notebook, exécutez le code suivant. Vous trouverez l’ID de ressource dans le volet Overview de votre espace de travail dans le portail Azure.

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

Soumettre des circuits au format QIR

La représentation intermédiaire quantique (QIR) est une représentation intermédiaire qui sert d’interface commune entre les langages de programmation quantique et les plateformes de calcul quantique ciblées. Pour plus d’informations, consultez Quantum Intermediate Representation.

Pour soumettre un circuit au format QIR, procédez comme suit :

  1. Créez le circuit QIR. Par exemple, lancez le code suivant dans une nouvelle cellule pour créer un circuit d'enchevêtrement 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. Créez une submit_qir_job fonction d’assistance pour envoyer le circuit QIR à un target. Dans cet exemple, les formats de données d’entrée et de sortie sont qir.v1 et microsoft.quantum-results.v1, respectivement.

    # 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. Envoyez le circuit QIR à un Azure Quantum target spécifique. Par exemple, pour soumettre le circuit QIR au simulateur targetIonQ, exécutez le code suivant :

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

Envoyer un circuit avec un format spécifique au fournisseur pour Azure Quantum

Chaque fournisseur Azure Quantum a son propre format pour représenter des circuits quantiques. Vous pouvez envoyer des circuits à Azure Quantum dans des formats spécifiques au fournisseur au lieu de langues QIR, telles que Q# ou Qiskit.

Envoyer un circuit à IonQ au format JSON

IonQ utilise le format JSON pour exécuter des circuits sur leur targets. Pour plus d’informations, consultez la documentation de l’API IonQ targets et IonQ.

L’exemple suivant crée une superposition entre trois qubits au format JSON.

  1. Dans une nouvelle cellule, créez un circuit quantique au format JSON.

    circuit = {
        "qubits": 3,
        "circuit": [
            {
            "gate": "h",
            "target": 0
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 1
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 2
            },
        ]
    }
    
  2. Envoyez le circuit à l’IonQ target. L’exemple suivant utilise le simulateur IonQ qui renvoie un objet Job.

    target = workspace.get_targets(name="ionq.simulator")
    job = target.submit(circuit)
    
  3. Une fois la tâche terminée, obtenez les résultats.

    results = job.get_results()
    print(results)
    

Envoyer un circuit à Pasqal au format SDK Pulser

Vous pouvez utiliser le SDK Pulser pour créer des séquences d’impulsions et les soumettre à Pasqal targets.

Installer le Kit de développement logiciel (SDK) Pulser

Pulser est un framework qui vous permet de créer, simuler et exécuter des séquences d’impulsions pour les appareils quantiques neutres à atomes. Pulser est conçu par PASQAL comme pass-through pour soumettre des expériences quantiques à leurs processeurs quantiques. Pour plus d’informations, consultez la documentation Pulser.

Pour soumettre les séquences d’impulsions, installez d’abord les packages du Kit de développement logiciel (SDK) Pulser :

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

Créer un registre quantique

Définissez à la fois un registre et une disposition. Le registre spécifie où organiser les atomes, et la disposition spécifie les positions des pièges qui capturent et structurent les atomes dans le registre.

Pour plus d’informations sur les dispositions, consultez la documentation Pulser.

Créez un devices objet pour importer l’ordinateur targetquantique Pasqal, FRESNEL_CAN1.

from pulser_pasqal import PasqalCloud

devices = PasqalCloud().fetch_available_devices()
QPU = devices["FRESNEL_CAN1"]
Dispositions pré-étalonnées

L’appareil définit une liste de dispositions pré-étalonnées. Vous pouvez créer votre registre à partir de l’une de ces dispositions.

Utilisez des dispositions pré-étalonnées si possible, car elles améliorent les performances du QPU.

L’exemple suivant utilise la première disposition pré-étalonnée sur l’appareil :

# 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()
Dispositions arbitraires

Utilisez une disposition personnalisée lorsque les dispositions pré-étalonnées ne répondent pas aux exigences de votre expérience.

Pour un registre arbitraire donné, un QPU neutre-atome place des pièges en fonction de la disposition, qui doit ensuite être étalonné. Étant donné que chaque étalonnage prend du temps, il est recommandé de réutiliser une disposition étalonnée existante si possible.

Pour créer une disposition arbitraire, choisissez l’une des options suivantes :

  • Générez automatiquement une disposition basée sur un registre spécifié. Pour les grands registres, ce processus peut produire des solutions sous-optimales. Par exemple :

    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) 
    
  • Définissez manuellement une disposition pour créer votre registre. Par exemple, créez une disposition arbitraire avec 20 pièges positionnés de façon aléatoire dans un plan 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()
    

Écrire une séquence d’impulsions

Les atomes neutres sont contrôlés avec des impulsions laser. Le SDK Pulser vous permet de créer des séquences d’impulsions à appliquer au registre quantique.

  1. Définissez les attributs de séquence d’impulsions en déclarant les canaux qui contrôlent les atomes. Pour créer un Sequence, fournissez une Register instance avec l’appareil sur lequel la séquence sera exécutée. Par exemple, le code suivant déclare 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")
    

    Remarque

    Vous pouvez utiliser l’appareil QPU = devices["FRESNEL_CAN1"] ou importer un appareil virtuel à partir de Pulser pour plus de flexibilité. L’utilisation d’une VirtualDevice fonctionnalité permet la création de séquences moins contrainte par les spécifications de l’appareil, ce qui vous permet d’exécuter sur un émulateur. Pour plus d’informations, consultez la documentation Pulser.

  2. Ajoutez des impulsions à votre séquence. Pour ce faire, créez et ajoutez des impulsions aux canaux que vous avez déclarés. Par exemple, le code suivant crée une impulsion et l’ajoute au 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()
    

    L’image suivante montre la séquence d’impulsions :

    Séquence d’impulsions

Convertir la séquence en chaîne JSON

Pour soumettre les séquences d’impulsions, convertissez les objets Pulser en une chaîne JSON qui peut être utilisée comme données d’entrée.

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

Envoyer la séquence d’impulsions à Pasqal target

  1. Définissez les formats de données d’entrée et de sortie appropriés. Par exemple, le code suivant définit le format de données d’entrée sur pasqal.pulser.v1 et le format pasqal.pulser-results.v1de données de sortie sur .

    # 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
    

    Remarque

    Le temps nécessaire pour exécuter un travail sur le QPU dépend des heures de file d’attente actuelles. Vous pouvez afficher le temps moyen de file d’attente d’un target dans le volet Fournisseurs de votre espace de travail.

  2. Envoyez le programme à Pasqal. Avant de soumettre votre code à du matériel quantique réel, il est recommandé de tester votre code sur l’émulateur 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
    }
    

Envoyer un circuit OpenQASM à Quantinuum

  1. Créez un circuit quantique dans la représentation OpenQASM. Par exemple, le code suivant crée un circuit de téléportation :

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

    Vous pouvez également charger le circuit à partir d’un fichier OpenQASM :

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Soumettez le circuit à Quantinuum target. L’exemple suivant soumet la tâche à l’un des simulateurs Quantinuum targets.

    target = workspace.get_targets(name="quantinuum.sim.h2-1sc")
    job = target.submit(circuit, shots=500)
    
  3. Attendez que le travail soit terminé, puis extrayez les résultats.

    results = job.get_results()
    print(results)
    

Remarque

Ces résultats retournent 000 pour chaque coup, ce qui n’est pas aléatoire. Cela est dû au fait que le validateur d’API vérifie uniquement si votre code peut s’exécuter sur le matériel Quantinuum, mais retourne 0 pour chaque mesure quantique. Pour un générateur de vrais nombres aléatoires, vous devez exécuter votre circuit sur du matériel quantique.

Soumettre un circuit Quil à Rigetti

Pour soumettre un travail Quil à un Rigetti target, utilisez le module qdk.azurePython.

  1. Chargez les importations requises.

    from azure.quantum import Workspace
    from azure.quantum.target.rigetti import Result, Rigetti, RigettiTarget, InputParams
    
  2. Créez un target objet et passez le nom du Rigetti target auquel vous souhaitez soumettre votre travail. Par exemple, le code suivant sélectionne le QVMtarget.

    target = Rigetti(workspace=workspace, name=RigettiTarget.QVM)
    
  3. Créez un programme Quil. Pour que votre programme soit accepté, vous devez configurer la lecture sur "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. Vous pouvez indexer un résultat avec le nom de la valeur lue. Dans le code suivant, data_per_shot est une liste de longueur num_shots, et chaque élément de la liste est une autre liste qui contient les données du registre de cette capture.

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

    Dans ce cas, étant donné que le type du registre est BIT, le type est entier et la valeur 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. Imprimez toutes les données.

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

Important

Vous ne pouvez pas envoyer plusieurs circuits sur un seul travail. Pour contourner ce problème, vous pouvez appeler la backend.run méthode pour envoyer chaque circuit de manière asynchrone, puis extraire les résultats de chaque travail. Par exemple :

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

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