Skip to main content
IBM Quantum Platform

Interfaccia fornitori

qiskit.providers

Questo modulo contiene le classi utilizzate per creare provider esterni per Qiskit. Per "provider" si intende qualsiasi entità che fornisca un servizio esterno a Qiskit. Un esempio tipico è quello di un provider backend che fornisce Backend oggetti utilizzabili per l'esecuzione QuantumCircuit di oggetti. Questo modulo contiene le classi astratte utilizzate per definire l'interfaccia tra un provider e Qiskit.


Supporto versione

Ciascuna classe astratta dell'interfaccia dei provider è soggetta a versioni individuali. Quando è necessario modificare un'interfaccia, viene creata una nuova classe astratta per definire la nuova interfaccia. Queste modifiche all'interfaccia non garantiscono la retrocompatibilità tra le versioni.

Modifiche alla versione

Ogni versione minore di qiskit può incrementare la versione di qualsiasi interfaccia backend di un singolo numero di versione. Sarà un aggregato di tutte le modifiche all'interfaccia per quella release su quell'interfaccia.

Politica di supporto delle versioni

Per consentire ai fornitori di avere il tempo di adattarsi ai cambiamenti di questa interfaccia, Qiskit supporterà più versioni di ogni classe contemporaneamente. Data la natura di una versione per release, la politica di deprezzamento delle versioni è un po' più conservativa della politica di deprezzamento standard. Qiskit supporterà una versione dell'interfaccia del provider per un minimo di 3 release minori o per la prima release dopo 6 mesi dalla release che ha introdotto una versione, a seconda di quale sia il periodo più lungo, prima di una potenziale deprecazione. Dopo di che la politica di deprezzamento standard si applicherà a quella versione dell'interfaccia. In questo modo i fornitori e gli utenti avranno il tempo sufficiente per adattarsi alle potenziali modifiche dell'interfaccia. Quindi, per esempio, diciamo che in 0.19.0 viene introdotto BackendV2 e che nei 3 mesi successivi al rilascio di 0.19.0 vengono rilasciati 0.20.0, 0.21.0 e 0.22.0, quindi 7 mesi dopo 0.19.0 viene rilasciato 0.23.0. In 0.23.0 possiamo deprecare BackendV2, che deve essere ancora supportato e non può essere rimosso fino al completamento della politica di deprezzamento.

Vale la pena sottolineare che la politica di supporto delle versioni di Qiskit non significa che i fornitori stessi avranno la stessa storia di supporto; possono (e probabilmente dovrebbero) aggiornare alle versioni più recenti non appena possono, la finestra di supporto è solo per le versioni supportate da Qiskit. Questa lunga finestra prima della deprecazione serve in parte a dare ai fornitori il tempo sufficiente per effettuare la propria deprecazione di una modifica potenzialmente impattante per l'utente finale in una parte dell'interfaccia rivolta all'utente, prima di aggiornare la propria versione. Ad esempio, supponiamo di aver modificato la firma in Backend.run() in BackendV34 in modo incompatibile con le versioni precedenti. Prima che Aer possa aggiornare la sua classe AerSimulator per essere basata sulla versione 34, è necessario deprecare la vecchia firma prima di passare alla versione successiva. Non è garantito che il passaggio ad Aer avvenga in contemporanea con Qiskit, quindi dobbiamo assicurarci che Aer abbia un tempo sufficiente per completare il suo ciclo di deprezzamento prima di rimuovere la versione 33 (cioè rendere la versione 34 obbligatoria/la versione minima).


Classi astratte

Backend

Rubrica 1
Rubrica 2
Backend()Tipo comune di base per tutte le classi astratte di Backend in versione.
BackendV2( [fornitore, nome, descrizione,...] )Classe astratta per i backend
QubitProperties( [t1, t2, frequenza] )Una rappresentazione di un oggetto QubitProperties .

Opzioni

Rubrica 1
Rubrica 2
Options(**kwargs)Oggetto Opzioni di base

Lavoro

Rubrica 1
Rubrica 2
Job()Tipo comune di base per tutte le classi astratte di Job versioned.
JobV1(backend, job_id, *kwargs)Classe per la gestione dei lavori

Stato del lavoro

Rubrica 1
Rubrica 2
JobStatus(*valori)Classe per il tipo enumerato di stato del lavoro.

