Créer des backends personnalisés et transpiler vers ceux-ci
# 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'
Versions des packages
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.5.2
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. Cette page 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
Avant de commencer, il est utile de comprendre l'usage et le rôle des objets Provider, BackendV2 et 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 : récupérer les objets backend que tu fournis. 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
BackendV2est composé de quatre parties principales :-
Une propriété
Target, qui contient une description des contraintes du backend et fournit un modèle de celui-ci pour le transpileur -
Une propriété
max_circuitsqui 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_optionspour définir les options configurables par l'utilisateur et leurs valeurs par défaut
-
Créer un BackendV2 personnalisé
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). Comme mentionné ci-dessus, ces objets contiennent plusieurs attributs, notamment un Target. Le Target contient des informations qui spécifient les attributs du backend — comme la Coupling Map, la liste des Instructions, 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 ou le 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 au sein de chaque puce et de CXGates 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.
La visualisation d'une coupling map nécessite l'installation de la bibliothèque graphviz.
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

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 et ). 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: 204
ECR gates: 8
SX gates: 373
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
)

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 code torique. 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))

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