Criar e transpilar para backends personalizados
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
# Don't use SVGs for this file because the images are too large,
# and the SVGs are much larger than their PNGs equivalents.
%config InlineBackend.figure_format='png'
Versões dos pacotes
O código desta página foi desenvolvido usando os seguintes requisitos. Recomendamos usar estas versões ou mais recentes.
qiskit[all]~=2.5.2
Um dos recursos mais poderosos do Qiskit é a capacidade de suportar configurações de dispositivos únicas. O Qiskit foi desenvolvido para ser agnóstico em relação ao provedor do hardware quântico que você usa, e os provedores podem configurar o objeto BackendV2 com suas próprias propriedades de dispositivo exclusivas. Este tópico demonstra como configurar seu próprio backend e transpilar circuitos quânticos para ele.
Você pode criar objetos BackendV2 únicos com diferentes geometrias ou gates de base e transpilar seus circuitos levando essas configurações em conta. O exemplo abaixo abrange um backend com uma rede de qubits disjunta, cujos gates de base são diferentes nas arestas em relação ao interior da malha.
Entender as interfaces Provider, BackendV2 e Target
Antes de começar, é útil entender o uso e a finalidade dos objetos Provider, BackendV2 e Target.
-
Se você tem um dispositivo quântico ou simulador que deseja integrar ao SDK do Qiskit, precisará escrever sua própria classe
Provider. Essa classe serve a um único propósito: obter objetos de backend que você fornecer. É aqui que qualquer tarefa de credencial e/ou autenticação necessária é tratada. Uma vez instanciado, o objeto provider fornecerá uma lista de backends, bem como a capacidade de adquirir/instanciar backends. -
Em seguida, as classes de backend fornecem a interface entre o SDK do Qiskit e o hardware ou simulador que executará os circuitos. Elas incluem todas as informações necessárias para descrever um backend ao transpilador, de modo que ele possa otimizar qualquer circuito de acordo com suas restrições. Um
BackendV2é composto de quatro partes principais:-
Uma propriedade
Target, que contém uma descrição das restrições do backend e fornece um modelo do backend para o transpilador -
Uma propriedade
max_circuitsque define um limite para o número de circuitos que um backend pode executar em um único job -
Um método
run()que aceita submissões de jobs -
Um conjunto de
_default_optionspara definir as opções configuráveis pelo usuário e seus valores padrão
-
Criar um BackendV2 personalizado
O objeto BackendV2 é uma classe abstrata usada para todos os objetos de backend criados por um provider (seja dentro de qiskit.providers ou de outra biblioteca como qiskit_ibm_runtime.IBMBackend). Conforme mencionado acima, esses objetos contêm vários atributos, incluindo um Target. O Target contém informações que especificam os atributos do backend — como o Coupling Map, lista de Instructions e outros — para o transpilador. Além do Target, também é possível definir detalhes no nível de pulso, como o DriveChannel ou ControlChannel.
O exemplo a seguir demonstra essa personalização criando um backend multi-chip simulado, onde cada chip possui uma conectividade heavy-hex. O exemplo especifica o conjunto de gates de dois qubits do backend como CZGates dentro de cada chip e CXGates entre chips. Primeiro, crie seu próprio BackendV2 e personalize seu Target com gates de um e dois qubits de acordo com as restrições descritas anteriormente.
Plotar um mapa de acoplamento requer que a biblioteca graphviz esteja instalada.
import numpy as np
import rustworkx as rx
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate, ECRGate
from qiskit.circuit import Measure, Delay, Parameter, Reset
from qiskit import QuantumCircuit, transpile
from qiskit.visualization import plot_gate_map
class FakeLOCCBackend(BackendV2):
"""Fake multi chip backend."""
def __init__(self, distance=3, number_of_chips=3):
"""Instantiate a new fake multi chip backend.
Args:
distance (int): The heavy hex code distance to use for each chips'
coupling map. This number **must** be odd. The distance relates
to the number of qubits by:
:math:`n = \\frac{5d^2 - 2d - 1}{2}` where :math:`n` is the
number of qubits and :math:`d` is the ``distance``
number_of_chips (int): The number of chips to have in the multichip backend
each chip will be a heavy hex graph of ``distance`` code distance.
"""
super().__init__(name="Fake LOCC backend")
# Create a heavy-hex graph using the
# rustworkx library, then instantiate a new target
self._graph = rx.generators.directed_heavy_hex_graph(
distance, bidirectional=False
)
num_qubits = len(self._graph) * number_of_chips
self._target = Target(
"Fake multi-chip backend", num_qubits=num_qubits
)
# Generate instruction properties for single qubit gates and a measurement, delay,
# and reset operation to every qubit in the backend.
rng = np.random.default_rng(seed=12345678942)
rz_props = {}
x_props = {}
sx_props = {}
measure_props = {}
delay_props = {}
# Add 1q gates. Globally use virtual rz, x, sx, and measure
for i in range(num_qubits):
qarg = (i,)
rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
x_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
sx_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
measure_props[qarg] = InstructionProperties(
error=rng.uniform(1e-3, 1e-1),
duration=rng.uniform(1e-8, 9e-7),
)
delay_props[qarg] = None
self._target.add_instruction(XGate(), x_props)
self._target.add_instruction(SXGate(), sx_props)
self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
self._target.add_instruction(Measure(), measure_props)
self._target.add_instruction(Reset(), measure_props)
self._target.add_instruction(Delay(Parameter("t")), delay_props)
# Add chip local 2q gate which is CZ
cz_props = {}
for i in range(number_of_chips):
for root_edge in self._graph.edge_list():
offset = i * len(self._graph)
edge = (root_edge[0] + offset, root_edge[1] + offset)
cz_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(CZGate(), cz_props)
cx_props = {}
# Add interchip 2q gates which are ecr (effectively CX)
# First determine which nodes to connect
node_indices = self._graph.node_indices()
edge_list = self._graph.edge_list()
inter_chip_nodes = {}
for node in node_indices:
count = 0
for edge in edge_list:
if node == edge[0]:
count += 1
if count == 1:
inter_chip_nodes[node] = count
# Create inter-chip ecr props
cx_props = {}
inter_chip_edges = list(inter_chip_nodes.keys())
for i in range(1, number_of_chips):
offset = i * len(self._graph)
edge = (
inter_chip_edges[1] + (len(self._graph) * (i - 1)),
inter_chip_edges[0] + offset,
)
cx_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(ECRGate(), cx_props)
@property
def target(self):
return self._target
@property
def max_circuits(self):
return None
@property
def graph(self):
return self._graph
@classmethod
def _default_options(cls):
return Options(shots=1024)
def run(self, circuit, **kwargs):
raise NotImplementedError(
"This backend does not contain a run method"
)
Visualizar backends
Você pode visualizar o grafo de conectividade dessa nova classe com o método plot_gate_map() do módulo qiskit.visualization. Esse método, junto com plot_coupling_map() e plot_circuit_layout(), são ferramentas úteis para visualizar o arranjo de qubits de um backend, e também a forma como um circuito é distribuído pelos qubits de um backend. Este exemplo cria um backend contendo três pequenos chips heavy-hex. Ele especifica um conjunto de coordenadas para organizar os qubits, bem como um conjunto de cores personalizadas para os diferentes gates de dois qubits.
backend = FakeLOCCBackend(3, 3)
target = backend.target
coupling_map_backend = target.build_coupling_map()
coordinates = [
(3, 1),
(3, -1),
(2, -2),
(1, 1),
(0, 0),
(-1, -1),
(-2, 2),
(-3, 1),
(-3, -1),
(2, 1),
(1, -1),
(-1, 1),
(-2, -1),
(3, 0),
(2, -1),
(0, 1),
(0, -1),
(-2, 1),
(-3, 0),
]
single_qubit_coordinates = []
total_qubit_coordinates = []
for coordinate in coordinates:
total_qubit_coordinates.append(coordinate)
for coordinate in coordinates:
total_qubit_coordinates.append(
(-1 * coordinate[0] + 1, coordinate[1] + 4)
)
for coordinate in coordinates:
total_qubit_coordinates.append((coordinate[0], coordinate[1] + 8))
line_colors = ["#adaaab" for edge in coupling_map_backend.get_edges()]
ecr_edges = []
# Get tuples for the edges which have an ecr instruction attached
for instruction in target.instructions:
if instruction[0].name == "ecr":
ecr_edges.append(instruction[1])
for i, edge in enumerate(coupling_map_backend.get_edges()):
if edge in ecr_edges:
line_colors[i] = "#000000"
print(backend.name)
plot_gate_map(
backend,
plot_directed=True,
qubit_coordinates=total_qubit_coordinates,
line_color=line_colors,
)
Fake LOCC backend