Eccezioni

QiskitBackendNotFoundError

exception qiskit.providers.QiskitBackendNotFoundError(*message)

GitHub

Basi: QiskitError

Classe base per gli errori generati durante la ricerca di un backend.

Impostare il messaggio di errore.

JobError

exception qiskit.providers.JobError(*message)

GitHub

Basi: QiskitError

Classe base per gli errori sollevati da Jobs.

Impostare il messaggio di errore.

JobTimeoutError

exception qiskit.providers.JobTimeoutError(*message)

GitHub

Basi: JobError

Classe base per gli errori di timeout generati dai lavori.

Impostare il messaggio di errore.


Scrivere un nuovo backend

Se disponi di un dispositivo quantistico o di un simulatore che desideri integrare con Qiskit, dovrai scrivere un backend. Un provider è un insieme di backend e fornirà a Qiskit un metodo per recuperare gli oggetti disponibili BackendV2 . L'oggetto BackendV2 fornisce sia le informazioni relative a un backend sia quelle relative al suo funzionamento, transpiler in modo che i circuiti possano essere compilati in un formato ottimizzato ed eseguibile sul backend. Fornisce inoltre il run() metodo che consente di eseguire gli QuantumCircuit oggetti. Ciò consente agli utenti e alle altre API di Qiskit di ottenere i risultati dell'esecuzione dei circuiti sui dispositivi in modo standard, indipendentemente dall'implementazione del backend. A grandi linee, i passaggi fondamentali per la creazione di un provider sono i seguenti:

  • Implementare una classe Provider che gestisca l'accesso ai backend.

  • Implementare una BackendV2 sottoclasse e il relativo run() metodo.

    • Aggiungi eventuali gate personalizzati per la base del backend all'istanza della sessione EquivalenceLibrary .
  • Implementare una JobV1 sottoclasse che gestisca l'interazione con un processo in esecuzione.

Per un semplice esempio di provider, vedere il file qiskit-aqt-provider

di servizi

Una classe provider ha un unico scopo: ottenere oggetti backend che consentono l'esecuzione di circuiti su un dispositivo o un simulatore. L'aspettativa è che le credenziali e/o l'autenticazione richieste siano gestite nell'inizializzazione di un oggetto provider. L'oggetto provider fornirà quindi un elenco di backend e metodi per filtrare e acquisire i backend (utilizzando le credenziali fornite, se necessario). Un esempio di classe di provider si presenta come segue:

from qiskit.providers.providerutils import filter_backends

from .backend import MyBackend

class MyProvider:

    def __init__(self, token=None):
        super().__init__()
        self.token = token
        self.backends = [MyBackend(provider=self)]

    def backends(self, name=None, **kwargs):
        if name:
            backends = [
                backend for backend in backends if backend.name() == name]
        return filter_backends(backends, filters=filters, **kwargs)

Assicurati che tutte le informazioni necessarie per l'autenticazione (se richiesta) siano presenti nella classe e che il metodo del backend corrisponda all'interfaccia richiesta. Il resto dipende dal singolo fornitore e da come decide di attuarlo.

Backend

Le classi di backend costituiscono il cuore del provider. Sono proprio queste classi a fornire l'interfaccia tra Qiskit e l'hardware o il simulatore che eseguirà i circuiti. Ciò comporta fornire al compilatore le informazioni necessarie per descrivere un backend, in modo che possa integrare e ottimizzare qualsiasi circuito per tale backend. Ogni oggetto backend deve presentare quattro elementi obbligatori: una target proprietà che definisca il modello del backend per il compilatore, una max_circuits proprietà che definisca un limite al numero di circuiti che il backend può eseguire in un singolo processo batch (se non vi è alcun limite None , è possibile utilizzare ), un run() metodo per accettare l'invio dei processi e un _default_options metodo per definire le opzioni configurabili dall'utente e i relativi valori predefiniti. Ad esempio, un esempio minimo funzionante potrebbe essere qualcosa del genere:

from qiskit.providers import BackendV2 as Backend
from qiskit.transpiler import Target
from qiskit.providers import Options
from qiskit.circuit import Parameter, Measure
from qiskit.circuit.library import PhaseGate, SXGate, UGate, CXGate, IGate


