def test_are_identical_pauli_words(self): """Tests for determining if two Pauli words have the same ``wires`` and ``name`` attributes.""" pauli_word_1 = PauliX(0) @ PauliY(1) pauli_word_2 = PauliY(1) @ PauliX(0) pauli_word_3 = Tensor(PauliX(0), PauliY(1)) pauli_word_4 = PauliX(1) @ PauliZ(2) assert are_identical_pauli_words(pauli_word_1, pauli_word_2) assert are_identical_pauli_words(pauli_word_1, pauli_word_3) assert not are_identical_pauli_words(pauli_word_1, pauli_word_4) assert not are_identical_pauli_words(pauli_word_3, pauli_word_4)
def test_binary_to_pauli_with_wire_map(self, vec, op): """Test conversion of Pauli in binary vector representation to operator form when ``wire_map`` is specified.""" wire_map = {"alice": 0, "bob": 1, "ancilla": 2} assert are_identical_pauli_words( binary_to_pauli(vec, wire_map=wire_map), op)
def pauli_mult(pauli_1, pauli_2, wire_map=None): """Multiply two Pauli words together and return the product as a Pauli word. Two Pauli operations can be multiplied together by taking the additive OR of their binary symplectic representations. Args: pauli_1 (.Operation): A Pauli word. pauli_2 (.Operation): A Pauli word to multiply with the first one. wire_map (dict[Union[str, int], int]): dictionary containing all wire labels used in the Pauli word as keys, and unique integer labels as their values. If no wire map is provided, the map will be constructed from the set of wires acted on by the input Pauli words. Returns: .Operation: The product of pauli_1 and pauli_2 as a Pauli word (ignoring the global phase). **Example** This function enables multiplication of Pauli group elements at the level of Pauli words, rather than matrices. For example, >>> from pennylane.grouping import pauli_mult >>> pauli_1 = qml.PauliX(0) @ qml.PauliZ(1) >>> pauli_2 = qml.PauliY(0) @ qml.PauliZ(1) >>> product = pauli_mult(pauli_1, pauli_2) >>> print(product) PauliZ(wires=[0]) """ if wire_map is None: wire_map = _wire_map_from_pauli_pair(pauli_1, pauli_2) # Check if pauli_1 and pauli_2 are the same; if so, the result is the Identity if are_identical_pauli_words(pauli_1, pauli_2): first_wire = list(pauli_1.wires)[0] return Identity(first_wire) # Compute binary symplectic representations pauli_1_binary = pauli_to_binary(pauli_1, wire_map=wire_map) pauli_2_binary = pauli_to_binary(pauli_2, wire_map=wire_map) bin_symp_1 = np.array([int(x) for x in pauli_1_binary]) bin_symp_2 = np.array([int(x) for x in pauli_2_binary]) # Shorthand for bitwise XOR of numpy arrays pauli_product = bin_symp_1 ^ bin_symp_2 return binary_to_pauli(pauli_product, wire_map=wire_map)
def test_are_identical_pauli_words_non_pauli_word_catch( self, non_pauli_word): """Tests TypeError raise for when non-Pauli word Pennylane operators/operations are given as input to are_identical_pauli_words.""" with pytest.raises(TypeError): are_identical_pauli_words(non_pauli_word, PauliZ(0) @ PauliZ(1)) with pytest.raises(TypeError): are_identical_pauli_words(non_pauli_word, PauliZ(0) @ PauliZ(1)) with pytest.raises(TypeError): are_identical_pauli_words( PauliX("a") @ Identity("b"), non_pauli_word) with pytest.raises(TypeError): are_identical_pauli_words(non_pauli_word, non_pauli_word)
def test_diagonalize_qwc_pauli_words(self, qwc_grouping, qwc_sol_tuple): """Tests for validating diagonalize_qwc_pauli_words solutions.""" qwc_rot, diag_qwc_grouping = diagonalize_qwc_pauli_words(qwc_grouping) qwc_rot_sol, diag_qwc_grouping_sol = qwc_sol_tuple assert all([ self.are_identical_rotation_gates(qwc_rot[i], qwc_rot_sol[i]) for i in range(len(qwc_rot)) ]) assert all([ are_identical_pauli_words(diag_qwc_grouping[i], diag_qwc_grouping_sol[i]) for i in range(len(diag_qwc_grouping)) ])
def test_commuting_partitioning(self, observables, com_partitions_sol): com_partitions = group_observables(observables, grouping_type="commuting") # assert the correct number of partitions: n_partitions = len(com_partitions_sol) assert len(com_partitions) == n_partitions # assert each partition is of the correct length: assert all( [len(com_partitions[i]) == len(com_partitions_sol[i]) for i in range(n_partitions)] ) # assert each partition contains the same Pauli terms as the solution partition: for i, partition in enumerate(com_partitions): for j, pauli in enumerate(partition): assert are_identical_pauli_words(pauli, com_partitions_sol[i][j])
def test_optimize_measurements_qwc_generic_case( self, observables, diagonalized_groupings_sol): """Generic test cases without coefficients.""" diagonalized_groupings = optimize_measurements( observables, grouping="qwc", colouring_method="rlf")[1] # assert the correct number of partitions: n_partitions = len(diagonalized_groupings_sol) assert len(diagonalized_groupings) == n_partitions # assert each partition is of the correct length: assert all([ len(diagonalized_groupings[i]) == len( diagonalized_groupings_sol[i]) for i in range(n_partitions) ]) # assert each partition contains the same Pauli terms as the solution partition: for i, partition in enumerate(diagonalized_groupings): for j, pauli in enumerate(partition): assert are_identical_pauli_words( pauli, diagonalized_groupings_sol[i][j])
def diagonalize_qwc_pauli_words(qwc_grouping): """Diagonalizes a list of mutually qubit-wise commutative Pauli words. Args: qwc_grouping (list[Observable]): a list of observables containing mutually qubit-wise commutative Pauli words Returns: tuple: * list[Operation]: an instance of the qwc_rotation template which diagonalizes the qubit-wise commuting grouping * list[Observable]: list of Pauli string observables diagonal in the computational basis Raises: ValueError: if any 2 elements in the input QWC grouping are not qubit-wise commutative **Example** >>> qwc_group = [qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliY(3), qml.PauliZ(1) @ qml.PauliY(3)] >>> diagonalize_qwc_pauli_words(qwc_group) ([RY(-1.5707963267948966, wires=[0]), RX(1.5707963267948966, wires=[3])], [Tensor(PauliZ(wires=[0]), PauliZ(wires=[1])), Tensor(PauliZ(wires=[0]), PauliZ(wires=[3])), Tensor(PauliZ(wires=[1]), PauliZ(wires=[3]))]) """ m_paulis = len(qwc_grouping) all_wires = Wires.all_wires( [pauli_word.wires for pauli_word in qwc_grouping]) wire_map = {label: ind for ind, label in enumerate(all_wires)} for i in range(m_paulis): for j in range(i + 1, m_paulis): if not is_qwc( pauli_to_binary(qwc_grouping[i], wire_map=wire_map), pauli_to_binary(qwc_grouping[j], wire_map=wire_map), ): raise ValueError( "{} and {} are not qubit-wise commuting.".format( qwc_grouping[i], qwc_grouping[j])) pauli_operators = [] diag_terms = [] paulis_with_identity = (qml.PauliX, qml.PauliY, qml.PauliZ, qml.Identity) for term in qwc_grouping: diag_terms.append(diagonalize_pauli_word(term)) if isinstance(term, Tensor): for sigma in term.obs: if sigma.name != "Identity": if not any([ are_identical_pauli_words(sigma, existing_pauli) for existing_pauli in pauli_operators ]): pauli_operators.append(sigma) elif isinstance(term, paulis_with_identity): sigma = term if sigma.name != "Identity": if not any([ are_identical_pauli_words(sigma, existing_pauli) for existing_pauli in pauli_operators ]): pauli_operators.append(sigma) unitary = qwc_rotation(pauli_operators) return unitary, diag_terms
def test_binary_to_pauli_no_wire_map(self, vec, op): """Test conversion of Pauli in binary vector representation to operator form when no ``wire_map`` is specified.""" assert are_identical_pauli_words(binary_to_pauli(vec), op)
def group_observables(observables, coefficients=None, grouping_type="qwc", method="rlf"): """Partitions a list of observables (Pauli operations and tensor products thereof) into groupings according to a binary relation (qubit-wise commuting, fully commuting, or anticommuting). Partitions are found by 1) mapping the list of observables to a graph where vertices represent observables and edges encode the binary relation, then 2) solving minimum clique cover for the graph using graph-coloring heuristic algorithms. **Example usage:** >>> observables = [qml.PauliY(0), qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(1)] >>> coefficients = [1.43, 4.21, 0.97] >>> obs_groupings, coeffs_groupings = group_observables( observables, coefficients, 'anticommuting', 'lf') >>> obs_groupings [[Tensor(PauliZ(wires=[1])), Tensor(PauliX(wires=[0]), PauliX(wires=[1]))], [Tensor(PauliY(wires=[0]))]] >>> coeffs_groupings [[0.97, 4.21], [1.43]] Args: observables (list[Observable]): a list of Pauli word `Observable` instances (Pauli operation instances and Tensor instances thereof) Keyword args: coefficients (list[scalar]): A list of scalar coefficients. If not specified, output `partitioned_coeffs` is not returned. grouping_type (str): The type of binary relation between Pauli words. Can be 'qwc', 'commuting', or 'anticommuting'. method (str): the graph coloring heuristic to use in solving minimum clique cover, which can be 'lf' (Largest First) or 'rlf' (Recursive Largest First) Returns: partitioned_paulis (list[list[Observable]]): A list of the obtained groupings. Each grouping is itself a list of Pauli word `Observable` instances. partitioned_coeffs (list[list[scalar]]): A list of coefficient groupings. Each coefficient grouping is itself a list of the grouping's corresponding coefficients. This is only output if coefficients are specified. Raises: IndexError: if the input list of coefficients is not of the same length as the input list of Pauli words """ if coefficients is not None: if len(coefficients) != len(observables): raise IndexError( "The coefficients list must be the same length as the observables list." ) pauli_grouping = PauliGroupingStrategy(observables, grouping_type=grouping_type, graph_colourer=method) partitioned_paulis = pauli_grouping.colour_pauli_graph() if coefficients is None: return partitioned_paulis partitioned_coeffs = [[0] * len(g) for g in partitioned_paulis] for i, partition in enumerate(partitioned_paulis): for j, pauli_word in enumerate(partition): for observable in observables: if are_identical_pauli_words(pauli_word, observable): partitioned_coeffs[i][j] = coefficients[observables.index( observable)] break return partitioned_paulis, partitioned_coeffs
def test_diagonalize_pauli_word(self, pauli_word, diag_pauli_word): """Tests `diagonalize_pauli_word` returns the correct diagonal Pauli word in computational basis for a given Pauli word.""" assert are_identical_pauli_words(diagonalize_pauli_word(pauli_word), diag_pauli_word)
def group_observables(observables, coefficients=None, grouping_type="qwc", method="rlf"): """Partitions a list of observables (Pauli operations and tensor products thereof) into groupings according to a binary relation (qubit-wise commuting, fully-commuting, or anticommuting). Partitions are found by 1) mapping the list of observables to a graph where vertices represent observables and edges encode the binary relation, then 2) solving minimum clique cover for the graph using graph-colouring heuristic algorithms. Args: observables (list[Observable]): a list of Pauli word ``Observable`` instances (Pauli operation instances and :class:`~.Tensor` instances thereof) coefficients (tensor_like): A tensor or list of coefficients. If not specified, output ``partitioned_coeffs`` is not returned. grouping_type (str): The type of binary relation between Pauli words. Can be ``'qwc'``, ``'commuting'``, or ``'anticommuting'``. method (str): the graph coloring heuristic to use in solving minimum clique cover, which can be ``'lf'`` (Largest First) or ``'rlf'`` (Recursive Largest First) Returns: tuple: * list[list[Observable]]: A list of the obtained groupings. Each grouping is itself a list of Pauli word ``Observable`` instances. * list[tensor_like]: A list of coefficient groupings. Each coefficient grouping is itself a tensor or list of the grouping's corresponding coefficients. This is only returned if coefficients are specified. Raises: IndexError: if the input list of coefficients is not of the same length as the input list of Pauli words **Example** >>> obs = [qml.PauliY(0), qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(1)] >>> coeffs = [1.43, 4.21, 0.97] >>> obs_groupings, coeffs_groupings = group_observables(obs, coeffs, 'anticommuting', 'lf') >>> obs_groupings [[PauliZ(wires=[1]), PauliX(wires=[0]) @ PauliX(wires=[1])], [PauliY(wires=[0])]] >>> coeffs_groupings [[0.97, 4.21], [1.43]] """ if coefficients is not None: if qml.math.shape(coefficients)[0] != len(observables): raise IndexError( "The coefficients list must be the same length as the observables list." ) pauli_grouping = PauliGroupingStrategy( observables, grouping_type=grouping_type, graph_colourer=method ) partitioned_paulis = pauli_grouping.colour_pauli_graph() if coefficients is None: return partitioned_paulis partitioned_coeffs = [ qml.math.cast_like([0] * len(g), coefficients) for g in partitioned_paulis ] observables = copy(observables) # we cannot delete elements from the coefficients tensor, so we # use a proxy list memorising the indices for this logic coeff_indices = list(range(qml.math.shape(coefficients)[0])) for i, partition in enumerate(partitioned_paulis): indices = [] for pauli_word in partition: # find index of this pauli word in remaining original observables, for observable in observables: if are_identical_pauli_words(pauli_word, observable): ind = observables.index(observable) indices.append(coeff_indices[ind]) observables.pop(ind) coeff_indices.pop(ind) break # add a tensor of coefficients to the grouped coefficients partitioned_coeffs[i] = qml.math.take(coefficients, indices, axis=0) # make sure the output is of the same format as the input # for these two frequent cases if isinstance(coefficients, list): partitioned_coeffs = [list(p) for p in partitioned_coeffs] return partitioned_paulis, partitioned_coeffs