diff --git a/projectq/cengines/_optimize.py b/projectq/cengines/_optimize.py index 1cce7317..90ab9b94 100755 --- a/projectq/cengines/_optimize.py +++ b/projectq/cengines/_optimize.py @@ -16,30 +16,33 @@ import warnings -from projectq.ops import FastForwardingGate, FlushGate, NotMergeable - -from ._basics import BasicEngine +from projectq.cengines import BasicEngine +from projectq.ops import FastForwardingGate, FlushGate, NotMergeable, XGate +from projectq.ops._basics import Commutability class LocalOptimizer(BasicEngine): """ - Circuit optimization compiler engine. - - LocalOptimizer is a compiler engine which optimizes locally (merging rotations, cancelling gates with their - inverse) in a local window of user- defined size. - - It stores all commands in a dict of lists, where each qubit has its own gate pipeline. After adding a gate, it - tries to merge / cancel successive gates using the get_merged and get_inverse functions of the gate (if - available). For examples, see BasicRotationGate. Once a list corresponding to a qubit contains >=m gates, the - pipeline is sent on to the next engine. + LocalOptimizer is a compiler engine which optimizes locally (e.g. merging + rotations, cancelling gates with their inverse) in a local window of user- + defined size. + It stores all commands in a dict of lists, where each qubit has its own + gate pipeline. After adding a gate, it tries to merge / cancel successive + gates using the get_merged and get_inverse functions of the gate (if + available). For examples, see BasicRotationGate. Once a list corresponding + to a qubit contains >=m gates, the pipeline is sent on to the next engine. """ - def __init__(self, cache_size=5, m=None): # pylint: disable=invalid-name + def __init__(self, cache_size=5, m=5, apply_commutation=True): # pylint: disable=invalid-name """ Initialize a LocalOptimizer object. Args: cache_size (int): Number of gates to cache per qubit, before sending on the first gate. + m (int): Number of gates to cache per qubit, before sending on the + first gate. + apply_commutation (Boolean): Indicates whether to consider commutation + rules during optimization. """ super().__init__() self._l = {} # dict of lists containing operations for each qubit @@ -52,6 +55,8 @@ def __init__(self, cache_size=5, m=None): # pylint: disable=invalid-name ) cache_size = m self._cache_size = cache_size # wait for m gates before sending on + self._m = m # wait for m gates before sending on + self._apply_commutation = apply_commutation # sends n gate operations of the qubit with index idx def _send_qubit_pipeline(self, idx, n_gates): @@ -113,13 +118,299 @@ def _get_gate_indices(self, idx, i, qubit_ids): indices.append(identical_indices[num_identical_to_skip]) return indices - def _optimize(self, idx, lim=None): + def _delete_command(self, idx, command_idx): + """ + Deletes the command at self._l[idx][command_idx] accounting + for all qubits in the optimizer dictionary. + + Args: + idx (int): qubit index + command_idx (int): command position in qubit idx's command list + """ + # List of the indices of the qubits that are involved + # in command + qubitids = [qb.id for sublist in self._l[idx][command_idx].all_qubits for qb in sublist] + # List of the command indices corresponding to the position + # of this command on each qubit id + commandidcs = self._get_gate_indices(idx, command_idx, qubitids) + for j in range(len(qubitids)): + try: + new_list = self._l[qubitids[j]][0 : commandidcs[j]] + self._l[qubitids[j]][commandidcs[j] + 1 :] + except IndexError: + # If there are no more commands after that being deleted. + new_list = self._l[qubitids[j]][0 : commandidcs[j]] + self._l[qubitids[j]] = new_list + + def _replace_command(self, idx, command_idx, new_command): + """ + Replaces the command at self._l[idx][command_idx] accounting + for all qubits in the optimizer dictionary. + + Args: + idx (int): qubit index + command_idx (int): command position in qubit idx's command list + new_command (Command): The command to replace the command + at self._l[idx][command_idx] + """ + # Check that the new command concerns the same qubits as the original + # command before starting the replacement process + assert new_command.all_qubits == self._l[idx][command_idx].all_qubits + # List of the indices of the qubits that are involved + # in command + qubitids = [qb.id for sublist in self._l[idx][command_idx].all_qubits for qb in sublist] + # List of the command indices corresponding to the position + # of this command on each qubit id + commandidcs = self._get_gate_indices(idx, command_idx, qubitids) + for j in range(len(qubitids)): + try: + new_list = ( + self._l[qubitids[j]][0 : commandidcs[j]] + + [new_command] + + self._l[qubitids[j]][commandidcs[j] + 1 :] + ) + except IndexError: + # If there are no more commands after that being replaced. + new_list = self._l[qubitids[j]][0 : commandidcs[j]] + [new_command] + self._l[qubitids[j]] = new_list + + def _can_cancel_by_commutation(self, idx, qubitids, commandidcs, inverse_command, apply_commutation): + """ + Determines whether inverse commands should be cancelled + with one another. i.e. the commands between the pair are all + commutable for each qubit involved in the command. + + Args: + idx (int): qubit index + qubitids (list of int): the qubit ids involved in the command we're examining. + commandidcs (list of int): command position in qubit idx's command list. + inverse_command (Command): the command to be cancelled with. + apply_commutation (bool): True/False depending on whether optimizer + is considering commutation rules. + + """ + erase = True + # We dont want to examine qubit idx because the optimizer + # has already checked that the gates between the current + # and mergeable gates are commutable (or a commutable list). + commandidcs.pop(qubitids.index(idx)) # Remove corresponding + # position of command for qubit idx from commandidcs + qubitids.remove(idx) # Remove qubitid representing the current + # qubit in optimizer + x = 1 + for j in range(len(qubitids)): + # Check that any gates between current gate and inverse + # gate are all commutable + this_command = self._l[qubitids[j]][commandidcs[j]] + future_command = self._l[qubitids[j]][commandidcs[j] + x] + while future_command != inverse_command: + if apply_commutation == False: + # If apply_commutation turned off, you should + # only get erase=True if commands are next to + # eachother on all qubits. i.e. if future_command + # and inverse_command are not equal (i.e. there + # are gates separating them), you don't want + # optimizer to look at whether the separating gates + # are commutable. + return False + if this_command.is_commutable(future_command) == 1: + x += 1 + future_command = self._l[qubitids[j]][commandidcs[j] + x] + erase = True + else: + erase = False + break + if this_command.is_commutable(future_command) == 2: + new_x = self._check_for_commutable_circuit(this_command, future_command, qubitids[j], commandidcs[j], 0) + if new_x > x: + x = new_x + future_command = self._l[qubitids[j]][commandidcs[j] + x] + erase = True + else: + erase = False + break + return erase + + def _can_merge_by_commutation(self, idx, qubitids, commandidcs, merged_command, apply_commutation): """ - Gate cancellation routine. + Determines whether mergeable commands should be merged + with one another. i.e. the commands between the pair are all + commutable for each qubit involved in the command. - Try to remove identity gates using the is_identity function, then merge or even cancel successive gates using - the get_merged and get_inverse functions of the gate (see, e.g., BasicRotationGate). + Args: + idx (int): qubit index + qubitids (list of int): the qubit ids involved in the command we're examining. + commandidcs (list of int): command position in qubit idx's command list. + merged_command (Command): the merged command we want to produce. + apply_commutation (bool): True/False depending on whether optimizer + is considering commutation rules. + """ + merge = True + # We dont want to examine qubit idx because the optimizer has already + # checked that the gates between the current and mergeable gates are + # commutable (or a commutable list). + commandidcs.pop(qubitids.index(idx)) # Remove corresponding position of command for qubit idx from commandidcs + qubitids.remove(idx) # Remove qubitid representing the current qubit in optimizer + for j in range(len(qubitids)): + # Check that any gates between current gate and mergeable + # gate are commutable + this_command = self._l[qubitids[j]][commandidcs[j]] + possible_command = None + merge = True + x = 1 + while possible_command != merged_command: + if not apply_commutation: + # If apply_commutation turned off, you should + # only get erase=True if commands are next to + # eachother on all qubits. + return False + future_command = self._l[qubitids[j]][commandidcs[j] + x] + try: + possible_command = this_command.get_merged(future_command) + except: + pass + if possible_command == merged_command: + merge = True + break + if this_command.is_commutable(future_command) == 1: + x += 1 + merge = True + continue + else: + merge = False + break + return merge + + def _check_for_commutable_circuit(self, command_i, next_command, idx, i, x): + """ + Checks if there is a commutable circuit separating two commands. + + Args: + command_i (Command) = current command + next_command (Command) = the next command + idx (int) = index of the current qubit in the optimizer + i (int) = index of the current command in the optimizer + x (int) = number of commutable gates infront of i we have found + (if there is a commutable circuit, we pretend we have found + x commutable gates where x is the length of the commutable circuit.) + + Returns: + x (int): If there is a commutable circuit the function returns the length x. + Otherwise, returns 0. + """ + # commutable_circuit_list is a temp variable just used to create relative_commutable_circuits + commutable_circuit_list = command_i.gate.get_commutable_circuit_list( + n=len(command_i._control_qubits), + ) + relative_commutable_circuits = [] + # Keep a list of circuits that start with + # next_command. + for relative_circuit in commutable_circuit_list: + if type(relative_circuit[0].gate) is type(next_command.gate): + relative_commutable_circuits.append(relative_circuit) + # Create dictionaries { absolute_qubit_idx : relative_qubit_idx } + # For the purposes of fast lookup, also { relative_qubit_idx : absolute_qubit_idx } + abs_to_rel = {idx: 0} + rel_to_abs = {0: idx} + # If the current command is a CNOT, we set the target qubit idx + # to 0 + if isinstance(command_i.gate, XGate): + if len(command_i._control_qubits) == 1: + # At this point we know we have a CNOT + # we reset the dictionaries so that the + # target qubit in the abs dictionary + # corresponds to the target qubit in the + # rel dictionary + abs_to_rel = {command_i.qubits[0][0].id: 0} + rel_to_abs = {0: command_i.qubits[0][0].id} + y = 0 + absolute_circuit = self._l[idx][i + x + 1 :] + # If no (more) relative commutable circuits to check against, + # break out of this while loop and move on to next command_i. + while relative_commutable_circuits: + # If all the viable relative_circuits have been deleted + # you want to just move on + relative_circuit = relative_commutable_circuits[0] + while y < len(relative_circuit): + # Check that there are still gates in the + # engine buffer + if y > (len(absolute_circuit) - 1): + # The absolute circuit is too short to match the relative_circuit + # i.e. if the absolute circuit is of len=3, you can't have absolute_circuit[3] + # only absolute_circuit[0] - absolute_circuit[2] + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + # Check if relative_circuit command + # matches the absolute_circuit command + next_command = absolute_circuit[y] + if not type(relative_circuit[y]._gate) is type(next_command.gate): + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + + # Now we know the gates are equal. + # We check the idcs don't contradict our dictionaries. + # remember next_command = absolute_circuit[y]. + for qubit in next_command.qubits: + # We know a and r should correspond in both dictionaries. + a = qubit[0].id + r = relative_circuit[y].relative_qubit_idcs[0] + if a in abs_to_rel.keys(): + # If a in abs_to_rel, r will be in rel_to_abs + if abs_to_rel[a] != r: + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + if r in rel_to_abs.keys(): + if rel_to_abs[r] != a: + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + abs_to_rel[a] = r + rel_to_abs[r] = a + if not relative_commutable_circuits: + break + # HERE: we know the qubit idcs don't contradict our dictionaries. + for ctrl_qubit in next_command.control_qubits: + # We know a and r should correspond in both dictionaries. + a = ctrl_qubit.id + r = relative_circuit[y].relative_ctrl_idcs[0] + if a in abs_to_rel.keys(): + # If a in abs_to_rel, r will be in rel_to_abs + if abs_to_rel[a] != r: + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + if r in rel_to_abs.keys(): + if rel_to_abs[r] != a: + if relative_commutable_circuits: + relative_commutable_circuits.pop(0) + break + abs_to_rel[a] = r + rel_to_abs[r] = a + if not relative_commutable_circuits: + break + # HERE: we know all relative/absolute qubits/ctrl qubits do not + # contradict dictionaries and are assigned. + y += 1 + if y == len(relative_circuit): + # Up to the yth term in relative_circuit, we have checked + # that absolute_circuit[y] == relative_circuit[y] + # This means absolute_circuit is commutable + # with command_i + # Set x = x+len(relative_circuit)-1 and continue through + # while loop as though the list was a commutable gate + x += len(relative_circuit) + relative_commutable_circuits = [] + return x + return x + + def _optimize(self, idx, lim=None): + """ + Try to remove identity gates using the is_identity function, + then merge or even cancel successive gates using the get_merged and + get_inverse functions of the gate (see, e.g., BasicRotationGate). It does so for all qubit command lists. """ # loop over all qubit indices @@ -129,71 +420,84 @@ def _optimize(self, idx, lim=None): limit = lim while i < limit - 1: - # can be dropped if the gate is equivalent to an identity gate - if self._l[idx][i].is_identity(): - # determine index of this gate on all qubits - qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] - gid = self._get_gate_indices(idx, i, qubitids) - for j, qubit_id in enumerate(qubitids): - new_list = ( - self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 1 :] # noqa: E203 # noqa: E203 - ) - self._l[qubitids[j]] = new_list # pylint: disable=undefined-loop-variable + command_i = self._l[idx][i] + + # Delete command i if it is equivalent to identity + if command_i.is_identity(): + self._delete_command(idx, i) i = 0 limit -= 1 continue - # can be dropped if two in a row are self-inverses - inv = self._l[idx][i].get_inverse() - - if inv == self._l[idx][i + 1]: - # determine index of this gate on all qubits - qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] - gid = self._get_gate_indices(idx, i, qubitids) - # check that there are no other gates between this and its - # inverse on any of the other qubits involved - erase = True - for j, qubit_id in enumerate(qubitids): - erase *= inv == self._l[qubit_id][gid[j] + 1] - - # drop these two gates if possible and goto next iteration - if erase: - for j, qubit_id in enumerate(qubitids): - new_list = ( - self._l[qubit_id][0 : gid[j]] + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 # noqa: E203 - ) - self._l[qubit_id] = new_list - i = 0 - limit -= 2 + x = 0 + while i + x + 1 < limit: + # At this point: + # Gate i is commutable with each gate up to i+x, so + # check if i and i+x+1 can be cancelled or merged + inv = self._l[idx][i].get_inverse() + if inv == self._l[idx][i + x + 1]: + # List of the indices of the qubits that are involved + # in command + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] + # List of the command indices corresponding to the position + # of this command on each qubit id + commandidcs = self._get_gate_indices(idx, i, qubitids) + erase = self._can_cancel_by_commutation(idx, qubitids, commandidcs, inv, self._apply_commutation) + if erase: + # Delete the inverse commands. Delete the later + # one first so the first index doesn't + # change before you delete it. + self._delete_command(idx, i + x + 1) + self._delete_command(idx, i) + i = 0 + limit -= 2 + break + try: + merged_command = self._l[idx][i].get_merged(self._l[idx][i + x + 1]) + # determine index of this gate on all qubits + qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] + commandidcs = self._get_gate_indices(idx, i, qubitids) + merge = self._can_merge_by_commutation( + idx, qubitids, commandidcs, merged_command, self._apply_commutation + ) + if merge: + # Delete command i+x+1 first because i+x+1 + # will not affect index of i + self._delete_command(idx, i + x + 1) + self._replace_command(idx, i, merged_command) + i = 0 + limit -= 1 + break + except NotMergeable: + # Unsuccessful in merging, see if gates are commutable + pass + + # If apply_commutation=False, then we want the optimizer to + # ignore commutation when optimizing + if not self._apply_commutation: + break + command_i = self._l[idx][i] + next_command = self._l[idx][i + x + 1] + # ----------------------------------------------------------# + # See if next_command is commutable with this_command. # # + # ----------------------------------------------------------# + commutability_check = command_i.is_commutable(next_command) + if commutability_check == Commutability.COMMUTABLE: + x = x + 1 continue - # gates are not each other's inverses --> check if they're - # mergeable - try: - merged_command = self._l[idx][i].get_merged(self._l[idx][i + 1]) - # determine index of this gate on all qubits - qubitids = [qb.id for sublist in self._l[idx][i].all_qubits for qb in sublist] - gid = self._get_gate_indices(idx, i, qubitids) - - merge = True - for j, qubit_id in enumerate(qubitids): - merged = self._l[qubit_id][gid[j]].get_merged(self._l[qubit_id][gid[j] + 1]) - merge *= merged == merged_command - - if merge: - for j, qubit_id in enumerate(qubitids): - self._l[qubit_id][gid[j]] = merged_command - new_list = ( - self._l[qubit_id][0 : gid[j] + 1] # noqa: E203 - + self._l[qubit_id][gid[j] + 2 :] # noqa: E203 - ) - self._l[qubit_id] = new_list - i = 0 - limit -= 1 + # ----------------------------------------------------------# + # See if next_command is part of a circuit which is # + # commutable with this_command. # + # ----------------------------------------------------------# + new_x = 0 + if commutability_check == Commutability.MAYBE_COMMUTABLE: + new_x = self._check_for_commutable_circuit(command_i, next_command, idx, i, x) + if new_x > x: + x = new_x continue - except NotMergeable: - pass # can't merge these two commands. - + else: + break i += 1 # next iteration: look at next gate return limit diff --git a/projectq/cengines/_optimize_test.py b/projectq/cengines/_optimize_test.py index 72aa4656..c12ccaab 100755 --- a/projectq/cengines/_optimize_test.py +++ b/projectq/cengines/_optimize_test.py @@ -25,10 +25,24 @@ ClassicalInstructionGate, FastForwardingGate, H, + Measure, + Ph, + R, Rx, + Rxx, Ry, + Ryy, + Rz, + Rzz, + S, + SqrtX, + T, X, + XGate, + Y, + Z, ) +from projectq.setups import restrictedgateset, trapped_ion_decomposer def test_local_optimizer_init_api_change(): @@ -39,12 +53,12 @@ def test_local_optimizer_init_api_change(): local_optimizer = _optimize.LocalOptimizer() assert local_optimizer._cache_size == 5 - local_optimizer = _optimize.LocalOptimizer(cache_size=10) + local_optimizer = _optimize.LocalOptimizer(m=10, cache_size=10) assert local_optimizer._cache_size == 10 def test_local_optimizer_caching(): - local_optimizer = _optimize.LocalOptimizer(cache_size=4) + local_optimizer = _optimize.LocalOptimizer(m=4, cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it caches for each qubit 3 gates @@ -132,19 +146,108 @@ def test_local_optimizer_cancel_inverse(): assert received_commands[1].control_qubits[0].id == qb0[0].id +def test_local_optimizer_cancel_separated_inverse(): + """Tests the situation where the next command on + this qubit is an inverse command, but another qubit + involved is separated from the inverse by only commutable + gates. The two commands should cancel.""" + local_optimizer = _optimize.LocalOptimizer(m=5) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + assert len(backend.received_commands) == 0 + Rxx(math.pi) | (qb0, qb1) + Rx(0.3) | qb1 + Rxx(-math.pi) | (qb0, qb1) + assert len(backend.received_commands) == 0 + Measure | qb0 + Measure | qb1 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 1 + assert received_commands[0].gate == Rx(0.3) + assert received_commands[0].qubits[0][0].id == qb1[0].id + + def test_local_optimizer_mergeable_gates(): local_optimizer = _optimize.LocalOptimizer(cache_size=4) backend = DummyEngine(save_commands=True) eng = MainEngine(backend=backend, engine_list=[local_optimizer]) # Test that it merges mergeable gates such as Rx qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() for _ in range(10): Rx(0.5) | qb0 - assert len(backend.received_commands) == 0 + for _ in range(10): + Ry(0.5) | qb0 + for _ in range(10): + Rz(0.5) | qb0 + # Test merge for Rxx, Ryy, Rzz with interchangeable qubits + Rxx(0.5) | (qb0, qb1) + Rxx(0.5) | (qb1, qb0) + Ryy(0.5) | (qb0, qb1) + Ryy(0.5) | (qb1, qb0) + Rzz(0.5) | (qb0, qb1) + Rzz(0.5) | (qb1, qb0) eng.flush() - # Expect allocate, one Rx gate, and flush gate - assert len(backend.received_commands) == 3 - assert backend.received_commands[1].gate == Rx(10 * 0.5) + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + # Expect one gate each of Rx, Ry, Rz, Rxx, Ryy, Rzz + assert len(received_commands) == 6 + assert received_commands[0].gate == Rx(10 * 0.5) + assert received_commands[1].gate == Ry(10 * 0.5) + assert received_commands[2].gate == Rz(10 * 0.5) + assert received_commands[3].gate == Rxx(1.0) + assert received_commands[4].gate == Ryy(1.0) + assert received_commands[5].gate == Rzz(1.0) + + +def test_local_optimizer_separated_mergeable_gates(): + """Tests the situation where the next command on this qubit + is a mergeable command, but another qubit involved is separated + from the mergeable command by only commutable gates. + The commands should merge. + """ + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + # assert len(backend.received_commands) == 0 + # Reminder: Rxx and Rx commute + Rxx(0.3) | (qb0, qb1) + Rx(math.pi) | qb1 + Rxx(0.8) | (qb0, qb1) + Rx(0.3) | qb1 + Rxx(1.2) | (qb0, qb1) + Ry(0.5) | qb1 + H | qb0 + assert len(backend.received_commands) == 0 + Measure | qb0 + Measure | qb1 + assert len(backend.received_commands) == 8 + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[0].gate == Rxx(2.3) + assert received_commands[1].gate == H + assert received_commands[2].gate == Rx(math.pi + 0.3) + assert received_commands[3].gate == Ry(0.5) + assert received_commands[0].qubits[0][0].id == qb0[0].id + assert received_commands[0].qubits[1][0].id == qb1[0].id + assert received_commands[1].qubits[0][0].id == qb0[0].id + assert received_commands[2].qubits[0][0].id == qb1[0].id + assert received_commands[3].qubits[0][0].id == qb1[0].id def test_local_optimizer_identity_gates(): @@ -164,3 +267,548 @@ def test_local_optimizer_identity_gates(): # Expect allocate, one Rx gate, and flush gate assert len(backend.received_commands) == 3 assert backend.received_commands[1].gate == Rx(0.5) + + +@pytest.mark.parametrize(["U", "Ru", "Ruu"], [[X, Rx, Rxx], [Y, Ry, Ryy], [Z, Rz, Rzz]]) +def test_local_optimizer_commutable_gates_parameterized_1(U, Ru, Ruu): + """Iterate through gates of the X, Y, Z type and + check that they correctly commute with eachother and with Ph. + """ + local_optimizer = _optimize.LocalOptimizer(m=5) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + # Check U and commutes through Ru, Ruu. + # Check Ph commutes through U, Ru, Ruu. + U | qb0 + Ru(0.4) | qb0 + Ruu(0.4) | (qb0, qb1) + Ph(0.4) | qb0 + U | qb0 + # Check Ru commutes through U, Ruu, Ph + # (the first two Us should have cancelled already) + # We should now have a circuit: Ru(0.4), Ruu(0.4), Ph(0.4), U + U | qb0 + Ru(0.4) | qb0 + # Check Ruu commutes through U, Ru, Ph + # We should now have a circuit: Ru(0.8), Ruu(0.4), Ph(0.4), U + Ru(0.4) | qb0 + Ruu(0.4) | (qb0, qb1) + # Check Ph commutes through U, Ru, Ruu + # We should now have a circuit: Ru(0.8), Ruu(0.8), Ph(0.4), U, Ru(0.4) + Ruu(0.4) | (qb0, qb1) + Ph(0.4) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 4 + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +@pytest.mark.parametrize("C", [Z, S, T]) +def test_local_optimizer_commutable_gates_parameterized_2(U, C): + """Tests that the Rzz, Ph, Rz, R gates commute through S, T, Z.""" + local_optimizer = _optimize.LocalOptimizer(m=5) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + Rzz(0.4) | (qb0, qb1) + U(0.4) | qb0 + C | qb0 + U(0.4) | qb0 + Rzz(0.4) | (qb0, qb1) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 3 + # assert received_commands[0].gate == Ph(0.8) + + +def test_local_optimizer_commutable_gates_SqrtX(): + """Tests that the X, Rx, Rxx, Ph gates commute through SqrtX.""" + local_optimizer = _optimize.LocalOptimizer(m=5) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + X | qb0 + Rx(0.4) | qb0 + Rxx(0.4) | (qb0, qb1) + SqrtX | qb0 + X | qb0 + Rx(0.4) | qb0 + Rxx(0.4) | (qb0, qb1) + Ph(0.4) | qb0 + SqrtX | qb0 + Ph(0.4) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_1(U): + """Example circuit where the Rzs should merge.""" + # Rzs should merge + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[0].gate == U(0.3) + assert len(received_commands) == 4 + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_2(U): + """Us shouldn't merge (Although in theory they should, + this would require a new update.)""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb1 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb1 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[1].gate == U(0.1) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_3(U): + """Us should not merge because they are operating on different qubits.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb1 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[1].gate == U(0.1) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_4(U): + """Us shouldn't merge because they are operating on different qubits.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb1 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[0].gate == U(0.1) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_5(U): + """Us shouldn't merge because CNOT is the wrong orientation.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb0, qb1) + H | qb0 + U(0.2) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[0].gate == U(0.1) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_6(U): + """Us shouldn't merge because the circuit is in the wrong + orientation. (In theory Rz, R would merge because there is + only a control between them but this would require a new update.) + Ph is missed from this example because Ph does commute through. + """ + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb1 + CNOT | (qb0, qb1) + H | qb1 + U(0.2) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + print(cmd) + assert received_commands[0].gate == U(0.1) + assert len(received_commands) == 5 + + +@pytest.mark.parametrize("U", [Rz, R]) +def test_local_optimizer_commutable_circuit_U_example_7(U): + """Us shouldn't merge. Second H on wrong qubit.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb1, qb0) + H | qb1 + U(0.2) | qb0 + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert received_commands[0].gate == U(0.1) + assert len(received_commands) == 5 + + +def test_local_optimizer_commutable_circuit_CNOT_example_1(): + """This example should commute.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb2, qb0) + H | qb0 + CNOT | (qb0, qb1) + H | qb0 + CNOT | (qb2, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 3 + assert received_commands[0].gate == H + + +def test_local_optimizer_commutable_circuit_CNOT_example_2(): + """This example should commute.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb1, qb0) + H | qb0 + CNOT | (qb0, qb2) + H | qb0 + CNOT | (qb1, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 3 + assert received_commands[0].gate == H + + +def test_local_optimizer_commutable_circuit_CNOT_example_3(): + """This example should commute.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb0, qb1) + H | qb1 + CNOT | (qb1, qb2) + H | qb1 + CNOT | (qb0, qb1) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 3 + assert received_commands[0].gate == H + + +def test_local_optimizer_commutable_circuit_CNOT_example_4(): + """This example shouldn't commute because the CNOT is the + wrong orientation.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb1, qb0) + H | qb1 + CNOT | (qb1, qb2) + H | qb1 + CNOT | (qb1, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + assert received_commands[0].gate.__class__ == XGate + + +def test_local_optimizer_commutable_circuit_CNOT_example_5(): + """This example shouldn't commute because the CNOT is the + wrong orientation.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb0, qb1) + H | qb1 + CNOT | (qb1, qb2) + H | qb1 + CNOT | (qb1, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + assert received_commands[0].gate.__class__ == XGate + + +def test_local_optimizer_commutable_circuit_CNOT_example_6(): + """This example shouldn't commute because the CNOT is the + wrong orientation. Same as example_3 with middle CNOT reversed.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb0, qb1) + H | qb1 + CNOT | (qb2, qb1) + H | qb1 + CNOT | (qb0, qb1) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + assert received_commands[0].gate.__class__ == XGate + + +def test_local_optimizer_commutable_circuit_CNOT_example_7(): + """This example shouldn't commute because the CNOT is the + wrong orientation. Same as example_1 with middle CNOT reversed.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb2, qb0) + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + CNOT | (qb2, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + assert received_commands[0].gate.__class__ == XGate + + +def test_local_optimizer_commutable_circuit_CNOT_example_8(): + """This example shouldn't commute because the CNOT is the + wrong orientation. Same as example_2 with middle CNOT reversed.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + CNOT | (qb1, qb0) + H | qb0 + CNOT | (qb2, qb0) + H | qb0 + CNOT | (qb1, qb0) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 5 + assert received_commands[0].gate.__class__ == XGate + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_CNOT_and_U_example_1(U): + """This example is to check everything works as expected when + the commutable circuit is on later commands in the optimizer + dictionary. The number of commmands should reduce from 10 to 7.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb0 + CNOT | (qb0, qb1) + H | qb1 + CNOT | (qb1, qb2) + H | qb1 + CNOT | (qb0, qb1) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 7 + assert received_commands[6].gate == H + + +@pytest.mark.parametrize("U", [Ph, Rz, R]) +def test_local_optimizer_commutable_circuit_CNOT_and_U_example_2(U): + """This example is to check everything works as expected when + the commutable circuit is on qubits 3, 4, 5.""" + local_optimizer = _optimize.LocalOptimizer(m=10) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + qb2 = eng.allocate_qubit() + qb3 = eng.allocate_qubit() + qb4 = eng.allocate_qubit() + U(0.1) | qb0 + H | qb0 + CNOT | (qb1, qb0) + H | qb0 + U(0.2) | qb0 + CNOT | (qb2, qb3) + H | qb3 + CNOT | (qb3, qb4) + H | qb3 + CNOT | (qb2, qb3) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 7 + assert received_commands[6].gate == H + + +def test_local_optimizer_apply_commutation_false(): + """Test that the local_optimizer behaves as if commutation isn't an option + if you set apply_commutation = False.""" + local_optimizer = _optimize.LocalOptimizer(m=10, apply_commutation=False) + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[local_optimizer]) + qb0 = eng.allocate_qubit() + qb1 = eng.allocate_qubit() + Rz(0.1) | qb0 # Rzs next to eachother should merge + Rz(0.4) | qb0 + Rzz(0.3) | (qb0, qb1) # Rzs either side of Rzz should not merge + Rz(0.2) | qb0 + H | qb0 # Hs next to eachother should cancel + H | qb0 + Ry(0.1) | qb1 # Ry should not merge with the Rz on the other side of + H | qb0 # a commutable list + CNOT | (qb0, qb1) + H | qb0 + Ry(0.2) | qb1 + Rxx(0.2) | (qb0, qb1) + Rx(0.1) | qb1 # Rxxs either side of Rx shouldn't merge + Rxx(0.1) | (qb0, qb1) + eng.flush() + received_commands = [] + # Remove Allocate and Deallocate gates + for cmd in backend.received_commands: + if not (isinstance(cmd.gate, FastForwardingGate) or isinstance(cmd.gate, ClassicalInstructionGate)): + received_commands.append(cmd) + assert len(received_commands) == 11 + assert received_commands[0].gate == Rz(0.5) + assert received_commands[2].gate == Rz(0.2) + assert received_commands[4].gate == Ry(0.1) + assert received_commands[7].gate == Ry(0.2) + assert received_commands[10].gate == Rxx(0.1) diff --git a/projectq/cengines/_replacer/_decomposition_rule.py b/projectq/cengines/_replacer/_decomposition_rule.py index 24392fe2..c80a5d9c 100755 --- a/projectq/cengines/_replacer/_decomposition_rule.py +++ b/projectq/cengines/_replacer/_decomposition_rule.py @@ -14,7 +14,7 @@ """Module containing the definition of a decomposition rule.""" -from projectq.ops import BasicGate +from projectq.ops._basics import BasicGate, BasicGateMeta class ThisIsNotAGateClassError(TypeError): @@ -54,6 +54,8 @@ def __init__(self, gate_class, gate_decomposer, gate_recognizer=lambda cmd: True "\nDid you pass in someGate instead of someGate.__class__?" ) if gate_class == type.__class__: + "\nDid you pass in someGate instead of someGate.__class__?" + if gate_class in (type.__class__, BasicGateMeta): raise ThisIsNotAGateClassError( "gate_class is type.__class__ instead of a type of BasicGate." "\nDid you pass in GateType.__class__ instead of GateType?" diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 85469658..44b10e7d 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -39,7 +39,7 @@ from projectq.types import BasicQubit -from ._command import Command, apply_command +from ._command import Command, Commutability, apply_command ANGLE_PRECISION = 12 ANGLE_TOLERANCE = 10**-ANGLE_PRECISION @@ -63,8 +63,28 @@ class NotInvertible(Exception): """ -class BasicGate: - """Base class of all gates. (Don't use it directly but derive from it).""" +class BasicGateMeta(type): + """ + Meta class for all gates; mainly used to ensure that all gate classes have some basic class variable defined. + """ + + def __new__(cls, name, bases, attrs): + def get_commutable_gates(self): + return self._commutable_gates + + return super().__new__( + cls, name, bases, {**attrs, get_commutable_gates.__name__: get_commutable_gates, '_commutable_gates': set()} + ) + + +class BasicGate(metaclass=BasicGateMeta): + """ + A list of gates that commute with this gate class + """ + + """ + Base class of all gates. (Don't use it directly but derive from it) + """ def __init__(self): """ @@ -123,6 +143,21 @@ def get_merged(self, other): """ raise NotMergeable("BasicGate: No get_merged() implemented.") + def get_commutable_gates(self): + return [] + + def get_commutable_circuit_list(self, n=0): + """ + Args: + n (int): The CNOT gate needs to be able to pass in parameter n in + this method. + + Returns: + _commutable_circuit_list (list): the list of commutable circuits + associated with this gate. + """ + return [] + @staticmethod def make_tuple_of_qureg(qubits): """ @@ -234,6 +269,24 @@ def is_identity(self): """Return True if the gate is an identity gate. In this base class, always returns False.""" return False + def is_commutable(self, other): + """Determine whether this gate is commutable with + another gate. + + Args: + other (Gate): The other gate. + + Returns: + commutability (Commutability) : An enum which + indicates whether the next gate is commutable, + not commutable or maybe commutable. + """ + for gate in self.get_commutable_gates(): + if type(other) is gate: + return Commutability.COMMUTABLE + else: + return Commutability.NOT_COMMUTABLE + class MatrixGate(BasicGate): """ diff --git a/projectq/ops/_basics_test.py b/projectq/ops/_basics_test.py index 35b19986..cf25cbf3 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -14,13 +14,14 @@ """Tests for projectq.ops._basics.""" import math +import sys import numpy as np import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import Command, X, _basics +from projectq.ops import NOT, Command, X, _basics, _gates, _metagates from projectq.types import Qubit, Qureg, WeakQubitRef @@ -349,3 +350,19 @@ def test_matrix_gate(): assert X == gate3 assert str(gate3) == "MatrixGate([[0, 1], [1, 0]])" assert hash(gate3) == hash("MatrixGate([[0, 1], [1, 0]])") + + +def test_is_commutable(): + """At the gate level is_commutable can return + true or false. Test that is_commutable is working + as expected.""" + gate1 = _basics.BasicRotationGate(math.pi) + gate2 = _basics.MatrixGate() + gate3 = _basics.BasicRotationGate(math.pi) + assert not gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + gate4 = _gates.Rz(math.pi) + gate5 = _gates.H + gate6 = _metagates.C(NOT) + assert not gate4.is_commutable(gate5) + assert not gate4.is_commutable(gate6) diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py old mode 100755 new mode 100644 index 481efcc1..159864c2 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -116,6 +116,7 @@ def __init__( self.control_qubits = controls # property self.engine = engine # property self.control_state = control_state # property + self._commutable_circuit_list = self.gate.get_commutable_circuit_list(n=len(self.control_qubits)) # property @property def qubits(self): @@ -163,6 +164,27 @@ def is_identity(self): """ return projectq.ops.is_identity(self.gate) + def is_commutable(self, other): + """ + Evaluate if this command is commutable with another command. + + Args: + other (Command): The other command. + + Returns: + Commutability value (int) : value of the commutability enum + """ + if not overlap(self.all_qubits, other.all_qubits): + return Commutability.NOT_COMMUTABLE + self._commutable_circuit_list = self.gate.get_commutable_circuit_list(len(self.control_qubits)) + # If other gate may be part of a list which is + # commutable with gate, return enum MAYBE_COMMUTABLE + for circuit in self._commutable_circuit_list: + if type(other.gate) is type(circuit[0]._gate): + return Commutability.MAYBE_COMMUTABLE + else: + return self.gate.is_commutable(other.gate) + def get_merged(self, other): """ Merge this command with another one and return the merged command object. @@ -358,3 +380,31 @@ def to_string(self, symbols=False): qstring = f"{qstring[:-2]} )" cstring = "C" * len(ctrlqubits) return f"{cstring + self.gate.to_string(symbols)} | {qstring}" + + +def overlap(tuple1, tuple2): + """ + Takes two tuples of lists, flattens them and counts the number + of common elements. Used to check if two commands have qubits + or control qubits in common. + + i.e. command1.all_qubits = [[control_qubits], [qubits]] + command2.all_qubits = [[control_qubits], [qubits]] + overlap(command1, command2) = 4 + means command1 and command2 have 4 qubits or control + qubits in common. + + """ + flat_tuple1 = [item for sublist in tuple1 for item in sublist] + flat_tuple2 = [item for sublist in tuple2 for item in sublist] + n = 0 + for element in flat_tuple1: + if element in flat_tuple2: + n += 1 + return n + + +class Commutability(IntEnum): + NOT_COMMUTABLE = 0 + COMMUTABLE = 1 + MAYBE_COMMUTABLE = 2 diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index 19db3644..cc521f39 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -22,7 +22,22 @@ from projectq import MainEngine from projectq.cengines import DummyEngine from projectq.meta import ComputeTag, canonical_ctrl_state -from projectq.ops import BasicGate, CtrlAll, NotMergeable, Rx, _command +from projectq.ops import ( + CNOT, + BasicGate, + CtrlAll, + H, + NotMergeable, + Ph, + Rx, + Rxx, + Ry, + Rz, + SqrtX, + X, + _command, +) +from projectq.ops._command import Commutability from projectq.types import Qubit, Qureg, WeakQubitRef @@ -144,6 +159,54 @@ def test_command_is_identity(main_engine): assert not cmd2.gate.is_identity() +def test_overlap(): + """Test the overlap function is working as + expected.""" + tuple1 = ([1, 2], [3]) + tuple2 = ([2], [3, 0]) + tuple3 = ([0, 0, 0],) + assert _command.overlap(tuple1, tuple2) == 2 + assert _command.overlap(tuple1, tuple3) == 0 + + +def test_command_is_commutable(main_engine): + """Check is_commutable function returns 0, 1, 2 + For False, True and Maybe + 'Maybe' refers to the situation where you + might have a commutable circuit + CNOT's commutable circuit wont be recognised at this + level because CNOT.__gate__ = ControlledGate + whereas in the optimizer CNOT.__gate__ = XGate.""" + qubit1 = Qureg([Qubit(main_engine, 0)]) + qubit2 = Qureg([Qubit(main_engine, 1)]) + cmd1 = _command.Command(main_engine, Rx(0.5), (qubit1,)) + cmd2 = _command.Command(main_engine, Rx(0.5), (qubit1,)) + cmd3 = _command.Command(main_engine, Rx(0.5), (qubit2,)) + cmd4 = _command.Command(main_engine, Rxx(0.5), (qubit1, qubit2)) + cmd5 = _command.Command(main_engine, Ry(0.2), (qubit1,)) + cmd6 = _command.Command(main_engine, Rz(0.2), (qubit1,)) + cmd7 = _command.Command(main_engine, H, (qubit1,)) + cmd8 = _command.Command(main_engine, CNOT, (qubit2,), qubit1) + cmd9 = _command.Command(main_engine, X, (qubit1,)) + cmd10 = _command.Command(main_engine, SqrtX, (qubit1,)) + cmd11 = _command.Command(main_engine, Ph(math.pi), (qubit1,)) + assert not cmd1.is_commutable(cmd2) # Identical qubits, identical gate + assert _command.overlap(cmd1.all_qubits, cmd3.all_qubits) == 0 + assert not cmd1.is_commutable(cmd3) # Different qubits, same gate + assert cmd3.is_commutable(cmd4) # Qubits in common, different but commutable gates + assert not cmd4.is_commutable(cmd5) # Qubits in common, different, non-commutable gates + assert ( + cmd6.is_commutable(cmd7) == Commutability.MAYBE_COMMUTABLE.value + ) # Rz has a commutable circuit which starts with H + assert not cmd7.is_commutable(cmd8) # H does not have a commutable circuit which starts with CNOT + assert cmd1.is_commutable(cmd9) # Rx commutes with X + assert cmd9.is_commutable(cmd1) + assert cmd10.is_commutable(cmd9) # SqrtX commutes with X + assert cmd9.is_commutable(cmd10) + assert cmd11.is_commutable(cmd9) # Ph commutes with X + assert cmd9.is_commutable(cmd11) + + def test_command_order_qubits(main_engine): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) @@ -278,6 +341,12 @@ def test_command_comparison(main_engine): cmd6.tags = ["TestTag"] cmd6.add_control_qubits(ctrl_qubit) assert cmd6 != cmd1 + # Test not equal because of location of ctrl qubits + ctrl_qubit2 = ctrl_qubit = Qureg([Qubit(main_engine, 2)]) + cmd7 = _command.Command(main_engine, Rx(0.5), (qubit,)) + cmd7.tags = ["TestTag"] + cmd7.add_control_qubits(ctrl_qubit2) + assert not cmd7 == cmd1 def test_command_str(main_engine): diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py old mode 100755 new mode 100644 index 10527a86..a61d6e76 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -56,7 +56,8 @@ SelfInverseGate, ) from ._command import apply_command -from ._metagates import get_inverse +from ._metagates import C, get_inverse +from ._relative_command import RelativeCommand class HGate(SelfInverseGate): @@ -88,6 +89,26 @@ def matrix(self): """Access to the matrix property of this gate.""" return np.matrix([[0, 1], [1, 0]]) + def get_commutable_circuit_list(self, n=0): + """Sets _commutable_circuit_list for C(NOT, n) where + n is the number of controls + + Args: + n (int): The number of controls on this gate.""" + if n == 1: + # i.e. this is a CNOT gate (one control) + # We define the qubit with the NOT as qb0, the qubit with + # the control as qb1, then all next qbs are labelled 2,3 etc. + return [ + [ + RelativeCommand(H, (0,)), + RelativeCommand(C(NOT), (2,), relative_ctrl_idcs=(0,)), + RelativeCommand(H, (0,)), + ] + ] + else: + return [] # don't change _commutable_circuit_list + #: Shortcut (instance of) :class:`projectq.ops.XGate` X = NOT = XGate() @@ -298,6 +319,21 @@ def matrix(self): class Rz(BasicRotationGate): """RotationZ gate class.""" + def get_commutable_circuit_list(self, n=0): + """Sets _commutable_circuit_list for C(NOT, n) where + n is the number of controls + + Args: + n (int): The number of controls on this gate.""" + + return [ + [ + RelativeCommand(H, (0,)), + RelativeCommand(C(NOT), (0,), relative_ctrl_idcs=(1,)), + RelativeCommand(H, (0,)), + ], + ] + @property def matrix(self): """Access to the matrix property of this gate.""" @@ -312,6 +348,10 @@ def matrix(self): class Rxx(BasicRotationGate): """RotationXX gate class.""" + def __init__(self, angle): + BasicRotationGate.__init__(self, angle) + self.interchangeable_qubit_indices = [[0, 1]] + @property def matrix(self): """Access to the matrix property of this gate.""" @@ -328,6 +368,10 @@ def matrix(self): class Ryy(BasicRotationGate): """RotationYY gate class.""" + def __init__(self, angle): + BasicRotationGate.__init__(self, angle) + self.interchangeable_qubit_indices = [[0, 1]] + @property def matrix(self): """Access to the matrix property of this gate.""" @@ -344,6 +388,10 @@ def matrix(self): class Rzz(BasicRotationGate): """RotationZZ gate class.""" + def __init__(self, angle): + BasicRotationGate.__init__(self, angle) + self.interchangeable_qubit_indices = [[0, 1]] + @property def matrix(self): """Access to the matrix property of this gate.""" @@ -358,7 +406,21 @@ def matrix(self): class R(BasicPhaseGate): - """Phase-shift gate (equivalent to Rz up to a global phase).""" + """Phase-shift gate (equivalent to Rz up to a global phase)""" + + def get_commutable_circuit_list(self, n=0): + """Sets _commutable_circuit_list for C(NOT, n) where + n is the number of controls + + Args: + n (int): The number of controls on this gate.""" + return [ + [ + RelativeCommand(H, (0,)), + RelativeCommand(C(NOT), (0,), relative_ctrl_idcs=(1,)), + RelativeCommand(H, (0,)), + ], + ] @property def matrix(self): @@ -538,3 +600,20 @@ def __eq__(self, other): def __hash__(self): """Compute the hash of the object.""" return hash(str(self)) + + +# Define commutation relations + + +def set_commutation_relations(commuting_gates): + for klass in commuting_gates: + klass._commutable_gates.update(commuting_gates) + klass._commutable_gates.discard(klass) + + +set_commutation_relations([XGate, SqrtXGate, Rx, Rxx, Ph]) +set_commutation_relations([YGate, Ry, Ryy, Ph]) +set_commutation_relations([ZGate, SGate, TGate, Rz, Rzz, Ph, R]) + +for klass in [HGate, EntangleGate, SwapGate]: + set_commutation_relations([klass, Ph]) diff --git a/projectq/ops/_gates_test.py b/projectq/ops/_gates_test.py index ab431b92..7e9a4769 100755 --- a/projectq/ops/_gates_test.py +++ b/projectq/ops/_gates_test.py @@ -20,7 +20,18 @@ import pytest from projectq import MainEngine -from projectq.ops import All, FlipBits, Measure, _gates, get_inverse +from projectq.ops import ( + CNOT, + NOT, + All, + C, + FlipBits, + H, + Measure, + RelativeCommand, + _gates, + get_inverse, +) def test_h_gate(): @@ -28,16 +39,33 @@ def test_h_gate(): assert gate == gate.get_inverse() assert str(gate) == "H" assert np.array_equal(gate.matrix, 1.0 / math.sqrt(2) * np.matrix([[1, 1], [1, -1]])) + assert np.array_equal(gate.matrix, 1.0 / math.sqrt(2) * np.matrix([[1, 1], [1, -1]])) assert isinstance(_gates.H, _gates.HGate) def test_x_gate(): - gate = _gates.XGate() - assert gate == gate.get_inverse() - assert str(gate) == "X" - assert np.array_equal(gate.matrix, np.matrix([[0, 1], [1, 0]])) + gate1 = _gates.XGate() + gate2 = CNOT + assert gate1 == gate1.get_inverse() + assert str(gate1) == "X" + assert np.array_equal(gate1.matrix, np.matrix([[0, 1], [1, 0]])) assert isinstance(_gates.X, _gates.XGate) assert isinstance(_gates.NOT, _gates.XGate) + gate3 = _gates.X + gate4 = _gates.Rx(math.pi) + gate5 = _gates.Ph(math.pi) + gate6 = _gates.SqrtX + assert gate3.is_commutable(gate4) + assert gate3.is_commutable(gate5) + assert gate3.is_commutable(gate6) + assert gate3.get_commutable_circuit_list() == [] + cmd1 = RelativeCommand(H, (0,)) + cmd2 = RelativeCommand(C(NOT), (2,), relative_ctrl_idcs=(0,)) + cmd3 = RelativeCommand(H, (0,)) + correct_commutable_circuit_list = [ + [cmd1, cmd2, cmd3], + ] + assert gate2.commutable_circuit_list == correct_commutable_circuit_list def test_y_gate(): @@ -80,6 +108,10 @@ def test_sqrtx_gate(): assert np.array_equal(gate.matrix, np.matrix([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]])) assert np.array_equal(gate.matrix * gate.matrix, np.matrix([[0j, 1], [1, 0]])) assert isinstance(_gates.SqrtX, _gates.SqrtXGate) + gate1 = _gates.SqrtX + gate2 = _gates.X + assert gate1.is_commutable(gate2) + assert gate2.is_commutable(gate1) def test_swap_gate(): @@ -129,6 +161,19 @@ def test_rx(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_rx_commutation(angle1, angle2): + gate1 = _gates.Rx(angle1) + gate2 = _gates.Rxx(angle2) + gate3 = _gates.Ry(angle1) + gate4 = _gates.X + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + assert gate1.is_commutable(gate4) + assert gate4.is_commutable(gate1) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_ry(angle): gate = _gates.Ry(angle) @@ -142,6 +187,16 @@ def test_ry(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_ry_commutation(angle1, angle2): + gate1 = _gates.Ry(angle1) + gate2 = _gates.Ryy(angle2) + gate3 = _gates.Rz(angle1) + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rz(angle): gate = _gates.Rz(angle) @@ -150,6 +205,20 @@ def test_rz(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_rz_commutation(angle1, angle2): + # At the gate level is_commutable can only return 0 or 1 + # to indicate whether the two gates are commutable + # At the command level is_commutable can return 2, to + # to indicate a possible commutable_circuit + gate1 = _gates.Rz(angle1) + gate2 = _gates.Rzz(angle2) + gate3 = _gates.Rx(angle1) + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rxx(angle): gate = _gates.Rxx(angle) @@ -165,6 +234,18 @@ def test_rxx(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_rxx_commutation(angle1, angle2): + gate1 = _gates.Rxx(angle1) + gate2 = _gates.Rx(angle2) + gate3 = _gates.Ry(angle1) + gate4 = _gates.Rxx(0.0) + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + assert gate4.is_identity() + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_ryy(angle): gate = _gates.Ryy(angle) @@ -180,6 +261,18 @@ def test_ryy(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_ryy_commutation(angle1, angle2): + gate1 = _gates.Ryy(angle1) + gate2 = _gates.Ry(angle2) + gate3 = _gates.Rx(angle1) + gate4 = _gates.Ryy(0.0) + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + assert gate4.is_identity() + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rzz(angle): gate = _gates.Rzz(angle) @@ -195,6 +288,18 @@ def test_rzz(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle1", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +@pytest.mark.parametrize("angle2", [4.1, 2.1, 0.2, 0, 2 * math.pi, 4 * math.pi]) +def test_rzz_commutation(angle1, angle2): + gate1 = _gates.Rzz(angle1) + gate2 = _gates.Rz(angle2) + gate3 = _gates.Ry(angle1) + gate4 = _gates.Rzz(0.0) + assert gate1.is_commutable(gate2) + assert not gate1.is_commutable(gate3) + assert gate4.is_identity() + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi]) def test_ph(angle): gate = _gates.Ph(angle) @@ -207,6 +312,17 @@ def test_ph(angle): assert gate == gate2 +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi]) +@pytest.mark.parametrize( + "other_gate", + [_gates.X, _gates.Y, _gates.Z, _gates.Rx(1), _gates.Ry(1), _gates.Rz(1), _gates.R(1)], +) +def test_ph_commutation(angle, other_gate): + gate = _gates.Ph(angle) + assert gate.is_commutable(other_gate) + assert other_gate.is_commutable(gate) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi]) def test_r(angle): gate = _gates.R(angle) diff --git a/projectq/ops/_metagates.py b/projectq/ops/_metagates.py index 0c1f89f3..83c6a0ed 100644 --- a/projectq/ops/_metagates.py +++ b/projectq/ops/_metagates.py @@ -222,6 +222,10 @@ def __eq__(self, other): """Compare two ControlledGate objects (return True if equal).""" return isinstance(other, self.__class__) and self._gate == other._gate and self._n == other._n + @property + def commutable_circuit_list(self): + return self._gate.get_commutable_circuit_list(self._n) + def C(gate, n_qubits=1): """ diff --git a/projectq/ops/_relative_command.py b/projectq/ops/_relative_command.py new file mode 100644 index 00000000..1166f3b8 --- /dev/null +++ b/projectq/ops/_relative_command.py @@ -0,0 +1,118 @@ +# Copyright 2017 ProjectQ-Framework (www.projectq.ch) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This file defines the RelativeCommand class. + +RelativeCommand should be used to represent a Command +when there is no main_engine, for example within Gate +definitions. Using RelativeCommand we can define +commutable_circuits for a particular gate. + +Example: + +.. code-block:: python + + class Rz(BasicRotationGate): + def __init__(self, angle): + BasicRotationGate.__init__(self, angle) + self._commutable_gates = [ + Rzz, + ] + self._commutable_circuit_list = [ + [ + RelativeCommand(H, (0,)), + RelativeCommand(C(NOT), (0,), relative_ctrl_idcs=(1,)), + RelativeCommand(H, (0,)), + ], + ] + +The _commutable_circuit_list has been defined using RelativeCommand +objects. The is_commutable function defined in the Command class looks +for _commutable_circuits in the definition of its' gate. Then we can +check if there might be a commutable circuit coming after a Command. +This is used in the _check_for_commutable_circuit function and in the +LocalOptimizer in _optimize.py + + cmd1 = _command.Command(main_engine, Rz(0.2), (qubit1,)) + cmd2 = _command.Command(main_engine, H, (qubit1,)) + if (cmd1.is_commutable(cmd2) == 2): + # Check for a commutable circuit + +Rz has a commutable circuit which starts with H. If is_commutable returns '2' +it indicates that the next command may be the start of a commutable circuit. + +""" + +from projectq.ops._metagates import ControlledGate + + +class RelativeCommand: + """Alternative representation of a Command. + + Class: + Used to represent commands when there is no engine. + i.e. in the definition of a relative commutable_circuit + within a gate class. + + Attributes: + gate: The gate class. + _gate: The true gate class if gate is a metagate. + relative_qubit_idcs: Tuple of integers, representing the + relative qubit idcs in a commutable_circuit. + relative_ctrl_idcs: Tuple of integers, representing the + relative control qubit idcs in a commutable_circuit. + """ + + def __init__(self, gate, relative_qubit_idcs, relative_ctrl_idcs=()): + self.gate = gate + self.relative_qubit_idcs = relative_qubit_idcs + self.relative_ctrl_idcs = relative_ctrl_idcs + self._gate = self.gate._gate if isinstance(self.gate, ControlledGate) else gate + + def __str__(self): + return self.to_string() + + def __eq__(self, other): + return self.equals(other) + + def to_string(self, symbols=False): + """ + Get string representation of this Command object. + """ + print('i should not exist') + qubits = self.relative_qubit_idcs + ctrlqubits = self.relative_ctrl_idcs + if len(ctrlqubits) > 0: + qubits = (self.relative_ctrl_idcs,) + qubits + qstring = "" + if len(qubits) == 1: + qstring = str(qubits) + else: + qstring = "( " + for qreg in range(len(qubits)): + qstring += str(qubits[qreg]) + qstring += ", " + qstring = qstring[:-2] + " )" + return self.gate.to_string(symbols) + " | " + qstring + + def equals(self, other): + if ( + (type(self.gate) is type(other.gate)) + and (self.relative_qubit_idcs == other.relative_qubit_idcs) + and (self.relative_ctrl_idcs == other.relative_ctrl_idcs) + and (type(self._gate) is type(self._gate)) + ): + return True + else: + return False diff --git a/projectq/ops/_relative_command_test.py b/projectq/ops/_relative_command_test.py new file mode 100644 index 00000000..a6ed9281 --- /dev/null +++ b/projectq/ops/_relative_command_test.py @@ -0,0 +1,18 @@ +from projectq.ops import CNOT, H, RelativeCommand, _basics + + +def test_relative_command_equals(): + cmd1 = RelativeCommand(H, 0) + cmd2 = RelativeCommand(H, 0) + cmd3 = RelativeCommand(H, 1) + cmd4 = RelativeCommand(CNOT, 0, 1) + cmd5 = RelativeCommand(CNOT, 0, 1) + cmd6 = RelativeCommand(CNOT, 1, 0) + cmd7 = RelativeCommand(CNOT, 0, 2) + cmd8 = RelativeCommand(CNOT, 2, 1) + assert cmd1 == cmd2 + assert cmd1 != cmd3 + assert cmd4 == cmd5 + assert cmd4 != cmd6 + assert cmd4 != cmd7 + assert cmd4 != cmd8 diff --git a/projectq/setups/restrictedgateset.py b/projectq/setups/restrictedgateset.py index 202bb990..be58f8ea 100644 --- a/projectq/setups/restrictedgateset.py +++ b/projectq/setups/restrictedgateset.py @@ -46,6 +46,7 @@ def get_engine_list( # pylint: disable=too-many-branches,too-many-statements two_qubit_gates=(CNOT,), other_gates=(), compiler_chooser=default_chooser, + apply_commutation=True, ): """ Return an engine list to compile to a restricted gate set. @@ -63,7 +64,9 @@ def get_engine_list( # pylint: disable=too-many-branches,too-many-statements Example: get_engine_list(one_qubit_gates=(Rz, Ry, Rx, H), two_qubit_gates=(CNOT,), - other_gates=(TimeEvolution,)) + other_gates=(TimeEvolution,) + compiler_chooser=chooser_Ry_reducer, + apply_commutation=True) Args: one_qubit_gates: "any" allows any one qubit gate, otherwise provide a tuple of the allowed gates. If the gates @@ -76,8 +79,9 @@ def get_engine_list( # pylint: disable=too-many-branches,too-many-statements Default is (CNOT,). other_gates: A tuple of the allowed gates. If the gates are instances of a class (e.g. QFT), it allows all gates which are equal to it. If the gate is a class, it allows all instances of this class. - compiler_chooser:function selecting the decomposition to use in the Autoreplacer engine - + compiler_chooser:function selecting the decomposition to use in the Autoreplacer engine. + apply_commutation: tells the LocalOptimizer engine whether to consider + commutation rules during optimization. Raises: TypeError: If input is for the gates is not "any" or a tuple. Also if element within tuple is not a class or instance of BasicGate (e.g. CRz which is a shortcut function) @@ -176,13 +180,13 @@ def low_level_gates(eng, cmd): # pylint: disable=unused-argument,too-many-retur AutoReplacer(rule_set, compiler_chooser), TagRemover(), InstructionFilter(high_level_gates), - LocalOptimizer(5), + LocalOptimizer(5, apply_commutation=apply_commutation), AutoReplacer(rule_set, compiler_chooser), TagRemover(), InstructionFilter(one_and_two_qubit_gates), - LocalOptimizer(5), + LocalOptimizer(5, apply_commutation=apply_commutation), AutoReplacer(rule_set, compiler_chooser), TagRemover(), InstructionFilter(low_level_gates), - LocalOptimizer(5), + LocalOptimizer(5, apply_commutation=apply_commutation), ]