Come inviare circuiti formattati specifici ad Azure Quantum

Informazioni su come usare il azure-quantumPython pacchetto 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 eseguire i circuiti in un notebook in portale di Azure, è necessario:

  • Un account Azure con una sottoscrizione attiva. Se non si ha un account Azure, registrarsi gratuitamente e iscriversi per ottenere una sottoscrizione con pagamento in base al consumo.
  • Un'area di lavoro di Azure Quantum. Per altre informazioni, vedere Creare un'area di lavoro di Azure Quantum.

Per sviluppare ed eseguire i circuiti in Visual Studio Code, è necessario anche:

Creare un nuovo notebook di Jupyter

È possibile creare un notebook in VS Code o direttamente nel portale di Azure Quantum.

  1. Accedere al portale di Azure e selezionare l'area di lavoro creata nel passaggio precedente.
  2. A sinistra selezionare Notebook.
  3. Fare clic su Notebook personali e quindi su Aggiungi nuovo.
  4. In Kernel Type (Tipo di kernel) selezionare IPython.
  5. Digitare un nome per il file e fare clic su Crea file.

Quando si apre il nuovo notebook, viene creato automaticamente il codice per la prima cella, in base alle informazioni sulla sottoscrizione e sull'area di lavoro.

from azure.quantum import Workspace
workspace = Workspace ( 
    resource_id = "", # Your resource_id 
    location = ""  # Your workspace location (for example, "westus") 
)

Inviare circuiti in formato QIR

La rappresentazione quantistica intermedia (QIR) è una rappresentazione intermedia che funge da interfaccia comune tra linguaggi/framework di programmazione quantistici e piattaforme di calcolo quantistico mirate. Per altre informazioni, vedere Rappresentazione intermedia quantistica.

  1. Creare il circuito QIR. Ad esempio, il codice seguente crea un semplice circuito di entanglement.

    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. Creare una submit_qir_job funzione helper per inviare il circuito QIR a un oggetto target. Si noti che i formati di dati di input e output vengono specificati rispettivamente come qir.v1 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. Selezionare un target e inviare il circuito QIR ad Azure Quantum. Ad esempio, per inviare il circuito QIR al simulatore targetIonQ:

    target = workspace.get_targets(name="ionq.simulator") 
    result = submit_qir_job(target, QIR_routine, "QIR routine")
    result
    
    {'Histogram': ['(0, 0)', 0.5, '(1, 1)', 0.5]}
    

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

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

Inviare un circuito a IonQ usando il formato JSON

  1. Creare un circuito quantistico usando il formato JSON indipendente dal linguaggio supportato da IonQ targets, come descritto nella documentazione dell'API IonQ. Ad esempio, il campione seguente crea una sovrapposizione tra tre qubit:

    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. Attendere il completamento del processo e quindi recuperare i risultati.

    results = job.get_results()
    print(results)
    
    .....
    {'duration': 8240356, 'histogram': {'0': 0.5, '7': 0.5}}
    
  4. È quindi possibile visualizzare i risultati usando Matplotlib.

    import pylab as pl
    pl.rcParams["font.size"] = 16
    hist = {format(n, "03b"): 0 for n in range(8)}
    hist.update({format(int(k), "03b"): v for k, v in results["histogram"].items()})
    pl.bar(hist.keys(), hist.values())
    pl.ylabel("Probabilities")
    

    Output del processo IonQ

  5. Prima di eseguire un processo nella QPU, è necessario stimare il costo dell'esecuzione.

    Nota

    Per i dettagli più aggiornati sui prezzi, vedere i prezzi di IonQ oppure trovare l'area di lavoro e visualizzare le opzioni relative ai prezzi nella scheda "Provider" dell'area di lavoro tramite: aka.ms/aq/myworkspaces.

Inviare un circuito a PASQAL usando Pulser SDK

Per inviare un circuito a PASQAL, è possibile usare Pulser SDK per creare sequenze di impulsi e inviarle a PASQAL target.

