From 127bcb4fcc647beb0968e241c3b610a2e187c25b Mon Sep 17 00:00:00 2001 From: Shih-Cheng Tu Date: Sun, 7 Jun 2026 14:49:08 +0800 Subject: [PATCH 1/4] Add sparse_matrix() to PauliString and PauliSum For each PauliString term, row/col indices and phases are computed directly via bitwise ops on basis states to avoid Kron product. For PauliSum, uses COO triplet accumulation instead of repeated CSR addition to avoid merging multiple sparse matrices. --- cirq-core/cirq/ops/linear_combinations.py | 46 ++++++++++++++-- .../cirq/ops/linear_combinations_test.py | 34 ++++++++++++ cirq-core/cirq/ops/pauli_string.py | 52 +++++++++++++++++++ cirq-core/cirq/ops/pauli_string_test.py | 7 +++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/ops/linear_combinations.py b/cirq-core/cirq/ops/linear_combinations.py index 584833ea4b4..9f5e025bb0a 100644 --- a/cirq-core/cirq/ops/linear_combinations.py +++ b/cirq-core/cirq/ops/linear_combinations.py @@ -21,6 +21,7 @@ import numpy as np import sympy +from scipy import sparse from sympy.logic.boolalg import And, Not, Or, Xor from cirq import linalg, protocols, qis, value @@ -32,8 +33,6 @@ from cirq.value.linear_dict import _format_terms if TYPE_CHECKING: - from scipy.sparse import csr_matrix - import cirq UnitPauliStringT = frozenset[tuple[raw_types.Qid, pauli_gates.Pauli]] @@ -589,6 +588,47 @@ def matrix(self, qubits: Iterable[raw_types.Qid] | None = None) -> np.ndarray: result += coeff * op.matrix(qubits) return result + def sparse_matrix(self, qubits: Iterable[raw_types.Qid] | None = None) -> sparse.csr_matrix: + """Returns the sparse matrix of this PauliSum in computational basis of qubits. + + Uses direct bit-manipulation for each term and accumulates COO triplets + to avoid repeated sparse matrix addition overhead. + + Args: + qubits: Ordered collection of qubits that determine the subspace + in which the matrix representation of the Pauli sum is to + be computed. If none is provided the default ordering of + `self.qubits` is used. Qubits present in `qubits` but absent from + `self.qubits` are acted on by the identity. + + Returns: + A scipy.sparse.csr_matrix representing the Pauli sum. + """ + qubits = self.qubits if qubits is None else tuple(qubits) + num_qubits = len(qubits) + dim = 2**num_qubits + all_data: list[np.ndarray] = [] + all_rows: list[np.ndarray] = [] + all_cols: list[np.ndarray] = [] + + for vec, coeff in self._linear_dict.items(): + op = _pauli_string_from_unit(vec) + term = coeff * op.sparse_matrix(qubits) + coo = term.tocoo() + all_data.append(coo.data) + all_rows.append(coo.row) + all_cols.append(coo.col) + + if not all_data: + return sparse.csr_matrix((dim, dim), dtype=np.complex128) + + data = np.concatenate(all_data) + rows = np.concatenate(all_rows) + cols = np.concatenate(all_cols) + result = sparse.coo_matrix((data, (rows, cols)), shape=(dim, dim)).tocsr() + result.eliminate_zeros() + return result + def _has_unitary_(self) -> bool: return linalg.is_unitary(self.matrix()) @@ -927,7 +967,7 @@ def from_projector_strings(cls, terms: ProjectorString | list[ProjectorString]) def copy(self) -> ProjectorSum: return ProjectorSum(self._linear_dict.copy()) - def matrix(self, projector_qids: Iterable[raw_types.Qid] | None = None) -> csr_matrix: + def matrix(self, projector_qids: Iterable[raw_types.Qid] | None = None) -> sparse.csr_matrix: """Returns the matrix of self in computational basis of qubits. Args: diff --git a/cirq-core/cirq/ops/linear_combinations_test.py b/cirq-core/cirq/ops/linear_combinations_test.py index 437966b8d7e..8c2e7b710e0 100644 --- a/cirq-core/cirq/ops/linear_combinations_test.py +++ b/cirq-core/cirq/ops/linear_combinations_test.py @@ -1078,6 +1078,40 @@ def test_pauli_sum_matrix() -> None: assert np.allclose(H3, paulisum.matrix([q[1], q[2], q[0]])) +def test_pauli_sum_sparse_matrix() -> None: + q = cirq.LineQubit.range(3) + paulisum = cirq.X(q[0]) * cirq.X(q[1]) + cirq.Z(q[0]) + H1 = np.array( + [[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 1.0, 0.0], [0.0, 1.0, -1.0, 0.0], [1.0, 0.0, 0.0, -1.0]] + ) + # Default qubit ordering. + assert np.allclose(H1, paulisum.sparse_matrix().toarray()) + # Explicit 2-qubit ordering (same as default). + assert np.allclose(H1, paulisum.sparse_matrix([q[0], q[1]]).toarray()) + # Reversed qubit ordering changes the matrix. + H2 = np.array( + [[1.0, 0.0, 0.0, 1.0], [0.0, -1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 0.0], [1.0, 0.0, 0.0, -1.0]] + ) + assert np.allclose(H2, paulisum.sparse_matrix([q[1], q[0]]).toarray()) + # Adding an extra idle qubit expands to 8x8. + H3 = np.array( + [ + [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0], + ] + ) + assert np.allclose(H3, paulisum.sparse_matrix([q[1], q[2], q[0]]).toarray()) + # Empty PauliSum should return a zero sparse matrix. + empty = cirq.PauliSum.from_pauli_strings([]) + assert np.allclose(empty.sparse_matrix([q[0], q[1]]).toarray(), np.zeros((4, 4))) + + def test_pauli_sum_repr() -> None: q = cirq.LineQubit.range(2) pstr1 = cirq.X(q[0]) * cirq.X(q[1]) diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index 99b32631a0d..f365b89229c 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -33,6 +33,7 @@ import numpy as np import sympy +from scipy import sparse from cirq import _compat, linalg, protocols, qis, value from cirq._compat import deprecated @@ -469,6 +470,57 @@ def matrix(self, qubits: Iterable[TKey] | None = None) -> np.ndarray: assert isinstance(self.coefficient, complex) return linalg.kron(self.coefficient, *[protocols.unitary(f) for f in factors]) + def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matrix: + """Returns the sparse matrix of self in computational basis of qubits. + + Uses a direct bit-manipulation algorithm that avoids Kronecker products + by computing row/col indices and phases for each basis state directly. + + Args: + qubits: Ordered collection of qubits that determine the subspace + in which the matrix representation of the Pauli string is to + be computed. Qubits absent from `self.qubits` are acted on by + the identity. Defaults to `self.qubits`. + + Returns: + A scipy.sparse.csr_matrix representing the Pauli string. + + Raises: + NotImplementedError: If this PauliString is parameterized. + """ + qubits = self.qubits if qubits is None else tuple(qubits) + if protocols.is_parameterized(self): + raise NotImplementedError('Cannot express as matrix when parameterized') + assert isinstance(self.coefficient, complex) + + n = len(qubits) + dim = 1 << n + qubit_to_idx = {q: i for i, q in enumerate(qubits)} + + x_mask = y_mask = z_mask = 0 + for q in qubits: + pauli = self.get(q) + if pauli is None: + continue + idx = qubit_to_idx[q] + bit = 1 << (n - 1 - idx) + if pauli is pauli_gates.X: + x_mask |= bit + elif pauli is pauli_gates.Y: + y_mask |= bit + elif pauli is pauli_gates.Z: + z_mask |= bit + + cols = np.arange(dim, dtype=np.int32) + rows = cols ^ x_mask ^ y_mask + + num_y = y_mask.bit_count() + y_phase = (1j**num_y) * ((-1.0) ** np.bitwise_count(cols & y_mask)) + z_phase = (-1.0) ** np.bitwise_count(cols & z_mask) + data = self.coefficient * y_phase * z_phase + + return sparse.coo_matrix((data, (rows, cols)), shape=(dim, dim)).tocsr() + def _has_unitary_(self) -> bool: if self._is_parameterized_(): return False diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 52ad4b0a6c0..99fe3c17e17 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -854,6 +854,11 @@ def test_matrix(pauli_string, qubits, expected_matrix) -> None: assert np.allclose(pauli_string.matrix(qubits), expected_matrix) +@pytest.mark.parametrize('pauli_string, qubits, expected_matrix', _pauli_string_matrix_cases()) +def test_sparse_matrix(pauli_string, qubits, expected_matrix) -> None: + assert np.allclose(pauli_string.sparse_matrix(qubits).toarray(), expected_matrix) + + def test_unitary_matrix() -> None: a, b = cirq.LineQubit.range(2) assert not cirq.has_unitary(2 * cirq.X(a) * cirq.Z(b)) @@ -2050,6 +2055,8 @@ def test_parameterization() -> None: pst.expectation_from_density_matrix(np.array([]), {}) with pytest.raises(NotImplementedError, match='as matrix when parameterized'): pst.matrix() + with pytest.raises(NotImplementedError, match='as matrix when parameterized'): + pst.sparse_matrix() assert pst**1 == pst assert pst**-1 == pst.with_coefficient(1.0 / t) assert (-pst) ** 1 == -pst From e4e84aeca33de5e89ff57fa728a2c27a8ee8846f Mon Sep 17 00:00:00 2001 From: Shih-Cheng Tu Date: Wed, 10 Jun 2026 21:43:17 +0800 Subject: [PATCH 2/4] Fix comment format and wording in sparse_matrix implementations In addition, update implementation details: - use np.where parity check for phases - iterate self.items() directly --- cirq-core/cirq/ops/linear_combinations.py | 13 ++++++++----- cirq-core/cirq/ops/pauli_string.py | 23 +++++++++++------------ cirq-core/cirq/ops/pauli_string_test.py | 8 ++++++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/cirq-core/cirq/ops/linear_combinations.py b/cirq-core/cirq/ops/linear_combinations.py index 9f5e025bb0a..684b2217978 100644 --- a/cirq-core/cirq/ops/linear_combinations.py +++ b/cirq-core/cirq/ops/linear_combinations.py @@ -589,10 +589,13 @@ def matrix(self, qubits: Iterable[raw_types.Qid] | None = None) -> np.ndarray: return result def sparse_matrix(self, qubits: Iterable[raw_types.Qid] | None = None) -> sparse.csr_matrix: - """Returns the sparse matrix of this PauliSum in computational basis of qubits. + """Returns the sparse matrix of this `PauliSum` in the computational basis of the qubits. - Uses direct bit-manipulation for each term and accumulates COO triplets - to avoid repeated sparse matrix addition overhead. + For each term we build the sparse matrix via direct bit-manipulation + (see `PauliString.sparse_matrix`) and collect its non-zero entries as + COO (COOrdinate) triplets (data, row, col). All triplets are + concatenated and a single sparse matrix is built at the end, avoiding + the overhead of adding sparse matrices term-by-term. Args: qubits: Ordered collection of qubits that determine the subspace @@ -602,7 +605,7 @@ def sparse_matrix(self, qubits: Iterable[raw_types.Qid] | None = None) -> sparse `self.qubits` are acted on by the identity. Returns: - A scipy.sparse.csr_matrix representing the Pauli sum. + A `scipy.sparse.csr_matrix` representing the Pauli sum. """ qubits = self.qubits if qubits is None else tuple(qubits) num_qubits = len(qubits) @@ -968,7 +971,7 @@ def copy(self) -> ProjectorSum: return ProjectorSum(self._linear_dict.copy()) def matrix(self, projector_qids: Iterable[raw_types.Qid] | None = None) -> sparse.csr_matrix: - """Returns the matrix of self in computational basis of qubits. + """Returns the matrix of self in the computational basis of the qubits. Args: projector_qids: Ordered collection of qubits that determine the subspace in which the diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index f365b89229c..f188abd147c 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -452,7 +452,7 @@ def __str__(self) -> str: return prefix + '*'.join(factors) def matrix(self, qubits: Iterable[TKey] | None = None) -> np.ndarray: - """Returns the matrix of self in computational basis of qubits. + """Returns the matrix of self in the computational basis of the qubits. Args: qubits: Ordered collection of qubits that determine the subspace @@ -461,17 +461,17 @@ def matrix(self, qubits: Iterable[TKey] | None = None) -> np.ndarray: the identity. Defaults to `self.qubits`. Raises: - NotImplementedError: If this PauliString is parameterized. + NotImplementedError: If this `PauliString` is parameterized. """ qubits = self.qubits if qubits is None else qubits factors = [self.get(q, default=identity.I) for q in qubits] if protocols.is_parameterized(self): - raise NotImplementedError('Cannot express as matrix when parameterized') + raise NotImplementedError('Cannot express a parameterized PauliString as a matrix.') assert isinstance(self.coefficient, complex) return linalg.kron(self.coefficient, *[protocols.unitary(f) for f in factors]) def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matrix: - """Returns the sparse matrix of self in computational basis of qubits. + """Returns the sparse matrix of self in the computational basis of the qubits. Uses a direct bit-manipulation algorithm that avoids Kronecker products by computing row/col indices and phases for each basis state directly. @@ -483,14 +483,14 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr the identity. Defaults to `self.qubits`. Returns: - A scipy.sparse.csr_matrix representing the Pauli string. + A `scipy.sparse.csr_matrix` representing the Pauli string. Raises: - NotImplementedError: If this PauliString is parameterized. + NotImplementedError: If this `PauliString` is parameterized. """ qubits = self.qubits if qubits is None else tuple(qubits) if protocols.is_parameterized(self): - raise NotImplementedError('Cannot express as matrix when parameterized') + raise NotImplementedError('Cannot express a parameterized PauliString as a matrix.') assert isinstance(self.coefficient, complex) n = len(qubits) @@ -498,9 +498,8 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr qubit_to_idx = {q: i for i, q in enumerate(qubits)} x_mask = y_mask = z_mask = 0 - for q in qubits: - pauli = self.get(q) - if pauli is None: + for q, pauli in self.items(): + if q not in qubit_to_idx: continue idx = qubit_to_idx[q] bit = 1 << (n - 1 - idx) @@ -515,8 +514,8 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr rows = cols ^ x_mask ^ y_mask num_y = y_mask.bit_count() - y_phase = (1j**num_y) * ((-1.0) ** np.bitwise_count(cols & y_mask)) - z_phase = (-1.0) ** np.bitwise_count(cols & z_mask) + y_phase = (1j**num_y) * np.where(np.bitwise_count(cols & y_mask) & 1, -1.0, 1.0) + z_phase = np.where(np.bitwise_count(cols & z_mask) & 1, -1.0, 1.0) data = self.coefficient * y_phase * z_phase return sparse.coo_matrix((data, (rows, cols)), shape=(dim, dim)).tocsr() diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 99fe3c17e17..465136d3c64 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -2053,9 +2053,13 @@ def test_parameterization() -> None: pst.expectation_from_state_vector(np.array([]), {}) with pytest.raises(NotImplementedError, match='parameterized'): pst.expectation_from_density_matrix(np.array([]), {}) - with pytest.raises(NotImplementedError, match='as matrix when parameterized'): + with pytest.raises( + NotImplementedError, match='Cannot express a parameterized PauliString as a matrix' + ): pst.matrix() - with pytest.raises(NotImplementedError, match='as matrix when parameterized'): + with pytest.raises( + NotImplementedError, match='Cannot express a parameterized PauliString as a matrix' + ): pst.sparse_matrix() assert pst**1 == pst assert pst**-1 == pst.with_coefficient(1.0 / t) From a4faba82c960142a448d06635bcab3fd987a76a0 Mon Sep 17 00:00:00 2001 From: Shih-Cheng Tu Date: Fri, 19 Jun 2026 14:20:59 +0800 Subject: [PATCH 3/4] Add assertion guard and parameterize sparse matrix test --- .../cirq/ops/linear_combinations_test.py | 59 +++++++++---------- cirq-core/cirq/ops/pauli_string.py | 6 ++ 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/cirq-core/cirq/ops/linear_combinations_test.py b/cirq-core/cirq/ops/linear_combinations_test.py index 8c2e7b710e0..a692f60340f 100644 --- a/cirq-core/cirq/ops/linear_combinations_test.py +++ b/cirq-core/cirq/ops/linear_combinations_test.py @@ -1078,38 +1078,35 @@ def test_pauli_sum_matrix() -> None: assert np.allclose(H3, paulisum.matrix([q[1], q[2], q[0]])) -def test_pauli_sum_sparse_matrix() -> None: - q = cirq.LineQubit.range(3) - paulisum = cirq.X(q[0]) * cirq.X(q[1]) + cirq.Z(q[0]) - H1 = np.array( - [[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 1.0, 0.0], [0.0, 1.0, -1.0, 0.0], [1.0, 0.0, 0.0, -1.0]] - ) - # Default qubit ordering. - assert np.allclose(H1, paulisum.sparse_matrix().toarray()) - # Explicit 2-qubit ordering (same as default). - assert np.allclose(H1, paulisum.sparse_matrix([q[0], q[1]]).toarray()) - # Reversed qubit ordering changes the matrix. - H2 = np.array( - [[1.0, 0.0, 0.0, 1.0], [0.0, -1.0, 1.0, 0.0], [0.0, 1.0, 1.0, 0.0], [1.0, 0.0, 0.0, -1.0]] - ) - assert np.allclose(H2, paulisum.sparse_matrix([q[1], q[0]]).toarray()) - # Adding an extra idle qubit expands to 8x8. - H3 = np.array( - [ - [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, -1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0], - ] - ) - assert np.allclose(H3, paulisum.sparse_matrix([q[1], q[2], q[0]]).toarray()) - # Empty PauliSum should return a zero sparse matrix. +@pytest.mark.parametrize( + 'paulisum, qubits', + ( + # Single term. + (cirq.X(q0), None), + # Two terms, default ordering. + (cirq.X(q0) * cirq.X(q1) + cirq.Z(q0), None), + # Three terms. + (cirq.X(q0) + cirq.Y(q1) + cirq.Z(q2), None), + # Reversed qubit ordering. + (cirq.X(q0) * cirq.X(q1) + cirq.Z(q0), [q1, q0]), + # Shuffled ordering with an idle qubit. + (cirq.X(q0) * cirq.X(q1) + cirq.Z(q0), [q1, q2, q0]), + # Complex coefficients. + ((1 + 2j) * cirq.X(q0) * cirq.Y(q1) - 0.5 * cirq.Z(q0), None), + # Identity factors included. + (cirq.X(q0) * cirq.I(q1) + cirq.Z(q1), None), + ), +) +def test_pauli_sum_sparse_matrix(paulisum, qubits) -> None: + actual = paulisum.sparse_matrix(qubits).toarray() + expected = paulisum.matrix(qubits) + assert np.allclose(actual, expected) + + +def test_pauli_sum_sparse_matrix_empty() -> None: + q = cirq.LineQubit.range(2) empty = cirq.PauliSum.from_pauli_strings([]) - assert np.allclose(empty.sparse_matrix([q[0], q[1]]).toarray(), np.zeros((4, 4))) + assert np.allclose(empty.sparse_matrix(q).toarray(), np.zeros((4, 4))) def test_pauli_sum_repr() -> None: diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index f188abd147c..dea04f25b48 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -487,6 +487,7 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr Raises: NotImplementedError: If this `PauliString` is parameterized. + AssertionError: If an unexpected Pauli gate instance is encountered. """ qubits = self.qubits if qubits is None else tuple(qubits) if protocols.is_parameterized(self): @@ -509,6 +510,11 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr y_mask |= bit elif pauli is pauli_gates.Z: z_mask |= bit + else: # pragma: no cover + raise AssertionError( + "Unhandled instance of Pauli gate. " + "Expected one of (cirq.X, cirq.Y, cirq.Z) identically." + ) cols = np.arange(dim, dtype=np.int32) rows = cols ^ x_mask ^ y_mask From a4ac720c036c0e7f43950b19205a6108c41cebe0 Mon Sep 17 00:00:00 2001 From: Shih-Cheng Tu Date: Thu, 25 Jun 2026 21:22:45 +0800 Subject: [PATCH 4/4] Replace identity check with value check ...and also add annotations, checking values by np.array_equal. --- .../cirq/ops/linear_combinations_test.py | 7 +++--- cirq-core/cirq/ops/pauli_string.py | 23 ++++++++++--------- cirq-core/cirq/ops/pauli_string_test.py | 8 +++++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/cirq-core/cirq/ops/linear_combinations_test.py b/cirq-core/cirq/ops/linear_combinations_test.py index a692f60340f..410f520bfbc 100644 --- a/cirq-core/cirq/ops/linear_combinations_test.py +++ b/cirq-core/cirq/ops/linear_combinations_test.py @@ -1097,16 +1097,17 @@ def test_pauli_sum_matrix() -> None: (cirq.X(q0) * cirq.I(q1) + cirq.Z(q1), None), ), ) -def test_pauli_sum_sparse_matrix(paulisum, qubits) -> None: +def test_pauli_sum_sparse_matrix(paulisum: cirq.PauliSum, qubits: list[cirq.Qid] | None) -> None: actual = paulisum.sparse_matrix(qubits).toarray() expected = paulisum.matrix(qubits) - assert np.allclose(actual, expected) + assert np.array_equal(actual, expected) def test_pauli_sum_sparse_matrix_empty() -> None: q = cirq.LineQubit.range(2) empty = cirq.PauliSum.from_pauli_strings([]) - assert np.allclose(empty.sparse_matrix(q).toarray(), np.zeros((4, 4))) + assert np.array_equal(empty.sparse_matrix().toarray(), np.zeros((1, 1))) + assert np.array_equal(empty.sparse_matrix(q).toarray(), np.zeros((4, 4))) def test_pauli_sum_repr() -> None: diff --git a/cirq-core/cirq/ops/pauli_string.py b/cirq-core/cirq/ops/pauli_string.py index dea04f25b48..9bdcbcf931a 100644 --- a/cirq-core/cirq/ops/pauli_string.py +++ b/cirq-core/cirq/ops/pauli_string.py @@ -504,17 +504,18 @@ def sparse_matrix(self, qubits: Iterable[TKey] | None = None) -> sparse.csr_matr continue idx = qubit_to_idx[q] bit = 1 << (n - 1 - idx) - if pauli is pauli_gates.X: - x_mask |= bit - elif pauli is pauli_gates.Y: - y_mask |= bit - elif pauli is pauli_gates.Z: - z_mask |= bit - else: # pragma: no cover - raise AssertionError( - "Unhandled instance of Pauli gate. " - "Expected one of (cirq.X, cirq.Y, cirq.Z) identically." - ) + match pauli: + case pauli_gates.X: + x_mask |= bit + case pauli_gates.Y: + y_mask |= bit + case pauli_gates.Z: + z_mask |= bit + case _: # pragma: no cover + raise AssertionError( + "Unhandled instance of Pauli gate. " + "Expected one of (cirq.X, cirq.Y, cirq.Z)." + ) cols = np.arange(dim, dtype=np.int32) rows = cols ^ x_mask ^ y_mask diff --git a/cirq-core/cirq/ops/pauli_string_test.py b/cirq-core/cirq/ops/pauli_string_test.py index 465136d3c64..167a9bac83c 100644 --- a/cirq-core/cirq/ops/pauli_string_test.py +++ b/cirq-core/cirq/ops/pauli_string_test.py @@ -850,12 +850,16 @@ def _pauli_string_matrix_cases(): @pytest.mark.parametrize('pauli_string, qubits, expected_matrix', _pauli_string_matrix_cases()) -def test_matrix(pauli_string, qubits, expected_matrix) -> None: +def test_matrix( + pauli_string: cirq.PauliString, qubits: tuple[cirq.Qid, ...] | None, expected_matrix: np.ndarray +) -> None: assert np.allclose(pauli_string.matrix(qubits), expected_matrix) @pytest.mark.parametrize('pauli_string, qubits, expected_matrix', _pauli_string_matrix_cases()) -def test_sparse_matrix(pauli_string, qubits, expected_matrix) -> None: +def test_sparse_matrix( + pauli_string: cirq.PauliString, qubits: tuple[cirq.Qid, ...] | None, expected_matrix: np.ndarray +) -> None: assert np.allclose(pauli_string.sparse_matrix(qubits).toarray(), expected_matrix)