Eigene Backends erstellen und Circuits gegen sie transpilieren
# 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'
Paketversionen
Der Code auf dieser Seite wurde mit den folgenden Paketversionen entwickelt. Wir empfehlen die Verwendung dieser oder neuerer Versionen.
qiskit[all]~=2.5.2
Eine der besonders leistungsfähigen Funktionen von Qiskit ist die Unterstützung einzigartiger Gerätekonfigurationen. Qiskit ist so konzipiert, dass es unabhängig vom Anbieter der genutzten Quantenhardware funktioniert, und Anbieter können das BackendV2-Objekt entsprechend ihrer eigenen Geräteeigenschaften konfigurieren. Diese Anleitung zeigt, wie du dein eigenes Backend konfigurierst und Quantum Circuits dafür transpilierst.
Du kannst einzigartige BackendV2-Objekte mit verschiedenen Geometrien oder Basis-Gates erstellen und deine Circuits mit diesen Konfigurationen transpilieren. Das folgende Beispiel behandelt ein Backend mit einem disjunkten Qubit-Gitter, dessen Basis-Gates an den Kanten anders sind als im Inneren.
Provider-, BackendV2- und Target-Schnittstellen verstehen
Vor dem Einstieg ist es hilfreich, die Verwendung und den Zweck der Objekte Provider, BackendV2 und Target zu verstehen.
-
Wenn du ein Quantengerät oder einen Simulator hast, den du in das Qiskit SDK integrieren möchtest, musst du deine eigene
Provider-Klasse schreiben. Diese Klasse hat einen einzigen Zweck: die Backend-Objekte abzurufen, die du bereitstellst. Hier werden alle erforderlichen Anmeldeinformationen und/oder Authentifizierungsaufgaben erledigt. Nach der Instanziierung stellt das Provider-Objekt eine Liste von Backends sowie die Möglichkeit bereit, Backends zu erhalten/zu instanziieren. -
Die Backend-Klassen wiederum stellen die Schnittstelle zwischen dem Qiskit SDK und der Hardware oder dem Simulator bereit, der die Circuits ausführt. Sie enthalten alle notwendigen Informationen, um ein Backend dem Transpiler zu beschreiben, sodass dieser jeden Circuit entsprechend seinen Einschränkungen optimieren kann. Ein
BackendV2besteht aus vier Hauptteilen:-
Eine
Target-Eigenschaft, die eine Beschreibung der Einschränkungen des Backends enthält und ein Modell des Backends für den Transpiler bereitstellt -
Eine
max_circuits-Eigenschaft, die eine Obergrenze für die Anzahl der Circuits definiert, die ein Backend in einem einzigen Job ausführen kann -
Eine
run()-Methode, die Job-Einreichungen entgegennimmt -
Ein Satz von
_default_options, um die vom Benutzer konfigurierbaren Optionen und ihre Standardwerte zu definieren
-
Ein eigenes BackendV2 erstellen
Das BackendV2-Objekt ist eine abstrakte Klasse, die für alle Backend-Objekte verwendet wird, die von einem Provider erstellt werden (entweder innerhalb von qiskit.providers oder einer anderen Bibliothek wie qiskit_ibm_runtime.IBMBackend). Wie oben erwähnt, enthalten diese Objekte mehrere Attribute, darunter ein Target. Das Target enthält Informationen, die die Attribute des Backends – wie die Coupling Map, die Liste der Instructions und andere – gegenüber dem Transpiler spezifizieren. Zusätzlich zum Target können auch Pulse-Level-Details wie der DriveChannel oder ControlChannel definiert werden.
Das folgende Beispiel veranschaulicht diese Anpassung, indem ein simuliertes Multi-Chip-Backend erstellt wird, bei dem jeder Chip eine Heavy-Hex-Konnektivität besitzt. Im Beispiel wird das Zwei-Qubit-Gate-Set des Backends auf CZGates innerhalb jedes Chips und CXGates zwischen den Chips festgelegt. Erstelle zunächst dein eigenes BackendV2 und passe dessen Target mit Ein- und Zwei-Qubit-Gates gemäß den zuvor beschriebenen Einschränkungen an.
Für die Darstellung einer Coupling Map muss die Bibliothek graphviz installiert sein.
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"
)
Backends visualisieren
Du kannst den Konnektivitätsgraphen dieser neuen Klasse mit der Methode plot_gate_map() aus dem Modul qiskit.visualization anzeigen. Diese Methode, zusammen mit plot_coupling_map() und plot_circuit_layout(), sind hilfreiche Werkzeuge zur Visualisierung der Qubit-Anordnung eines Backends sowie der Anordnung eines Circuits über die Qubits eines Backends. Dieses Beispiel erstellt ein Backend mit drei kleinen Heavy-Hex-Chips. Es gibt einen Satz von Koordinaten für die Anordnung der Qubits sowie benutzerdefinierte Farben für die unterschiedlichen Zwei-Qubit-Gates an.
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

Jedes Qubit ist beschriftet, und farbige Pfeile stellen die Zwei-Qubit-Gates dar. Graue Pfeile sind die CZ-Gates und die schwarzen Pfeile sind die Chip-übergreifenden CX-Gates (diese verbinden Qubits und ). Die Richtung des Pfeils gibt die Standardrichtung an, in der diese Gates ausgeführt werden; sie legen fest, welche Qubits standardmäßig Kontroll-/Ziel-Qubits für jeden Zwei-Qubit-Kanal sind.
Gegen eigene Backends transpilieren
Nachdem ein eigenes Backend mit einem individuellen Target definiert wurde, ist es unkompliziert, Quantum Circuits gegen dieses Backend zu transpilieren, da alle relevanten Einschränkungen (Basis-Gates, Qubit-Konnektivität usw.), die für Transpiler-Passes benötigt werden, in diesem Attribut enthalten sind. Das nächste Beispiel erstellt einen Circuit, der einen großen GHZ-Zustand erzeugt, und transpiliert ihn gegen das oben erstellte Backend.
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
Der transpilierte Circuit enthält nun eine Mischung aus CZ- und ECR-Gates, die wir als Basis-Gates im Target des Backends festgelegt haben. Es gibt auch deutlich mehr Gates als zu Beginn, da nach der Wahl eines Layouts SWAP-Anweisungen eingefügt werden müssen. Im Folgenden wird das Visualisierungswerkzeug plot_circuit_layout() verwendet, um anzuzeigen, welche Qubits und Zwei-Qubit-Kanäle in diesem Circuit genutzt wurden.
from qiskit.visualization import plot_circuit_layout
plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)

Einzigartige Backends erstellen
Das Paket rustworkx enthält eine umfangreiche Bibliothek verschiedener Graphen und ermöglicht die Erstellung benutzerdefinierter Graphen. Der optisch interessante Code unten erstellt ein Backend, das vom torischen Code inspiriert ist. Du kannst das Backend anschließend mit den Funktionen aus dem Abschnitt Backends visualisieren darstellen.
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