Installare Pulser SDK

Pulser è un framework per la composizione, la simulazione e l'esecuzione di sequenze di impulsi per dispositivi quantistici atom neutrali. È 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
except ImportError:
    !pip -q install pulser

Creare un registro quantistico

  1. Prima di tutto, si crea un oggetto "devices" per importare il computer targetquantistico PASQAL, Fresnel. Fresnel QPU fornisce layout di trap predefiniti che è possibile usare per creare il registro quantistico. Per configurare i registri quantistici, è necessario disporre una matrice di qubit.

    from pulser_pasqal import PasqalCloud
    
    devices = PasqalCloud().fetch_available_devices()
    QPU = devices["FRESNEL"]
    
    # List all available calibrated register layouts
    for calibrated_register_layout in QPU.calibrated_register_layouts.keys():
        print(calibrated_register_layout)
    
  2. Successivamente, si definisce il layout per il registro qubit. In questo esempio si usa un oggetto TriangularLatticeLayout(61, 5.0µm) come layout trap.

    layout = QPU.calibrated_register_layouts[
        "TriangularLatticeLayout(61, 5.0µm)"
    ]
    layout.draw()
    

    L'immagine seguente mostra la visualizzazione del layout scelto.

    Diagramma del layout scelto per il registro qubit. Il layout mostra 60 punti in una matrice di 40 volte 30 micrometri.

  3. Definire quindi il registro qubit selezionando un set di trap dal layout. In questo esempio, il layout ha 60 trap e si selezionano 7 trap usando i relativi ID per definire un registro quantistico di 7 qubit.

    reg = layout.define_register(*[30, 21, 26, 35, 39, 34, 25])
    reg.draw()
    

    L'immagine seguente mostra la visualizzazione finale del registro qubit.

    Diagramma che mostra il registro quantistico finale dopo aver selezionato 7 punti dal layout a 60 punti.

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. Prima di tutto, definisci gli attributi della sequenza di impulsi dichiarando i canali che verranno usati per controllare gli atomi. Per creare un Sequenceoggetto , è necessario fornire un'istanza Register insieme al dispositivo in cui verrà eseguita la sequenza. Ad esempio, il codice seguente dichiara un canale: ch0.

    Nota

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

    from pulser import Sequence
    
    seq = Sequence(reg, QPU)
    # print the available channels for your sequence
    print(seq.available_channels)
    # Declare a channel. In this example we will be using `rydberg_global`
    seq.declare_channel("ch0", "rydberg_global")
    
  2. Aggiungere impulsi alla sequenza. A tale scopo, si creano e si aggiungono impulsi 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, è necessario 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 PASQAL target

  1. Prima di tutto, è necessario 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 selezionando il pannello Provider dell'area di lavoro.

  2. Inviare il programma a PASQAL. Prima di inviare il codice all'hardware quantistico reale, è possibile testare il codice usando l'emulatore pasqal.sim.emu-tn come target.

    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
    }
    

Esplorare le funzionalità avanzate dell'emulatore

L'emulatore di PASQAL offre funzionalità avanzate non ancora supportate da Fresnel QPU. È possibile organizzare il registro in modo personalizzato senza limitazioni dai layout pre-calibrati. Ad esempio, il codice seguente crea un reticolo quadrato 4x4 di qubit:

import numpy as np
from pulser import Register, Sequence

L = 4
square = np.array([[i, j] for i in range(L) for j in range(L)], dtype=float)
square -= np.mean(square, axis=0)
square *= 5

qubits = dict(enumerate(square))
custom_reg = Register(qubits)
custom_reg.draw()

seq = Sequence(custom_reg, QPU)

Tracciato di un reticolo quadrato 4x4 con 16 qubit.

Dopo aver definito un registro personalizzato, è possibile seguire esattamente gli stessi passaggi descritti nella sezione precedente per inviare una sequenza specificata nell'emulatore.