Cada qubit está rotulado, e setas coloridas representam os gates de dois qubits. As setas cinzas são os gates CZ e as setas pretas são os gates CX inter-chip (que conectam os qubits e ). A direção da seta indica a direção padrão em que esses gates são executados; elas especificam quais qubits são controle/alvo por padrão para cada canal de dois qubits.
Transpilar para backends personalizados
Agora que um backend personalizado com seu próprio Target único foi definido, é simples transpilar circuitos quânticos para esse backend, já que todas as restrições relevantes (gates de base, conectividade de qubits e assim por diante) necessárias para as passagens do transpilador estão contidas nesse atributo. O próximo exemplo constrói um circuito que cria um grande estado GHZ e o transpila para o backend construído acima.
from qiskit.transpiler import generate_preset_pass_manager
num_qubits = 50
ghz = QuantumCircuit(num_qubits)
ghz.h(range(num_qubits))
ghz.cx(0, range(1, num_qubits))
op_counts = ghz.count_ops()
print("Pre-Transpilation: ")
print(f"CX gates: {op_counts['cx']}")
print(f"H gates: {op_counts['h']}")
print("\n", 30 * "#", "\n")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
transpiled_ghz = pm.run(ghz)
op_counts = transpiled_ghz.count_ops()
print("Post-Transpilation: ")
print(f"CZ gates: {op_counts['cz']}")
print(f"ECR gates: {op_counts['ecr']}")
print(f"SX gates: {op_counts['sx']}")
print(f"RZ gates: {op_counts['rz']}")
Pre-Transpilation:
CX gates: 49
H gates: 50
##############################
Post-Transpilation:
CZ gates: 204
ECR gates: 8
SX gates: 373
RZ gates: 216
O circuito transpilado agora contém uma mistura de gates CZ e ECR, que especificamos como gates de base no Target do backend. Há também bem mais gates do que no circuito original, devido à necessidade de inserir instruções SWAP após a escolha de um layout. Abaixo, a ferramenta de visualização plot_circuit_layout() é usada para especificar quais qubits e canais de dois qubits foram utilizados neste circuito.
from qiskit.visualization import plot_circuit_layout
plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)

