Condividi tramite


Come inviare circuiti formattati specifici a Azure Quantum

Scopri come usare il modulo qdk.azurePython per inviare circuiti in formati specifici al servizio Azure Quantum. Questo articolo illustra come inviare circuiti nei formati seguenti:

Per altre informazioni, vedere Circuiti quantistici.

Prerequisiti

Per sviluppare ed eseguire i circuiti in Visual Studio Code (VS Code), è necessario disporre degli elementi seguenti:

Creare un nuovo notebook jupyter e connettersi all'area di lavoro Quantum

Per connettersi all'area di lavoro in un notebook di Jupyter in VS Code, seguire questa procedura:

  1. In VS Code aprire il menu Visualizza e scegliere Riquadro comandi.

  2. Inserisci Crea: Nuovo Jupyter Notebook. Viene aperto un file Jupyter Notebook vuoto in una nuova scheda.

  3. Nella prima cella del notebook eseguire il codice seguente. È possibile trovare l'ID risorsa nel riquadro Overview dell'area di lavoro nel portale di Azure.

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

Inviare circuiti in formato QIR

Quantum Intermediate Representation (QIR) è una rappresentazione intermedia che funge da interfaccia comune tra i linguaggi di programmazione quantistica e le piattaforme di calcolo quantistico di destinazione. Per altre informazioni, vedere Rappresentazione intermedia quantistica.

Per inviare un circuito in formato QIR, seguire questa procedura:

  1. Creare il circuito QIR. Ad esempio, eseguire il codice seguente in una nuova cella per creare un circuito di entanglement semplice.

    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. Crea una submit_qir_job funzione helper per inviare il circuito QIR a un oggetto target. In questo esempio i formati di dati di input e output sono qir.v1 rispettivamente e microsoft.quantum-results.v1.

    # 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. Inviare il circuito QIR a uno specifico Azure Quantum target. Ad esempio, per inviare il circuito QIR al simulatore targetIonQ, eseguire il codice seguente:

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

Inviare un circuito con un formato specifico del provider a Azure Quantum

Ogni provider Azure Quantum ha un proprio formato per rappresentare i circuiti quantistici. È possibile inviare circuiti a Azure Quantum in formati specifici del provider anziché in linguaggi QIR, ad esempio Q# o Qiskit.

Inviare un circuito a IonQ in formato JSON

IonQ usa il formato JSON per eseguire circuiti nel suo targets. Per altre informazioni, vedere IonQ targets e la documentazione dell'API IonQ.

L'esempio seguente crea una sovrapposizione tra tre qubit in formato JSON.

  1. In una nuova cella creare un circuito quantistico in formato JSON.

    circuit = {
        "qubits": 3,
        "circuit": [
            {
            "gate": "h",
            "target": 0
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 1
            },
            {
            "gate": "cnot",
            "control": 0,
            "target": 2
            },
        ]
    }
    
  2. Inviare il circuito a IonQ target. Nell'esempio seguente viene utilizzato il simulatore IonQ, che restituisce un oggetto Job.

    target = workspace.get_targets(name="ionq.simulator")
    job = target.submit(circuit)
    
  3. Al termine del processo, ottieni i risultati.

    results = job.get_results()
    print(results)
    

Inviare un circuito a PASQAL in formato Pulser SDK

È possibile usare Pulser SDK per creare sequenze di impulsi e inviarle a PASQAL targets.

Installare Pulser SDK

Pulser è un framework che consente di creare, simulare ed eseguire sequenze di impulsi per dispositivi quantistici atom neutrali. Pulser è progettato da PASQAL come pass-through per inviare esperimenti quantistici ai processori quantistici. Per altre informazioni, vedere la documentazione di Pulser.

Per inviare le sequenze di impulsi, installare prima di tutto i pacchetti Pulser SDK:

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

Creare un registro quantistico

Definire sia un registro che un layout. Il registro specifica dove disporre gli atomi e il layout specifica le posizioni delle trappole che acquisisce e strutturano gli atomi all'interno del registro.

Per informazioni dettagliate sui layout, vedere la documentazione di Pulser.

Creare un devices oggetto per importare il computer targetquantistico PASQAL , Fresnel.

from pulser_pasqal import PasqalCloud

devices = PasqalCloud().fetch_available_devices()
QPU = devices["FRESNEL"]
Layout pre-calibrato

Il dispositivo definisce un elenco di layout pre-calibrati. È possibile compilare il registro da uno di questi layout.

Usare layout pre-calibrati quando possibile perché migliorano le prestazioni della QPU.

L'esempio seguente usa il primo layout pre-calibrato nel 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()
Layout arbitrari

Usa un layout personalizzato quando i layout pre-calibrati non soddisfano i requisiti dell'esperimento.

