Skip to content

Commit

Permalink
Merge pull request #608 from qiboteam/sabre
Browse files Browse the repository at this point in the history
Sabre
  • Loading branch information
Simone-Bordoni authored Oct 30, 2023
2 parents c6d5b14 + 5313048 commit 53737e1
Show file tree
Hide file tree
Showing 9 changed files with 811 additions and 154 deletions.
66 changes: 64 additions & 2 deletions src/qibolab/transpilers/blocks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional, Union

from qibo import Circuit
from qibo.gates import Gate

Expand All @@ -12,13 +14,13 @@ class Block:
Args:
qubits (tuple): qubits where the block is acting.
gates (list): list of gates that compose the block.
name (str): name of the block.
name (str or int): name of the block.
Properties:
entangled (bool): True if the block entangles the qubits (there is at least one two qubit gate).
"""

def __init__(self, qubits: tuple, gates: list, name: str = None):
def __init__(self, qubits: tuple, gates: list, name: Optional[Union[str, int]] = None):
self.qubits = qubits
self.gates = gates
self.name = name
Expand Down Expand Up @@ -68,6 +70,16 @@ def fuse(self, block: "Block", name: str = None):
raise BlockingError("In order to fuse two blocks their qubits must coincide.")
return Block(qubits=self.qubits, gates=self.gates + block.gates, name=name)

def on_qubits(self, new_qubits):
"""Return a new block acting on the new qubits.
Args:
new_qubits (tuple): new qubits where the block is acting.
"""
qubits_dict = dict(zip(self.qubits, new_qubits))
new_gates = [gate.on_qubits(qubits_dict) for gate in self.gates]
return Block(qubits=new_qubits, gates=new_gates, name=self.name)

# TODO: use real QM properties to check commutation
def commute(self, block: "Block"):
"""Check if a block commutes with the current one.
Expand All @@ -92,6 +104,56 @@ def kak_decompose(self): # pragma: no cover
raise NotImplementedError


class CircuitBlocks:
"""A CircuitBlocks contains a quantum circuit decomposed in two qubits blocks.
Args:
circuit (qibo.models.Circuit): circuit to be decomposed.
index_names (bool): assign names to the blocks
"""

def __init__(self, circuit: Circuit, index_names: bool = False):
self.block_list = block_decomposition(circuit)
self._index_names = index_names
if index_names:
for index, block in enumerate(self.block_list):
block.rename(index)
self.qubits = circuit.nqubits

def __call__(self):
return self.block_list

def search_by_index(self, index: int):
"""Find a block from its index, requires index_names == True"""
if not self._index_names:
raise BlockingError("You need to assign index names in order to use search_by_index.")
for block in self.block_list:
if block.name == index:
return block
raise BlockingError("No block found with index {}.".format(index))

def add_block(self, block: "Block"):
"""Add a two qubits block."""
if not set(block.qubits).issubset(range(self.qubits)):
raise BlockingError("The block can't be added to the circuit because it acts on different qubits")
self.block_list.append(block)

def circuit(self):
"""Return the quantum circuit."""
circuit = Circuit(self.qubits)
for block in self.block_list:
for gate in block.gates:
circuit.add(gate)
return circuit

def remove_block(self, block: "Block"):
"""Remove a block from the circuit blocks."""
try:
self.block_list.remove(block)
except ValueError:
raise BlockingError("The block you are trying to remove is not present in the circuit blocks.")


