Skip to main content
IBM Quantum Platform

Interface des fournisseurs

qiskit.providers

Ce module contient les classes utilisées pour créer des fournisseurs externes pour Qiskit. Un fournisseur est tout élément qui fournit un service externe à Qiskit. L'exemple typique est celui d'un fournisseur backend qui fournit Backend des objets pouvant être utilisés pour exécuter QuantumCircuit des objets. Ce module contient les classes abstraites utilisées pour définir l'interface entre un fournisseur et Qiskit.


Versions prises en charge

Chaque classe abstraite de l'interface des fournisseurs est versionnée individuellement. Lorsque nous devons modifier une interface, une nouvelle classe abstraite est créée pour définir la nouvelle interface. Il n'est pas garanti que ces changements d'interface soient rétrocompatibles d'une version à l'autre.

Modifications de version

Chaque version mineure de qiskit peut incrémenter la version de n'importe quelle interface backend d'un seul numéro de version. Il s'agit d'un agrégat de tous les changements d'interface pour cette version sur cette interface.

Règle de prise en charge de version

Pour permettre aux fournisseurs d'avoir le temps de s'adapter aux changements de cette interface, Qiskit prendra en charge plusieurs versions de chaque classe à la fois. Compte tenu de la nature d'une version par version, la politique de dépréciation de la version est un peu plus conservatrice que la politique de dépréciation standard. Qiskit supportera une version d'interface fournisseur pour un minimum de 3 versions mineures ou la première version après 6 mois à partir de la version qui a introduit une version, selon ce qui est le plus long, avant une dépréciation potentielle. Ensuite, la politique de dépréciation standard s'appliquera à cette version de l'interface. Les fournisseurs et les utilisateurs disposeront ainsi de suffisamment de temps pour s'adapter à d'éventuels changements radicaux dans l'interface. Par exemple, disons que 0.19.0 BackendV2 est présenté et que dans les 3 mois suivant la sortie de 0.19.0, nous sortons 0.20.0, 0.21.0 et 0.22.0, puis 7 mois après 0.19.0, nous sortons 0.23.0. Dans 0.23.0, nous pouvons déprécier BackendV2, qui doit toujours être pris en charge et ne peut être supprimé tant que la politique de dépréciation n'est pas achevée.

