diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index e8f18702ecc..341278a3b75 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -250,6 +250,7 @@ PhasedXPowGate, PhasedXZGate, PhaseFlipChannel, + ProjectorString, RandomGateChannel, qft, Qid, diff --git a/cirq-core/cirq/json_resolver_cache.py b/cirq-core/cirq/json_resolver_cache.py index 88b721cf361..7377dc7602e 100644 --- a/cirq-core/cirq/json_resolver_cache.py +++ b/cirq-core/cirq/json_resolver_cache.py @@ -117,6 +117,7 @@ def two_qubit_matrix_gate(matrix): 'PhasedISwapPowGate': cirq.PhasedISwapPowGate, 'PhasedXPowGate': cirq.PhasedXPowGate, 'PhasedXZGate': cirq.PhasedXZGate, + 'ProjectorString': cirq.ProjectorString, 'RandomGateChannel': cirq.RandomGateChannel, 'QuantumFourierTransformGate': cirq.QuantumFourierTransformGate, 'RepetitionsStoppingCriteria': cirq.work.RepetitionsStoppingCriteria, diff --git a/cirq-core/cirq/ops/__init__.py b/cirq-core/cirq/ops/__init__.py index 02ea527eb9c..1de578a4477 100644 --- a/cirq-core/cirq/ops/__init__.py +++ b/cirq-core/cirq/ops/__init__.py @@ -131,6 +131,10 @@ ParallelGateOperation, ) +from cirq.ops.projector import ( + ProjectorString, +) + from cirq.ops.controlled_operation import ( ControlledOperation, ) diff --git a/cirq-core/cirq/ops/projector.py b/cirq-core/cirq/ops/projector.py new file mode 100644 index 00000000000..d01d16cd881 --- /dev/null +++ b/cirq-core/cirq/ops/projector.py @@ -0,0 +1,159 @@ +import itertools +from typing import ( + Any, + Dict, + Iterable, + List, + Mapping, + Optional, + Union, +) + +import numpy as np +from scipy.sparse import csr_matrix + +from cirq import value +from cirq.ops import raw_types + + +def _check_qids_dimension(qids): + """A utility to check that we only have Qubits.""" + for qid in qids: + if qid.dimension != 2: + raise ValueError(f"Only qubits are supported, but {qid} has dimension {qid.dimension}") + + +@value.value_equality +class ProjectorString: + def __init__( + self, + projector_dict: Dict[raw_types.Qid, int], + coefficient: Union[int, float, complex] = 1, + ): + """Contructor for ProjectorString + + Args: + projector_dict: A python dictionary mapping from cirq.Qid to integers. A key value pair + represents the desired computational basis state for that qubit. + coefficient: Initial scalar coefficient. Defaults to 1. + """ + _check_qids_dimension(projector_dict.keys()) + self._projector_dict = projector_dict + self._coefficient = complex(coefficient) + + @property + def projector_dict(self) -> Dict[raw_types.Qid, int]: + return self._projector_dict + + @property + def coefficient(self) -> complex: + return self._coefficient + + def matrix(self, projector_qids: Optional[Iterable[raw_types.Qid]] = None) -> csr_matrix: + """Returns the matrix of self in computational basis of qubits. + + Args: + projector_qids: Ordered collection of qubits that determine the subspace + in which the matrix representation of the ProjectorString is to + be computed. Qbits absent from self.qubits are acted on by + the identity. Defaults to the qubits of the projector_dict. + + Returns: + A sparse matrix that is the projection in the specified basis. + """ + projector_qids = self._projector_dict.keys() if projector_qids is None else projector_qids + _check_qids_dimension(projector_qids) + idx_to_keep = [ + [self._projector_dict[qid]] if qid in self._projector_dict else [0, 1] + for qid in projector_qids + ] + + total_d = np.prod([qid.dimension for qid in projector_qids]) + + ones_idx = [] + for idx in itertools.product(*idx_to_keep): + d = total_d + kron_idx = 0 + for i, qid in zip(idx, projector_qids): + d //= qid.dimension + kron_idx += i * d + ones_idx.append(kron_idx) + + return csr_matrix( + ([self._coefficient] * len(ones_idx), (ones_idx, ones_idx)), shape=(total_d, total_d) + ) + + def _get_idx_to_keep(self, qid_map: Mapping[raw_types.Qid, int]): + num_qubits = len(qid_map) + idx_to_keep: List[Any] = [slice(0, 2)] * num_qubits + for q in self.projector_dict.keys(): + idx_to_keep[qid_map[q]] = self.projector_dict[q] + return tuple(idx_to_keep) + + def expectation_from_state_vector( + self, + state_vector: np.ndarray, + qid_map: Mapping[raw_types.Qid, int], + ) -> complex: + """Expectation of the projection from a state vector. + + Computes the expectation value of this ProjectorString on the provided state vector. + + Args: + state_vector: An array representing a valid state vector. + qubit_map: A map from all qubits used in this ProjectorString to the + indices of the qubits that `state_vector` is defined over. + Returns: + The expectation value of the input state. + """ + _check_qids_dimension(qid_map.keys()) + num_qubits = len(qid_map) + index = self._get_idx_to_keep(qid_map) + return self._coefficient * np.sum( + np.abs(np.reshape(state_vector, (2,) * num_qubits)[index]) ** 2 + ) + + def expectation_from_density_matrix( + self, + state: np.ndarray, + qid_map: Mapping[raw_types.Qid, int], + ) -> complex: + """Expectation of the projection from a density matrix. + + Computes the expectation value of this ProjectorString on the provided state. + + Args: + state: An array representing a valid density matrix. + qubit_map: A map from all qubits used in this ProjectorString to the + indices of the qubits that `state_vector` is defined over. + Returns: + The expectation value of the input state. + """ + _check_qids_dimension(qid_map.keys()) + num_qubits = len(qid_map) + index = self._get_idx_to_keep(qid_map) * 2 + result = np.reshape(state, (2,) * (2 * num_qubits))[index] + while any(result.shape): + result = np.trace(result, axis1=0, axis2=len(result.shape) // 2) + return self._coefficient * result + + def __repr__(self) -> str: + return ( + f"cirq.ProjectorString(projector_dict={self._projector_dict}," + + f"coefficient={self._coefficient})" + ) + + def _json_dict_(self) -> Dict[str, Any]: + return { + 'cirq_type': self.__class__.__name__, + 'projector_dict': list(self._projector_dict.items()), + 'coefficient': self._coefficient, + } + + @classmethod + def _from_json_dict_(cls, projector_dict, coefficient, **kwargs): + return cls(projector_dict=dict(projector_dict), coefficient=coefficient) + + def _value_equality_values_(self) -> Any: + projector_dict = sorted(self._projector_dict.items()) + return (tuple(projector_dict), self._coefficient) diff --git a/cirq-core/cirq/ops/projector_test.py b/cirq-core/cirq/ops/projector_test.py new file mode 100644 index 00000000000..c6a62178d93 --- /dev/null +++ b/cirq-core/cirq/ops/projector_test.py @@ -0,0 +1,255 @@ +import numpy as np +import pytest + +import cirq + + +def test_projector_matrix(): + q0 = cirq.NamedQubit('q0') + + zero_projector = cirq.ProjectorString({q0: 0}) + one_projector = cirq.ProjectorString({q0: 1}) + coeff_projector = cirq.ProjectorString({q0: 0}, 1.23 + 4.56j) + + np.testing.assert_allclose(zero_projector.matrix().toarray(), [[1.0, 0.0], [0.0, 0.0]]) + np.testing.assert_allclose(one_projector.matrix().toarray(), [[0.0, 0.0], [0.0, 1.0]]) + np.testing.assert_allclose( + coeff_projector.matrix().toarray(), [[1.23 + 4.56j, 0.0], [0.0, 0.0]] + ) + + +def test_projector_repr(): + q0 = cirq.NamedQubit('q0') + + assert ( + repr(cirq.ProjectorString({q0: 0})) + == "cirq.ProjectorString(projector_dict={cirq.NamedQubit('q0'): 0},coefficient=(1+0j))" + ) + + +def test_projector_from_np_array(): + q0 = cirq.NamedQubit('q0') + + zero_projector = cirq.ProjectorString({q0: 0}) + np.testing.assert_allclose(zero_projector.matrix().toarray(), [[1.0, 0.0], [0.0, 0.0]]) + + +def test_projector_matrix_missing_qid(): + q0, q1 = cirq.LineQubit.range(2) + proj = cirq.ProjectorString({q0: 0}) + proj_with_coefficient = cirq.ProjectorString({q0: 0}, 1.23 + 4.56j) + + np.testing.assert_allclose(proj.matrix().toarray(), np.diag([1.0, 0.0])) + np.testing.assert_allclose(proj.matrix([q0]).toarray(), np.diag([1.0, 0.0])) + np.testing.assert_allclose(proj.matrix([q1]).toarray(), np.diag([1.0, 1.0])) + + np.testing.assert_allclose(proj.matrix([q0, q1]).toarray(), np.diag([1.0, 1.0, 0.0, 0.0])) + np.testing.assert_allclose(proj.matrix([q1, q0]).toarray(), np.diag([1.0, 0.0, 1.0, 0.0])) + + np.testing.assert_allclose( + proj_with_coefficient.matrix([q1, q0]).toarray(), + np.diag([1.23 + 4.56j, 0.0, 1.23 + 4.56j, 0.0]), + ) + + +def test_equality(): + q0 = cirq.NamedQubit('q0') + + obj1a = cirq.ProjectorString({q0: 0}) + obj1b = cirq.ProjectorString({q0: 0}) + obj2 = cirq.ProjectorString({q0: 1}) + obj3 = cirq.ProjectorString({q0: 1}, coefficient=0.20160913) + + eq = cirq.testing.EqualsTester() + eq.add_equality_group(obj1a, obj1b) + eq.add_equality_group(obj2) + eq.add_equality_group(obj3) + + +def test_get_values(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({q0: 0}, 1.23 + 4.56j) + + assert len(d.projector_dict) == 1 + assert d.projector_dict[q0] == 0 + assert d.coefficient == 1.23 + 4.56j + + +def test_expectation_from_state_vector_basis_states_empty(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({}) + + np.testing.assert_allclose(d.expectation_from_state_vector(np.array([1.0, 0.0]), {q0: 0}), 1.0) + + +def test_expectation_from_state_vector_basis_states_single_qubits(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({q0: 0}) + + np.testing.assert_allclose(d.expectation_from_state_vector(np.array([1.0, 0.0]), {q0: 0}), 1.0) + np.testing.assert_allclose(d.expectation_from_state_vector(np.array([0.0, 1.0]), {q0: 0}), 0.0) + + +def test_expectation_from_state_vector_basis_states_three_qubits(): + q0 = cirq.NamedQubit('q0') + q1 = cirq.NamedQubit('q1') + q2 = cirq.NamedQubit('q2') + d_1qbit = cirq.ProjectorString({q1: 1}) + d_2qbits = cirq.ProjectorString({q0: 0, q1: 1}) + + state_vector = cirq.testing.random_superposition(8) + + # If the mapping of state_vector is {q0: 0, q1: 1, q2: 2}, then the coefficients are: + # 0: (q0, q1, q2) = (0, 0, 0) + # 1: (q0, q1, q2) = (0, 0, 1) + # 2: (q0, q1, q2) = (0, 1, 0) -> Projected on + # 3: (q0, q1, q2) = (0, 1, 1) -> Projected on + # 4: (q0, q1, q2) = (1, 0, 0) + # 5: (q0, q1, q2) = (1, 0, 1) + # 6: (q0, q1, q2) = (1, 1, 0) + # 7: (q0, q1, q2) = (1, 1, 1) + np.testing.assert_allclose( + d_2qbits.expectation_from_state_vector(state_vector, {q0: 0, q1: 1, q2: 2}), + sum(abs(state_vector[i]) ** 2 for i in [2, 3]), + ) + + # Same as above except it's only for q1=1, which happens for indices 2, 3, 6, and 7: + np.testing.assert_allclose( + d_1qbit.expectation_from_state_vector(state_vector, {q0: 0, q1: 1, q2: 2}), + sum(abs(state_vector[i]) ** 2 for i in [2, 3, 6, 7]), + ) + + # Here we have a different mapping, but the idea is the same. + # 0: (q0 ,q2, q1) = (0, 0, 0) + # 1: (q0, q2, q1) = (0, 0, 1) -> Projected on + # 2: (q0, q2, q1) = (0, 1, 0) + # 3: (q0, q2, q1) = (0, 1, 1) -> Projected on + # 4: (q0, q2, q1) = (1, 0, 0) + # 5: (q0, q2, q1) = (1, 0, 1) + # 6: (q0, q2, q1) = (1, 1, 0) + # 7: (q0, q2, q1) = (1, 1, 1) + np.testing.assert_allclose( + d_2qbits.expectation_from_state_vector(state_vector, {q0: 0, q1: 2, q2: 1}), + sum(abs(state_vector[i]) ** 2 for i in [1, 3]), + ) + + # Same as above except it's only for q1=1, which happens for indices 1, 3, 5, and 7: + np.testing.assert_allclose( + d_1qbit.expectation_from_state_vector(state_vector, {q0: 0, q1: 2, q2: 1}), + sum(abs(state_vector[i]) ** 2 for i in [1, 3, 5, 7]), + ) + + +def test_expectation_from_density_matrix_three_qubits(): + q0 = cirq.NamedQubit('q0') + q1 = cirq.NamedQubit('q1') + q2 = cirq.NamedQubit('q2') + d_1qbit = cirq.ProjectorString({q1: 1}) + d_2qbits = cirq.ProjectorString({q0: 0, q1: 1}) + + state = cirq.testing.random_density_matrix(8) + + # If the mapping of state is {q0: 0, q1: 1, q2: 2}, then the coefficients are: + # 0: (q0, q1, q2) = (0, 0, 0) + # 1: (q0, q1, q2) = (0, 0, 1) + # 2: (q0, q1, q2) = (0, 1, 0) -> Projected on + # 3: (q0, q1, q2) = (0, 1, 1) -> Projected on + # 4: (q0, q1, q2) = (1, 0, 0) + # 5: (q0, q1, q2) = (1, 0, 1) + # 6: (q0, q1, q2) = (1, 1, 0) + # 7: (q0, q1, q2) = (1, 1, 1) + np.testing.assert_allclose( + d_2qbits.expectation_from_density_matrix(state, {q0: 0, q1: 1, q2: 2}), + sum(state[i][i].real for i in [2, 3]), + ) + + # Same as above except it's only for q1=1, which happens for indices 2, 3, 6, and 7: + np.testing.assert_allclose( + d_1qbit.expectation_from_density_matrix(state, {q0: 0, q1: 1, q2: 2}), + sum(state[i][i].real for i in [2, 3, 6, 7]), + ) + + # Here we have a different mapping, but the idea is the same. + # 0: (q0 ,q2, q1) = (0, 0, 0) + # 1: (q0, q2, q1) = (0, 0, 1) -> Projected on + # 2: (q0, q2, q1) = (0, 1, 0) + # 3: (q0, q2, q1) = (0, 1, 1) -> Projected on + # 4: (q0, q2, q1) = (1, 0, 0) + # 5: (q0, q2, q1) = (1, 0, 1) + # 6: (q0, q2, q1) = (1, 1, 0) + # 7: (q0, q2, q1) = (1, 1, 1) + np.testing.assert_allclose( + d_2qbits.expectation_from_density_matrix(state, {q0: 0, q1: 2, q2: 1}), + sum(state[i][i].real for i in [1, 3]), + ) + + # Same as above except it's only for q1=1, which happens for indices 1, 3, 5, and 7: + np.testing.assert_allclose( + d_1qbit.expectation_from_density_matrix(state, {q0: 0, q1: 2, q2: 1}), + sum(state[i][i].real for i in [1, 3, 5, 7]), + ) + + +def test_consistency_state_vector_and_density_matrix(): + q0 = cirq.NamedQubit('q0') + q1 = cirq.NamedQubit('q1') + q2 = cirq.NamedQubit('q2') + + state_vector = cirq.testing.random_superposition(8) + state = np.einsum('i,j->ij', state_vector, np.conj(state_vector)) + + for proj_qubit in q0, q1, q2: + for proj_idx in [0, 1]: + d = cirq.ProjectorString({proj_qubit: proj_idx}) + + np.testing.assert_allclose( + d.expectation_from_state_vector(state_vector, {q0: 0, q1: 1, q2: 2}), + d.expectation_from_density_matrix(state, {q0: 0, q1: 1, q2: 2}), + ) + + +def test_expectation_higher_dims(): + qubit = cirq.NamedQid('q0', dimension=2) + qutrit = cirq.NamedQid('q1', dimension=3) + + with pytest.raises(ValueError, match="Only qubits are supported"): + cirq.ProjectorString({qutrit: 0}) + + d = cirq.ProjectorString({qubit: 0}) + with pytest.raises(ValueError, match="Only qubits are supported"): + _ = (d.expectation_from_state_vector(np.zeros(2 * 3), {qubit: 0, qutrit: 0}),) + + +def test_expectation_with_coefficient(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({q0: 0}, coefficient=(0.6 + 0.4j)) + + np.testing.assert_allclose( + d.expectation_from_state_vector(np.array([[1.0, 0.0]]), qid_map={q0: 0}), 0.6 + 0.4j + ) + + np.testing.assert_allclose( + d.expectation_from_density_matrix(np.array([[1.0, 0.0], [0.0, 0.0]]), {q0: 0}), 0.6 + 0.4j + ) + + +def test_expectation_from_density_matrix_basis_states_empty(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({}) + + np.testing.assert_allclose( + d.expectation_from_density_matrix(np.array([[1.0, 0.0], [0.0, 0.0]]), {q0: 0}), 1.0 + ) + + +def test_expectation_from_density_matrix_basis_states_single_qubits(): + q0 = cirq.NamedQubit('q0') + d = cirq.ProjectorString({q0: 0}) + + np.testing.assert_allclose( + d.expectation_from_density_matrix(np.array([[1.0, 0.0], [0.0, 0.0]]), {q0: 0}), 1.0 + ) + + np.testing.assert_allclose( + d.expectation_from_density_matrix(np.array([[0.0, 0.0], [0.0, 1.0]]), {q0: 0}), 0.0 + ) diff --git a/cirq-core/cirq/protocols/json_test_data/ProjectorString.json b/cirq-core/cirq/protocols/json_test_data/ProjectorString.json new file mode 100644 index 00000000000..bbd97991afc --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/ProjectorString.json @@ -0,0 +1,17 @@ +{ + "cirq_type": "ProjectorString", + "projector_dict": [ + [ + { + "cirq_type": "NamedQubit", + "name": "q0" + }, + 0 + ] + ], + "coefficient": { + "cirq_type": "complex", + "real": 20.160913, + "imag": 0.0 + } +} \ No newline at end of file diff --git a/cirq-core/cirq/protocols/json_test_data/ProjectorString.repr b/cirq-core/cirq/protocols/json_test_data/ProjectorString.repr new file mode 100644 index 00000000000..374e6d491eb --- /dev/null +++ b/cirq-core/cirq/protocols/json_test_data/ProjectorString.repr @@ -0,0 +1 @@ +cirq.ProjectorString(projector_dict={cirq.NamedQubit('q0'): 0}, coefficient=20.160913) \ No newline at end of file