from typing import Optional
import networkx as nx
import numpy as np
from qibo import gates
from qibo.backends.numpy import NumpyBackend
from qibo.config import raise_error
from qibo.models.circuit import Circuit
from qibo.quantum_info.random_ensembles import random_statevector
from qibo.transpiler._exceptions import (
ConnectivityError,
DecompositionError,
PlacementError,
TranspilerPipelineError,
)
from qibo.transpiler.optimizer import Preprocessing
from qibo.transpiler.unroller import NativeGates
[docs]def assert_transpiling(
original_circuit: Circuit,
transpiled_circuit: Circuit,
connectivity: nx.Graph,
final_layout: dict,
native_gates: NativeGates = NativeGates.default(),
check_circuit_equivalence=True,
):
"""Check that all transpiler passes have been executed correctly.
Args:
original_circuit (:class:`qibo.models.circuit.Circuit`): Circuit before transpiling.
transpiled_circuit (:class:`qibo.models.circuit.Circuit`): Circuit after transpiling.
connectivity (:class:`networkx.Graph`): Hardware connectivity.
final_layout (dict): Final {logical: physical} qubit mapping.
native_gates (:class:`qibo.transpiler.unroller.NativeGates`, optional): Native gates supported by the hardware.
Defaults to :class:`qibo.transpiler.unroller.NativeGates.default()`.
check_circuit_equivalence (bool, optional): Check if the transpiled circuit is equivalent to the original one.
Defaults to ``True``.
"""
assert_connectivity(circuit=transpiled_circuit, connectivity=connectivity)
assert_decomposition(
circuit=transpiled_circuit,
native_gates=native_gates,
)
if original_circuit.nqubits != transpiled_circuit.nqubits:
qubit_matcher = Preprocessing(connectivity=connectivity)
original_circuit = qubit_matcher(circuit=original_circuit)
assert_placement(circuit=original_circuit, connectivity=connectivity)
assert_placement(circuit=transpiled_circuit, connectivity=connectivity)
if check_circuit_equivalence:
assert_circuit_equivalence(
original_circuit=original_circuit,
transpiled_circuit=transpiled_circuit,
final_layout=final_layout,
)
[docs]def assert_circuit_equivalence(
original_circuit: Circuit,
transpiled_circuit: Circuit,
final_layout: dict,
test_states: Optional[list] = None,
ntests: int = 3,
):
"""Checks that the transpiled circuit is equivalent to the original one.
Args:
original_circuit (:class:`qibo.models.circuit.Circuit`): Circuit before transpiling.
transpiled_circuit (:class:`qibo.models.circuit.Circuit`): Circuit after transpiling.
final_layout (dict): Final {logical: physical} qubit mapping.
test_states (list, optional): List of states to test the equivalence.
If ``None``, ``ntests`` random states will be tested. Defauts to ``None``.
ntests (int, optional): Number of random states to test the equivalence. Defaults to :math: `3`.
"""
backend = NumpyBackend()
if transpiled_circuit.nqubits != original_circuit.nqubits:
raise_error(
ValueError,
f"Transpiled circuit ({transpiled_circuit.nqubits}) and original circuit "
+ f"({original_circuit.nqubits}) do not have the same number of qubits.",
)
if test_states is None:
test_states = [
random_statevector(dims=2**original_circuit.nqubits, backend=backend)
for _ in range(ntests)
]
ordering = list(final_layout.values())
for i, state in enumerate(test_states):
target_state = backend.execute_circuit(
original_circuit, initial_state=state
).state()
final_state = backend.execute_circuit(
transpiled_circuit,
initial_state=state,
).state()
final_state = _transpose_qubits(final_state, ordering)
fidelity = np.abs(np.dot(np.conj(target_state), final_state))
try:
np.testing.assert_allclose(fidelity, 1.0)
except AssertionError:
raise_error(TranspilerPipelineError, "Circuit equivalence not satisfied.")
def _transpose_qubits(state: np.ndarray, qubits_ordering: np.ndarray):
"""Reorders qubits of a given state vector.
Args:
state (np.ndarray): State vector to reorder.
qubits_ordering (np.ndarray): Final qubit ordering.
"""
original_shape = state.shape
state = np.reshape(state, len(qubits_ordering) * (2,))
state = np.transpose(state, qubits_ordering)
return np.reshape(state, original_shape)
[docs]def assert_placement(circuit: Circuit, connectivity: nx.Graph):
"""Check if the layout of the circuit is consistent with the circuit and connectivity graph.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): Circuit to check.
connectivity (:class:`networkx.Graph`, optional): Hardware connectivity.
"""
if connectivity is None:
raise_error(
ValueError,
"Connectivity graph is not provided",
)
if circuit.nqubits != len(circuit.wire_names) or circuit.nqubits != len(
connectivity.nodes
):
raise_error(
PlacementError,
f"Number of qubits in the circuit ({circuit.nqubits}) "
+ f"does not match either the number of qubits in the layout ({len(circuit.wire_names)}) "
+ f"or the connectivity graph ({len(connectivity.nodes)}).",
)
if set(circuit.wire_names) != set(connectivity.nodes):
raise_error(
PlacementError,
"Some physical qubits in the layout may be missing or duplicated.",
)
[docs]def assert_connectivity(connectivity: nx.Graph, circuit: Circuit):
"""Assert if a circuit can be executed on Hardware.
No gates acting on more than two qubits.
All two-qubit operations can be performed on hardware.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): Circuit to check.
connectivity (:class:`networkx.Graph`): Hardware connectivity.
"""
layout = circuit.wire_names
for gate in circuit.queue:
if len(gate.qubits) > 2 and not isinstance(gate, gates.M):
raise_error(ConnectivityError, f"{gate.name} acts on more than two qubits.")
if len(gate.qubits) == 2:
physical_qubits = (layout[gate.qubits[0]], layout[gate.qubits[1]])
if physical_qubits not in connectivity.edges:
raise_error(
ConnectivityError,
f"The circuit does not respect the connectivity. {gate.name} acts on {physical_qubits} but only the following qubits are directly connected: {connectivity.edges}.",
)
[docs]def assert_decomposition(
circuit: Circuit,
native_gates: NativeGates,
):
"""Checks if a circuit has been correctly decomposed into native gates.
Args:
circuit (:class:`qibo.models.circuit.Circuit`): Circuit to check.
native_gates (:class:`qibo.transpiler.unroller.NativeGates`): Native gates supported by the hardware.
"""
for gate in circuit.queue:
if isinstance(gate, gates.M):
continue
if len(gate.qubits) <= 2:
try:
native_type_gate = NativeGates.from_gate(gate)
if not native_type_gate & native_gates:
raise_error(
DecompositionError,
f"{gate.name} is not a native gate.",
)
except ValueError:
raise_error(
DecompositionError,
f"{gate.name} is not a native gate.",
)
else:
raise_error(
DecompositionError, f"{gate.name} acts on more than two qubits."
)