Il convient de souligner que la politique de Qiskit en matière de support de version ne signifie pas que les fournisseurs eux-mêmes auront la même histoire de support, ils peuvent (et devraient sans doute) mettre à jour vers des versions plus récentes dès qu'ils le peuvent, la fenêtre de support ne concerne que les versions supportées par Qiskit. Cette longue période avant la dépréciation vise en partie à donner aux fournisseurs suffisamment de temps pour procéder à leur propre dépréciation d'une modification susceptible d'avoir un impact sur l'utilisateur final dans une partie de l'interface en contact avec l'utilisateur, avant d'actualiser leur version. Par exemple, supposons que nous ayons modifié la signature en Backend.run() dans BackendV34 d'une manière incompatible avec le passé. Avant qu'Aer puisse mettre à jour sa classe AerSimulator pour qu'elle soit basée sur la version 34, il faudrait que l'ancienne signature soit dépréciée avant le passage à la nouvelle version. Le passage à Aer n'est pas garanti en même temps que Qiskit, nous devons donc nous assurer qu'il y a suffisamment de temps pour qu'Aer termine son cycle de dépréciation avant de supprimer la version 33 (c'est-à-dire rendre la version 34 obligatoire/la version minimale).


Classes abstraites

Back-end

Chronique « 1 »
Chronique « 2 »
Backend()Type commun de base pour toutes les classes abstraites versionnées du backend.
BackendV2( [prestataire, nom, description,...] )Classe abstraite pour les backends
QubitProperties( [t1, t2, fréquence] )Une représentation d'un objet QubitProperties .

Options

Chronique « 1 »
Chronique « 2 »
Options(**kwargs)Objet d'options de base

Travail

Chronique « 1 »
Chronique « 2 »
Job()Type commun de base pour toutes les classes abstraites de Job versionné.
JobV1(backend, job_id, **kwargs)Classe pour la gestion des emplois

État du travail

Chronique « 1 »
Chronique « 2 »
JobStatus(*valeurs)Classe pour le type énuméré de l'état de l'emploi.

Exceptions

QiskitBackendNotFoundError

exception qiskit.providers.QiskitBackendNotFoundError(*message)

GitHub

Bases : QiskitError

Classe de base pour les erreurs générées lors de la recherche d'un backend.

Définir le message d'erreur.

JobError

exception qiskit.providers.JobError(*message)

GitHub

Bases : QiskitError

Classe de base pour les erreurs soulevées par les emplois.

Définir le message d'erreur.

JobTimeoutError

exception qiskit.providers.JobTimeoutError(*message)

GitHub

Bases : JobError

Classe de base pour les erreurs de dépassement de délai soulevées par les travaux.

Définir le message d'erreur.


Écrire un nouveau backend

Si vous disposez d'un dispositif quantique ou d'un simulateur que vous souhaitez intégrer à Qiskit, vous devrez développer un backend. Un fournisseur est un ensemble de backends et permet à Qiskit d'accéder aux objets disponibles BackendV2 . Cet BackendV2 objet fournit à la fois des informations décrivant un backend et son fonctionnement, transpiler afin que les circuits puissent être compilés en un code optimisé et exécutable sur ce backend. Elle fournit également la run() méthode permettant d'exécuter les QuantumCircuit objets. Cela permet aux utilisateurs et aux autres API Qiskit d'obtenir les résultats de l'exécution de circuits sur des périphériques selon une procédure standard, quelle que soit la manière dont le backend est implémenté. Dans les grandes lignes, les étapes fondamentales pour écrire un fournisseur sont les suivantes :

  • Implémenter une classe Provider qui gère l'accès au(x) backend(s).

  • Implémentez une BackendV2 sous-classe et sa run() méthode.

    • Ajoutez les portes personnalisées correspondant à la base du backend à l'instance de session EquivalenceLibrary .
  • Implémentez une JobV1 sous-classe chargée de gérer les interactions avec une tâche en cours d'exécution.

Pour un exemple simple de fournisseur, voir le document qiskit-aqt-provider

Fournisseur

Une classe de fournisseur n'a qu'un seul objectif : obtenir des objets backend qui permettent d'exécuter des circuits sur un appareil ou un simulateur. On s'attend à ce que les informations d'identification et/ou d'authentification requises soient traitées lors de l'initialisation d'un objet fournisseur. L'objet fournisseur fournira alors une liste de backends, ainsi que des méthodes pour filtrer et acquérir des backends (en utilisant les informations d'identification fournies si nécessaire). Un exemple de classe de fournisseur ressemble à ceci :

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)

Assurez-vous que toutes les informations nécessaires à l'authentification (le cas échéant) sont présentes dans la classe et que la méthode du backend correspond à l'interface requise. Pour le reste, c'est au prestataire concerné de décider comment procéder.

Back-end

Les classes backend constituent le cœur du fournisseur. Ce sont ces classes qui assurent l'interface entre Qiskit et le matériel ou le simulateur chargé d'exécuter les circuits. Cela implique notamment de fournir au compilateur les informations nécessaires pour décrire un backend, afin qu'il puisse intégrer et optimiser n'importe quel circuit destiné à ce backend. Chaque objet « backend » doit comporter quatre éléments obligatoires : une target propriété permettant de définir le modèle du backend pour le compilateur, une max_circuits propriété permettant de définir une limite au nombre de circuits que le backend peut exécuter au cours d’un même travail par lots (si aucune limite None n’est définie, la valeur par défaut peut être utilisée), une run() méthode permettant d’accepter les soumissions de travaux, et une _default_options méthode permettant de définir les options configurables par l’utilisateur ainsi que leurs valeurs par défaut. Par exemple, un exemple minimal fonctionnel pourrait ressembler à ceci :

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)

Interface du transpilateur du backend

L'élément clé de l'objet Backend réside dans la manière dont il se décrit au compilateur. Ceci est géré par la Target classe qui définit un modèle de backend pour le transcompilateur. Un objet backend devra renvoyer un Target objet à partir de target l'attribut que la transpile() fonction utilisera comme modèle de cible backend pour la compilation.

