Saltar al contenido principal

Crear y transpilar con 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>Versiones de paquetes</b></summary>

El código de esta página fue desarrollado con los siguientes requisitos.
Se recomienda usar estas versiones o más recientes.

qiskit[all]~=2.3.0

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

Una de las características más potentes de Qiskit es la capacidad de admitir configuraciones de dispositivos únicas. Qiskit está diseñado para ser agnóstico al proveedor del hardware cuántico que uses, y los proveedores pueden configurar el objeto `BackendV2` con las propiedades únicas de su dispositivo. Este tema muestra cómo configurar tu propio backend y transpilar circuitos cuánticos con él.

Puedes crear objetos `BackendV2` únicos con diferentes geometrías o puertas base y transpilar tus circuitos teniendo en cuenta esas configuraciones. El ejemplo a continuación cubre un backend con una red de qubits disjunta, cuyas puertas base difieren en los bordes respecto al interior.
## Entender las interfaces Provider, BackendV2 y Target \{#understand-the-provider-backendv2-and-target-interfaces}

Antes de comenzar, es útil comprender el uso y el propósito de los objetos [`Provider`](../api/qiskit/providers), [`BackendV2`](../api/qiskit/qiskit.providers.BackendV2) y [`Target`](../api/qiskit/qiskit.transpiler.Target).

- Si tienes un dispositivo cuántico o simulador que quieres integrar en el SDK de Qiskit, necesitas escribir tu propia clase `Provider`. Esta clase tiene un único propósito: obtener los objetos backend que tú proporcionas. Aquí se gestionan las credenciales y las tareas de autenticación necesarias. Una vez instanciado, el objeto proveedor proporcionará una lista de backends, así como la capacidad de adquirir e instanciar backends.

- A continuación, las clases backend proporcionan la interfaz entre el SDK de Qiskit y el hardware o simulador que ejecutará los circuitos. Incluyen toda la información necesaria para describir un backend al transpilador, de modo que pueda optimizar cualquier circuito según sus restricciones. Un `BackendV2` está compuesto de cuatro partes principales:
- Una propiedad [`Target`](../api/qiskit/qiskit.transpiler.Target), que contiene una descripción de las restricciones del backend y proporciona un modelo del backend para el transpilador
- Una propiedad `max_circuits` que define un límite en el número de circuitos que un backend puede ejecutar en un solo trabajo
- Un método `run()` que acepta envíos de trabajos
- Un conjunto de `_default_options` para definir las opciones configurables por el usuario y sus valores predeterminados
## Crear un BackendV2 personalizado \{#create-a-custom-backendv2}

El objeto `BackendV2` es una clase abstracta usada para todos los objetos backend creados por un proveedor (ya sea dentro de `qiskit.providers` o en otra biblioteca como [`qiskit_ibm_runtime.IBMBackend`](../api/qiskit-ibm-runtime/ibm-backend)). Como se menciónó anteriormente, estos objetos contienen varios atributos, incluyendo un [`Target`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.Target). El `Target` contiene información que especifica los atributos del backend —como el [`Coupling Map`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.CouplingMap), la lista de [`Instructions`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.Instruction) y otros— al transpilador. Además del `Target`, también se pueden definir detalles a nivel de pulso, como el [`DriveChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.DriveChannel) o el [`ControlChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.ControlChannel).

El siguiente ejemplo demuestra esta personalización creando un backend simulado de múltiples chips, donde cada chip posee una conectividad heavy-hex. El ejemplo especifica que el conjunto de puertas de dos qubits del backend sean [`CZGates`](../api/qiskit/qiskit.circuit.library.CZGate) dentro de cada chip y [`CXGates`](../api/qiskit/qiskit.circuit.library.ECRGate) entre chips. Primero, crea tu propio `BackendV2` y personaliza su `Target` con puertas de uno y dos qubits según las restricciones descritas anteriormente.

<Admonition type="tip" title="Biblioteca graphviz">
Para trazar un mapa de acoplamiento se requiere tener instalada la biblioteca [`graphviz`](https://graphviz.org/).
</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

Puedes ver el grafo de conectividad de esta nueva clase con el método plot_gate_map() del módulo qiskit.visualization. Este método, junto con plot_coupling_map() y plot_circuit_layout(), son herramientas útiles para visualizar la disposición de los qubits de un backend, así como la forma en que un circuito se distribuye entre los qubits de un backend. Este ejemplo crea un backend que contiene tres pequeños chips heavy-hex. Se especifica un conjunto de coordenadas para organizar los qubits, así como un conjunto de colores personalizados para las diferentes puertas de dos 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

Salida de la celda de código anterior

Cada qubit está etiquetado y las flechas de colores representan las puertas de dos qubits. Las flechas grises corresponden a las puertas CZ y las flechas negras son las puertas CX entre chips (que conectan los qubits 6216 \rightarrow 21 y 254025 \rightarrow 40). La dirección de la flecha indica la dirección predeterminada en que se ejecutan estas puertas; especifica qué qubits son control/objetivo de forma predeterminada para cada canal de dos qubits.

Transpilar con backends personalizados

Ahora que se ha definido un backend personalizado con su propio Target único, es sencillo transpilar circuitos cuánticos con este backend, ya que todas las restricciones relevantes (puertas base, conectividad de qubits, etc.) necesarias para los pasos del transpilador están contenidas dentro de este atributo. El siguiente ejemplo construye un circuito que crea un estado GHZ grande y lo transpila con el backend construido anteriormente.

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

El circuito transpilado ahora contiene una mezcla de puertas CZ y ECR, que especificamos como puertas base en el Target del backend. También hay bastantes más puertas que al inicio, debido a la necesidad de insertar instrucciones SWAP al elegir un diseño. A continuación, se usa la herramienta de visualización plot_circuit_layout() para indicar qué qubits y canales de dos qubits se usaron en este circuito.

from qiskit.visualization import plot_circuit_layout

plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)

Salida de la celda de código anterior

Crear backends únicos

El paquete rustworkx contiene una amplia biblioteca de grafos diferentes y permite la creación de grafos personalizados. El código visualmente interesante que aparece a continuación crea un backend inspirado en el código tórico. Luego puedes visualizar el backend usando las funciones de la sección 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))

Salida de la celda 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