Skip to main content
IBM Quantum Platform

Crea e transpilare su backend personalizzati

  • Il codice di questa pagina è stato sviluppato in base ai seguenti requisiti. Si consiglia di utilizzare queste versioni o versioni più recenti.

    qiskit[all]~=2.5.1
    

Una delle caratteristiche più potenti di Qiskit è la capacità di supportare configurazioni uniche dei dispositivi. Qiskit è costruito in modo da essere agnostico rispetto al fornitore dell'hardware quantistico utilizzato e i fornitori possono configurare l'oggetto BackendV2 in base alle proprietà del proprio dispositivo. Questo argomento mostra come configurare il proprio backend e transpilare i circuiti quantistici con esso.

È possibile creare oggetti BackendV2 unici con diverse geometrie o porte di base e transpilare i circuiti tenendo conto di queste configurazioni. L'esempio seguente riguarda un backend con un reticolo di qubit disgiunto, le cui porte di base sono diverse lungo i bordi rispetto all'interno del bulk.


Comprendere le interfacce Provider, BackendV2 e Target

Prima di iniziare, è utile comprendere l'uso e lo scopo dei caratteri Provider, BackendV2, e Target e degli oggetti.

  • Se si dispone di un dispositivo o di un simulatore quantistico che si desidera integrare nell'SDK Qiskit, è necessario scrivere la propria classe Provider . Questa classe ha un unico scopo: ottenere gli oggetti backend forniti dall'utente. È qui che vengono gestite tutte le attività di credenziali e/o autenticazione richieste. Una volta istanziato, l'oggetto provider fornirà un elenco di backend e la possibilità di acquisire/instanziare backend.

  • Successivamente, le classi backend forniscono l'interfaccia tra l'SDK Qiskit e l'hardware o il simulatore che eseguirà i circuiti. Includono tutte le informazioni necessarie per descrivere un backend al transpiler, in modo che possa ottimizzare qualsiasi circuito in base ai suoi vincoli. Il sito BackendV2 è costituito da quattro parti principali:

    • A Target che contiene una descrizione dei vincoli del backend e fornisce un modello del backend per il transpiler
    • Una proprietà di max_circuits che definisce un limite al numero di circuiti che un backend può eseguire in un singolo lavoro
    • Un metodo run() che accetta l'invio di un lavoro
    • Un insieme di _default_options per definire le opzioni configurabili dall'utente e i loro valori predefiniti

Crea un BackendV2 personalizzato

L'oggetto BackendV2 è una classe astratta utilizzata per tutti gli oggetti di backend creati da un provider (all'interno di qiskit.providers o di un'altra libreria, come ad esempio qiskit_ibm_runtime.IBMBackend). Come già accennato, questi oggetti contengono diversi attributi, tra cui un elemento Target. L'indirizzo Target contiene informazioni che specificano gli attributi del backend, come ad esempio gli attributi Coupling Map, elenco di Instructions, e altri - al transpiler. Oltre a Target, si possono definire anche dettagli a livello di impulso, come il valore di DriveChannel o ControlChannel.

L'esempio seguente dimostra questa personalizzazione creando un backend multi-chip simulato, in cui ogni chip possiede una connettività heavy-hex. L'esempio specifica che il set di gate a due qubit del backend deve essere CZGates all'interno di ciascun chip e CXGates tra i chip. Per prima cosa, creare il proprio BackendV2 e personalizzare il suo Target con porte a uno o due qubit secondo i vincoli precedentemente descritti.

libreria graphviz

Per tracciare una mappa di accoppiamento è necessario che la libreria graphviz sia installata.

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"
        )

Visualizza i backend

È possibile visualizzare il grafico di connettività di questa nuova classe con il metodo plot_gate_map() del modulo qiskit.visualization . Questo metodo, insieme a plot_coupling_map() e plot_circuit_layout()sono strumenti utili per visualizzare la disposizione dei qubit di un backend e la disposizione di un circuito tra i qubit di un backend. Questo esempio crea un backend contenente tre piccoli chip heavy-hex. Specifica un insieme di coordinate per la disposizione dei qubit e un insieme di colori personalizzati per le diverse porte a due qubit.

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

Ogni qubit è etichettato e le frecce colorate rappresentano le porte a due qubit. Le frecce grigie sono i gate CZ e le frecce nere sono i gate CX inter-chip (collegano i qubit 6216 \rightarrow 21 e 254025 \rightarrow 40 ). La direzione della freccia indica la direzione predefinita di esecuzione di questi gate; essi specificano quali qubit sono controllati/target per default per ogni canale a due qubit.


Transpile rispetto ai backend personalizzati

Ora che è stato definito un backend personalizzato con il suo unico Target è stato definito, è semplice transpilare i circuiti quantistici contro questo backend, poiché tutti i vincoli rilevanti (porte di base, connettività dei qubit e così via) necessari per i passaggi del transpiler sono contenuti in questo attributo. L'esempio successivo costruisce un circuito che crea un grande stato GHZ e lo trasporta contro il backend costruito sopra.

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

Il circuito transpilato ora contiene una combinazione di CZ porte ECR e, che abbiamo specificato come porte di base nel backend Target. Ci sono anche molti più gate rispetto a quelli iniziali, poiché è necessario inserire istruzioni SWAP dopo aver scelto un layout. Di seguito, lo strumento plot_circuit_layout() di visualizzazione viene utilizzato per specificare quali qubit e canali a due qubit sono stati utilizzati in questo circuito.

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

Crea backend unici

Il pacchetto rustworkx contiene un'ampia libreria di grafici diversi e consente la creazione di grafici personalizzati. Il codice visivamente interessante che segue crea un backend ispirato al codice torico. È quindi possibile visualizzare il backend utilizzando le funzioni della sezione Visualizza backend.

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
Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.