Pular para o conteúdo principal

Criar e transpilar para backends personalizados

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit rustworkx
# 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'
```json

{/* cspell:ignore multichip interchip Lasciate ogne speranza voi ch'intrate */}
{/*
DO NOT EDIT THIS CELL!!!
This cell's content is generated automatically by a script. Anything you add
here will be removed next time the notebook is run. To add new content, create
a new cell before or after this one.
*/}

<details>
<summary><b>Versões dos pacotes</b></summary>

O código desta página foi desenvolvido usando os seguintes requisitos.
Recomendamos usar estas versões ou mais recentes.

qiskit[all]~=2.3.0

</details>
{/* cspell:ignore LOCC */}

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 \{#understand-the-provider-backendv2-and-target-interfaces}

Antes de começar, é útil entender o uso e a finalidade dos objetos [`Provider`](../api/qiskit/providers), [`BackendV2`](../api/qiskit/qiskit.providers.BackendV2) e [`Target`](../api/qiskit/qiskit.transpiler.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`](../api/qiskit/qiskit.transpiler.Target), que contém uma descrição das restrições do backend e fornece um modelo do backend para o transpilador
- Uma propriedade `max_circuits` que define um limite no 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_options` para definir as opções configuráveis pelo usuário e seus valores padrão
## Criar um BackendV2 personalizado \{#create-a-custom-backendv2}

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`](../api/qiskit-ibm-runtime/ibm-backend)). Conforme mencionado acima, esses objetos contêm vários atributos, incluindo um [`Target`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.Target). O `Target` contém informações que especificam os atributos do backend — como o [`Coupling Map`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.CouplingMap), lista de [`Instructions`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.Instruction) e outros — para o transpilador. Além do `Target`, também é possível definir detalhes no nível de pulso, como o [`DriveChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.DriveChannel) ou [`ControlChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.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`](../api/qiskit/qiskit.circuit.library.CZGate) dentro de cada chip e [`CXGates`](../api/qiskit/qiskit.circuit.library.ECRGate) 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.

<Admonition type="tip" title="biblioteca graphviz">
Plotar um mapa de acoplamento requer que a biblioteca [`graphviz`](https://graphviz.org/) esteja instalada.
</Admonition>

```python
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, bem como 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

Saída da célula de código anterior

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 6216 \rightarrow 21 e 254025 \rightarrow 40). 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: 151
ECR gates: 6
SX gates: 295
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 muito mais gates do que você começou, 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
)

Saída da célula de código anterior

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))

Saída da célula de código anterior

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: 867
X gates: 18
SX gates: 1630
RZ gates: 1174