Interface dos fornecedores
qiskit.providers
Este módulo contém as classes usadas para criar provedores externos para o Qiskit. Um provedor é qualquer coisa que forneça um serviço externo ao Qiskit. O exemplo típico disso é um provedor de back-end que fornece Backend objetos que podem ser usados para executar objetos QuantumCircuit objetos. Este módulo contém as classes abstratas que são usadas para definir a interface entre um provedor e o Qiskit.
Suporte a Versões
Cada classe abstrata da interface de provedores é versionada individualmente. Quando precisarmos fazer uma alteração em uma interface, uma nova classe abstrata será criada para definir a nova interface. Não há garantia de que essas alterações na interface sejam compatíveis com versões anteriores.
Alterações na versão
Cada lançamento de versão secundária do qiskit pode incrementar a versão de qualquer interface de backend em um único número de versão. Será um agregado de todas as alterações de interface para essa versão nessa interface.
Política de suporte de versão
Para permitir que os provedores tenham tempo de se ajustar às mudanças nessa interface, o Qiskit oferecerá suporte a várias versões de cada classe ao mesmo tempo. Dada a natureza de uma versão por lançamento, a política de depreciação de versão é um pouco mais conservadora do que a política de depreciação padrão. O Qiskit oferecerá suporte a uma versão de interface de provedor por um mínimo de 3 versões secundárias ou a primeira versão após 6 meses do lançamento que introduziu uma versão, o que for mais longo, antes de uma possível depreciação. Depois disso, a política de depreciação padrão será aplicada a essa versão da interface. Isso dará aos provedores e usuários tempo suficiente para se adaptarem às possíveis mudanças significativas na interface. Por exemplo, digamos que em 0.19.0 BackendV2 é apresentado e, 3 meses após o lançamento de 0.19.0, lançamos 0.20.0, 0.21.0 e 0.22.0 e, 7 meses após 0.19.0, lançamos 0.23.0. Em 0.23.0, podemos descontinuar o BackendV2, e ele ainda precisa ter suporte e não pode ser removido até que a política de descontinuação seja concluída.
Vale ressaltar que a política de suporte à versão do Qiskit não significa que os próprios provedores terão a mesma história de suporte. Eles podem (e, sem dúvida, devem) atualizar para versões mais recentes assim que puderem, a janela de suporte é apenas para as versões suportadas pelo Qiskit. Parte dessa longa janela antes da descontinuação é para dar aos provedores tempo suficiente para fazer sua própria descontinuação de uma possível alteração que afete o usuário final em uma parte da interface voltada para o usuário antes de atualizar sua versão. Por exemplo, digamos que alteramos a assinatura para Backend.run() em BackendV34 de uma forma incompatível com as versões anteriores. Antes que a Aer pudesse atualizar sua classe AerSimulator para se basear na versão 34, ela precisaria descontinuar a assinatura antiga antes de fazer a mudança. Não há garantia de que a mudança para o Aer seja feita em sincronia com o Qiskit, portanto, precisamos garantir que haja tempo suficiente para que o Aer conclua seu ciclo de depreciação antes de remover a versão 33 (ou seja, tornar a versão 34 obrigatória/ mínima).
Classes abstratas
Back-end
Backend() | Tipo comum básico para todas as classes abstratas de backend com versão. |
BackendV2([provedor, nome, descrição,...] ) | Classe abstrata para backends |
QubitProperties( [t1, t2, frequência] ) | Uma representação de um objeto QubitProperties . |
Opções
Options(**kwargs) | Objeto de opções de base |
Tarefa
Job() | Tipo comum básico para todas as classes abstratas do Job com controle de versão. |
JobV1(backend, job_id, **kwargs) | Classe para lidar com trabalhos |
Status da tarefa
JobStatus(*valores) | Classe para o tipo enumerado de status do trabalho. |
Exceções
QiskitBackendNotFoundError
exception qiskit.providers.QiskitBackendNotFoundError(*message)
Bases: QiskitError
Classe base para erros gerados durante a procura de um backend.
Defina a mensagem de erro.
JobError
exception qiskit.providers.JobError(*message)
Bases: QiskitError
Classe base para erros gerados por Jobs.
Defina a mensagem de erro.
JobTimeoutError
exception qiskit.providers.JobTimeoutError(*message)
Bases: JobError
Classe base para erros de tempo limite gerados por trabalhos.
Defina a mensagem de erro.
Escrevendo um novo backend
Se você tiver um dispositivo ou simulador quântico que gostaria de integrar ao Qiskit, precisará escrever um backend. Um provedor é uma coleção de backends e fornecerá ao Qiskit um método para obter objetos BackendV2 objetos. O objeto BackendV2 fornece informações que descrevem um backend e sua operação para o objeto transpiler para que os circuitos possam ser compilados em algo que seja otimizado e possa ser executado no backend. Ele também fornece o método run() que pode executar os objetos QuantumCircuit objetos. Isso permite que os usuários e outras APIs do Qiskit obtenham resultados da execução de circuitos em dispositivos de forma padrão, independentemente de como o backend é implementado. Em um nível elevado, as etapas básicas para escrever um provedor são:
Implemente uma classe
Providerque lida com o acesso ao(s) backend(s).Implementar uma
BackendV2subclasse e seu métodorun()método.
- Adicione quaisquer portas personalizadas para a base do backend à instância da sessão
EquivalenceLibraryinstância.Implemente uma subclasse
JobV1que lida com a interação com um trabalho em execução.
Para obter um exemplo simples de um provedor, consulte o qiskit-aqt-provider
Provedor
Uma classe de provedor tem uma única finalidade: obter objetos de back-end que permitem a execução de circuitos em um dispositivo ou simulador. A expectativa é que todas as credenciais e/ou autenticação necessárias sejam tratadas na inicialização de um objeto de provedor. O objeto do provedor fornecerá uma lista de back-ends e métodos para filtrar e adquirir back-ends (usando as credenciais fornecidas, se necessário). Um exemplo de classe de provedor tem a seguinte aparência:
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)Certifique-se de que todas as informações necessárias para a autenticação (se for o caso) estejam presentes na classe e que o método do backend corresponda à interface exigida. O resto depende de cada provedor específico quanto à forma de implementação.
Back-end
As classes de back-end são o núcleo do provedor. Essas classes são o que fornecem a interface entre o Qiskit e o hardware ou simulador que executará os circuitos. Isso inclui o fornecimento das informações necessárias para descrever um backend para o compilador, de modo que ele possa incorporar e otimizar qualquer circuito para o backend. Há quatro itens obrigatórios em cada objeto de backend: uma propriedade target para definir o modelo do backend para o compilador, uma propriedade max_circuits para definir um limite no número de circuitos que o backend pode executar em um único trabalho em lote (se não houver limite, None poderá ser usado), um método run() para aceitar envios de trabalho e um método _default_options para definir as opções configuráveis pelo usuário e seus valores padrão. Por exemplo, um exemplo mínimo de trabalho seria algo como:
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 do transpiler do backend
A peça-chave do objeto Backend é como ele se descreve para o compilador. Isso é tratado com a classe Target que define um modelo de backend para o transpilador. Um objeto de backend precisará retornar um objeto Target do atributo target que a função transpile() usará como seu modelo de um destino de backend para compilação.
Portões personalizados
-
Se o seu backend não usar portas na biblioteca de circuitos Qiskit (
qiskit.circuit.library), você poderá integrar o suporte a isso em seu provedor. O método básico para fazer isso é primeiro definir uma subclasseGatepara cada porta personalizada no conjunto de base. Por exemplo: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 = qcO importante é garantir que, para qualquer porta personalizada na base do seu backend, o atributo name da porta personalizada (o primeiro parâmetro em
super().__init__()na definição__init__acima) não entre em conflito com o nome de nenhuma outra porta. O atributo name é usado para identificar a porta no conjunto de base para o transpilador. Se houver um conflito, o transpilador não saberá qual porta usar. -
Adicione o portão personalizado ao destino de seu backend. Isso pode ser feito com o método
Target.add_instruction()método. Você precisará adicionar uma instância deSYGatee seus parâmetros ao destino para que o transpilador saiba que ele existe. Por exemplo, supondo que isso faça parte de sua implementaçãoBackendV2para seu 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)As chaves em
sy_propsdefinem os qubits em que o backendSYGatepode ser usado, e os valores definem as propriedades deSYGatenesse qubit. Para portas multiqubit, as chaves de tuplas contêm todas as combinações de qubit nas quais a porta funciona (a ordem é significativa, ou seja(0, 1)é diferente de(1, 0)). -
Depois de definir as portas personalizadas a serem usadas para o conjunto de base do backend, você precisará adicionar regras de equivalência à biblioteca de equivalência padrão para que a função
transpile()função etranspilerpossam converter um circuito arbitrário usando o conjunto de base personalizado. Isso pode ser feito definindo circuitos equivalentes, em termos da porta personalizada, para portas padrão. Normalmente, se você puder converter de umCXGate(se sua base não incluir uma porta padrão de 2 qubits) e algumas portas de rotação de um único qubit comumente usadas, como aHGateeUGateisso deve ser suficiente para o transpilador traduzir qualquer circuito para as portas de base personalizadas. Porém, quanto mais regras de equivalência forem definidas de portas padrão para sua base, mais eficiente será a tradução de um circuito arbitrário para a base de destino (embora nem sempre, e há uma margem de retorno cada vez menor).Por exemplo, se você adicionasse algumas regras para o
SYGatepersonalizado acima, poderíamos definir oU2GateeHGate: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)Você desejará que isso seja executado na importação para que, assim que o pacote do provedor for importado, ele seja executado. Isso garantirá que sempre que a
BasisTranslatorseja executada com as portas personalizadas, as regras de equivalência serão definidas.Também vale a pena observar que, dependendo da base que você estiver usando, algumas passagens de otimização no transpilador, como
Optimize1qGatesDecompositionpodem não ser capazes de operar com sua base personalizada. Para o nosso exemploSYGate, oOptimize1qGatesDecompositionnão conseguirá simplificar as execuções de portas de um único qubit na base SY. Isso ocorre porque a classeOneQubitEulerDecomposernão sabe como trabalhar na base SY. Para resolver isso, a classeSYGateprecisaria ser adicionada ao Qiskit e aOneQubitEulerDecomposeratualizada para suportar a decomposição paraSYGate. A longo prazo, essa é provavelmente a melhor direção para os portões de base personalizados e a contribuição com as definições e o suporte no transpilador garantirá que ele continue a ser bem suportado pelo Qiskit no futuro.
Passagens personalizadas do transpiler
O transpilador oferece suporte à capacidade dos backends de fornecer implementações personalizadas do estágio do transpilador para facilitar otimizações específicas de hardware e transformações de circuito. Atualmente, há dois estágios compatíveis, get_translation_stage_plugin() e get_scheduling_stage_plugin() , que permitem que um backend especifique nomes de plug-ins de cadeia de caracteres a serem usados como estágios padrão de tradução e agendamento, respectivamente. Esses pontos de conexão em uma classe BackendV2 podem ser usados se o seu backend tiver requisitos de compilação que não são atendidos pela interface atual do backend/Target atual. Considere também a possibilidade de enviar um problema do Github descrevendo seu caso de uso, pois há interesse em aprimorar essas interfaces para poder descrever mais arquiteturas de hardware com mais profundidade.
Para aproveitar esses pontos de conexão, você só precisa adicionar os métodos à sua implementação BackendV2 e fazer com que eles retornem uma string com o nome do plugin. Por exemplo:
class Mybackend(BackendV2):
def get_scheduling_stage_plugin(self):
return "SpecialDD"
def get_translation_stage_plugin(self):
return "BasisTranslatorWithCustom1qOptimization"Esse trecho de uma implementação de backend agora fará com que a função transpile() use o plug-in SpecialDD para a etapa de agendamento e o plug-in BasisTranslatorWithCustom1qOptimization para a etapa de tradução por padrão quando o destino estiver definido como Mybackend. Observe que os usuários podem substituir essas opções selecionando explicitamente um nome de plug-in diferente. Para que essa interface funcione, os plug-ins de estágio do transpilador devem ser implementados para o nome do plug-in retornado. Você pode consultar a qiskit.transpiler.preset_passmanagers.plugin para obter detalhes sobre como implementar plug-ins. A expectativa típica é que, se o seu backend exigir passes personalizados como parte de um estágio de compilação, o pacote do provedor incluirá os plug-ins do estágio do transpilador que usam esses passes. No entanto, isso não é obrigatório e qualquer método válido (de um método interno ou plug-in externo) pode ser usado.
Dessa forma, se essas duas etapas de compilação forem necessárias para a execução ou para fornecer uma saída eficiente em Mybackend , o transpilador poderá executar essas etapas personalizadas sem nenhuma entrada manual do usuário.
Variáveis em tempo real
O transpilador tratará automaticamente as variáveis clássicas digitadas em tempo real (consulte qiskit.circuit.classical) e tratará a instrução Store como uma "diretriz" incorporada, semelhante à instrução Barrier. Não é necessário nenhum tratamento especial dos backends para permitir isso.
Se o seu backend não puder lidar com variáveis e armazenamento clássicos, recomendamos que você comente isso na sua documentação e insira uma verificação no seu método run() (consulte Backend.run Method ) para rejeitar com entusiasmo os circuitos que os contêm. Você pode examinar QuantumCircuit.num_vars a presença de variáveis no nível superior. Se você aceitar operações de fluxo de controle, talvez seja necessário pesquisar recursivamente as variáveis internas blocks de cada um para variáveis locais de escopo com QuantumCircuit.num_declared_vars.
Por exemplo, uma função para verificar a presença de locais de armazenamento manual ou armazenamentos manuais na memória:
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 FalseLimites angulares em Gates
Se o seu backend tiver restrições sobre os valores de parâmetros permitidos para qualquer porta no destino, você poderá modelar isso com limites de ângulo no parâmetro Target. Quando você adiciona a instrução com a palavra-chave add_instruction() você pode usar o argumento da palavra-chave angle_bounds que recebe uma lista de tuplas para o limite superior e inferior do parâmetro de uma porta.
Por exemplo, este trecho de código em vez do exemplo que adiciona o PhaseGate no exemplo acima:
lam = Parameter("λ")
p_props = {(qubit,): None for qubit in range(5)}
self._target.add_instruction(PhaseGate(lam), p_props, angle_bounds=[(0, math.pi)])definirá os limites de PhaseGate para ficarem entre 0 e (inclusive). Isso modela a restrição de ângulo no Target nos valores de ângulo para o parâmetro lam em PhaseGate. A passagem WrapAngles transpiler é usada para transformar qualquer PhaseGate fora dos limites de ângulo especificados. Você precisará escrever uma função que receba os valores de ângulo para o portão e retorne um DAGCircuit. Por exemplo:
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)Essa função transformará os portões fora dos limites em um que respeite os limites de ângulo no alvo e as outras restrições do alvo (embora não muito bem).
Backend.run Método
De importância fundamental é o run() método utilizado para enviar circuitos a um dispositivo ou simulador. O método run lida com o envio dos circuitos para o backend para serem executados e retorna um Job objeto. Dependendo do tipo de backend, isso normalmente envolve serializar o objeto do circuito no formato da API usado por um backend. Como as necessidades de serialização do backend podem variar (e, no caso de simuladores locais, a serialização pode nem ser necessária), espera-se que o método run do backend lide com essa conversão.
Um exemplo de método de execução seria algo como:
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)Opções de backend
Muitas vezes, existem várias opções de backend que determinam como um circuito é executado. Um exemplo típico disso é algo como o número de, shots que representa quantas vezes o circuito deve ser executado. As opções disponíveis para um backend são definidas por meio de um Options objeto. Este objeto é inicialmente criado pelo _default_options método de uma classe Backend. As opções padrão retornam um objeto Options inicializado com todos os valores padrão para todas as opções suportadas por um backend. Por exemplo, se o backend suportar apenas shots o _default_options método, ele ficaria assim:
@classmethod
def _default_options(cls):
return Options(shots=1024)Você também pode definir validadores em um objeto Options para fornecer limites e validação sobre os valores fornecidos pelo usuário com base no que é aceitável para o seu backend. Por exemplo, se a opção "shots" definida acima puder ser definida com qualquer valor entre 1 e 4096, você poderá definir o validador no objeto de opções do seu backend:
self.options.set_validator("shots", (1, 4096))você pode consultar a set_validator() para obter uma lista completa das opções de validação.
Tarefa
A saída do método run é um objeto JobV1 objeto. Espera-se que cada provedor implemente uma subclasse de trabalho personalizada que defina o comportamento do provedor. Há dois tipos de trabalhos, dependendo do método de execução do backend: sincronizado ou assíncrono. Por padrão, os trabalhos são considerados assíncronos e espera-se que ele represente um identificador para a execução assíncrona dos circuitos enviados com Backend.run(). Um objeto de trabalho assíncrono oferece aos usuários a capacidade de consultar o status da execução, cancelar um trabalho em execução e bloquear até que a execução seja concluída. O result é o principal método voltado para o usuário, que bloqueará até que a execução seja concluída e, em seguida, retornará um objeto Result com os resultados do trabalho.
Para alguns back-ends (principalmente simuladores locais), a execução de circuitos é uma operação síncrona e não há necessidade de retornar um identificador para um trabalho em execução em outro lugar. Para trabalhos de sincronização, espera-se que o método run no backend bloqueie até que um objeto Result seja gerado e o trabalho de sincronização retorne com esse objeto Result interno.
Um exemplo de classe de trabalho para um backend baseado em API assíncrona seria algo parecido com:
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 NotImplementedErrore para um trabalho de sincronização:
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.DONEPrimitivas
Embora não faça parte diretamente da interface do provedor, o módulo qiskit.primitives é fortemente acoplado aos provedores. Especificamente, as interfaces primitivas, como BaseSampler e BaseEstimator, foram projetadas para permitir que as implementações do provedor forneçam implementações personalizadas otimizadas para os back-ends do provedor. Isso pode incluir personalizações como transformações de circuitos, pré e pós-processamento adicionais, lotes, armazenamento em cache, atenuação de erros etc. O conceito do módulo qiskit.primitives é permitir isso explicitamente, pois os objetos primitivos são abstrações de nível superior para produzir resultados processados de nível superior (como distribuições de probabilidade e valores de expectativa) que abstraem a mecânica de obter o melhor resultado de forma eficiente, para se concentrar em aplicativos de nível superior que usam esses resultados.
Por exemplo, se seus back-ends fossem adequados para aproveitar a mitigação de medição do mthree para melhorar a qualidade dos resultados, você poderia implementar uma implementação Sampler específica do provedor que aproveitasse a classe M3Mitigation internamente para executar os circuitos e retornar quase-probabilidades diretamente do mthree no resultado. Isso permitiria que os algoritmos obtivessem os melhores resultados com a atenuação aplicada diretamente de seus back-ends. Você pode consultar a documentação em qiskit.primitives sobre como escrever implementações personalizadas. Além disso, as implementações incorporadas: Sampler, Estimator, BackendSampler e BackendEstimator podem servir como referências/modelos sobre como implementá-las também.