Condividi tramite


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

È necessario definire sia un registro che un layout prima di procedere. Il registro specifica dove verranno disposti gli atomi, mentre il layout specifica il posizionamento delle trappole necessarie per catturare e strutturare questi atomi all'interno del registro.

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

  • Prima di tutto, si crea un oggetto "devices" per importare il computer targetquantistico PASQAL, Fresnel.

    from pulser_pasqal import PasqalCloud
    
    devices = PasqalCloud().fetch_available_devices()
    QPU = devices["FRESNEL"]
    
Layout pre-calibrati

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

Questa è l'opzione consigliata perché migliorerà le prestazioni della QPU.

  • Opzione 1: Definire il registro usando layout pre-calibrati

    Esaminare i layout disponibili in Fresnel e definire il registro da questo layout. Per altre informazioni su come eseguire questa operazione, vedere la documentazione di Pulser.

    Esempio:

    # let's say we are interested in 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,3,5]
    reg = layout.define_register(*traps)
    # You can draw the resulting register to verify it matches your expectations
    reg.draw()
    
Layout arbitrari

Se i layout pre-calibrati non soddisfano i requisiti dell'esperimento, è possibile creare un layout personalizzato.

Per qualsiasi registro arbitrario, una QPU neutra inserisce trappole in base al layout, che deve quindi subire la calibrazione. Poiché ogni calibrazione richiede tempo, è in genere consigliabile riutilizzare un layout calibrato esistente quando possibile

  • Opzione 2: derivare automaticamente un layout dal registro definito

    Questa opzione consente la generazione automatica di un layout in base a un registro specificato. Tuttavia, per registri di grandi dimensioni, questo processo può produrre soluzioni sub-ottimali a causa di limitazioni nell'algoritmo usato per creare il layout.

    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) 
    
  • Opzione 3: Definire il registro usando un layout definito manualmente

    • Creare un layout arbitrario con 20 trappole posizionate in modo casuale in un piano 2D
    import numpy as np
    
    # Generating 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)
    # Creating the layout
    layout = RegisterLayout(traps, slug="random_20")
    
    • Definire il registro con ID trap specifici
    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. 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
    }
    

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())