Skip to main content
IBM Quantum Platform

Erstellen und transpilieren Sie gegen benutzerdefinierte Backends

  • Der Code auf dieser Seite wurde unter Berücksichtigung der folgenden Anforderungen entwickelt. Wir empfehlen die Verwendung dieser oder neuerer Versionen.

    qiskit[all]~=2.5.2
    

Eine der leistungsstärksten Funktionen von Qiskit ist die Fähigkeit, einzigartige Gerätekonfigurationen zu unterstützen. Qiskit ist unabhängig vom Anbieter der von Ihnen verwendeten Quantenhardware, und Anbieter können das Objekt BackendV2 mit ihren eigenen Geräteeigenschaften konfigurieren. In diesem Thema wird gezeigt, wie Sie Ihr eigenes Backend konfigurieren und Quantenschaltungen gegen dieses umsetzen können.

Sie können einzigartige BackendV2 Objekte mit verschiedenen Geometrien oder Basisgattern erstellen und Ihre Schaltungen mit diesen Konfigurationen im Hinterkopf umsetzen. Das nachstehende Beispiel bezieht sich auf ein Backend mit einem disjunkten Qubit-Gitter, dessen Basistore sich entlang der Kanten von denen des Bulk unterscheiden.


Verstehen Sie die Schnittstellen „Provider“, „ BackendV2 “ und „Target“

Bevor Sie beginnen, ist es hilfreich, die Verwendung und den Zweck der Provider, BackendV2, und Target objekte zu verstehen.

  • Wenn Sie ein Quantengerät oder einen Quantensimulator haben, den Sie in das Qiskit SDK integrieren möchten, müssen Sie Ihre eigene Provider Klasse schreiben. Diese Klasse dient einem einzigen Zweck: dem Abrufen von Backend-Objekten, die Sie bereitstellen. Hier werden alle erforderlichen Anmeldeinformationen und/oder Authentifizierungsaufgaben bearbeitet. Nach der Instanziierung bietet das Anbieterobjekt eine Liste von Backends sowie die Möglichkeit, Backends zu erwerben/einzuführen.

  • Die Backend-Klassen bilden die Schnittstelle zwischen dem Qiskit SDK und der Hardware oder dem Simulator, der die Schaltungen ausführt. Sie enthalten alle notwendigen Informationen, um ein Backend für den Transpiler zu beschreiben, so dass dieser jede Schaltung entsprechend ihrer Einschränkungen optimieren kann. Eine BackendV2 besteht aus vier Hauptbestandteilen:

    • A Target eigenschaft, die eine Beschreibung der Beschränkungen des Backends enthält und ein Modell des Backends für den Transpiler bereitstellt
    • Eine Eigenschaft von max_circuits , die die Anzahl der Schaltungen begrenzt, die ein Backend in einem einzigen Auftrag ausführen kann
    • Eine run() -Methode, die Auftragseingaben akzeptiert
    • Ein Satz von _default_options , um die vom Benutzer konfigurierbaren Optionen und ihre Standardwerte zu definieren

Erstellen Sie eine benutzerdefinierte Datei „ BackendV2 “

Das BackendV2 Objekt ist eine abstrakte Klasse, die für alle Backend-Objekte verwendet wird, die von einem Anbieter (entweder innerhalb von qiskit.providers oder einer anderen Bibliothek wie qiskit_ibm_runtime.IBMBackend). Wie bereits erwähnt, enthalten diese Objekte mehrere Attribute, darunter ein Target. Die Target enthält Informationen, die die Attribute des Backends spezifizieren - wie z. B. die Coupling Map, Liste der Instructions, und andere - an den Transpiler weitergeben. Zusätzlich zur Target kann man auch Details auf Impulsebene definieren, wie z. B. die DriveChannel oder ControlChannel.