Portails sur mesure

  1. Si votre backend n'utilise pas les portes de la bibliothèque de circuits Qiskit (qiskit.circuit.library), vous pouvez intégrer la prise en charge de celles-ci dans votre fournisseur. La méthode de base consiste tout d'abord à définir une Gate sous-classe pour chaque porte personnalisée de l'ensemble de base. Par exemple :

    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

    Il est essentiel de veiller à ce que l'attribut name (premier paramètre sur super().__init__() dans la définition de __init__ ci-dessus) de toutes les portes personnalisées de l'ensemble de base de votre backend n'entre pas en conflit avec le nom d'autres portes. L'attribut name est utilisé pour identifier la porte dans l'ensemble de base pour le transpileur. En cas de conflit, le transpondeur ne saura pas quelle porte utiliser.

  2. Ajoutez la passerelle personnalisée à la cible de votre backend. Pour cela, on peut utiliser la Target.add_instruction() méthode. Vous devrez ajouter une instance de SYGate et ses paramètres à la cible afin que le transcompilateur sache qu'elle existe. Par exemple, en supposant que cela fasse partie de la BackendV2 mise en œuvre de votre 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)

    Les clés de sy_props définissent les qubits sur lesquels le backend SYGate peut être utilisé, et les valeurs définissent les propriétés de SYGate sur ce qubit. Pour les portes multiqubits, les clés tuple contiennent toutes les combinaisons de qubits sur lesquelles la porte fonctionne (l'ordre est important, à savoir (0, 1) est différent de (1, 0)).

  3. Une fois que vous avez défini les portes personnalisées à utiliser pour l'ensemble de base du backend, vous devez ajouter des règles d'équivalence à la bibliothèque d'équivalence standard afin que la transpile() fonction et transpiler le module puissent convertir un circuit quelconque à l'aide de cet ensemble de base personnalisé. Pour ce faire, on peut définir des circuits équivalents, en termes de porte personnalisée, pour les portes standard. En règle générale, si vous pouvez effectuer la conversion à partir d’un CXGate (si votre base ne comprend pas de porte standard à 2 qubits) et de certaines portes de rotation à un seul qubit couramment utilisées, comme le HGate et UGate le, cela devrait suffire au transpileur pour traduire n’importe quel circuit en portes de la base personnalisée. Cependant, plus on définit de règles d'équivalence entre les portes standard et votre base, plus la conversion d'un circuit arbitraire vers la base cible sera efficace (même si ce n'est pas toujours le cas, et que le gain diminue progressivement).

    Par exemple, si vous deviez ajouter des règles pour la personnalisation SYGate ci-dessus, nous pourrions définir les éléments U2Gate et 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)

    Vous devrez veiller à ce que cette commande soit exécutée lors de l'importation, afin qu'elle soit lancée dès que le paquet du fournisseur sera importé. Cela garantira que, chaque fois que le BasisTranslator passage sera exécuté avec les portes personnalisées, les règles d'équivalence seront définies.

    Il convient également de noter que, selon la base que vous utilisez, certaines étapes d'optimisation du transcompilateur, telles que Optimize1qGatesDecomposition, pourraient ne pas fonctionner avec votre base personnalisée. Dans notre SYGate exemple, il Optimize1qGatesDecomposition ne sera pas possible de simplifier les séquences de portes à un seul qubit dans la base SY. En effet, cette OneQubitEulerDecomposer classe ne sait pas comment fonctionner dans la base SY. Pour résoudre ce problème, il faudrait ajouter cette SYGate classe à Qiskit et OneQubitEulerDecomposer la mettre à jour afin qu’elle prenne en charge la décomposition en SYGate. À plus long terme, c'est sans doute la meilleure voie à suivre pour les portes personnalisées, et le fait d'apporter les définitions et la prise en charge dans le transpileur garantira que cette fonctionnalité continuera d'être bien prise en charge par Qiskit à l'avenir.

Passages de transcompilateur personnalisés

Le transpileur permet aux backends de fournir des implémentations personnalisées des étapes de transpilation afin de faciliter les optimisations spécifiques au matériel et les transformations de circuits. Actuellement, deux étapes sont prises en charge, get_translation_stage_plugin() et get_scheduling_stage_plugin() , qui permettent à un backend de spécifier des noms de plugins sous forme de chaînes de caractères à utiliser respectivement comme étapes par défaut pour la traduction et la planification. Ces points de raccordement d'une BackendV2 classe peuvent être utilisés si votre backend a des exigences de compilation qui ne sont pas satisfaites par le backend ouTarget l'interface actuel(le). Nous vous invitons également à créer un ticket sur GitHub décrivant votre cas d'utilisation, car nous souhaitons améliorer ces interfaces afin de pouvoir décrire davantage d'architectures matérielles de manière plus détaillée.

Pour tirer parti de ces points d'accrochage, il vous suffit d'ajouter les méthodes à votre BackendV2 implémentation et de faire en sorte qu'elles renvoient un nom de plugin sous forme de chaîne de caractères. Par exemple :

class Mybackend(BackendV2):

    def get_scheduling_stage_plugin(self):
        return "SpecialDD"

    def get_translation_stage_plugin(self):
        return "BasisTranslatorWithCustom1qOptimization"

Dans cet exemple de mise en œuvre du backend, la fonction transpile() utilisera par défaut le plugin SpecialDD pour l'étape de planification et le plugin BasisTranslatorWithCustom1qOptimization pour l'étape de traduction lorsque la cible est fixée à Mybackend. Notez que les utilisateurs peuvent outrepasser ces choix en sélectionnant explicitement un nom de plugin différent. Pour que cette interface fonctionne, les plugins d'étape du transpondeur doivent être implémentés pour le nom de plugin renvoyé. Vous pouvez vous référer à la qiskit.transpiler.preset_passmanagers.plugin pour plus de détails sur l'implémentation des plugins. En règle générale, si votre backend nécessite des passes personnalisées dans le cadre d'une étape de compilation, le paquet du fournisseur inclura les plugins de l'étape de transposition qui utilisent ces passes. Toutefois, cela n'est pas obligatoire et toute méthode valide (provenant d'une méthode intégrée ou d'un plugin externe) peut être utilisée.

De cette façon, si ces deux étapes de compilation sont nécessaires pour fonctionner ou fournir un résultat efficace sur Mybackend , le transpileur sera en mesure d'effectuer ces étapes personnalisées sans aucune intervention manuelle de la part de l'utilisateur.

Variables en temps réel

Le transpileur traitera automatiquement les variables classiques typées en temps réel (voir qiskit.circuit.classical) et traitera l'instruction Store comme une "directive" intégrée, similaire à Barrier. Aucune manipulation particulière de la part des backends n'est nécessaire pour permettre cela.

Si votre backend ne prend pas en charge les variables et le stockage classiques, nous vous recommandons de le mentionner dans votre documentation et d'insérer une vérification dans votre code. run() méthode (voir la méthode Backend.run ) pour rejeter immédiatement les circuits qui les contiennent. Vous pouvez vérifier QuantumCircuit.num_vars la présence de variables au niveau supérieur. QuantumCircuit.num_declared_varsSi vous acceptez les opérations de contrôle de flux, vous devrez peut-être rechercher de manière récursive, au sein blocks de chaque for, les variables locales à la portée à l'aide de ..

Par exemple, une fonction permettant de vérifier la présence d'emplacements de stockage manuel ou d'enregistrements manuels dans la mémoire :

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

Limites angulaires sur les portes

Si votre backend impose des contraintes sur les valeurs autorisées des paramètres pour une porte quelconque de la cible, vous pouvez modéliser cela en définissant des limites d'angle sur le Target. Lorsque vous ajoutez l'instruction avec le add_instruction() , vous pouvez utiliser l'argument angle_bounds de mot-clé, qui prend en paramètre une liste de tuples correspondant aux limites supérieure et inférieure du paramètre d'une porte.

Par exemple, cet extrait de code, au lieu de l'exemple qui ajoute le PhaseGate dans l'exemple ci-dessus :

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

définira les limites de PhaseGate entre 0 et π\pi (inclus). Target Cela modélise la contrainte d'angle sur les valeurs d'angle du lam paramètre sur PhaseGate. La passe de WrapAngles transpilation sert à transformer tout élément PhaseGate situé en dehors des limites d'angle spécifiées. Vous devrez écrire une fonction qui prend en paramètre les valeurs d'angle de la porte et renvoie un DAGCircuit. Par exemple :

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)