Criar backends únicos
O pacote rustworkx contém uma ampla biblioteca de grafos diferentes e permite a criação de grafos personalizados. O código visualmente interessante abaixo cria um backend inspirado no código tórico. Você pode então visualizar o backend usando as funções da seção Visualizar backends.
class FakeTorusBackend(BackendV2):
"""Fake multi chip backend."""
def __init__(self):
"""Instantiate a new backend that is inspired by a toric code"""
super().__init__(name="Fake LOCC backend")
graph = rx.generators.directed_grid_graph(20, 20)
for column in range(20):
graph.add_edge(column, 19 * 20 + column, None)
for row in range(20):
graph.add_edge(row * 20, row * 20 + 19, None)
num_qubits = len(graph)
rng = np.random.default_rng(seed=12345678942)
rz_props = {}
x_props = {}
sx_props = {}
measure_props = {}
delay_props = {}
self._target = Target("Fake Kookaburra", num_qubits=num_qubits)
# Add 1q gates. Globally use virtual rz, x, sx, and measure
for i in range(num_qubits):
qarg = (i,)
rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
x_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
sx_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
measure_props[qarg] = InstructionProperties(
error=rng.uniform(1e-3, 1e-1),
duration=rng.uniform(1e-8, 9e-7),
)
delay_props[qarg] = None
self._target.add_instruction(XGate(), x_props)
self._target.add_instruction(SXGate(), sx_props)
self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
self._target.add_instruction(Measure(), measure_props)
self._target.add_instruction(Reset(), measure_props)
self._target.add_instruction(Delay(Parameter("t")), delay_props)
cz_props = {}
for edge in graph.edge_list():
cz_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(CZGate(), cz_props)
@property
def target(self):
return self._target
@property
def max_circuits(self):
return None
@classmethod
def _default_options(cls):
return Options(shots=1024)
def run(self, circuit, **kwargs):
raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")
backend = FakeTorusBackend()
# We set `figsize` to a smaller size to make the documentation website faster
# to load. Normally, you do not need to set the argument.
plot_gate_map(backend, figsize=(4, 4))

num_qubits = int(backend.num_qubits / 2)
full_device_bv = QuantumCircuit(num_qubits, num_qubits - 1)
full_device_bv.x(num_qubits - 1)
full_device_bv.h(range(num_qubits))
full_device_bv.cx(range(num_qubits - 1), num_qubits - 1)
full_device_bv.h(range(num_qubits))
full_device_bv.measure(range(num_qubits - 1), range(num_qubits - 1))
tqc = transpile(full_device_bv, backend, optimization_level=3)
op_counts = tqc.count_ops()
print(f"CZ gates: {op_counts['cz']}")
print(f"X gates: {op_counts['x']}")
print(f"SX gates: {op_counts['sx']}")
print(f"RZ gates: {op_counts['rz']}")
CZ gates: 548
X gates: 139
SX gates: 871
RZ gates: 797