Aller au contenu principal

Créer des backends personnalisés et transpiler vers ceux-ci

# 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>Versions des packages</b></summary>

Le code de cette page a été développé avec les dépendances suivantes.
Nous recommandons d'utiliser ces versions ou des versions plus récentes.

qiskit[all]~=2.3.0

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

L'une des fonctionnalités les plus puissantes de Qiskit est sa capacité à prendre en charge des configurations de dispositifs uniques. Qiskit est conçu pour être agnostique vis-à-vis du fournisseur de matériel quantique que tu utilises, et les fournisseurs peuvent configurer l'objet `BackendV2` selon les propriétés spécifiques à leur dispositif. Ce sujet montre comment configurer ton propre backend et transpiler des circuits quantiques vers celui-ci.

Tu peux créer des objets `BackendV2` uniques avec différentes géométries ou portes de base, puis transpiler tes circuits en tenant compte de ces configurations. L'exemple ci-dessous couvre un backend avec un réseau de qubits disjoint, dont les portes de base diffèrent selon qu'on se trouve sur les bords ou dans le cœur du réseau.

## Comprendre les interfaces Provider, BackendV2 et Target \{#understand-the-provider-backendv2-and-target-interfaces}

Avant de commencer, il est utile de comprendre l'usage et le rôle des objets [`Provider`](../api/qiskit/providers), [`BackendV2`](../api/qiskit/qiskit.providers.BackendV2) et [`Target`](../api/qiskit/qiskit.transpiler.Target).

- Si tu disposes d'un dispositif quantique ou d'un simulateur que tu souhaites intégrer au SDK Qiskit, tu dois écrire ta propre classe `Provider`. Cette classe a un seul objectif : fournir les objets backend que tu exposes. C'est là que sont gérées toutes les tâches d'authentification et/ou de gestion des identifiants. Une fois instancié, l'objet provider fournira une liste de backends ainsi que la capacité de les acquérir/instancier.

- Ensuite, les classes backend servent d'interface entre le SDK Qiskit et le matériel ou simulateur qui exécutera les circuits. Elles contiennent toutes les informations nécessaires pour décrire un backend au transpileur, afin qu'il puisse optimiser tout circuit en respectant ses contraintes. Un `BackendV2` est composé de quatre parties principales :
- Une propriété [`Target`](../api/qiskit/qiskit.transpiler.Target), qui contient une description des contraintes du backend et fournit un modèle de celui-ci pour le transpileur
- Une propriété `max_circuits` qui définit la limite du nombre de circuits qu'un backend peut exécuter dans un seul job
- Une méthode `run()` qui accepte les soumissions de jobs
- Un ensemble de `_default_options` pour définir les options configurables par l'utilisateur et leurs valeurs par défaut

## Créer un BackendV2 personnalisé \{#create-a-custom-backendv2}

L'objet `BackendV2` est une classe abstraite utilisée pour tous les objets backend créés par un fournisseur (que ce soit au sein de `qiskit.providers` ou d'une autre bibliothèque comme [`qiskit_ibm_runtime.IBMBackend`](../api/qiskit-ibm-runtime/ibm-backend)). Comme mentionné ci-dessus, ces objets contiennent plusieurs attributs, notamment un [`Target`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.Target). Le `Target` contient des informations qui spécifient les attributs du backend — comme la [`Coupling Map`](https://docs.quantum.ibm.com/api/qiskit/qiskit.transpiler.CouplingMap), la liste des [`Instructions`](https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.Instruction), et d'autres — à destination du transpileur. En plus du `Target`, il est également possible de définir des détails au niveau des impulsions, comme le [`DriveChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.DriveChannel) ou le [`ControlChannel`](https://docs.quantum.ibm.com/api/qiskit/1.4/qiskit.pulse.channels.ControlChannel).

L'exemple suivant illustre cette personnalisation en créant un backend multi-puce simulé, où chaque puce possède une connectivité heavy-hex. L'exemple spécifie que l'ensemble de portes à deux qubits est composé de [`CZGates`](../api/qiskit/qiskit.circuit.library.CZGate) au sein de chaque puce et de [`CXGates`](../api/qiskit/qiskit.circuit.library.ECRGate) entre les puces. Pour commencer, crée ton propre `BackendV2` et personnalise son `Target` avec des portes à un et deux qubits conformément aux contraintes décrites précédemment.

<Admonition type="tip" title="Bibliothèque graphviz">
La visualisation d'une coupling map nécessite l'installation de la bibliothèque [`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"
)

Visualiser les backends

Tu peux afficher le graphe de connectivité de cette nouvelle classe avec la méthode plot_gate_map() du module qiskit.visualization. Cette méthode, ainsi que plot_coupling_map() et plot_circuit_layout(), sont des outils utiles pour visualiser l'arrangement des qubits d'un backend, ainsi que la façon dont un circuit est disposé sur ces qubits. Cet exemple crée un backend contenant trois petites puces heavy-hex. Il spécifie un ensemble de coordonnées pour disposer les qubits, ainsi qu'un ensemble de couleurs personnalisées pour les différentes portes à deux 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

Output of the previous code cell

Chaque qubit est étiqueté, et des flèches colorées représentent les portes à deux qubits. Les flèches grises correspondent aux portes CZ et les flèches noires aux portes CX inter-puces (qui connectent les qubits 6216 \rightarrow 21 et 254025 \rightarrow 40). La direction de la flèche indique le sens par défaut dans lequel ces portes sont exécutées ; elle précise quels qubits sont contrôle/cible par défaut pour chaque canal à deux qubits.

Transpiler vers des backends personnalisés

Maintenant qu'un backend personnalisé avec son propre Target unique a été défini, il est simple de transpiler des circuits quantiques vers ce backend, puisque toutes les contraintes pertinentes (portes de base, connectivité des qubits, etc.) nécessaires aux passes du transpileur sont contenues dans cet attribut. L'exemple suivant construit un circuit qui crée un grand état GHZ et le transpile vers le backend construit ci-dessus.

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

Le circuit transpilé contient désormais un mélange de portes CZ et ECR, que nous avons spécifiées comme portes de base dans le Target du backend. Le nombre de portes est également bien plus élevé qu'au départ, en raison de la nécessité d'insérer des instructions SWAP après le choix d'un placement. Ci-dessous, l'outil de visualisation plot_circuit_layout() est utilisé pour indiquer quels qubits et canaux à deux qubits ont été utilisés dans ce circuit.

from qiskit.visualization import plot_circuit_layout

plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)

Output of the previous code cell

Créer des backends uniques

Le package rustworkx contient une grande bibliothèque de graphes variés et permet la création de graphes personnalisés. Le code visuellement intéressant ci-dessous crée un backend inspiré du toric code. Tu peux ensuite visualiser ce backend à l'aide des fonctions de la section Visualiser les 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 of the previous code cell

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