Das folgende Beispiel veranschaulicht diese Anpassung, indem es ein simuliertes Multi-Chip-Backend erstellt, bei dem jeder Chip über eine Heavy-Hex-Konnektivität verfügt. Das Beispiel legt fest, dass das Zwei-Qubit-Gate des Backends CZGates innerhalb jedes Chips und CXGates zwischen den Chips. Erstellen Sie zunächst Ihr eigenes BackendV2 und passen Sie dessen Target mit Ein- und Zwei-Qubit-Gattern gemäß den zuvor beschriebenen Einschränkungen an.

graphviz-Bibliothek

Für die Darstellung einer Kopplungskarte muss die graphviz bibliothek 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

Sie können den Konnektivitätsgraphen dieser neuen Klasse mit der plot_gate_map() methode aus dem Modul qiskit.visualization anzeigen lassen. Diese Methode, zusammen mit plot_coupling_map() und plot_circuit_layout()sind hilfreiche Werkzeuge, um die Qubit-Anordnung eines Backends zu visualisieren und um zu sehen, wie ein Schaltkreis über die Qubits eines Backends verteilt ist. In diesem Beispiel wird ein Backend mit drei kleinen Heavy-Hex-Chips erstellt. Sie gibt eine Reihe von Koordinaten für die Anordnung der Qubits sowie eine Reihe von benutzerdefinierten Farben für die verschiedenen Zwei-Qubit-Tore 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,
)

Output:

Fake LOCC backend
Output of the previous code cell

Jedes Qubit ist beschriftet, und farbige Pfeile stellen die Zwei-Qubit-Tore dar. Graue Pfeile sind die CZ-Gatter und die schwarzen Pfeile sind die Inter-Chip-CX-Gatter (diese verbinden die Qubits 6→216 \rightarrow 21 und 25→4025 \rightarrow 40 ). Die Richtung des Pfeils gibt die Standardrichtung an, in der diese Gatter ausgeführt werden; sie legen fest, welche Qubits standardmäßig für jeden Zwei-Qubit-Kanal Kontroll-/Zielgrößen sind.


Transpile gegen benutzerdefinierte Backends

Nachdem nun ein benutzerdefiniertes Backend mit seinem eigenen eindeutigen Target definiert wurde, ist es ein Leichtes, Quantenschaltungen gegen dieses Backend zu transpilieren, da alle relevanten Einschränkungen (Basisgatter, Qubit-Konnektivität usw.), die für Transpiler-Durchläufe benötigt werden, in diesem Attribut enthalten sind. Im nächsten Beispiel wird eine Schaltung aufgebaut, die einen großen GHZ-Zustand erzeugt und diesen mit dem oben konstruierten Backend abgleicht.

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: 204
ECR gates: 8
SX gates: 373
RZ gates: 216

Der transpilierte Schaltkreis enthält nun eine Mischung aus CZ und ECR Gattern, die wir als Basisgatter im Backend-Modul spezifiziert Target haben. Es gibt auch deutlich mehr Gates als zu Beginn, da nach der Auswahl eines Layouts SWAP-Befehle eingefügt werden müssen. Im Folgenden wird das plot_circuit_layout() Visualisierungstool verwendet, um anzugeben, welche Qubits und Zwei-Qubit-Kanäle in dieser Schaltung verwendet wurden.

from qiskit.visualization import plot_circuit_layout

plot_circuit_layout(
    transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)

Output:

Output of the previous code cell

Erstellen Sie einzigartige Backends

Das rustworkx-Paket enthält eine große Bibliothek mit verschiedenen Graphen und ermöglicht die Erstellung von benutzerdefinierten Graphen. Der folgende visuell interessante Code erstellt ein Backend, das vom torischen Code inspiriert ist. Anschließend können Sie das Backend mit den Funktionen aus dem Abschnitt Visualisierung von Backends visualisieren.

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:

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']}")

Output:

CZ gates: 548
X gates: 139
SX gates: 871
RZ gates: 797
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.