Crie e transpile em backends personalizados
O código desta página foi desenvolvido usando os seguintes requisitos. Recomendamos o uso dessas versões ou de versões mais recentes.
qiskit[all]~=2.5.0
Um dos recursos mais avançados do Qiskit é a capacidade de oferecer suporte a configurações exclusivas de dispositivos. O Qiskit foi desenvolvido para ser independente do fornecedor do hardware quântico que você usa, e os fornecedores podem configurar o objeto BackendV2 de acordo com as propriedades exclusivas de seus próprios dispositivos. Este tópico demonstra como configurar seu próprio backend e transpilar circuitos quânticos para ele.
Você pode criar objetos BackendV2 exclusivos com diferentes geometrias ou portas de base e transpilar seus circuitos com essas configurações em mente. O exemplo abaixo abrange um backend com uma rede de qubit disjunta, cujas portas de base são diferentes ao longo das bordas de dentro do volume.
Entenda as interfaces Provider, BackendV2 e Target
Antes de começar, é útil entender o uso e a finalidade do Provider, BackendV2, e Target objetos.
-
Se você tiver um dispositivo ou simulador quântico que deseja integrar ao Qiskit SDK, precisará escrever sua própria classe
Provider. Essa classe tem uma única finalidade: obter objetos de back-end fornecidos por você. É aqui que todas as tarefas de credenciais e/ou autenticação necessárias são tratadas. Uma vez instanciado, o objeto do provedor fornecerá uma lista de back-ends, bem como a capacidade de adquirir/instanciar back-ends. -
Em seguida, as classes de back-end fornecem a interface entre o Qiskit SDK e o hardware ou simulador que executará os circuitos. Eles incluem todas as informações necessárias para descrever um backend para o transpilador, de modo que ele possa otimizar qualquer circuito de acordo com suas restrições. O site
BackendV2é composto de quatro partes principais:- A
Targetque contém uma descrição das restrições do backend e fornece um modelo do backend para o transpilador - Uma propriedade do site
max_circuitsque define um limite para o número de circuitos que um backend pode executar em um único trabalho - Um método
run()que aceita envios de trabalhos - Um conjunto de
_default_optionspara definir as opções configuráveis pelo usuário e seus valores padrão
- A
Crie um arquivo personalizado BackendV2
O objeto BackendV2 é uma classe abstrata usada para todos os objetos de backend criados por um provedor (em qiskit.providers ou em outra biblioteca, como qiskit_ibm_runtime.IBMBackend). Como mencionado acima, esses objetos contêm vários atributos, incluindo um atributo Target. O site Target contém informações que especificam os atributos do backend, como a lista Coupling Map, lista de Instructionse outros - para o transpilador. Além do Target, também é possível definir detalhes em nível de pulso, como o DriveChannel ou ControlChannel.
O exemplo a seguir demonstra essa personalização criando um backend simulado de vários chips, em que cada chip possui uma conectividade heavy-hex. O exemplo especifica que o conjunto de portas de dois qubits do backend é CZGates dentro de cada chip e CXGates entre os chips. Primeiro, crie seu próprio site BackendV2 e personalize-o Target com portas de um e dois qubits de acordo com as restrições descritas anteriormente.
A plotagem de 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 gráfico de conectividade dessa nova classe com o método plot_gate_map() do módulo qiskit.visualization . Esse método, juntamente com plot_coupling_map() e plot_circuit_layout()são ferramentas úteis para visualizar a disposição dos qubits de um backend, bem como a forma como um circuito é disposto nos 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 portões 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,
)Output:
Fake LOCC backend
Cada qubit é rotulado e as setas coloridas representam as portas de dois qubits. As setas cinzas são as portas CZ e as setas pretas são as portas CX entre chips (elas conectam os qubits e ). A direção da seta indica a direção padrão em que essas portas são executadas; elas especificam quais qubits são controle/alvos por padrão para cada canal de dois qubits.
Transpilar contra backends personalizados
Agora que um backend personalizado com seu próprio e exclusivo Target foi definido, é fácil transpilar circuitos quânticos para esse backend, pois todas as restrições relevantes (portas de base, conectividade de qubit 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 estado GHZ grande 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']}")Output:
Pre-Transpilation:
CX gates: 49
H gates: 50
##############################
Post-Transpilation:
CZ gates: 204
ECR gates: 8
SX gates: 374
RZ gates: 215
O circuito transposto agora contém uma mistura de CZ portas ECR e, que especificamos como portas básicas no backend Target. Há também muito mais portas do que quando você começou, devido à necessidade de inserir instruções SWAP após escolher um layout. Abaixo, a ferramenta plot_circuit_layout() de visualização é usada para especificar quais qubits e canais de dois qubits foram usados neste circuito.
from qiskit.visualization import plot_circuit_layout
plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)Output:
Crie backends exclusivos
O pacote rustworkx contém uma grande biblioteca de gráficos diferentes e permite a criação de gráficos personalizados. O código visualmente interessante abaixo cria um backend inspirado no código tórico. Em seguida, é possível 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))Output:
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']}")Output:
CZ gates: 563
X gates: 6
SX gates: 1182
RZ gates: 1078