def block_decomposition(circuit: Circuit, fuse: bool = True):
"""Decompose a circuit into blocks of gates acting on two qubits.
Expand Down
87 changes: 75 additions & 12 deletions src/qibolab/transpilers/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import numpy as np
from qibo.backends import NumpyBackend
from qibo.models import Circuit
from qibo.quantum_info.random_ensembles import random_statevector

from qibolab.native import NativeType

Expand All @@ -17,33 +18,95 @@ class TranspilerPipelineError(Exception):
"""Raise when an error occurs in the transpiler pipeline"""


# TODO: rearrange qubits based on the final qubit map (or it will not work for routed circuit)
def assert_cirucuit_equivalence(original_circuit: Circuit, transpiled_circuit: Circuit):
def assert_circuit_equivalence(
original_circuit: Circuit,
transpiled_circuit: Circuit,
final_map: dict,
initial_map: dict = None,
test_states: list = None,
ntests: int = 3,
):
"""Checks that the transpiled circuit agrees with the original using simulation.
Args:
original_circuit (qibo.models.Circuit): Original circuit.
transpiled_circuit (qibo.models.Circuit): Transpiled circuit.
final_map (dict): logical-physical qubit mapping after routing.
initial_map (dict): logical_physical qubit mapping before routing, if None trivial initial map is used.
test_states (list): states on which the test is performed, if None 'ntests' random states will be tested.
ntests (int): number of random states tested.
"""
backend = NumpyBackend()
target_state = backend.execute_circuit(original_circuit).state()
final_state = backend.execute_circuit(transpiled_circuit).state()
fidelity = np.abs(np.dot(np.conj(target_state), final_state))
np.testing.assert_allclose(fidelity, 1.0)
ordering = np.argsort(np.array(list(final_map.values())))
if transpiled_circuit.nqubits != original_circuit.nqubits:
raise ValueError("Transpiled and original circuit 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)]
if initial_map is not None:
reordered_test_states = []
initial_map = np.array(list(initial_map.values()))
reordered_test_states = [transpose_qubits(initial_state, initial_map) for initial_state in test_states]
else:
reordered_test_states = test_states

for i in range(len(test_states)):
target_state = backend.execute_circuit(original_circuit, initial_state=test_states[i]).state()
final_state = backend.execute_circuit(transpiled_circuit, initial_state=reordered_test_states[i]).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 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): final state of the circuit.
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)


def assert_transpiling(
circuit: Circuit,
original_circuit: Circuit,
transpiled_circuit: Circuit,
connectivity: nx.Graph,
initial_layout: dict,
final_layout: dict,
native_gates: NativeType = NativeType.CZ,
check_circuit_equivalence=True,
):
"""Check that all transpiler passes have been executed correctly"""
assert_connectivity(circuit=circuit, connectivity=connectivity)
assert_decomposition(circuit=circuit, two_qubit_natives=native_gates)
assert_placement(circuit=circuit, layout=initial_layout)
assert_placement(circuit=circuit, layout=final_layout)
"""Check that all transpiler passes have been executed correctly.
Args:
original_circuit (qibo.models.Circuit): circuit before transpiling.
transpiled_circuit (qibo.models.Circuit): circuit after transpiling.
connectivity (nx.Graph): chip qubits connectivity.
initial_layout (dict): initial physical-logical qubit mapping.
final_layout (dict): final physical-logical qubit mapping.
native_gates: (NativeType): native gates supported by the hardware.
"""
assert_connectivity(circuit=transpiled_circuit, connectivity=connectivity)
assert_decomposition(circuit=transpiled_circuit, two_qubit_natives=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, layout=initial_layout)
assert_placement(circuit=transpiled_circuit, layout=final_layout)
if check_circuit_equivalence:
assert_circuit_equivalence(
original_circuit=original_circuit,
transpiled_circuit=transpiled_circuit,
initial_map=initial_layout,
final_map=final_layout,
)


class Passes:
Expand Down
15 changes: 7 additions & 8 deletions src/qibolab/transpilers/placer.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,8 @@ def __call__(self, circuit: Circuit):
self.connectivity.number_of_edges() == circuit_subgraph.number_of_edges()
or i == len(gates_qubits_pairs) - 1
):
keys = list(result.mapping.keys())
keys.sort()
return {i: result.mapping[i] for i in keys}
return dict(sorted(result.mapping.items()))
break
return {"q" + str(i): result.mapping[i] for i in range(len(result.mapping))}


class Random(Placer):
Expand Down Expand Up @@ -184,16 +182,17 @@ def __call__(self, circuit):
keys = list(self.connectivity.nodes())
final_mapping = dict(zip(keys, range(nodes)))
final_graph = nx.relabel_nodes(self.connectivity, final_mapping)
final_mapping = {"q" + str(i): final_mapping[i] for i in range(len(final_mapping))}
final_cost = self.cost(final_graph, gates_qubits_pairs)
for _ in range(self.samples):
mapping = dict(zip(keys, random.sample(range(nodes), nodes)))
graph = nx.relabel_nodes(self.connectivity, mapping)
cost = self.cost(graph, gates_qubits_pairs)
if cost == 0:
return mapping
return {"q" + str(i): mapping[i] for i in range(len(mapping))}
if cost < final_cost:
final_graph = graph
final_mapping = mapping
final_mapping = {"q" + str(i): mapping[i] for i in range(len(mapping))}
final_cost = cost
return final_mapping

Expand All @@ -211,8 +210,8 @@ def cost(graph, gates_qubits_pairs):
"""
for allowed, gate in enumerate(gates_qubits_pairs):
if gate not in graph.edges():
break
return len(gates_qubits_pairs) - allowed
return len(gates_qubits_pairs) - allowed - 1
return 0


class ReverseTraversal(Placer):
Expand Down
Loading

0 comments on commit 53737e1

Please sign in to comment.