Skip to main content
IBM Quantum Platform

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 BackendV2 est constitué de quatre parties principales :

    • A Target qui contient une description des contraintes du backend et fournit un modèle du backend pour le transpondeur
    • Une propriété max_circuits qui 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_options pour définir les options configurables par l'utilisateur et leurs valeurs par défaut

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.

bibliothèque graphviz

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
Output of the previous code cell

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 6216 \rightarrow 21 et 254025 \rightarrow 40 ). 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:

Output of the previous code cell

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:

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: 580
X gates: 129
SX gates: 936
RZ gates: 840
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.