class Mybackend(Backend):

    def __init__(self):
        super().__init__()

        # Create Target
        self._target = Target("Target for My Backend")
        # Instead of None for this and below instructions you can define
        # a qiskit.transpiler.InstructionProperties object to define properties
        # for an instruction.
        lam = Parameter("λ")
        p_props = {(qubit,): None for qubit in range(5)}
        self._target.add_instruction(PhaseGate(lam), p_props)
        sx_props = {(qubit,): None for qubit in range(5)}
        self._target.add_instruction(SXGate(), sx_props)
        phi = Parameter("φ")
        theta = Parameter("ϴ")
        u_props = {(qubit,): None for qubit in range(5)}
        self._target.add_instruction(UGate(theta, phi, lam), u_props)
        cx_props = {edge: None for edge in [(0, 1), (1, 2), (2, 3), (3, 4)]}
        self._target.add_instruction(CXGate(), cx_props)
        meas_props = {(qubit,): None for qubit in range(5)}
        self._target.add_instruction(Measure(), meas_props)
        id_props = {(qubit,): None for qubit in range(5)}
        self._target.add_instruction(IGate(), id_props)

        # Set option validators
        self.options.set_validator("shots", (1, 4096))
        self.options.set_validator("memory", bool)

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return 1024

    @classmethod
    def _default_options(cls):
        return Options(shots=1024, memory=False)

    def run(self, circuits, **kwargs):
        # serialize circuits submit to backend and create a job
        for kwarg in kwargs:
            if not hasattr(self.options, kwarg):
                warnings.warn(
                    "Option %s is not used by this backend" % kwarg,
                    UserWarning, stacklevel=2)
        options = {
            'shots': kwargs.get('shots', self.options.shots),
            'memory': kwargs.get('memory', self.options.memory),
        }
        job_json = convert_to_wire_format(circuit, options)
        job_handle = submit_to_backend(job_json)
        return MyJob(self.job_handle, job_json, circuit)

Interfaccia Transpiler del backend

L'elemento fondamentale dell'oggetto Backend è il modo in cui esso si descrive al compilatore. Questo viene gestito tramite la Target classe che definisce un modello di backend per il transpiler. Un oggetto backend dovrà restituire un Target oggetto dall'attributo target che la transpile() funzione utilizzerà come modello di una destinazione backend per la compilazione.

