Créer et transcompiler par rapport à des backends personnalisés
Le code de cette page a été développé en tenant compte des exigences suivantes. Nous recommandons d'utiliser ces versions ou des versions plus récentes.
qiskit[all]~=2.5.1
L'une des caractéristiques les plus puissantes de Qiskit est sa capacité à prendre en charge des configurations d'appareils uniques. Qiskit est conçu pour être indépendant du fournisseur du matériel quantique que vous utilisez, et les fournisseurs peuvent configurer l'objet BackendV2 en fonction de leurs propres propriétés. Cette rubrique montre comment configurer son propre backend et transposer les circuits quantiques.
Vous pouvez créer des objets BackendV2 uniques avec différentes géométries ou portes de base et transposer vos circuits en tenant compte de ces configurations. L'exemple ci-dessous couvre un backend avec un réseau de qubits disjoints, dont les portes de base sont différentes le long des bords par rapport à l'intérieur de la masse.
Comprendre les interfaces Provider, BackendV2 et Target
Avant de commencer, il est utile de comprendre l'utilisation et l'objectif de l'élément Provider, BackendV2, et Target et des objets.
-
Si vous avez un dispositif quantique ou un simulateur que vous voulez intégrer dans le SDK Qiskit, vous devez écrire votre propre classe
Provider. Cette classe n'a qu'un seul but : récupérer les objets backend que vous lui fournissez. C'est là que sont traitées toutes les tâches d'identification et/ou d'authentification requises. Une fois instancié, l'objet fournisseur fournira une liste de backends ainsi que la possibilité d'acquérir/instaurer des backends. -
Ensuite, les classes backend fournissent l'interface entre le SDK Qiskit et le matériel ou le simulateur qui exécutera les circuits. Ils comprennent toutes les informations nécessaires pour décrire un backend au transpileur afin qu'il puisse optimiser n'importe quel circuit en fonction de ses contraintes. Un site
BackendV2est constitué de quatre parties principales :- A
Targetqui contient une description des contraintes du backend et fournit un modèle du backend pour le transpondeur - Une propriété
max_circuitsqui définit une limite sur le nombre de circuits qu'un backend peut exécuter en un seul travail - Une méthode
run()qui accepte les soumissions de travaux - Un ensemble de
_default_optionspour définir les options configurables par l'utilisateur et leurs valeurs par défaut
- A
Créer un fichier personnalisé BackendV2
L'objet BackendV2 est une classe abstraite utilisée pour tous les objets backend créés par un fournisseur (soit dans qiskit.providers , soit dans une autre bibliothèque telle que qiskit_ibm_runtime.IBMBackend). Comme indiqué plus haut, ces objets contiennent plusieurs attributs, dont un attribut Target. Le site Target contient des informations qui précisent les attributs du backend - tels que la liste des attributs du backend Coupling Map, la liste des Instructionset d'autres - au transpileur. Outre le site Target, il est possible de définir des détails au niveau de l'impulsion, tels que les paramètres DriveChannel ou ControlChannel.
L'exemple suivant démontre cette personnalisation en créant un backend multi-puces simulé, où chaque puce possède une connectivité heavy-hex. L'exemple spécifie que l'ensemble de portes à deux qubits du backend doit être CZGates dans chaque puce et CXGates entre les puces. Tout d'abord, créez votre propre site BackendV2 et personnalisez son site Target avec des portes à un ou deux qubits selon les contraintes décrites précédemment.
Le tracé d'une carte de couplage nécessite l'installation de la bibliothèque graphviz soit installée.
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
Vous pouvez visualiser le graphe de connectivité de cette nouvelle classe avec la méthode plot_gate_map() du module qiskit.visualization . Cette méthode, ainsi que les méthodes plot_coupling_map() et plot_circuit_layout()sont des outils utiles pour visualiser la disposition des qubits d'un backend, ainsi que la façon dont un circuit est disposé sur les qubits d'un backend. 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,
)Output:
Fake LOCC backend
Chaque qubit est étiqueté et les flèches colorées représentent les portes à deux qubits. Les flèches grises représentent les portes CZ et les flèches noires les portes CX inter-puces (elles relient les qubits et ). La direction de la flèche indique la direction par défaut dans laquelle ces portes sont exécutées; elles spécifient quels qubits sont contrôlés/cibles par défaut pour chaque canal à deux qubits.
Transpiler contre des backends personnalisés
Maintenant qu'un backend personnalisé avec son propre et unique Target a été défini, il est facile de transposer les circuits quantiques dans 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 état GHZ important et le transpose sur 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']}")Output:
Pre-Transpilation:
CX gates: 49
H gates: 50
##############################
Post-Transpilation:
CZ gates: 207
ECR gates: 8
SX gates: 379
RZ gates: 211
Le circuit transpilé contient désormais un mélange de portes CZ ECR et, que nous avons spécifiées comme portes de base dans le du backend Target. Il y a également beaucoup plus de portes qu'au départ, car il faut insérer des instructions SWAP après avoir choisi une disposition. Ci-dessous, l'outil plot_circuit_layout() de visualisation est utilisé pour spécifier 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:
Créer des backends uniques
Le paquet rustworkx contient une large bibliothèque de graphiques différents et permet de créer des graphiques personnalisés. Le code visuellement intéressant ci-dessous crée un backend inspiré du code torique. Vous pouvez ensuite visualiser le 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:
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: 580
X gates: 129
SX gates: 936
RZ gates: 840