Cette fonction transforme les portes hors limites en une porte qui respecte les limites d'angle dans la cible et les autres contraintes de la cible (même si ce n'est pas particulièrement bien).

Backend.run Méthode

La run() méthode utilisée pour soumettre concrètement les circuits à un dispositif ou à un simulateur revêt une importance capitale. La méthode run se charge d'envoyer les circuits au backend pour qu'ils soient exécutés et de renvoyer un Job objet. Selon le type de backend, cela implique généralement de sérialiser l'objet « circuit » au format API utilisé par ce backend. Étant donné que les besoins en matière de sérialisation peuvent varier d'un backend à l'autre (et que, dans le cas des simulateurs locaux, la sérialisation peut même s'avérer superflue), on s'attend à ce que la méthode du run backend se charge de cette conversion.

Un exemple de méthode d'exécution serait quelque chose comme :

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)

Options backend

Il existe souvent plusieurs options de backend qui déterminent le fonctionnement d'un circuit. L'exemple typique correspond à quelque chose comme le nombre de shots , qui indique le nombre de fois où le circuit doit être exécuté. Les options disponibles pour un backend sont définies à l'aide d'un Options objet. Cet objet est initialement créé par la _default_options méthode d'une classe Backend. Les options par défaut renvoient un objet initialisé Options contenant toutes les valeurs par défaut pour l'ensemble des options prises en charge par un backend. Par exemple, si le backend ne prend shots en charge que la _default_options méthode, celle-ci ressemblerait à ceci :

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