Per un registro arbitrario dato, una QPU di atomi neutri posiziona le trappole secondo il layout, che deve poi essere calibrato. Poiché ogni calibrazione richiede tempo, è consigliabile riutilizzare un layout calibrato esistente quando possibile.

Per creare un layout arbitrario, scegliere una delle opzioni seguenti:

  • Generare automaticamente un layout in base a un registro specificato. Per registri di grandi dimensioni, questo processo può produrre soluzioni sub-ottimali. Ad esempio:

    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) 
    
  • Definire manualmente un layout per creare il registro. Ad esempio, creare un layout arbitrario con 20 trap posizionati in modo casuale in un piano 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()
    

Scrivere una sequenza di impulsi

Gli atomi neutri sono controllati con impulsi laser. Pulser SDK consente di creare sequenze di impulsi da applicare al registro quantistico.

  1. Definire gli attributi della sequenza di impulsi dichiarando i canali che controllano gli atomi. Per creare un Sequence, fornire un'istanza di Register insieme al dispositivo dove la sequenza verrà eseguita. Ad esempio, il codice seguente dichiara un canale: 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

    È possibile usare il QPU = devices["FRESNEL"] dispositivo o importare un dispositivo virtuale da Pulser per una maggiore flessibilità. L'uso di VirtualDevice permette la creazione di sequenze meno vincolate dalle specifiche del dispositivo, consentendo così l'esecuzione su un emulatore. Per altre informazioni, vedere la documentazione di Pulser.

  2. Aggiungi impulsi alla tua sequenza. Per fare ciò, creare impulsi e aggiungerli ai canali dichiarati. Ad esempio, il codice seguente crea un impulso e lo aggiunge al canale 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'immagine seguente mostra la sequenza di impulsi:

    Sequenza di impulsi

Convertire la sequenza in una stringa JSON

Per inviare le sequenze di impulsi, convertire gli oggetti Pulser in una stringa JSON che può essere usata come dati di input.

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

Inviare la sequenza di impulsi a un PASQAL target

  1. Impostare i formati di dati di input e output appropriati. Ad esempio, il codice seguente imposta il formato dei dati di input su pasqal.pulser.v1 e il formato dei dati di output su 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

    Il tempo necessario per eseguire un processo nella QPU dipende dai tempi correnti della coda. È possibile visualizzare il tempo medio della coda per un target oggetto nel riquadro Provider dell'area di lavoro.

  2. Inviare il programma a PASQAL. Prima di inviare il codice all'hardware quantistico reale, è consigliabile testare il codice nell'emulatore 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
    }
    

Inviare un circuito OpenQASM a Quantinuum

  1. Creare un circuito quantistico nella rappresentazione OpenQASM. Ad esempio, il codice seguente crea un circuito di teletrasporto:

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

    In alternativa, caricare il circuito da un file OpenQASM:

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Invia il circuito a Quantinuum target. Nell'esempio seguente l'operazione viene inviata a uno dei simulatori Quantinuum targets.

    target = workspace.get_targets(name="quantinuum.sim.h2-1sc")
    job = target.submit(circuit, shots=500)
    
  3. Attendere il completamento del processo e quindi recuperare i risultati.

    results = job.get_results()
    print(results)
    

Nota

Questi risultati restituiscono 000 per ogni colpo, che non è casuale. Ciò è dovuto al fatto che il validator API controlla solo se il codice può essere eseguito nell'hardware Quantinuum, ma restituisce 0 per ogni misura quantistica. Per un vero generatore di numeri casuali, è necessario eseguire il circuito su hardware quantistico.

Inviare un circuito Quil a Rigetti

Per inviare un lavoro Quil a un Rigetti target, usare il modulo qdk.azurePython.

  1. Caricare le importazioni necessarie.

    from azure.quantum import Workspace
    from azure.quantum.target.rigetti import Result, Rigetti, RigettiTarget, InputParams
    
  2. Creare un oggetto target e fornire il nome del Rigetti target al quale si desidera inviare il processo. Ad esempio, il codice seguente seleziona QVMtarget.

    target = Rigetti(workspace=workspace, name=RigettiTarget.QVM)
    
  3. Creare un programma Quil. Affinché il programma venga accettato, è necessario impostare il readout su "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. È possibile indicizzare un oggetto Result con il nome del readout. Nel codice seguente, data_per_shot è un elenco di lunghezza num_shots, e ogni elemento nell'elenco è un altro elenco che contiene i dati per il registro da quello scatto.

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

    In questo caso, poiché il tipo del registro è BIT, il tipo è integer e il valore 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. Stampa tutti i dati.

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

Importante

Non è possibile inviare più circuiti in un singolo lavoro. Come soluzione alternativa, è possibile chiamare il backend.run metodo per inviare ogni circuito in modo asincrono e quindi recuperare i risultati di ogni processo. Ad esempio:

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

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