Nota

La funzionalità di registrazione personalizzata sarà presto disponibile in FRESNEL.

Inviare un circuito a Quantinuum usando OpenQASM

  1. Creare un circuito quantistico nella rappresentazione OpenQASM. Ad esempio, il campione 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];
    """
    

    Facoltativamente, è possibile caricare il circuito da un file:

    with open("my_teleport.qasm", "r") as f:
        circuit = f.read()
    
  2. Inviare il circuito a Quantinuum target. L'esempio seguente usa il validator dell'API Quantinuum, che restituisce un oggetto Job.

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

    results = job.get_results()
    print(results)
    
    ........
    {'c0': ['000',
    '000',
    '000',
    '000',
    '000',
    '000',
    '000',
    ...
    ]}
    
  4. È quindi possibile visualizzare i risultati usando Matplotlib.

    import pylab as pl
    pl.hist(results["c0"])
    pl.ylabel("Counts")
    pl.xlabel("Bitstring")
    

    Output del processo Quantinuum

    Osservando l'istogramma, è possibile notare che il generatore di numeri casuali ha restituito 0 ogni volta, che non è molto casuale. Ciò è dovuto al fatto che, il validator dell'API garantisce che il codice verrà eseguito correttamente nell'hardware Quantinuum, restituisce anche 0 per ogni misurazione quantistica. Per un vero generatore di numeri casuali, è necessario eseguire il circuito su hardware quantistico.

  5. Prima di eseguire un processo nella QPU, è necessario stimare il costo dell'esecuzione.

    Nota

    Per i dettagli più aggiornati sui prezzi, vedere Prezzi di Azure Quantum o trovare l'area di lavoro e visualizzare le opzioni dei prezzi nella scheda "Provider" dell'area di lavoro tramite: aka.ms/aq/myworkspaces.

Inviare un circuito a Rigetti usando Quil

Il modo più semplice per inviare processi Quil consiste nell'usare il pacchetto pyquil-for-azure-quantum , in quanto consente di usare gli strumenti e la documentazione della libreria pyQuil . Senza questo pacchetto, pyQuil può essere usato per costruire programmi Quil, ma non per inviarli ad Azure Quantum.

È anche possibile costruire manualmente i programmi Quil e inviarli usando direttamente il azure-quantum pacchetto.

  1. Prima di tutto, caricare le importazioni necessarie.

    from pyquil.gates import CNOT, MEASURE, H
    from pyquil.quil import Program
    from pyquil.quilbase import Declare
    from pyquil_for_azure_quantum import get_qpu, get_qvm
    
  2. Usare la get_qvm funzione o get_qpu per ottenere una connessione alla QVM o alla QPU.

    qc = get_qvm()  # For simulation
    # qc = get_qpu("Ankaa-9Q-3") for submitting to a QPU
    
  3. Creare un programma Quil. Qualsiasi programma Quil valido viene accettato, ma il readout deve essere denominato ro.

    program = Program(
        Declare("ro", "BIT", 2),
        H(0),
        CNOT(0, 1),
        MEASURE(0, ("ro", 0)),
        MEASURE(1, ("ro", 1)),
    ).wrap_in_numshots_loop(5)
    
    # Optionally pass to_native_gates=False to .compile() to skip the compilation stage
    
    result = qc.run(qc.compile(program))
    data_per_shot = result.readout_data["ro"]
    
  4. In questo caso, data_per_shot è una numpy matrice, in modo da poter usare i numpy metodi.

    assert data_per_shot.shape == (5, 2)
    ro_data_first_shot = data_per_shot[0]
    assert ro_data_first_shot[0] == 1 or ro_data_first_shot[0] == 0
    
  5. Stampa tutti i dati.

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

Importante

L'invio di più circuiti in un singolo processo non è attualmente supportato. Come soluzione alternativa è possibile chiamare il backend.run metodo per inviare ogni circuito in modo asincrono, 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())