Basi personalizzate Gates

  1. Se il tuo backend non utilizza i gate della libreria dei circuiti di Qiskit (qiskit.circuit.library), puoi integrare il supporto per questa funzionalità nel tuo provider. Il metodo di base per farlo consiste innanzitutto nel definire una Gate sottoclasse per ciascun gate personalizzato presente nell'insieme di base. Ad esempio:

    import numpy as np
    
    from qiskit.circuit import Gate
    from qiskit.circuit import QuantumCircuit
    
    class SYGate(Gate):
        def __init__(self, label=None):
            super().__init__("sy", 1, [], label=label)
    
        def _define(self):
            qc = QuantumCircuit(1)
            qc.ry(np.pi / 2, 0)
            self.definition = qc

    La cosa fondamentale da assicurare è che per qualsiasi gate personalizzato nel set di basi del vostro Backend l'attributo name del vostro gate personalizzato (il primo parametro di super().__init__() nella definizione di __init__ di cui sopra) non sia in conflitto con il nome di altri gate. L'attributo name è utilizzato per identificare il gate nell'insieme di basi per il transpiler. In caso di conflitto, il transpiler non saprà quale gate utilizzare.

  2. Aggiungi il gate personalizzato alla destinazione del tuo backend. Questo si può fare utilizzando il Target.add_instruction() metodo. Dovrai aggiungere un'istanza di SYGate e i relativi parametri alla destinazione, in modo che il transpiler ne riconosca l'esistenza. Ad esempio, supponendo che questo faccia parte dell'implementazione del tuo BackendV2 backend:

    from qiskit.transpiler import InstructionProperties
    
    sy_props = {
        (0,): InstructionProperties(duration=2.3e-6, error=0.0002),
        (1,): InstructionProperties(duration=2.1e-6, error=0.0001),
        (2,): InstructionProperties(duration=2.5e-6, error=0.0003),
        (3,): InstructionProperties(duration=2.2e-6, error=0.0004),
    }
    self.target.add_instruction(SYGate(), sy_props)

    Le chiavi di sy_props definiscono i qubit su cui può essere utilizzato il backend SYGate e i valori definiscono le proprietà di SYGate su quel qubit. Per i gate multiqubit, le chiavi delle tuple contengono tutte le combinazioni di qubit su cui lavora il gate (l'ordine è significativo, ovvero (0, 1) è diverso da (1, 0)).

  3. Dopo aver definito i gate personalizzati da utilizzare per l'insieme di base del backend, è necessario aggiungere le regole di equivalenza alla libreria di equivalenze standard, in modo che la transpile() funzione e transpiler il modulo possano convertire un circuito arbitrario utilizzando l'insieme di base personalizzato. Ciò può essere fatto definendo circuiti equivalenti, in termini della porta personalizzata, per le porte standard. In genere, se è possibile effettuare la conversione da un CXGate (nel caso in cui la base non includa un gate standard a 2 qubit) e da alcuni gate di rotazione a singolo qubit comunemente utilizzati, come il HGate e UGate il, ciò dovrebbe essere sufficiente affinché il transpiler traduca qualsiasi circuito nei gate della base personalizzata. Tuttavia, più regole di equivalenza vengono definite tra le porte standard e la propria base, più efficiente sarà la conversione da un circuito arbitrario alla base di destinazione (anche se non sempre, e il margine di rendimento tende a diminuire).

    U2Gate Ad esempio, se volessi aggiungere alcune regole per la personalizzazione SYGate di cui sopra, potremmo definire e HGate:

    from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary
    from qiskit.circuit.library import HGate
    from qiskit.circuit.library import ZGate
    from qiskit.circuit.library import RZGate
    from qiskit.circuit.library import U2Gate
    
    
    # H => Z SY
    q = qiskit.QuantumRegister(1, "q")
    def_sy_h = qiskit.QuantumCircuit(q)
    def_sy_h.append(ZGate(), [q[0]], [])
    def_sy_h.append(SYGate(), [q[0]], [])
    SessionEquivalenceLibrary.add_equivalence(
        HGate(), def_sy_h)
    
    # u2 => Z SY Z
    phi = qiskit.circuit.Parameter('phi')
    lam = qiskit.circuit.Parameter('lambda')
    q = qiskit.QuantumRegister(1, "q")
    def_sy_u2 = qiskit.QuantumCircuit(q)
    def_sy_u2.append(RZGate(lam), [q[0]], [])
    def_sy_u2.append(SYGate(), [q[0]], [])
    def_sy_u2.append(RZGate(phi), [q[0]], [])
    SessionEquivalenceLibrary.add_equivalence(
        U2Gate(phi, lam), def_sy_u2)

    Ti conviene impostare l'esecuzione all'importazione, in modo che venga eseguito non appena viene importato il pacchetto del provider. In questo modo si garantirà che, ogni volta che BasisTranslator il percorso viene eseguito con i cancelli personalizzati, le regole di equivalenza siano definite.

    Vale inoltre la pena notare che, a seconda della base utilizzata, alcune fasi di ottimizzazione del transpiler, come ad esempio Optimize1qGatesDecomposition, potrebbero non essere in grado di funzionare con la base personalizzata. Nel nostro SYGate esempio, non sarà Optimize1qGatesDecomposition possibile semplificare le sequenze di porte su un singolo qubit nella base SY. Questo perché la OneQubitEulerDecomposer classe non sa come operare nella base SY. Per risolvere questo problema, la SYGate classe dovrebbe essere aggiunta a Qiskit e OneQubitEulerDecomposer aggiornata in modo da supportare la scomposizione in SYGate. A lungo termine, questa è probabilmente la direzione migliore da seguire per i gate personalizzati; fornire le definizioni e il supporto nel transpiler garantirà che continuino a essere ben supportati da Qiskit anche in futuro.

Passaggi personalizzati del transpiler

Il transpiler consente ai backend di fornire implementazioni personalizzate delle fasi di transpilazione per facilitare ottimizzazioni specifiche per l'hardware e trasformazioni dei circuiti. Attualmente sono supportate due fasi, get_translation_stage_plugin() e get_scheduling_stage_plugin() , che consentono a un backend di specificare nomi di plugin sotto forma di stringa da utilizzare rispettivamente come fase di traduzione e fase di pianificazione predefinite. Questi punti di aggancio all'interno di una BackendV2 classe possono essere utilizzati se il backend presenta requisiti di compilazione che non vengono soddisfatti dal backend/dall'interfacciaTarget attuale. Ti invitiamo inoltre a inviare una segnalazione su GitHub descrivendo il tuo caso d'uso, poiché c'è interesse a migliorare queste interfacce per poter descrivere un maggior numero di architetture hardware in modo più approfondito.

Per sfruttare questi punti di aggancio, basta aggiungere i metodi alla propria BackendV2 implementazione e fare in modo che restituiscano il nome del plugin sotto forma di stringa. Ad esempio:

class Mybackend(BackendV2):

    def get_scheduling_stage_plugin(self):
        return "SpecialDD"

    def get_translation_stage_plugin(self):
        return "BasisTranslatorWithCustom1qOptimization"

Questo frammento di implementazione del backend prevede che la funzione transpile() utilizzi il plugin SpecialDD per la fase di programmazione e il plugin BasisTranslatorWithCustom1qOptimization per la fase di traduzione, come impostazione predefinita quando il target è impostato su Mybackend. Si noti che gli utenti possono ignorare queste scelte selezionando esplicitamente un nome di plugin diverso. Affinché questa interfaccia funzioni, i plugin dello stadio del transpiler devono essere implementati per il nome del plugin restituito. Si può fare riferimento alla qiskit.transpiler.preset_passmanagers.plugin per i dettagli su come implementare i plugin. L'aspettativa tipica è che se il backend richiede passaggi personalizzati come parte di una fase di compilazione, il pacchetto del provider includa i plugin della fase di transpiler che utilizzano tali passaggi. Tuttavia, questo non è obbligatorio e si può usare qualsiasi metodo valido (da un metodo incorporato o da un plugin esterno).

In questo modo, se questi due passaggi di compilazione sono necessari per l'esecuzione o per fornire un output efficiente su Mybackend , il transpiler sarà in grado di eseguire questi passaggi personalizzati senza alcun input manuale da parte dell'utente.

Variabili in tempo reale

Il transpiler gestirà automaticamente le variabili classiche digitate in tempo reale (vedere qiskit.circuit.classical) e tratterà l'istruzione Store come una "direttiva" incorporata, simile a Barrier. Non è necessaria alcuna gestione speciale da parte dei backend per consentire questa operazione.

Se il tuo backend non è in grado di gestire variabili e archiviazione classiche, ti consigliamo di commentare questo aspetto nella tua documentazione e di inserire un controllo nel tuo run() metodo (vedere Metodo Backend.run ) per rifiutare immediatamente i circuiti che li contengono. È possibile verificare QuantumCircuit.num_vars la presenza di variabili al livello superiore. Se si accettano le operazioni relative al flusso di controllo, potrebbe essere necessario cercare in modo ricorsivo all’interno blocks di ciascuna variabile locale dell’ambito con QuantumCircuit.num_declared_vars.

Ad esempio, una funzione per verificare la presenza di posizioni di memorizzazione manuali o di memorizzazioni manuali nella memoria:

from qiskit.circuit import Store, ControlFlowOp, QuantumCircuit

def has_realtime_logic(circuit: QuantumCircuit) -> bool:
    if circuit.num_vars:
        return True
    for instruction in circuit.data:
        if isinstance(instruction.operation, Store):
            return True
        elif isinstance(instruction.operation, ControlFlowOp):
            for block in instruction.operation.blocks:
                if has_realtime_logic(block):
                    return True
    return False

Limiti angolari su Gates

Se il tuo backend prevede dei vincoli sui valori consentiti dei parametri per qualsiasi gate nel target, puoi modellare questa situazione impostando dei limiti angolari sul Target. Quando si aggiunge l'istruzione con il add_instruction() , è possibile utilizzare l'argomento angle_bounds chiave, che accetta un elenco di tuple corrispondenti ai limiti superiore e inferiore del parametro di una porta logica.

Ad esempio, questo frammento di codice, invece dell'esempio che aggiunge il PhaseGate nell'esempio sopra riportato:

lam = Parameter("λ")
p_props = {(qubit,): None for qubit in range(5)}
self._target.add_instruction(PhaseGate(lam), p_props, angle_bounds=[(0, math.pi)])

imposterà gli intervalli di PhaseGate tra 0 e π\pi (compresi). Questo modella il vincolo angolare relativo Target ai valori angolari del lam parametro su PhaseGate. Il passaggio del WrapAngles transpiler viene utilizzato per trasformare qualsiasi valore PhaseGate al di fuori dei limiti angolari specificati. Dovrai scrivere una funzione che accetti i valori degli angoli del cancello e restituisca un DAGCircuit. Ad esempio:

from qiskit.transpiler.passes import WrapAngles

def fold_phase(angles: List[float], qubits: List[int]) -> DAGCircuit:
    angle = angles[0]
    if angle > 0:
        number_of_gates = angle / math.pi
    else:
        number_of_gates = (6.28 - angle) / math.pi
    dag = DAGCircuit()
    dag.add_qubits([Qubit()])
    for _ in range(int(number_of_gates)):
        dag.apply_operation_back(PhaseGate(math.pi), [dag.qubits[0]])
    return dag

WrapAngles.DEFAULT_REGISTRY.add_wrapper("phase", fold_phase)

Questa funzione trasformerà le porte fuori limite in una che rispetta i limiti angolari dell'obiettivo e gli altri vincoli dell'obiettivo (anche se non particolarmente bene).

Backend.run Metodo

Di fondamentale importanza è il run() metodo utilizzato per inviare effettivamente i circuiti a un dispositivo o a un simulatore. Il metodo run si occupa di inviare i circuiti al backend affinché vengano eseguiti e di restituire un Job oggetto. A seconda del tipo di backend, ciò comporta in genere la serializzazione dell'oggetto circuito nel formato API utilizzato dal backend. Poiché le esigenze di serializzazione del backend possono variare (e, nel caso dei simulatori locali, la serializzazione potrebbe addirittura non essere necessaria), ci si aspetta che sia il metodo del run backend a gestire questa conversione.

Un esempio di metodo di esecuzione potrebbe essere qualcosa di simile:

def run(self, circuits, **kwargs):
    for kwarg in kwargs:
        if not hasattr(self.options, kwarg):
            warnings.warn(
                "Option %s is not used by this backend" % kwarg,
                UserWarning, stacklevel=2)
    options = {
        'shots': kwargs.get('shots', self.options.shots),
        'memory': kwargs.get('memory', self.options.memory),
    }
    job_json = convert_to_wire_format(circuit, options)
    job_handle = submit_to_backend(job_json)
    return MyJob(self.job_handle, job_json, circuit)

Opzioni backend

Spesso esistono diverse opzioni per il backend che determinano il funzionamento di un circuito. Un esempio tipico è il numero di shots , che indica quante volte deve essere eseguito il circuito. Le opzioni disponibili per un backend vengono definite tramite un Options oggetto. Questo oggetto viene inizialmente creato dal _default_options metodo di una classe Backend. Le opzioni predefinite restituiscono un oggetto inizializzato Options con tutti i valori predefiniti per tutte le opzioni supportate da un backend. Ad esempio, se il backend supportasse shots solo il _default_options metodo, sarebbe simile a questo:

@classmethod
def _default_options(cls):
    return Options(shots=1024)

È inoltre possibile impostare dei validatori su un Options oggetto per definire limiti e verificare la validità dei valori forniti dall'utente in base a ciò che è accettabile per il proprio backend. Ad esempio, se l'opzione "shots" definita sopra può assumere qualsiasi valore compreso tra 1 e 4096, è possibile impostare il validatore sull'oggetto delle opzioni del proprio backend con:

self.options.set_validator("shots", (1, 4096))

Per un elenco completo delle opzioni di convalida, è possibile consultare la set_validator() documentazione.

Lavoro

Il risultato del metodo run è un JobV1 oggetto. Ogni provider deve implementare una sottoclasse personalizzata di Job che definisca il comportamento del provider stesso. Esistono due tipi di operazioni a seconda del metodo di esecuzione del backend: sincrono o asincrono. Per impostazione predefinita, i lavori sono considerati asincroni e si presume che rappresentino un riferimento all'esecuzione asincrona dei circuiti inviati con Backend.run(). Un oggetto di processo asincrono consente agli utenti di verificare lo stato dell'esecuzione, annullare un processo in corso e attendere fino al completamento dell'esecuzione. Il result è il metodo principale a disposizione dell'utente, che rimarrà in attesa fino al completamento dell'esecuzione e successivamente restituirà un Result oggetto con i risultati del processo.

Per alcuni backend (principalmente simulatori locali) l'esecuzione dei circuiti è un'operazione sincrona e non è necessario restituire un identificativo a un processo in esecuzione altrove. Per i processi di sincronizzazione, è previsto che il run metodo sul backend rimanga in attesa fino a quando non venga generato un Result oggetto e che il processo di sincronizzazione restituisca tale oggetto interno Result .

Un esempio di classe di lavoro per un backend basato su API async potrebbe essere simile:

from qiskit.providers import JobV1 as Job
from qiskit.providers import JobError
from qiskit.providers import JobTimeoutError
from qiskit.providers.jobstatus import JobStatus
from qiskit.result import Result


class MyJob(Job):
    def __init__(self, backend, job_id, job_json, circuits):
        super().__init__(backend, job_id)
        self._backend = backend
        self.job_json = job_json
        self.circuits = circuits

    def _wait_for_result(self, timeout=None, wait=5):
        start_time = time.time()
        result = None
        while True:
            elapsed = time.time() - start_time
            if timeout and elapsed >= timeout:
                raise JobTimeoutError('Timed out waiting for result')
            result = get_job_status(self._job_id)
            if result['status'] == 'complete':
                break
            if result['status'] == 'error':
                raise JobError('Job error')
            time.sleep(wait)
        return result

    def result(self, timeout=None, wait=5):
        result = self._wait_for_result(timeout, wait)
        results = [{'success': True, 'shots': len(result['counts']),
                    'data': result['counts']}]
        return Result.from_dict({
            'results': results,
            'backend_name': self._backend.configuration().backend_name,
            'backend_version': self._backend.configuration().backend_version,
            'job_id': self._job_id,
            'success': True,
        })

    def status(self):
        result = get_job_status(self._job_id)
        if result['status'] == 'running':
            status = JobStatus.RUNNING
        elif result['status'] == 'complete':
            status = JobStatus.DONE
        else:
            status = JobStatus.ERROR
        return status

    def submit(self):
        raise NotImplementedError

e per un lavoro di sincronizzazione:

class MySyncJob(Job):
    _async = False

    def __init__(self, backend, job_id, result):
        super().__init__(backend, job_id)
        self._result = result

    def submit(self):
        return

    def result(self):
        return self._result

    def status(self):
        return JobStatus.DONE

Primitivi

Anche se non fa direttamente parte dell'interfaccia del fornitore, il modulo qiskit.primitives è strettamente legato ai provider. In particolare, le interfacce primitive, come BaseSampler e BaseEstimator, sono progettate per consentire alle implementazioni dei provider di fornire implementazioni personalizzate ottimizzate per i backend dei provider. Ciò può includere personalizzazioni come trasformazioni dei circuiti, pre- e post-elaborazione aggiuntiva, batching, caching, mitigazione degli errori, ecc. Il concetto del modulo qiskit.primitives è quello di consentire esplicitamente questo, poiché gli oggetti primitivi sono astrazioni di livello superiore per produrre output elaborati di livello superiore (come distribuzioni di probabilità e valori di aspettativa) che astraggono dalla meccanica per ottenere il miglior risultato in modo efficiente, per concentrarsi su applicazioni di livello superiore che utilizzano questi output.

Ad esempio, se i vostri backend fossero adatti a sfruttare la mitigazione delle misure di mthree per migliorare la qualità dei risultati, potreste implementare un'implementazione di Sampler specifica del provider che sfrutti internamente la classe M3Mitigation per eseguire i circuiti e restituire le quasi-probabilità direttamente da mthree nel risultato. In questo modo gli algoritmi otterrebbero i migliori risultati con la mitigazione applicata direttamente dai vostri backend. Si può fare riferimento alla documentazione in qiskit.primitives su come scrivere implementazioni personalizzate. Inoltre, le implementazioni integrate: Sampler, Estimator, BackendSampler, e BackendEstimator possono servire come riferimenti/modelli su come implementare anche queste.

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.