Vous pouvez également définir des validateurs sur un Options objet afin d'imposer des contraintes et de valider les valeurs fournies par l'utilisateur en fonction des critères acceptables pour votre backend. Par exemple, si l'option "shots" définie ci-dessus peut prendre n'importe quelle valeur comprise entre 1 et 4096, vous pouvez configurer le validateur sur l'objet « options » de votre backend comme suit :

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

Vous pouvez consulter la set_validator() documentation pour obtenir la liste complète des options de validation.

Travail

La run méthode renvoie un JobV1 objet. Chaque fournisseur doit implémenter une sous-classe de tâche personnalisée qui définit le comportement du fournisseur. Il existe deux types de tâches selon le mode d'exécution du backend : synchrone ou asynchrone. Par défaut, les tâches sont considérées comme asynchrones et sont censées servir de point d'accès à l'exécution asynchrone des circuits soumis avec Backend.run(). Un objet de tâche asynchrone permet aux utilisateurs de consulter l'état d'exécution, d'annuler une tâche en cours d'exécution et d'attendre que celle-ci soit terminée. La méthode result est la principale méthode accessible à l'utilisateur; elle reste en attente jusqu'à la fin de l'exécution, puis renvoie un Result objet contenant les résultats de la tâche.

Pour certains backends (principalement les simulateurs locaux), l'exécution des circuits est une opération synchrone et il n'est pas nécessaire de renvoyer un identifiant à un travail en cours d'exécution ailleurs. Pour les tâches de synchronisation, la run méthode du backend est censée rester bloquée jusqu’à ce qu’un Result objet soit généré, puis la tâche de synchronisation renverra cet objet interne Result .

Un exemple de classe de travail pour un backend basé sur une API asynchrone ressemblerait à quelque chose comme :

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

et pour un travail de synchronisation :

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

Primitifs

Bien qu'il ne fasse pas directement partie de l'interface du fournisseur, le module qiskit.primitives est étroitement lié aux fournisseurs. En particulier, les interfaces primitives, telles que BaseSampler et BaseEstimator, sont conçues pour permettre aux fournisseurs de fournir des implémentations personnalisées qui sont optimisées pour les backends des fournisseurs. Il peut s'agir de personnalisations telles que des transformations de circuits, des prétraitements et post-traitements supplémentaires, la mise en lots, la mise en cache, l'atténuation des erreurs, etc. Le concept du module qiskit.primitives est de permettre explicitement cela, car les objets primitifs sont des abstractions de plus haut niveau pour produire des résultats traités de plus haut niveau (tels que des distributions de probabilité et des valeurs d'espérance) qui font abstraction des mécanismes permettant d'obtenir le meilleur résultat de manière efficace, afin de se concentrer sur des applications de plus haut niveau utilisant ces résultats.

Par exemple, si vos backends sont bien adaptés pour tirer parti de l'atténuation des mesures de mthree afin d'améliorer la qualité des résultats, vous pourriez mettre en œuvre une implémentation Sampler spécifique au fournisseur qui tire parti de la classe M3Mitigation en interne pour exécuter les circuits et renvoyer des quasi-probabilités directement à partir de mthree dans le résultat. Cela permettrait aux algorithmes d'obtenir les meilleurs résultats grâce à l'atténuation appliquée directement à partir de vos backends. Vous pouvez vous référer à la documentation de qiskit.primitives sur la manière d'écrire des implémentations personnalisées. De plus, les implémentations intégrées : Sampler, Estimator, BackendSampler, et BackendEstimator peuvent servir de références/modèles sur la manière de les mettre en œuvre également.

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.