From 6b05d2e9acc99a6ae7aa201c02e16c5d295ebefe Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:04:25 +0400 Subject: [PATCH 01/31] Implement new fusion --- src/qibo/abstractions/gates.py | 24 +++++++++++ src/qibo/core/circuit.py | 77 ++++++++++++++++++++++++++++++---- src/qibo/core/gates.py | 28 +++++++++++++ 3 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index 9430f440c2..db07348866 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -1550,3 +1550,27 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): seed=seed) # this case can only be applied to density matrices self.density_matrix = True + + +class FusedGate(Gate): + + def __init__(self, *q): + super().__init__() + self.name = "fused" + self.target_qubits = tuple(q) + self.init_args = list(q) + self.qubit_set = set(q) + self.gates = [] + + def add(self, gate): + if not set(gate.qubits).issubset(self.qubit_set): + raise_error(ValueError, "Cannot add gate that targets {} " + "in fused gate acting on {}." + "".format(gate.qubits, self.qubits)) + if isinstance(gate, self.__class__): + self.gates.extend(gate.gates) + else: + self.gates.append(gate) + + def __iter__(self): + return iter(self.gates) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 0df42da7c8..c1c2adddad 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -93,14 +93,77 @@ def fuse(self): # that is equivalent to applying the five gates of the original # circuit. """ + from qibo import gates from qibo.abstractions.circuit import _Queue - new_circuit = self._fuse_copy() - new_circuit.fusion_groups = self.fusion.FusionGroup.from_queue( - new_circuit.queue) - new_circuit.queue = _Queue(self.nqubits) - for group in new_circuit.fusion_groups: - for gate in group.gates: - new_circuit.queue.append(gate) + + # Fuse one qubit gates + temp_queue = _Queue(self.nqubits) + one_qubit_cache = {} + for gate in self.queue: + qubits = gate.qubits + if len(qubits) == 1: + q = qubits[0] + if q in one_qubit_cache: + one_qubit_cache.get(q).add(gate) + else: + one_qubit_cache[q] = gates.FusedGate(q) + one_qubit_cache[q].add(gate) + + else: + for q in qubits: + if q in one_qubit_cache: + temp_queue.append(one_qubit_cache.pop(q)) + temp_queue.append(gate) + + for gate in one_qubit_cache.values(): + temp_queue.append(gate) + + # Fuse two qubit gates + queue = _Queue(self.nqubits) + one_qubit_cache, two_qubit_cache = {}, {} + pair_map = {} # int -> (int, int) + for gate in temp_queue: + qubits = gate.qubits + if len(qubits) == 1: + q = qubits[0] + assert q not in one_qubit_cache + p = pair_map.get(q) + if p in two_qubit_cache: + two_qubit_cache.get(p).add(gate) + else: + one_qubit_cache[q] = gate + + elif len(qubits) == 2: + p = tuple(sorted(qubits)) + if p in two_qubit_cache: + two_qubit_cache.get(p).add(gate) + else: + two_qubit_cache[p] = gates.FusedGate(*p) + for q in p: + r = pair_map.get(q) + if r in two_qubit_cache: + queue.append(two_qubit_cache.pop(r)) + pair_map[q] = p + if q in one_qubit_cache: + two_qubit_cache[p].add(one_qubit_cache.pop(q)) + two_qubit_cache[p].add(gate) + + else: + for q in qubits: + if q in one_qubit_cache: + queue.append(one_qubit_cache.pop(q)) + p = pair_map.get(q) + if p in two_qubit_cache: + queue.append(two_qubit_cache.pop(p)) + queue.append(gate) + + for gate in one_qubit_cache.values(): + queue.append(gate) + for gate in two_qubit_cache.values(): + queue.append(gate) + + new_circuit = self.__class__(**self.init_kwargs) + new_circuit.queue = queue return new_circuit def _eager_execute(self, state): diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 2efe419c03..1f7d9ddce9 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1072,3 +1072,31 @@ def _density_matrix_call(self, state): self._target_qubits = original_targets return K.reshape(state, shape) return K._state_vector_call(self, state) + + +class FusedGate(MatrixGate, abstract_gates.FusedGate): + + def __init__(self, *q): + BackendGate.__init__(self) + abstract_gates.FusedGate.__init__(self, *q) + if len(self.target_qubits) == 1: + self.gate_op = K.op.apply_gate + elif len(self.target_qubits) == 2: + self.gate_op = K.op.apply_two_qubit_gate + else: + raise_error(NotImplementedError, "Fused gates can target up to two qubits.") + + def _construct_unitary(self): + matrix = K.qnp.eye(2 ** len(self.target_qubits)) + for gate in self.gates: + gmatrix = K.to_numpy(gate.matrix) + if len(gate.qubits) < len(self.target_qubits): + if gate.qubits[0] == self.target_qubits[0]: + gmatrix = K.qnp.kron(gmatrix, K.qnp.eye(2)) + else: + gmatrix = K.qnp.kron(K.qnp.eye(2), gmatrix) + elif gate.qubits != self.target_qubits: + gmatrix = K.qnp.reshape(gate.matrix, 4 * (2,)) + gmatrix = K.qnp.transpose(gmatrix, [1, 0, 3, 2]) + matrix = gmatrix @ matrix + return K.cast(matrix) From 5c1eabb9b2d2f3efab657cff9eb20bbe81f74ad4 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 8 Sep 2021 19:22:57 +0400 Subject: [PATCH 02/31] Remove _fuse_copy --- src/qibo/core/circuit.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index c1c2adddad..1743bfbb84 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -47,32 +47,6 @@ def _add_layer(self, gate): self._set_nqubits(gate.additional_unitary) self.queue.append(gate.additional_unitary) - def _fuse_copy(self): - """Helper method for ``circuit.fuse``. - - For standard (non-distributed) circuits this creates a copy of the - circuit with deep-copying the parametrized gates only. - For distributed circuits a fully deep copy should be created. - """ - import copy - from qibo.abstractions.abstract_gates import ParametrizedGate - new_circuit = self.__class__(**self.init_kwargs) - for gate in self.queue: - if isinstance(gate, ParametrizedGate): - if gate.trainable: - new_gate = copy.copy(gate) - new_circuit.queue.append(new_gate) - new_circuit.parametrized_gates.append(new_gate) - new_circuit.trainable_gates.append(new_gate) - else: - new_circuit.queue.append(gate) - new_circuit.parametrized_gates.append(gate) - else: - new_circuit.queue.append(gate) - new_circuit.measurement_gate = copy.copy(self.measurement_gate) - new_circuit.measurement_tuples = dict(self.measurement_tuples) - return new_circuit - def fuse(self): """Creates an equivalent ``Circuit`` with gates fused up to two-qubits. @@ -89,9 +63,8 @@ def fuse(self): c.add([gates.Y(0), gates.Y(1)]) # create circuit with fused gates fused_c = c.fuse() - # now ``fused_c`` contains only one ``gates.Unitary`` gate - # that is equivalent to applying the five gates of the original - # circuit. + # now ``fused_c`` contains a single gate that is equivalent + # to applying the five gates of the original circuit. """ from qibo import gates from qibo.abstractions.circuit import _Queue From 7878049631bd58172ddc94d246c143933d01f2c8 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 10 Sep 2021 21:43:21 +0400 Subject: [PATCH 03/31] Remove one qubit fusion --- src/qibo/core/circuit.py | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 1743bfbb84..7828875d66 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -69,48 +69,24 @@ def fuse(self): from qibo import gates from qibo.abstractions.circuit import _Queue - # Fuse one qubit gates - temp_queue = _Queue(self.nqubits) - one_qubit_cache = {} - for gate in self.queue: - qubits = gate.qubits - if len(qubits) == 1: - q = qubits[0] - if q in one_qubit_cache: - one_qubit_cache.get(q).add(gate) - else: - one_qubit_cache[q] = gates.FusedGate(q) - one_qubit_cache[q].add(gate) - - else: - for q in qubits: - if q in one_qubit_cache: - temp_queue.append(one_qubit_cache.pop(q)) - temp_queue.append(gate) - - for gate in one_qubit_cache.values(): - temp_queue.append(gate) - - # Fuse two qubit gates queue = _Queue(self.nqubits) one_qubit_cache, two_qubit_cache = {}, {} pair_map = {} # int -> (int, int) - for gate in temp_queue: + for gate in self.queue: qubits = gate.qubits if len(qubits) == 1: q = qubits[0] - assert q not in one_qubit_cache p = pair_map.get(q) if p in two_qubit_cache: two_qubit_cache.get(p).add(gate) else: - one_qubit_cache[q] = gate + if q not in one_qubit_cache: + one_qubit_cache[q] = gates.FusedGate(q) + one_qubit_cache.get(q).add(gate) elif len(qubits) == 2: p = tuple(sorted(qubits)) - if p in two_qubit_cache: - two_qubit_cache.get(p).add(gate) - else: + if p not in two_qubit_cache: two_qubit_cache[p] = gates.FusedGate(*p) for q in p: r = pair_map.get(q) @@ -119,7 +95,7 @@ def fuse(self): pair_map[q] = p if q in one_qubit_cache: two_qubit_cache[p].add(one_qubit_cache.pop(q)) - two_qubit_cache[p].add(gate) + two_qubit_cache.get(p).add(gate) else: for q in qubits: From 233a5e957961b7aade363ac31abee09cefd48473 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sat, 11 Sep 2021 00:23:24 +0400 Subject: [PATCH 04/31] Fix FusedGate with single gate --- src/qibo/core/circuit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 7828875d66..43135af60a 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -111,6 +111,10 @@ def fuse(self): for gate in two_qubit_cache.values(): queue.append(gate) + for i, gate in enumerate(queue): + if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1: + queue[i] = gate.gates[0] + new_circuit = self.__class__(**self.init_kwargs) new_circuit.queue = queue return new_circuit From 2340102e4e28fd6b93ce241f1dfd2ebdc239379e Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 13 Sep 2021 15:44:57 +0400 Subject: [PATCH 05/31] Add missing reshape --- src/qibo/core/gates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 1f7d9ddce9..6b471e7b28 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1098,5 +1098,6 @@ def _construct_unitary(self): elif gate.qubits != self.target_qubits: gmatrix = K.qnp.reshape(gate.matrix, 4 * (2,)) gmatrix = K.qnp.transpose(gmatrix, [1, 0, 3, 2]) + gmatrix = K.qnp.reshape(gmatrix, (4, 4)) matrix = gmatrix @ matrix return K.cast(matrix) From 3778607a5ff15bdf67768c3bcb02724a781a8d0e Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 13 Sep 2021 16:38:31 +0400 Subject: [PATCH 06/31] Fix QFT fusion --- src/qibo/core/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 43135af60a..f0cdafc271 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -90,7 +90,7 @@ def fuse(self): two_qubit_cache[p] = gates.FusedGate(*p) for q in p: r = pair_map.get(q) - if r in two_qubit_cache: + if r in two_qubit_cache and r != p: queue.append(two_qubit_cache.pop(r)) pair_map[q] = p if q in one_qubit_cache: From b3a354d80347d6d467966a7164f565750eedf6aa Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 13 Sep 2021 18:59:46 +0400 Subject: [PATCH 07/31] Update fusion algorithm --- src/qibo/core/circuit.py | 79 +++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index f0cdafc271..adf9ae66d5 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -70,50 +70,69 @@ def fuse(self): from qibo.abstractions.circuit import _Queue queue = _Queue(self.nqubits) - one_qubit_cache, two_qubit_cache = {}, {} - pair_map = {} # int -> (int, int) + gate_map = {} + removed_gates = set() for gate in self.queue: qubits = gate.qubits if len(qubits) == 1: q = qubits[0] - p = pair_map.get(q) - if p in two_qubit_cache: - two_qubit_cache.get(p).add(gate) - else: - if q not in one_qubit_cache: - one_qubit_cache[q] = gates.FusedGate(q) - one_qubit_cache.get(q).add(gate) + if q not in gate_map: + gate_map[q] = gates.FusedGate(q) + gate_map.get(q).add(gate) elif len(qubits) == 2: - p = tuple(sorted(qubits)) - if p not in two_qubit_cache: - two_qubit_cache[p] = gates.FusedGate(*p) - for q in p: - r = pair_map.get(q) - if r in two_qubit_cache and r != p: - queue.append(two_qubit_cache.pop(r)) - pair_map[q] = p - if q in one_qubit_cache: - two_qubit_cache[p].add(one_qubit_cache.pop(q)) - two_qubit_cache.get(p).add(gate) + q0, q1 = tuple(sorted(qubits)) + if q0 in gate_map or q1 in gate_map: + if gate_map.get(q0) == gate_map.get(q1): + gate_map.get(q0).add(gate) + else: + fgate = gates.FusedGate(q0, q1) + if q0 in gate_map: + ogate = gate_map.pop(q0) + if len(ogate.target_qubits) == 1: + fgate.add(ogate) + elif ogate in removed_gates: + removed_gates.remove(ogate) + queue.append(ogate) + else: + removed_gates.add(ogate) + if q1 in gate_map: + ogate = gate_map.pop(q1) + if len(ogate.target_qubits) == 1: + fgate.add(ogate) + elif ogate in removed_gates: + removed_gates.remove(ogate) + queue.append(ogate) + else: + removed_gates.add(ogate) + fgate.add(gate) + gate_map[q0], gate_map[q1] = fgate, fgate + + else: + fgate = gates.FusedGate(q0, q1) + fgate.add(gate) + gate_map[q0], gate_map[q1] = fgate, fgate else: for q in qubits: - if q in one_qubit_cache: - queue.append(one_qubit_cache.pop(q)) - p = pair_map.get(q) - if p in two_qubit_cache: - queue.append(two_qubit_cache.pop(p)) + if q in gate_map: + ogate = gate_map.pop(q) + if ogate in removed_gates: + removed_gates.remove(ogate) + queue.append(ogate) + else: + removed_gates.add(ogate) queue.append(gate) - for gate in one_qubit_cache.values(): - queue.append(gate) - for gate in two_qubit_cache.values(): - queue.append(gate) - for i, gate in enumerate(queue): if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1: queue[i] = gate.gates[0] + removed_gates |= set(gate_map.values()) + for gate in removed_gates: + if len(gate.gates) == 1: + queue.append(gate.gates[0]) + else: + queue.append(gate) new_circuit = self.__class__(**self.init_kwargs) new_circuit.queue = queue From 42cd3352547f4c4b38d97bdffb767f588fe8131d Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 13 Sep 2021 19:29:28 +0400 Subject: [PATCH 08/31] Simplify fusion code --- src/qibo/core/circuit.py | 85 +++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index adf9ae66d5..76e61696e0 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -8,6 +8,31 @@ from typing import List, Tuple +class FusedGates(dict): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.removed = set() + + def pop(self, key, fused_gate=None): + if key not in self: + return None + + gate = super().pop(key) + if fused_gate is not None and len(gate.target_qubits) == 1: + fused_gate.add(gate) + return None + elif gate in self.removed: + self.removed.remove(gate) + if len(gate.gates) == 1: + return gate.gates[0] + else: + return gate + else: + self.removed.add(gate) + return None + + class Circuit(circuit.AbstractCircuit): """Backend implementation of :class:`qibo.abstractions.circuit.AbstractCircuit`. @@ -70,65 +95,43 @@ def fuse(self): from qibo.abstractions.circuit import _Queue queue = _Queue(self.nqubits) - gate_map = {} - removed_gates = set() + fused_gates = FusedGates() for gate in self.queue: qubits = gate.qubits if len(qubits) == 1: q = qubits[0] - if q not in gate_map: - gate_map[q] = gates.FusedGate(q) - gate_map.get(q).add(gate) + if q not in fused_gates: + fused_gates[q] = gates.FusedGate(q) + fused_gates.get(q).add(gate) elif len(qubits) == 2: q0, q1 = tuple(sorted(qubits)) - if q0 in gate_map or q1 in gate_map: - if gate_map.get(q0) == gate_map.get(q1): - gate_map.get(q0).add(gate) + if q0 in fused_gates or q1 in fused_gates: + if fused_gates.get(q0) == fused_gates.get(q1): + fused_gates.get(q0).add(gate) else: fgate = gates.FusedGate(q0, q1) - if q0 in gate_map: - ogate = gate_map.pop(q0) - if len(ogate.target_qubits) == 1: - fgate.add(ogate) - elif ogate in removed_gates: - removed_gates.remove(ogate) - queue.append(ogate) - else: - removed_gates.add(ogate) - if q1 in gate_map: - ogate = gate_map.pop(q1) - if len(ogate.target_qubits) == 1: - fgate.add(ogate) - elif ogate in removed_gates: - removed_gates.remove(ogate) - queue.append(ogate) - else: - removed_gates.add(ogate) + ogate = fused_gates.pop(q0, fgate) + if ogate is not None: + queue.append(ogate) + ogate = fused_gates.pop(q1, fgate) + if ogate is not None: + queue.append(ogate) fgate.add(gate) - gate_map[q0], gate_map[q1] = fgate, fgate - + fused_gates[q0], fused_gates[q1] = fgate, fgate else: fgate = gates.FusedGate(q0, q1) fgate.add(gate) - gate_map[q0], gate_map[q1] = fgate, fgate + fused_gates[q0], fused_gates[q1] = fgate, fgate else: for q in qubits: - if q in gate_map: - ogate = gate_map.pop(q) - if ogate in removed_gates: - removed_gates.remove(ogate) - queue.append(ogate) - else: - removed_gates.add(ogate) + ogate = fused_gates.pop(q) + if ogate is not None: + queue.append(ogate) queue.append(gate) - for i, gate in enumerate(queue): - if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1: - queue[i] = gate.gates[0] - removed_gates |= set(gate_map.values()) - for gate in removed_gates: + for gate in fused_gates.removed | set(fused_gates.values()): if len(gate.gates) == 1: queue.append(gate.gates[0]) else: From 509d1ec14b4368784168c784ce24db37ff3cbe7b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 13 Sep 2021 20:14:47 +0400 Subject: [PATCH 09/31] Unskip tests --- src/qibo/core/gates.py | 13 +++---- src/qibo/tests/test_core_fusion.py | 56 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 6b471e7b28..c0cd126898 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1079,12 +1079,13 @@ class FusedGate(MatrixGate, abstract_gates.FusedGate): def __init__(self, *q): BackendGate.__init__(self) abstract_gates.FusedGate.__init__(self, *q) - if len(self.target_qubits) == 1: - self.gate_op = K.op.apply_gate - elif len(self.target_qubits) == 2: - self.gate_op = K.op.apply_two_qubit_gate - else: - raise_error(NotImplementedError, "Fused gates can target up to two qubits.") + if self.gate_op: + if len(self.target_qubits) == 1: + self.gate_op = K.op.apply_gate + elif len(self.target_qubits) == 2: + self.gate_op = K.op.apply_two_qubit_gate + else: + raise_error(NotImplementedError, "Fused gates can target up to two qubits.") def _construct_unitary(self): matrix = K.qnp.eye(2 ** len(self.target_qubits)) diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index 35b779594a..cda254efe0 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -6,40 +6,37 @@ from qibo.models import Circuit -def test_fusion_group_from_queue_single_group(): - """Check fusion that creates a single ``FusionGroup``.""" +def test_single_fusion_gate(): + """Check circuit fusion that creates a single ``FusedGate``.""" queue = [gates.H(0), gates.X(1), gates.CZ(0, 1)] - fused_groups = fusion.FusionGroup.from_queue(queue) - assert len(fused_groups) == 1 - group = fused_groups[0] - assert group.gates0 == [[queue[0]], []] - assert group.gates1 == [[queue[1]], []] - assert group.two_qubit_gates == [queue[2]] + c = Circuit(2) + c.add(queue) + c = c.fuse() + assert len(c.queue) == 1 + gate = c.queue[0] + for gate, target in zip(gate.gates, queue): + assert gate == target -def test_fusion_group_from_queue_two_groups(): - """Check fusion that creates two ``FusionGroup``s.""" +def test_two_fusion_gate(): + """Check fusion that creates two ``FusedGate``s.""" queue = [gates.X(0), gates.H(1), gates.RX(2, theta=0.1234).controlled_by(1), gates.H(2), gates.Y(1), gates.H(0)] - fused_groups = fusion.FusionGroup.from_queue(queue) - assert len(fused_groups) == 2 - group1, group2 = fused_groups - assert group1.gates0 == [[queue[0], queue[5]]] - assert group1.gates1 == [[queue[1]]] - assert group1.two_qubit_gates == [] - assert group2.gates0 == [[], [queue[4]]] - assert group2.gates1 == [[], [queue[3]]] - assert group2.two_qubit_gates == [queue[2]] - - -def test_fusion_group_first_gate(): - group = fusion.FusionGroup() - with pytest.raises(ValueError): - group.first_gate(2) + c = Circuit(3) + c.add(queue) + c = c.fuse() + assert len(c.queue) == 2 + gate1, gate2 = c.queue + if len(gate1.gates) > len(gate2.gates): + gate1, gate2 = gate2, gate1 + assert gate1.gates == [queue[0], queue[-1]] + assert gate2.gates == queue[1:-1] + +@pytest.mark.skip def test_fusion_group_add(): group = fusion.FusionGroup() group.add(gates.Flatten(np.ones(4))) @@ -69,6 +66,7 @@ def test_fusion_group_add(): group.add(gates.TOFFOLI(0, 1, 2)) +@pytest.mark.skip def test_fusion_group_calculate(backend): queue = [gates.H(0), gates.H(1), gates.CNOT(0, 1), gates.X(0), gates.X(1)] @@ -92,7 +90,7 @@ def test_fusion_group_calculate(backend): def test_fuse_circuit_two_qubit_only(backend): - """Check gate fusion in circuit with two-qubit gates only.""" + """Check circuit fusion in circuit with two-qubit gates only.""" c = Circuit(2) c.add(gates.CNOT(0, 1)) c.add(gates.RX(0, theta=0.1234).controlled_by(1)) @@ -103,6 +101,7 @@ def test_fuse_circuit_two_qubit_only(backend): K.assert_allclose(fused_c(), c()) +@pytest.mark.skip @pytest.mark.parametrize("nqubits", [4, 5, 10, 11]) @pytest.mark.parametrize("nlayers", [1, 2]) def test_variational_layer_fusion(backend, nqubits, nlayers): @@ -122,6 +121,7 @@ def test_variational_layer_fusion(backend, nqubits, nlayers): K.assert_allclose(fused_c(), c()) +@pytest.mark.skip @pytest.mark.parametrize("nqubits", [4, 5]) @pytest.mark.parametrize("ngates", [10, 20]) def test_random_circuit_fusion(backend, nqubits, ngates): @@ -145,7 +145,7 @@ def test_random_circuit_fusion(backend, nqubits, ngates): def test_controlled_by_gates_fusion(backend): - """Check gate fusion in circuit that contains ``controlled_by`` gates.""" + """Check circuit fusion that contains ``controlled_by`` gates.""" c = Circuit(4) c.add((gates.H(i) for i in range(4))) c.add(gates.RX(1, theta=0.1234).controlled_by(0)) @@ -157,6 +157,7 @@ def test_controlled_by_gates_fusion(backend): K.assert_allclose(fused_c(), c()) +@pytest.mark.skip def test_callbacks_fusion(backend): """Check entropy calculation in fused circuit.""" from qibo import callbacks @@ -173,6 +174,7 @@ def test_callbacks_fusion(backend): K.assert_allclose(entropy[:], target_entropy, atol=1e-7) +@pytest.mark.skip def test_set_parameters_fusion(backend): """Check gate fusion when ``circuit.set_parameters`` is used.""" c = Circuit(2) From b4d9bd91a7f4bedfa3f90a454d31e240eab2d8d2 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:01:23 +0400 Subject: [PATCH 10/31] Remove old fusion file --- src/qibo/core/fusion.py | 356 ---------------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 src/qibo/core/fusion.py diff --git a/src/qibo/core/fusion.py b/src/qibo/core/fusion.py deleted file mode 100644 index bd9d774aed..0000000000 --- a/src/qibo/core/fusion.py +++ /dev/null @@ -1,356 +0,0 @@ -import functools -import operator -from qibo import K, gates -from qibo.config import raise_error -from qibo.abstractions.abstract_gates import ParametrizedGate -from typing import List, Optional, Set, Tuple - - -class FusionGroup: - """Group of one-qubit and two-qubit gates that act in two specific gates. - - These gates can be fused into a single two-qubit gate represented by a - general 4x4 matrix. - - Args: - qubit0 (int): Id of the first qubit that the ``FusionGroup`` act. - qubit1 (int): Id of the first qubit that the ``FusionGroup`` act. - gates0 (list): List of lists of one-qubit gates to be applied to ``qubit0``. - One qubit gates are split in groups according to when the two qubit gates are applied (see example). - Has ``len(gates0) = len(two_qubit_gates) + 1``. - gates1 (list): Same as ``gates0`` but for ``qubit1``. - two_qubit_gates (list): List of two qubit gates acting on ``qubit0`` and ``qubit1``. - - Example: - :: - - queue = [gates.H(0), gates.H(1), gates.CNOT(0, 1), gates.X(0), gates.X(1)] - group = fusion.FusionGroup.from_queue(queue) - # results to the following values for the attributes: - group.qubit0 = 0 - group.qubit1 = 1 - group.gates0 = [[gates.H(0)], [gates.X(0)]] - group.gates1 = [[gates.H(1)], [gates.X(1)]] - group.two_qubit_gates = [gates.CNOT(0, 1)] - """ - - # ``FusionGroup`` cannot start with these gates because it is more - # efficient to apply them on their own - _efficient_gates = {"CNOT", "CZ", "SWAP", "CU1"} - - def __init__(self): - self.qubit0 = None - self.qubit1 = None - self.gates0 = [[]] - self.gates1 = [[]] - self.two_qubit_gates = [] - - self.completed = False - self.special_gate = None - self._fused_gates = None - if K.op is not None: - self.K = K.np - else: - self.K = K - - @property - def qubits(self) -> Set[int]: - """Set of ids of the two qubits that the ``FusionGroup`` acts on.""" - if self.qubit0 is None: - return {} - if self.qubit1 is None: - return {self.qubit0} - return {self.qubit0, self.qubit1} - - @property - def gates(self) -> Tuple["Gate"]: - """Tuple with fused gates. - - These gates have equivalent action with all the original gates that - were added in the ``FusionGroup``. - """ - if self._fused_gates is None: - self._fused_gates = self.calculate() - return self._fused_gates - - def update(self) -> Tuple["Gate"]: - """Recalculates fused gates. - - This is used automatically by circuit objects in order to repeat the - calculation of fused gates after the parameters of original gates have - been changed using ``circuit.set_parameters``. - It assumes that the parameters of the gate objects contained in the - current ``FusionGroup`` have already been updated. - """ - updated_gates = self.calculate() - for gate, new_gate in zip(self.gates, updated_gates): - if isinstance(gate, ParametrizedGate): - gate.parameters = new_gate.parameters - return self._fused_gates - - def first_gate(self, i: int) -> Optional["Gate"]: - """First one-qubit gate of the group.""" - if i < 0 or i > 1: - raise_error(ValueError, f"Invalid integer {i} given in FusionGroup.first_gate.") - gates = self.gates0 if i == 0 else self.gates1 - for group in gates: - if group: - return group[0] - return None - - def is_efficient(self, gate: "Gate") -> bool: - """Checks if given two-qubit ``gate`` is efficient. - - Efficient gates are not fused if they are in the start or in the end. - """ - return gate.__class__.__name__ in self._efficient_gates - - @classmethod - def from_queue(cls, queue: List["Gate"]) -> List["FusionGroup"]: - """Fuses a queue of gates by combining up to two-qubit gates. - - Args: - queue (list): List of gates. - - Returns: - List of ``FusionGroup`` objects that correspond to the fused gates. - """ - group_queue = [] - remaining_queue = list(queue) - while remaining_queue: - gates = iter(remaining_queue) - gate = next(gates) - new_group = cls() - new_group.add(gate) - remaining_queue = [] - for gate in gates: - if new_group.completed: - remaining_queue.append(gate) - break - - if new_group.can_add(gate): - commutes = True - for blocking_gate in remaining_queue: - commutes = commutes and gate.commutes(blocking_gate) - if not commutes: - break - - if commutes: - new_group.add(gate) - else: - remaining_queue.append(gate) - else: - remaining_queue.append(gate) - - new_group.completed = True - remaining_queue.extend(gates) - group_queue.append(new_group) - - return group_queue - - def can_add(self, gate: "Gate") -> bool: - """Checks if ``gate`` can be added in the ``FusionGroup``.""" - if self.completed: - return False - qubits = self.qubits - if not qubits: - return True - - if len(gate.qubits) == 1: - return (len(qubits) == 1 or gate.qubits[0] in qubits) - if len(gate.qubits) == 2: - targets = set(gate.qubits) - if targets == qubits: - return True - if (self.qubit1 is None and self.qubit0 in targets and - not self.is_efficient(gate)): - return True - return False - - def add(self, gate: "Gate"): - """Adds a gate in the group. - - Raises: - ValueError: If the gate cannot be added in the group (eg. because - it acts on different qubits). - RuntimeError: If the group is completed. - """ - if self.completed: - raise_error(RuntimeError, "Cannot add gates to completed FusionGroup.") - - if not gate.qubits: - self._add_special_gate(gate) - elif len(gate.qubits) == 1: - self._add_one_qubit_gate(gate) - elif len(gate.qubits) == 2: - self._add_two_qubit_gate(gate) - else: - raise_error(ValueError, "Cannot add gate acting on {} qubits in fusion " - "group.".format(len(gate.qubits))) - - def _add_special_gate(self, gate: "Gate"): - """Adds ``CallbackGate`` or ``Flatten`` on ``FusionGroup``.""" - if self.qubits or self.special_gate is not None: - raise_error(ValueError, "Cannot add special gate on fusion group.") - self.special_gate = gate - self.completed = True - - def _add_one_qubit_gate(self, gate: "Gate"): - """Adds one-qubit gate to ``FusionGroup``.""" - qubit = gate.qubits[0] - if self.qubit0 is None or self.qubit0 == qubit: - self.qubit0 = qubit - self.gates0[-1].append(gate) - elif self.qubit1 is None or self.qubit1 == qubit: - self.qubit1 = qubit - self.gates1[-1].append(gate) - else: - raise_error(ValueError, "Cannot add gate on qubit {} in fusion group of " - "qubits {} and {}." - "".format(qubit, self.qubit0, self.qubit1)) - - def _add_two_qubit_gate(self, gate: "Gate"): - """Adds two-qubit gate to ``FusionGroup``.""" - qubit0, qubit1 = gate.qubits - if self.qubit0 is None: - self.qubit0, self.qubit1 = qubit0, qubit1 - self.two_qubit_gates.append(gate) - if self.is_efficient(gate): - self.completed = True - - # case not used by the current scheme - elif self.qubit1 is None: # pragma: no cover - raise_error(NotImplementedError) - - if self.is_efficient(gate): - raise_error(ValueError, "It is not efficient to add {} in FusionGroup " - "with only one qubit set.".format(gate)) - - if self.qubit0 == qubit0: - self.qubit1 = qubit1 - self.two_qubit_gates.append(gate) - elif self.qubit0 == qubit1: - self.qubit1 = qubit0 - self.two_qubit_gates.append(gate) - else: - raise_error(ValueError, "Cannot add gate on qubits {} and {} in " - "fusion group of qubit {}." - "".format(qubit0, qubit1, self.qubit0)) - - else: - if self.qubits != {qubit0, qubit1}: - raise_error(ValueError, "Cannot add gate on qubits {} and {} in " - "fusion group of qubits {} and {}." - "".format(qubit0, qubit1, self.qubit0, self.qubit1)) - self.two_qubit_gates.append(gate) - - self.gates0.append([]) - self.gates1.append([]) - - def _one_qubit_matrix(self, gate0: "Gate", gate1: "Gate"): - """Calculates Kroneker product of two one-qubit gates. - - Args: - gate0: Gate that acts on ``self.qubit0``. - gate1: Gate that acts on ``self.qubit1``. - - Returns: - 4x4 matrix that corresponds to the Kronecker product of the 2x2 - gate matrices. - """ - if K.op is not None: - return self.K.kron(gate0.matrix, gate1.matrix) - else: - matrix = self.K.tensordot(gate0.matrix, gate1.matrix, axes=0) - matrix = self.K.transpose(matrix, [0, 2, 1, 3]) - return self.K.reshape(matrix, (4, 4)) - - def _two_qubit_matrix(self, gate: "Gate"): - """Calculates the 4x4 unitary matrix of a two-qubit gate. - - Args: - gate: Two-qubit gate acting on ``(self.qubit0, self.qubit1)``. - - Returns: - 4x4 unitary matrix corresponding to the gate. - """ - matrix = gate.matrix - if gate.qubits == (self.qubit1, self.qubit0): - matrix = self.K.reshape(matrix, 4 * (2,)) - matrix = self.K.transpose(matrix, [1, 0, 3, 2]) - matrix = self.K.reshape(matrix, (4, 4)) - else: - assert gate.qubits == (self.qubit0, self.qubit1) - return matrix - - def calculate(self): - """Calculates fused gate.""" - if not self.completed: - raise_error(RuntimeError, "Cannot calculate fused gates for incomplete " - "FusionGroup.") - # Case 1: Special gate - if self.special_gate is not None: - assert not self.gates0[0] and not self.gates1[0] - assert not self.two_qubit_gates - return (self.special_gate,) - - # Case 2: Two-qubit gates only (no one-qubit gates) - if self.first_gate(0) is None and self.first_gate(1) is None: - assert self.two_qubit_gates - # Case 2a: One two-qubit gate only - if len(self.two_qubit_gates) == 1: - return (self.two_qubit_gates[0],) - - # Case 2b: Two or more two-qubit gates - fused_matrix = self._two_qubit_matrix(self.two_qubit_gates[0]) - for gate in self.two_qubit_gates[1:]: - matrix = self._two_qubit_matrix(gate) - fused_matrix = self.K.matmul(matrix, fused_matrix) - return (gates.Unitary(fused_matrix, self.qubit0, self.qubit1),) - - # Case 3: One-qubit gates exist - if not self.gates0[-1] and not self.gates1[-1]: - self.gates0.pop() - self.gates1.pop() - - # Fuse one-qubit gates - ident0 = gates.I(self.qubit0) - gates0 = (functools.reduce(operator.matmul, reversed(gates), ident0) - if len(gates) != 1 else gates[0] for gates in self.gates0) - if self.qubit1 is not None: - ident1 = gates.I(self.qubit1) - gates1 = (functools.reduce(operator.matmul, reversed(gates), ident1) - if len(gates) != 1 else gates[0] for gates in self.gates1) - - # Case 3a: One-qubit gates only (no two-qubit gates) - if not self.two_qubit_gates: - gates0 = list(gates0)[::-1] - fused_gate0 = (functools.reduce(operator.matmul, gates0, ident0) - if len(gates0) != 1 else gates0[0]) - if self.qubit1 is None: - return (fused_gate0,) - - gates1 = list(gates1)[::-1] - fused_gate1 = (functools.reduce(operator.matmul, gates1, ident1) - if len(gates1) != 1 else gates1[0]) - return (fused_gate0, fused_gate1) - - # Case 3b: One-qubit and two-qubit gates exist - fused_matrix = self._one_qubit_matrix(next(gates0), next(gates1)) - for g0, g1, g2 in zip(gates0, gates1, self.two_qubit_gates): - matrix = self._one_qubit_matrix(g0, g1) - matrix2 = self._two_qubit_matrix(g2) - fused_matrix = self.K.matmul(self.K.matmul(matrix, matrix2), - fused_matrix) - - if len(self.two_qubit_gates) == len(self.gates0): - g2 = self.two_qubit_gates[-1] - if self.is_efficient(g2): - fused_gate = gates.Unitary(fused_matrix, self.qubit0, self.qubit1) - return (fused_gate, g2) - - matrix2 = self._two_qubit_matrix(g2) - fused_matrix = self.K.matmul(matrix2, fused_matrix) - - fused_gate = gates.Unitary(fused_matrix, self.qubit0, self.qubit1) - return (fused_gate,) From ef71abed3c70537b9476cf2c6e3f0ec05089314c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:01:36 +0400 Subject: [PATCH 11/31] Unskip more tests --- src/qibo/tests/test_core_fusion.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index cda254efe0..db368e1188 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -2,7 +2,6 @@ import numpy as np import pytest from qibo import gates, K -from qibo.core import fusion from qibo.models import Circuit @@ -35,7 +34,6 @@ def test_two_fusion_gate(): assert gate2.gates == queue[1:-1] - @pytest.mark.skip def test_fusion_group_add(): group = fusion.FusionGroup() @@ -101,7 +99,6 @@ def test_fuse_circuit_two_qubit_only(backend): K.assert_allclose(fused_c(), c()) -@pytest.mark.skip @pytest.mark.parametrize("nqubits", [4, 5, 10, 11]) @pytest.mark.parametrize("nlayers", [1, 2]) def test_variational_layer_fusion(backend, nqubits, nlayers): @@ -114,14 +111,13 @@ def test_variational_layer_fusion(backend, nqubits, nlayers): c.add((gates.RY(i, next(theta_iter)) for i in range(nqubits))) c.add((gates.CZ(i, i + 1) for i in range(0, nqubits - 1, 2))) c.add((gates.RY(i, next(theta_iter)) for i in range(nqubits))) - c.add((gates.CZ(i, i + 1) for i in range(1, nqubits - 2, 2))) + c.add((gates.CZ(i, i + 1) for i in range(1, nqubits - 1, 2))) c.add(gates.CZ(0, nqubits - 1)) fused_c = c.fuse() K.assert_allclose(fused_c(), c()) -@pytest.mark.skip @pytest.mark.parametrize("nqubits", [4, 5]) @pytest.mark.parametrize("ngates", [10, 20]) def test_random_circuit_fusion(backend, nqubits, ngates): @@ -139,7 +135,6 @@ def test_random_circuit_fusion(backend, nqubits, ngates): while q0 == q1: q0, q1 = np.random.randint(0, nqubits, (2,)) c.add(gate(q0, q1)) - fused_c = c.fuse() K.assert_allclose(fused_c(), c()) From 3cb67e9f037b61067c18cf7f4256e5f29706f53f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:02:51 +0400 Subject: [PATCH 12/31] Remove fusion import --- src/qibo/core/circuit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 76e61696e0..414dafbfd5 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -48,7 +48,6 @@ class Circuit(circuit.AbstractCircuit): Args: nqubits (int): Total number of qubits in the circuit. """ - from qibo.core import fusion def __init__(self, nqubits): super(Circuit, self).__init__(nqubits) From 5067e45e691537bbfc6a368fb30c45f9382bd956 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 14 Sep 2021 20:03:18 +0400 Subject: [PATCH 13/31] Fix variational test --- src/qibo/core/circuit.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 414dafbfd5..5ec177cfee 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -8,11 +8,16 @@ from typing import List, Tuple -class FusedGates(dict): +class FusedGates(collections.OrderedDict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.removed = set() + self.removed = collections.OrderedDict() + + def simplify(self, gate): + if len(gate.gates) == 1: + gate = gate.gates[0] + return gate def pop(self, key, fused_gate=None): if key not in self: @@ -23,15 +28,20 @@ def pop(self, key, fused_gate=None): fused_gate.add(gate) return None elif gate in self.removed: - self.removed.remove(gate) - if len(gate.gates) == 1: - return gate.gates[0] - else: - return gate + self.removed.pop(gate) + return self.simplify(gate) else: - self.removed.add(gate) + self.removed[gate] = None return None + def popall(self): + for gate in self.removed.keys(): + yield self.simplify(gate) + for gate in self.values(): + if gate not in self.removed: + self.removed[gate] = None + yield self.simplify(gate) + class Circuit(circuit.AbstractCircuit): """Backend implementation of :class:`qibo.abstractions.circuit.AbstractCircuit`. @@ -130,11 +140,7 @@ def fuse(self): queue.append(ogate) queue.append(gate) - for gate in fused_gates.removed | set(fused_gates.values()): - if len(gate.gates) == 1: - queue.append(gate.gates[0]) - else: - queue.append(gate) + queue.extend(fused_gates.popall()) new_circuit = self.__class__(**self.init_kwargs) new_circuit.queue = queue From 8d06b1d7cf3f0b5c8d6e0738623ed620296c0d0c Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:22:56 +0400 Subject: [PATCH 14/31] Fix fusion for all circuits --- src/qibo/core/circuit.py | 59 ++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 5ec177cfee..f60711c4d2 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -103,8 +103,19 @@ def fuse(self): from qibo import gates from qibo.abstractions.circuit import _Queue - queue = _Queue(self.nqubits) - fused_gates = FusedGates() + class FusedQueue(_Queue): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set = set() + + def append(self, gate): + if gate not in self.set: + self.set.add(gate) + super().append(gate) + + fused_queue = FusedQueue(self.nqubits) + fused_gates = collections.OrderedDict() for gate in self.queue: qubits = gate.qubits if len(qubits) == 1: @@ -115,32 +126,40 @@ def fuse(self): elif len(qubits) == 2: q0, q1 = tuple(sorted(qubits)) - if q0 in fused_gates or q1 in fused_gates: - if fused_gates.get(q0) == fused_gates.get(q1): - fused_gates.get(q0).add(gate) - else: - fgate = gates.FusedGate(q0, q1) - ogate = fused_gates.pop(q0, fgate) - if ogate is not None: - queue.append(ogate) - ogate = fused_gates.pop(q1, fgate) - if ogate is not None: - queue.append(ogate) - fgate.add(gate) - fused_gates[q0], fused_gates[q1] = fgate, fgate + if (q0 in fused_gates and q1 in fused_gates and + fused_gates.get(q0) == fused_gates.get(q1)): + fused_gates.get(q0).add(gate) else: fgate = gates.FusedGate(q0, q1) + if q0 in fused_gates: + ogate = fused_gates.pop(q0) + if len(ogate.target_qubits) == 1: + fgate.add(ogate) + else: + fused_queue.append(ogate) + if q1 in fused_gates: + ogate = fused_gates.pop(q1) + if len(ogate.target_qubits) == 1: + fgate.add(ogate) + else: + fused_queue.append(ogate) fgate.add(gate) fused_gates[q0], fused_gates[q1] = fgate, fgate else: for q in qubits: - ogate = fused_gates.pop(q) - if ogate is not None: - queue.append(ogate) - queue.append(gate) + if q in fused_gates: + fused_queue.append(fused_gates.pop(q)) + fused_queue.append(gate) + + for gate in fused_gates.values(): + fused_queue.append(gate) - queue.extend(fused_gates.popall()) + queue = _Queue(self.nqubits) + for gate in fused_queue: + if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1: + gate = gate.gates[0] + queue.append(gate) new_circuit = self.__class__(**self.init_kwargs) new_circuit.queue = queue From 2f9c17bb33e521cbab8c06b3f0615fc470598f63 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:23:22 +0400 Subject: [PATCH 15/31] Remove old fusion class --- src/qibo/core/circuit.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index f60711c4d2..65194c9814 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -8,41 +8,6 @@ from typing import List, Tuple -class FusedGates(collections.OrderedDict): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.removed = collections.OrderedDict() - - def simplify(self, gate): - if len(gate.gates) == 1: - gate = gate.gates[0] - return gate - - def pop(self, key, fused_gate=None): - if key not in self: - return None - - gate = super().pop(key) - if fused_gate is not None and len(gate.target_qubits) == 1: - fused_gate.add(gate) - return None - elif gate in self.removed: - self.removed.pop(gate) - return self.simplify(gate) - else: - self.removed[gate] = None - return None - - def popall(self): - for gate in self.removed.keys(): - yield self.simplify(gate) - for gate in self.values(): - if gate not in self.removed: - self.removed[gate] = None - yield self.simplify(gate) - - class Circuit(circuit.AbstractCircuit): """Backend implementation of :class:`qibo.abstractions.circuit.AbstractCircuit`. From ebe13825ef7484a47432873629f4bf0b3ff487a7 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:37:24 +0400 Subject: [PATCH 16/31] Fix CallbackGate with fusion --- src/qibo/core/circuit.py | 7 ++++ src/qibo/tests/test_core_fusion.py | 51 ++++-------------------------- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 65194c9814..c63abce42f 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -67,6 +67,7 @@ def fuse(self): """ from qibo import gates from qibo.abstractions.circuit import _Queue + from qibo.abstractions.abstract_gates import SpecialGate class FusedQueue(_Queue): @@ -111,6 +112,12 @@ def append(self, gate): fgate.add(gate) fused_gates[q0], fused_gates[q1] = fgate, fgate + elif isinstance(gate, SpecialGate): + for g in fused_gates.values(): + fused_queue.append(g) + fused_gates = collections.OrderedDict() + fused_queue.append(gate) + else: for q in qubits: if q in fused_gates: diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index db368e1188..c551029fd9 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -34,57 +34,21 @@ def test_two_fusion_gate(): assert gate2.gates == queue[1:-1] -@pytest.mark.skip -def test_fusion_group_add(): - group = fusion.FusionGroup() - group.add(gates.Flatten(np.ones(4))) - assert not group.can_add(gates.H(0)) - with pytest.raises(RuntimeError): - group.add(gates.H(0)) - - group = fusion.FusionGroup() - group.can_add(gates.H(0)) - group.add(gates.H(0)) - group.can_add(gates.RX(0, theta=0.1234).controlled_by(1)) - with pytest.raises(ValueError): - group.add(gates.Flatten(np.ones(4))) - - group = fusion.FusionGroup() - group.add(gates.RX(0, theta=0.1234).controlled_by(1)) - with pytest.raises(ValueError): - group.add(gates.H(2)) - - group = fusion.FusionGroup() - group.add(gates.RX(0, theta=0.1234).controlled_by(1)) - with pytest.raises(ValueError): - group.add(gates.CZ(1, 2)) - - group = fusion.FusionGroup() - with pytest.raises(ValueError): - group.add(gates.TOFFOLI(0, 1, 2)) - - -@pytest.mark.skip -def test_fusion_group_calculate(backend): +def test_fusedgate_matrix_calculation(backend): queue = [gates.H(0), gates.H(1), gates.CNOT(0, 1), gates.X(0), gates.X(1)] - group = fusion.FusionGroup.from_queue(queue) - assert len(group) == 1 - group = group[0] - - assert len(group.gates) == 1 - gate = group.gates[0] + circuit = Circuit(2) + circuit.add(queue) + circuit = circuit.fuse() + assert len(circuit.queue) == 1 + fused_gate = circuit.queue[0] x = np.array([[0, 1], [1, 0]]) h = np.array([[1, 1], [1, -1]]) / np.sqrt(2) cnot = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]) target_matrix = np.kron(x, x) @ cnot @ np.kron(h, h) - K.assert_allclose(gate.matrix, target_matrix) - - group = fusion.FusionGroup() - with pytest.raises(RuntimeError): - group.calculate() + K.assert_allclose(fused_gate.matrix, target_matrix) def test_fuse_circuit_two_qubit_only(backend): @@ -152,7 +116,6 @@ def test_controlled_by_gates_fusion(backend): K.assert_allclose(fused_c(), c()) -@pytest.mark.skip def test_callbacks_fusion(backend): """Check entropy calculation in fused circuit.""" from qibo import callbacks From c6c36fa5a77ba7360c53c479fb686f99eb4f2add Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:42:34 +0400 Subject: [PATCH 17/31] Implement FusedGate._dagger --- src/qibo/abstractions/gates.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index db07348866..6f8e55a4a8 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -1574,3 +1574,9 @@ def add(self, gate): def __iter__(self): return iter(self.gates) + + def _dagger(self): + dagger = self.__class__(*self.init_args) + for gate in self.gates[::-1]: + dagger.add(gate.dagger()) + return dagger From f53e4196ef8f67858613336d1e6c4937b0c9231f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:46:30 +0400 Subject: [PATCH 18/31] Skip parametrized tests temporarily --- src/qibo/tests/test_core_circuit_parametrized.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qibo/tests/test_core_circuit_parametrized.py b/src/qibo/tests/test_core_circuit_parametrized.py index da1e77b64c..65a69384ad 100644 --- a/src/qibo/tests/test_core_circuit_parametrized.py +++ b/src/qibo/tests/test_core_circuit_parametrized.py @@ -186,6 +186,7 @@ def test_set_parameters_with_double_variationallayer(backend, nqubits, trainable K.assert_allclose(c(), target_c()) +@pytest.mark.skip @pytest.mark.parametrize("trainable", [True, False]) def test_set_parameters_with_gate_fusion(backend, trainable, accelerators): """Check updating parameters of fused circuit.""" From 1b6a473f56dbaa66bdbe91e6ba1234f5dabbd271 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 05:49:44 +0400 Subject: [PATCH 19/31] Fix typo --- src/qibo/core/gates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index c0cd126898..f6c1ed4337 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1097,7 +1097,7 @@ def _construct_unitary(self): else: gmatrix = K.qnp.kron(K.qnp.eye(2), gmatrix) elif gate.qubits != self.target_qubits: - gmatrix = K.qnp.reshape(gate.matrix, 4 * (2,)) + gmatrix = K.qnp.reshape(gmatrix, 4 * (2,)) gmatrix = K.qnp.transpose(gmatrix, [1, 0, 3, 2]) gmatrix = K.qnp.reshape(gmatrix, (4, 4)) matrix = gmatrix @ matrix From 8c36c3d983263ef81a8252fd5179e0ec939e6067 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 20:11:49 +0400 Subject: [PATCH 20/31] Remove fusion groups --- src/qibo/abstractions/circuit.py | 24 +++++++++++------------- src/qibo/core/distcircuit.py | 3 --- src/qibo/tests/test_abstract_circuit.py | 3 --- src/qibo/tests/test_core_circuit.py | 3 +-- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/qibo/abstractions/circuit.py b/src/qibo/abstractions/circuit.py index 3138b5e813..69160bdee5 100644 --- a/src/qibo/abstractions/circuit.py +++ b/src/qibo/abstractions/circuit.py @@ -94,8 +94,6 @@ def __init__(self, nqubits): self.measurement_gate = None self.measurement_gate_result = None - self.fusion_groups = [] - self._final_state = None self.density_matrix = False self.repeated_execution = False @@ -195,6 +193,11 @@ def copy(self, deep: bool = False): new_circuit = self.__class__(**self.init_kwargs) if deep: for gate in self.queue: + if isinstance(gate, gates.FusedGate): # pragma: no cover + # impractical case + raise_error(NotImplementedError, "Cannot create deep copy " + "of fused circuit.") + new_gate = copy.copy(gate) new_circuit.queue.append(new_gate) if isinstance(gate, gates.ParametrizedGate): @@ -202,16 +205,12 @@ def copy(self, deep: bool = False): if gate.trainable: new_circuit.trainable_gates.append(new_gate) new_circuit.measurement_gate = copy.copy(self.measurement_gate) - if self.fusion_groups: # pragma: no cover - # impractical case - raise_error(NotImplementedError, "Cannot create deep copy of fused " - "circuit.") + else: new_circuit.queue = copy.copy(self.queue) new_circuit.parametrized_gates = list(self.parametrized_gates) new_circuit.trainable_gates = list(self.trainable_gates) new_circuit.measurement_gate = self.measurement_gate - new_circuit.fusion_groups = list(self.fusion_groups) new_circuit.measurement_tuples = dict(self.measurement_tuples) return new_circuit @@ -500,6 +499,7 @@ def _set_parameters_list(self, parameters: List, n: int): Also works if ``parameters`` is ``np.ndarray`` or ``tf.Tensor``. """ + # TODO: Treat fused gates if n == len(self.trainable_gates): for i, gate in enumerate(self.trainable_gates): gate.parameters = parameters[i] @@ -517,9 +517,6 @@ def _set_parameters_list(self, parameters: List, n: int): "the circuit contains {} parametrized gates." "".format(n, len(self.trainable_gates))) - for fusion_group in self.fusion_groups: - fusion_group.update() - def set_parameters(self, parameters): """Updates the parameters of the circuit's parametrized gates. @@ -569,9 +566,10 @@ def set_parameters(self, parameters): elif isinstance(parameters, self.param_tensor_types): self._set_parameters_list(parameters, int(parameters.shape[0])) elif isinstance(parameters, dict): - if self.fusion_groups: - raise_error(TypeError, "Cannot accept new parameters as dictionary " - "for fused circuits. Use list, tuple or array.") + for gate in self.queue: + if isinstance(gate, gates.FusedGate): + raise_error(TypeError, "Cannot accept new parameters as dictionary " + "for fused circuits. Use list, tuple or array.") diff = set(parameters.keys()) - self.trainable_gates.set if diff: raise_error(KeyError, "Dictionary contains gates {} which are " diff --git a/src/qibo/core/distcircuit.py b/src/qibo/core/distcircuit.py index f9d4d8f4d3..43d93272dd 100644 --- a/src/qibo/core/distcircuit.py +++ b/src/qibo/core/distcircuit.py @@ -76,9 +76,6 @@ def copy(self, deep: bool = True): "circuits because they modify gate objects.") return super().copy(deep) - def _fuse_copy(self): - return self.copy(deep=True) - def fuse(self): if self.queues.queues: raise_error(RuntimeError, "Cannot fuse distributed circuit after " diff --git a/src/qibo/tests/test_abstract_circuit.py b/src/qibo/tests/test_abstract_circuit.py index 9aeb905f41..dfa3ba1ef7 100644 --- a/src/qibo/tests/test_abstract_circuit.py +++ b/src/qibo/tests/test_abstract_circuit.py @@ -470,9 +470,6 @@ def test_circuit_set_parameters_errors(): c.set_parameters({0.3568}) with pytest.raises(ValueError): c.queue[2].parameters = [0.1234, 0.4321, 0.156] - c.fusion_groups = ["test"] - with pytest.raises(TypeError): - c.set_parameters({gates.RX(0, theta=1.0): 0.568}) def test_circuit_draw(): diff --git a/src/qibo/tests/test_core_circuit.py b/src/qibo/tests/test_core_circuit.py index 7fb51d5339..bad5d5bc5b 100644 --- a/src/qibo/tests/test_core_circuit.py +++ b/src/qibo/tests/test_core_circuit.py @@ -31,8 +31,7 @@ def test_circuit_add_layer(backend, nqubits, accelerators): for gate in c.queue: assert isinstance(gate, gates.Unitary) -# TODO: Test `_fuse_copy` -# TODO: Test `fuse` +# :meth:`qibo.core.circuit.Circuit` is tested in `test_core_fusion.py` def test_eager_execute(backend, accelerators): c = Circuit(4, accelerators) From aad0af1a5ec21f43d153c8c2eb0d4f896881980d Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 20:23:29 +0400 Subject: [PATCH 21/31] Update fusion parametrized gates --- src/qibo/abstractions/circuit.py | 13 ++++++------- src/qibo/core/circuit.py | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/qibo/abstractions/circuit.py b/src/qibo/abstractions/circuit.py index 69160bdee5..567ac185ac 100644 --- a/src/qibo/abstractions/circuit.py +++ b/src/qibo/abstractions/circuit.py @@ -18,13 +18,13 @@ class _ParametrizedGates(list): total number of parameters. """ - def __init__(self): - super(_ParametrizedGates, self).__init__(self) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.set = set() self.nparams = 0 - def append(self, gate: gates.ParametrizedGate): - super(_ParametrizedGates, self).append(gate) + def append(self, gate): + super().append(gate) self.set.add(gate) self.nparams += gate.nparams @@ -208,8 +208,8 @@ def copy(self, deep: bool = False): else: new_circuit.queue = copy.copy(self.queue) - new_circuit.parametrized_gates = list(self.parametrized_gates) - new_circuit.trainable_gates = list(self.trainable_gates) + new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates) + new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates) new_circuit.measurement_gate = self.measurement_gate new_circuit.measurement_tuples = dict(self.measurement_tuples) return new_circuit @@ -499,7 +499,6 @@ def _set_parameters_list(self, parameters: List, n: int): Also works if ``parameters`` is ``np.ndarray`` or ``tf.Tensor``. """ - # TODO: Treat fused gates if n == len(self.trainable_gates): for i, gate in enumerate(self.trainable_gates): gate.parameters = parameters[i] diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index c63abce42f..f185111afe 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -66,7 +66,7 @@ def fuse(self): # to applying the five gates of the original circuit. """ from qibo import gates - from qibo.abstractions.circuit import _Queue + from qibo.abstractions.circuit import _Queue, _ParametrizedGates from qibo.abstractions.abstract_gates import SpecialGate class FusedQueue(_Queue): @@ -133,7 +133,7 @@ def append(self, gate): gate = gate.gates[0] queue.append(gate) - new_circuit = self.__class__(**self.init_kwargs) + new_circuit = self.copy(deep=False) new_circuit.queue = queue return new_circuit From cf9157af3c21a3f082e9ec726487c2a8294a7948 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 20:36:03 +0400 Subject: [PATCH 22/31] Fix shallow copy in fuse --- src/qibo/abstractions/circuit.py | 18 ++++++++++++------ src/qibo/core/circuit.py | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/qibo/abstractions/circuit.py b/src/qibo/abstractions/circuit.py index 567ac185ac..fba17cce3e 100644 --- a/src/qibo/abstractions/circuit.py +++ b/src/qibo/abstractions/circuit.py @@ -178,6 +178,15 @@ def on_qubits(self, *q): for gate in self.queue: yield gate.on_qubits(*q) + def _shallow_copy(self): + """Helper method for :meth:`qibo.abstractions.circuit.AbstractCircuit.copy`.""" + new_circuit = self.__class__(**self.init_kwargs) + new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates) + new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates) + new_circuit.measurement_gate = self.measurement_gate + new_circuit.measurement_tuples = dict(self.measurement_tuples) + return new_circuit + def copy(self, deep: bool = False): """Creates a copy of the current ``circuit`` as a new ``Circuit`` model. @@ -190,8 +199,8 @@ def copy(self, deep: bool = False): The copied circuit object. """ import copy - new_circuit = self.__class__(**self.init_kwargs) if deep: + new_circuit = self.__class__(**self.init_kwargs) for gate in self.queue: if isinstance(gate, gates.FusedGate): # pragma: no cover # impractical case @@ -205,13 +214,10 @@ def copy(self, deep: bool = False): if gate.trainable: new_circuit.trainable_gates.append(new_gate) new_circuit.measurement_gate = copy.copy(self.measurement_gate) - + new_circuit.measurement_tuples = dict(self.measurement_tuples) else: + new_circuit = self._shallow_copy() new_circuit.queue = copy.copy(self.queue) - new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates) - new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates) - new_circuit.measurement_gate = self.measurement_gate - new_circuit.measurement_tuples = dict(self.measurement_tuples) return new_circuit def invert(self): diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index f185111afe..4443d6ea83 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -133,7 +133,7 @@ def append(self, gate): gate = gate.gates[0] queue.append(gate) - new_circuit = self.copy(deep=False) + new_circuit = self._shallow_copy() new_circuit.queue = queue return new_circuit From 9853df8b287adeb92a35bb3c2eac2016be8664ea Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 20:47:17 +0400 Subject: [PATCH 23/31] Fix set_parameters for fused circuits --- src/qibo/abstractions/abstract_gates.py | 7 +++++++ src/qibo/abstractions/circuit.py | 12 +++++++----- src/qibo/core/gates.py | 5 +++++ src/qibo/tests/test_core_fusion.py | 1 - 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index 5dc87d375d..8d343b154a 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -498,6 +498,13 @@ def _construct_unitary(self): # pragma: no cover """Constructs the gate's unitary matrix.""" return raise_error(NotImplementedError) + def _reset_unitary(self): + """Resets the gate matrices back to ``None``. + + Useful when the gate matrix need to be recalculated. + """ + self._matrix = None + @property @abstractmethod def cache(self): # pragma: no cover diff --git a/src/qibo/abstractions/circuit.py b/src/qibo/abstractions/circuit.py index fba17cce3e..6cdb91a0a8 100644 --- a/src/qibo/abstractions/circuit.py +++ b/src/qibo/abstractions/circuit.py @@ -179,7 +179,8 @@ def on_qubits(self, *q): yield gate.on_qubits(*q) def _shallow_copy(self): - """Helper method for :meth:`qibo.abstractions.circuit.AbstractCircuit.copy`.""" + """Helper method for :meth:`qibo.abstractions.circuit.AbstractCircuit.copy` + and :meth:`qibo.core.circuit.Circuit.fuse`.""" new_circuit = self.__class__(**self.init_kwargs) new_circuit.parametrized_gates = _ParametrizedGates(self.parametrized_gates) new_circuit.trainable_gates = _ParametrizedGates(self.trainable_gates) @@ -571,10 +572,6 @@ def set_parameters(self, parameters): elif isinstance(parameters, self.param_tensor_types): self._set_parameters_list(parameters, int(parameters.shape[0])) elif isinstance(parameters, dict): - for gate in self.queue: - if isinstance(gate, gates.FusedGate): - raise_error(TypeError, "Cannot accept new parameters as dictionary " - "for fused circuits. Use list, tuple or array.") diff = set(parameters.keys()) - self.trainable_gates.set if diff: raise_error(KeyError, "Dictionary contains gates {} which are " @@ -585,6 +582,11 @@ def set_parameters(self, parameters): else: raise_error(TypeError, "Invalid type of parameters {}." "".format(type(parameters))) + # Reset ``FusedGate`` matrices so that they are recalculated with the + # updated parameters. + for gate in self.queue: + if isinstance(gate, gates.FusedGate): + gate._reset_unitary() def get_parameters(self, format: str = "list", include_not_trainable: bool = False diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index f6c1ed4337..5dcefd3278 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -44,6 +44,11 @@ def _control_unitary(unitary): part2 = K.concatenate([zeros, unitary], axis=0) return K.concatenate([part1, part2], axis=1) + def _reset_unitary(self): + super()._reset_unitary() + self._native_op_matrix = None + self._custom_op_matrix = None + @property def cache(self): if self._cache is None: diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index c551029fd9..12d7a9cdfb 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -132,7 +132,6 @@ def test_callbacks_fusion(backend): K.assert_allclose(entropy[:], target_entropy, atol=1e-7) -@pytest.mark.skip def test_set_parameters_fusion(backend): """Check gate fusion when ``circuit.set_parameters`` is used.""" c = Circuit(2) From 28de310d43844aca2d35848e9489f56fb204dde8 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 21:25:49 +0400 Subject: [PATCH 24/31] Use _reset_unitary in parameter setter --- src/qibo/abstractions/abstract_gates.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index 8d343b154a..f795a5b7f2 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -411,9 +411,7 @@ def parameters(self, x): # ``circuit.set_parameters`` method works properly. # pylint: disable=E1101 if isinstance(self, BaseBackendGate): - self._matrix = None - self._native_op_matrix = None - self._custom_op_matrix = None + self._reset_unitary() for devgate in self.device_gates: devgate.parameters = x From ae80c4425fb930156ee2430bc9968daf15202582 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 21:31:18 +0400 Subject: [PATCH 25/31] Disable fusion for distributed circuits --- src/qibo/core/distcircuit.py | 6 ++---- src/qibo/core/gates.py | 2 ++ src/qibo/tests/test_core_circuit_features.py | 6 +++++- src/qibo/tests/test_core_circuit_parametrized.py | 13 +++++++++---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/qibo/core/distcircuit.py b/src/qibo/core/distcircuit.py index 43d93272dd..84020054e5 100644 --- a/src/qibo/core/distcircuit.py +++ b/src/qibo/core/distcircuit.py @@ -77,10 +77,8 @@ def copy(self, deep: bool = True): return super().copy(deep) def fuse(self): - if self.queues.queues: - raise_error(RuntimeError, "Cannot fuse distributed circuit after " - "its first execution.") - return super().fuse() + raise_error(NotImplementedError, "Fusion is not implemented for " + "distributed circuits.") def with_noise(self, noise_map, measurement_noise=None): raise_error(NotImplementedError, "Distributed circuit does not support " diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 5dcefd3278..1cbebd1c87 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -48,6 +48,8 @@ def _reset_unitary(self): super()._reset_unitary() self._native_op_matrix = None self._custom_op_matrix = None + for gate in self.device_gates: + gate._reset_unitary() @property def cache(self): diff --git a/src/qibo/tests/test_core_circuit_features.py b/src/qibo/tests/test_core_circuit_features.py index c6dc85ab73..30251523f9 100644 --- a/src/qibo/tests/test_core_circuit_features.py +++ b/src/qibo/tests/test_core_circuit_features.py @@ -78,7 +78,11 @@ def test_inverse_circuit_execution(backend, accelerators, fuse): c.add(gates.fSim(0, 2, theta=0.1, phi=0.3)) c.add(gates.CU2(0, 1, phi=0.1, lam=0.1)) if fuse: - c = c.fuse() + if accelerators: + with pytest.raises(NotImplementedError): + c = c.fuse() + else: + c = c.fuse() invc = c.invert() target_state = np.ones(2 ** 4) / 4 final_state = invc(c(np.copy(target_state))) diff --git a/src/qibo/tests/test_core_circuit_parametrized.py b/src/qibo/tests/test_core_circuit_parametrized.py index 65a69384ad..003b8f21d9 100644 --- a/src/qibo/tests/test_core_circuit_parametrized.py +++ b/src/qibo/tests/test_core_circuit_parametrized.py @@ -186,12 +186,11 @@ def test_set_parameters_with_double_variationallayer(backend, nqubits, trainable K.assert_allclose(c(), target_c()) -@pytest.mark.skip @pytest.mark.parametrize("trainable", [True, False]) -def test_set_parameters_with_gate_fusion(backend, trainable, accelerators): +def test_set_parameters_with_gate_fusion(backend, trainable): """Check updating parameters of fused circuit.""" params = np.random.random(9) - c = Circuit(5, accelerators) + c = Circuit(5) c.add(gates.RX(0, theta=params[0], trainable=trainable)) c.add(gates.RY(1, theta=params[1])) c.add(gates.CZ(0, 1)) @@ -203,7 +202,13 @@ def test_set_parameters_with_gate_fusion(backend, trainable, accelerators): c.add(gates.RZ(1, theta=params[8])) fused_c = c.fuse() - K.assert_allclose(c(), fused_c()) + for gate in fused_c.queue: + print(gate, gate.name, gate.target_qubits, gate.device_gates) + final_state = fused_c() + target_state = c() + for gate in fused_c.queue: + print(gate, gate.name, gate.target_qubits, gate.device_gates) + K.assert_allclose(final_state, target_state) if trainable: new_params = np.random.random(9) From d055fbaee930e8039a47b71706bfb49e2d7b4070 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 21:40:42 +0400 Subject: [PATCH 26/31] Fix pylint --- src/qibo/tests/test_core_distcircuit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/qibo/tests/test_core_distcircuit.py b/src/qibo/tests/test_core_distcircuit.py index e32c334eac..e235f1929a 100644 --- a/src/qibo/tests/test_core_distcircuit.py +++ b/src/qibo/tests/test_core_distcircuit.py @@ -56,9 +56,8 @@ def test_distributed_circuit_various_errors(backend): def test_distributed_circuit_fusion(backend, accelerators): c = DistributedCircuit(4, accelerators) c.add((gates.H(i) for i in range(4))) - final_state = c() - with pytest.raises(RuntimeError): - fused_c = c.fuse() + with pytest.raises(NotImplementedError): + c.fuse() def test_distributed_circuit_set_gates(backend): From 0e0d81ac5dc2bb76655d42f5f689d3b36c7ffea3 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 15 Sep 2021 21:43:55 +0400 Subject: [PATCH 27/31] Remove print --- src/qibo/tests/test_core_circuit_parametrized.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/qibo/tests/test_core_circuit_parametrized.py b/src/qibo/tests/test_core_circuit_parametrized.py index 003b8f21d9..e026fb9814 100644 --- a/src/qibo/tests/test_core_circuit_parametrized.py +++ b/src/qibo/tests/test_core_circuit_parametrized.py @@ -202,12 +202,8 @@ def test_set_parameters_with_gate_fusion(backend, trainable): c.add(gates.RZ(1, theta=params[8])) fused_c = c.fuse() - for gate in fused_c.queue: - print(gate, gate.name, gate.target_qubits, gate.device_gates) final_state = fused_c() target_state = c() - for gate in fused_c.queue: - print(gate, gate.name, gate.target_qubits, gate.device_gates) K.assert_allclose(final_state, target_state) if trainable: From 4b406dd904cb27cf44dd042773c383254341da1f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 16 Sep 2021 00:47:31 +0400 Subject: [PATCH 28/31] Improve coverage --- src/qibo/tests/test_abstract_gates.py | 8 ++++++++ src/qibo/tests/test_core_fusion.py | 3 ++- src/qibo/tests/test_core_gates.py | 21 ++++++++++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/qibo/tests/test_abstract_gates.py b/src/qibo/tests/test_abstract_gates.py index 64187c273b..98b07bb044 100644 --- a/src/qibo/tests/test_abstract_gates.py +++ b/src/qibo/tests/test_abstract_gates.py @@ -375,3 +375,11 @@ def test_special_gate(): assert not gate.commutes(gates.H(0)) with pytest.raises(NotImplementedError): gate.on_qubits(1) + + +def test_fused_gate(): + gate = gates.FusedGate(0, 1) + gate.add(gates.H(0)) + gate.add(gates.CNOT(0, 1)) + with pytest.raises(ValueError): + gate.add(gates.CZ(1, 2)) diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index 12d7a9cdfb..4979173ccb 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -28,7 +28,8 @@ def test_two_fusion_gate(): c = c.fuse() assert len(c.queue) == 2 gate1, gate2 = c.queue - if len(gate1.gates) > len(gate2.gates): + if len(gate1.gates) > len(gate2.gates): # pragma: no cover + # disabling coverage as this may not always happen gate1, gate2 = gate2, gate1 assert gate1.gates == [queue[0], queue[-1]] assert gate2.gates == queue[1:-1] diff --git a/src/qibo/tests/test_core_gates.py b/src/qibo/tests/test_core_gates.py index dec04ea2e6..52da58ac8f 100644 --- a/src/qibo/tests/test_core_gates.py +++ b/src/qibo/tests/test_core_gates.py @@ -1,4 +1,4 @@ -"""Test gates defined in `qibo/core/cgates.py` and `qibo/core/gates.py`.""" +"""Test gates defined in `qibo/core/gates.py`.""" import pytest import numpy as np from qibo import gates, K @@ -522,3 +522,22 @@ def test_thermal_relaxation_channel_errors(backend, t1, t2, time, excpop): with pytest.raises(ValueError): gate = gates.ThermalRelaxationChannel( 0, t1, t2, time, excited_population=excpop) + + +def test_fused_gate_init(backend): + gate = gates.FusedGate(0) + gate = gates.FusedGate(0, 1) + if K.op is not None: + with pytest.raises(NotImplementedError): + gate = gates.FusedGate(0, 1, 2) + + +def test_fused_gate_construct_unitary(backend): + gate = gates.FusedGate(0, 1) + gate.add(gates.H(0)) + gate.add(gates.H(1)) + gate.add(gates.CZ(0, 1)) + hmatrix = np.array([[1, 1], [1, -1]]) / np.sqrt(2) + czmatrix = np.diag([1, 1, 1, -1]) + target_matrix = czmatrix @ np.kron(hmatrix, hmatrix) + K.assert_allclose(gate.matrix, target_matrix) From c4376fa14696f6cd0b3a0107d31c2a6fb3485c15 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 16 Sep 2021 00:58:06 +0400 Subject: [PATCH 29/31] Add fusion test with Toffoli --- src/qibo/core/circuit.py | 2 +- src/qibo/tests/test_core_fusion.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 4443d6ea83..5779938e04 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -66,7 +66,7 @@ def fuse(self): # to applying the five gates of the original circuit. """ from qibo import gates - from qibo.abstractions.circuit import _Queue, _ParametrizedGates + from qibo.abstractions.circuit import _Queue from qibo.abstractions.abstract_gates import SpecialGate class FusedQueue(_Queue): diff --git a/src/qibo/tests/test_core_fusion.py b/src/qibo/tests/test_core_fusion.py index 4979173ccb..17e703af1e 100644 --- a/src/qibo/tests/test_core_fusion.py +++ b/src/qibo/tests/test_core_fusion.py @@ -52,7 +52,7 @@ def test_fusedgate_matrix_calculation(backend): K.assert_allclose(fused_gate.matrix, target_matrix) -def test_fuse_circuit_two_qubit_only(backend): +def test_fuse_circuit_two_qubit_gates(backend): """Check circuit fusion in circuit with two-qubit gates only.""" c = Circuit(2) c.add(gates.CNOT(0, 1)) @@ -64,6 +64,21 @@ def test_fuse_circuit_two_qubit_only(backend): K.assert_allclose(fused_c(), c()) +def test_fuse_circuit_three_qubit_gate(backend): + """Check circuit fusion in circuit with three-qubit gate.""" + c = Circuit(4) + c.add((gates.H(i) for i in range(4))) + c.add(gates.CZ(0, 1)) + c.add(gates.CZ(2, 3)) + c.add(gates.TOFFOLI(0, 1, 2)) + c.add(gates.SWAP(1, 2)) + c.add((gates.H(i) for i in range(4))) + c.add(gates.CNOT(0, 1)) + c.add(gates.CZ(2, 3)) + fused_c = c.fuse() + K.assert_allclose(fused_c(), c(), atol=1e-12) + + @pytest.mark.parametrize("nqubits", [4, 5, 10, 11]) @pytest.mark.parametrize("nlayers", [1, 2]) def test_variational_layer_fusion(backend, nqubits, nlayers): From 95ac9db73a453dc9b24f9aead3cecead18f3c2f2 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 21 Sep 2021 16:57:36 +0400 Subject: [PATCH 30/31] Update docstrings --- src/qibo/abstractions/gates.py | 8 ++++++++ src/qibo/core/circuit.py | 4 ++-- src/qibo/core/gates.py | 20 ++++++++++++++++++++ src/qibo/tests/test_core_circuit.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index 44c68cdf79..1e58f9bbb0 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -1553,6 +1553,14 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): class FusedGate(Gate): + """Collection of gates that will be fused and applied as single gate during simulation. + + This gate is constructed automatically by :meth:`qibo.core.circuits.Circuit.fuse` + and should not be used by user. + :class:`qibo.abstractions.gates.FusedGate` works with arbitrary number of + target qubits however the backend implementation + :class:`qibo.core.gates.FusedGate` assumes two target qubits. + """ def __init__(self, *q): super().__init__() diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 5779938e04..a0dab8a67b 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -47,10 +47,10 @@ def _add_layer(self, gate): self.queue.append(gate.additional_unitary) def fuse(self): - """Creates an equivalent ``Circuit`` with gates fused up to two-qubits. + """Creates an equivalent :class:`qibo.core.circuit.Circuit` with gates fused up to two-qubits. Returns: - The equivalent ``Circuit`` object where the gates are fused. + The equivalent :class:`qibo.core.circuit.Circuit` object where the gates are fused. Example: :: diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 1cbebd1c87..80861b6d47 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1087,6 +1087,7 @@ def __init__(self, *q): BackendGate.__init__(self) abstract_gates.FusedGate.__init__(self, *q) if self.gate_op: + # Custom kernels currently support up to two target qubits if len(self.target_qubits) == 1: self.gate_op = K.op.apply_gate elif len(self.target_qubits) == 2: @@ -1095,17 +1096,36 @@ def __init__(self, *q): raise_error(NotImplementedError, "Fused gates can target up to two qubits.") def _construct_unitary(self): + """Constructs a single unitary by multiplying the matrices of the gates that are fused. + + This matrix is used to perform a single update in the state during + simulation instead of applying the fused gates one by one. + + Note that this method assumes maximum two target qubits and should be + update if the fusion algorithm is extended to gates of higher rank. + """ matrix = K.qnp.eye(2 ** len(self.target_qubits)) for gate in self.gates: + # transfer gate matrix to numpy as it is more efficient for + # small tensor calculations gmatrix = K.to_numpy(gate.matrix) if len(gate.qubits) < len(self.target_qubits): + # fuse one-qubit gate (2x2 matrix) to a two-qubit ``FusedGate`` + # Kronecker product with identity is needed to make the + # original matrix 4x4. if gate.qubits[0] == self.target_qubits[0]: + # gate target qubit is the first ```FusedGate`` target gmatrix = K.qnp.kron(gmatrix, K.qnp.eye(2)) else: + # gate target qubit is the second ``FusedGate`` target gmatrix = K.qnp.kron(K.qnp.eye(2), gmatrix) elif gate.qubits != self.target_qubits: + # fuse two-qubit gate (4x4 matrix) for which the target qubits + # are in opposite order compared to the ``FusedGate`` and + # the corresponding matrix has to be transposed before fusion gmatrix = K.qnp.reshape(gmatrix, 4 * (2,)) gmatrix = K.qnp.transpose(gmatrix, [1, 0, 3, 2]) gmatrix = K.qnp.reshape(gmatrix, (4, 4)) + # fuse the individual gate matrix to the total ``FusedGate`` matrix matrix = gmatrix @ matrix return K.cast(matrix) diff --git a/src/qibo/tests/test_core_circuit.py b/src/qibo/tests/test_core_circuit.py index bad5d5bc5b..6a12591163 100644 --- a/src/qibo/tests/test_core_circuit.py +++ b/src/qibo/tests/test_core_circuit.py @@ -31,7 +31,7 @@ def test_circuit_add_layer(backend, nqubits, accelerators): for gate in c.queue: assert isinstance(gate, gates.Unitary) -# :meth:`qibo.core.circuit.Circuit` is tested in `test_core_fusion.py` +# :meth:`qibo.core.circuit.Circuit.fuse` is tested in `test_core_fusion.py` def test_eager_execute(backend, accelerators): c = Circuit(4, accelerators) From c3e79978570bdaecc66b2d311d217cbfc000f92b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:16:33 +0400 Subject: [PATCH 31/31] Add docstrings in fusion algorithm --- src/qibo/core/circuit.py | 43 ++++++++++++++++++++++++++++++++++++++++ src/qibo/core/gates.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index a0dab8a67b..b6f16699dd 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -49,6 +49,8 @@ def _add_layer(self, gate): def fuse(self): """Creates an equivalent :class:`qibo.core.circuit.Circuit` with gates fused up to two-qubits. + The current fusion algorithm with create up to two-qubit fused gates. + Returns: The equivalent :class:`qibo.core.circuit.Circuit` object where the gates are fused. @@ -70,69 +72,110 @@ def fuse(self): from qibo.abstractions.abstract_gates import SpecialGate class FusedQueue(_Queue): + """Helper queue implementation that checks if a gate already exists + in queue to avoid re-appending it. + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set = set() def append(self, gate): + """Appends a gate in queue only if it is not already in.""" + # Use a ``set`` instead of the original ``list`` to check if + # the gate already exists in queue as lookup is typically + # more efficient for sets + # (although actual performance difference is probably negligible) if gate not in self.set: self.set.add(gate) super().append(gate) + # new circuit queue that will hold the fused gates fused_queue = FusedQueue(self.nqubits) + # dictionary that maps each qubit id (int) to the corresponding + # active ``FusedGate`` that is part of fused_gates = collections.OrderedDict() + # use ``OrderedDict`` so that the original gate order is not changed for gate in self.queue: qubits = gate.qubits if len(qubits) == 1: + # add one-qubit gates to the active ``FusedGate`` of this qubit + # or create a new one if it does not exist q = qubits[0] if q not in fused_gates: fused_gates[q] = gates.FusedGate(q) fused_gates.get(q).add(gate) elif len(qubits) == 2: + # fuse two-qubit gates q0, q1 = tuple(sorted(qubits)) if (q0 in fused_gates and q1 in fused_gates and fused_gates.get(q0) == fused_gates.get(q1)): + # if the target qubit pair is compatible with the active + # ``FusedGate`` of both qubits then add it to the ``FusedGate`` fused_gates.get(q0).add(gate) else: + # otherwise we need to create a new ``FusedGate`` and + # update the active gates of both target qubits fgate = gates.FusedGate(q0, q1) if q0 in fused_gates: + # first qubit has existing active gate ogate = fused_gates.pop(q0) if len(ogate.target_qubits) == 1: + # existing active gate is one-qubit so we just add + # it to the new ``FusedGate`` fgate.add(ogate) else: + # existing active gate is two-qubit so we need to + # add it to the new queue fused_queue.append(ogate) if q1 in fused_gates: + # second qubit has existing active gate ogate = fused_gates.pop(q1) if len(ogate.target_qubits) == 1: + # existing active gate is one-qubit so we just add + # it to the new ``FusedGate`` fgate.add(ogate) else: + # existing active gate is two-qubit so we need to + # add it to the new queue fused_queue.append(ogate) + # add the two-qubit gate to the newly created ``FusedGate`` + # and update the active ``FusedGate``s of both target qubits fgate.add(gate) fused_gates[q0], fused_gates[q1] = fgate, fgate elif isinstance(gate, SpecialGate): + # ``SpecialGate``s act on all qubits (like a barrier) so we + # so we need to temporarily stop the fusion, add all active + # gates in the new queue and restart fusion after the barrier for g in fused_gates.values(): fused_queue.append(g) fused_gates = collections.OrderedDict() fused_queue.append(gate) else: + # gate has more than two target qubits so it cannot be included + # in the ``FusedGate``s which support up to two qubits. + # Therefore we deactivate the ``FusedGate``s of all target qubits for q in qubits: if q in fused_gates: fused_queue.append(fused_gates.pop(q)) fused_queue.append(gate) for gate in fused_gates.values(): + # add remaining active ``FusedGate``s in the new queue fused_queue.append(gate) queue = _Queue(self.nqubits) for gate in fused_queue: if isinstance(gate, gates.FusedGate) and len(gate.gates) == 1: + # replace ``FusedGate``s that contain only one gate by this + # gate for efficiency gate = gate.gates[0] queue.append(gate) + # create a circuit and assign the new queue new_circuit = self._shallow_copy() new_circuit.queue = queue return new_circuit diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 80861b6d47..84d75b3dc3 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -1102,7 +1102,7 @@ def _construct_unitary(self): simulation instead of applying the fused gates one by one. Note that this method assumes maximum two target qubits and should be - update if the fusion algorithm is extended to gates of higher rank. + updated if the fusion algorithm is extended to gates of higher rank. """ matrix = K.qnp.eye(2 ** len(self.target_qubits)) for gate in self.gates: