diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 35435d9735a..d48f921304d 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -3,6 +3,10 @@ # Release 0.39.0-dev (development release)

New features since last release

+ +* A new `qml.vn_entanglement_entropy` measurement process has been added which measures the + Von Neumann entanglement entropy of a quantum state. + [(#5911)](https://github.com/PennyLaneAI/pennylane/pull/5911)

Improvements 🛠

diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 3db66c953d2..7b7a32c751c 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -67,6 +67,7 @@ state, var, vn_entropy, + vn_entanglement_entropy, purity, mutual_info, classical_shadow, @@ -183,7 +184,7 @@ def __getattr__(name): if name == "QubitDevice": warn( "QubitDevice will no longer be accessible top level. Please access " - " the class as pennylane.devices.QubitDevice", + "the class as pennylane.devices.QubitDevice", PennyLaneDeprecationWarning, ) return pennylane.devices._qubit_device.QubitDevice # pylint:disable=protected-access @@ -191,7 +192,7 @@ def __getattr__(name): if name == "QutritDevice": warn( "QutritDevice will no longer be accessible top level. Please access " - " the class as pennylane.devices.QutritDevice", + "the class as pennylane.devices.QutritDevice", PennyLaneDeprecationWarning, ) return pennylane.devices._qutrit_device.QutritDevice # pylint:disable=protected-access @@ -199,7 +200,7 @@ def __getattr__(name): if name == "Device": warn( "Device will no longer be accessible top level. Please access " - " the class as pennylane.devices.LegacyDevice", + "the class as pennylane.devices.LegacyDevice", PennyLaneDeprecationWarning, ) return pennylane.devices._legacy_device.Device # pylint:disable=protected-access diff --git a/pennylane/devices/_qubit_device.py b/pennylane/devices/_qubit_device.py index a246283e501..c4873bf2799 100644 --- a/pennylane/devices/_qubit_device.py +++ b/pennylane/devices/_qubit_device.py @@ -50,6 +50,7 @@ StateMeasurement, StateMP, VarianceMP, + VnEntanglementEntropyMP, VnEntropyMP, ) from pennylane.operation import Operation, operation_derivative @@ -718,6 +719,29 @@ def statistics( ) result = self.vn_entropy(wires=obs.wires, log_base=obs.log_base) + elif isinstance(m, VnEntanglementEntropyMP): + if self.wires.labels != tuple(range(self.num_wires)): + raise qml.QuantumFunctionError( + "Returning the Von Neumann entanglement entropy is not supported when using custom wire labels" + ) + + if self._shot_vector is not None: + raise NotImplementedError( + "Returning the Von Neumann entanglement entropy is not supported with shot vectors." + ) + + if self.shots is not None: + warnings.warn( + "Requested Von Neumann entanglement entropy with finite shots; the returned " + "state information is analytic and is unaffected by sampling. To silence " + "this warning, set shots=None on the device.", + UserWarning, + ) + wires0, wires1 = obs.raw_wires + result = self.vn_entanglement_entropy( + wires0=wires0, wires1=wires1, log_base=obs.log_base + ) + elif isinstance(m, MutualInfoMP): if self.wires.labels != tuple(range(self.num_wires)): raise qml.QuantumFunctionError( @@ -777,6 +801,7 @@ def statistics( VarianceMP, ProbabilityMP, VnEntropyMP, + VnEntanglementEntropyMP, MutualInfoMP, ShadowExpvalMP, ), @@ -1008,7 +1033,7 @@ def vn_entropy(self, wires, log_base): """ try: state = self.density_matrix(wires=self.wires) - except qml.QuantumFunctionError as e: # pragma: no cover + except (qml.QuantumFunctionError, NotImplementedError) as e: # pragma: no cover raise NotImplementedError( f"Cannot compute the Von Neumman entropy with device {self.name} that is not capable of returning the " f"state. " @@ -1016,6 +1041,36 @@ def vn_entropy(self, wires, log_base): wires = wires.tolist() return qml.math.vn_entropy(state, indices=wires, c_dtype=self.C_DTYPE, base=log_base) + def vn_entanglement_entropy(self, wires0, wires1, log_base): + r"""Returns the Von Neumann entanglement entropy prior to measurement. + + .. math:: + + S(\rho_A) = -\text{Tr}[\rho_A \log \rho_A] = -\text{Tr}[\rho_B \log \rho_B] = S(\rho_B) + + Args: + wires0 (Sequence[int] or int): the wires of the first subsystem + wires1 (Sequence[int] or int): the wires of the second subsystem + log_base (float): Base for the logarithm. + + Returns: + float: returns the Von Neumann entropy + """ + try: + state = self.density_matrix(wires=self.wires) + except (qml.QuantumFunctionError, NotImplementedError) as e: # pragma: no cover + raise NotImplementedError( + f"Cannot compute the Von Neumman entropy with device {self.name} that is not capable of returning the " + f"state. " + ) from e + + wires0 = wires0.tolist() + wires1 = wires1.tolist() + + return qml.math.vn_entanglement_entropy( + state, indices0=wires0, indices1=wires1, c_dtype=self.C_DTYPE, base=log_base + ) + def mutual_info(self, wires0, wires1, log_base): r"""Returns the mutual information prior to measurement: @@ -1035,7 +1090,7 @@ def mutual_info(self, wires0, wires1, log_base): """ try: state = self.density_matrix(wires=self.wires) - except qml.QuantumFunctionError as e: # pragma: no cover + except (qml.QuantumFunctionError, NotImplementedError) as e: # pragma: no cover raise NotImplementedError( f"Cannot compute the mutual information with device {self.name} that is not capable of returning the " f"state. " diff --git a/pennylane/devices/_qutrit_device.py b/pennylane/devices/_qutrit_device.py index 53e3fdd31db..c721bfe6901 100644 --- a/pennylane/devices/_qutrit_device.py +++ b/pennylane/devices/_qutrit_device.py @@ -183,6 +183,28 @@ def vn_entropy(self, wires, log_base): "Unsupported return type specified for observable Von Neumann entropy" ) + def vn_entanglement_entropy(self, wires0, wires1, log_base): + r"""Returns the Von Neumann entanglement entropy prior to measurement. + + .. math:: + + S(\rho_A) = -\text{Tr}[\rho_A \log \rho_A] = -\text{Tr}[\rho_B \log \rho_B] = S(\rho_B) + + Args: + wires0 (Sequence[int] or int): the wires of the first subsystem + wires1 (Sequence[int] or int): the wires of the second subsystem + log_base (float): Base for the logarithm. + + Returns: + float: returns the Von Neumann entropy + """ + # TODO: Add support for VnEntanglementEntropy return type. Currently, qml.math is hard coded to calculate this for qubit + # states (see `qml.math.vn_entanglement_entropy()`), so it needs to be updated before VnEntanglementEntropy can be supported for qutrits. + # For now, if a user tries to request this return type, an error will be raised. + raise qml.QuantumFunctionError( + "Unsupported return type specified for observable Von Neumann entanglement entropy" + ) + def mutual_info(self, wires0, wires1, log_base): r"""Returns the mutual information prior to measurement: diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py index d0284e4b3e8..dd902c557ea 100644 --- a/pennylane/devices/default_mixed.py +++ b/pennylane/devices/default_mixed.py @@ -41,6 +41,7 @@ SampleMP, StateMP, VarianceMP, + VnEntanglementEntropyMP, VnEntropyMP, ) from pennylane.operation import Channel @@ -637,6 +638,9 @@ def _snapshot_measurements(self, density_matrix, measurement): density_matrix, indices=map_wires, c_dtype=self.C_DTYPE, base=base ) + elif isinstance(measurement, VnEntanglementEntropyMP): + snap_result = measurement.process_density_matrix(density_matrix, wire_order=self.wires) + elif isinstance(measurement, MutualInfoMP): base = measurement.log_base wires0, wires1 = list(map(self.map_wires, measurement.raw_wires)) @@ -762,9 +766,9 @@ def execute(self, circuit, **kwargs): # not specified or all wires specified. self.measured_wires = self.wires return super().execute(circuit, **kwargs) - if isinstance(m, (VnEntropyMP, MutualInfoMP)): - # VnEntropy, MutualInfo: Computed for the state - # prior to measurement. So, readout error need not be applied to the + if isinstance(m, (VnEntropyMP, VnEntanglementEntropyMP, MutualInfoMP)): + # VnEntropy, VnEntanglementEntropyMP, MutualInfo: Computed for the state + # prior to measurement. So, readout error need not be applied on the # corresponding device wires. continue wires_list.append(m.wires) diff --git a/pennylane/measurements/__init__.py b/pennylane/measurements/__init__.py index 3129a92069d..d0eeaa5d7fd 100644 --- a/pennylane/measurements/__init__.py +++ b/pennylane/measurements/__init__.py @@ -288,6 +288,7 @@ def circuit(x): StateMeasurement, Variance, VnEntropy, + VnEntanglementEntropy, ) from .mid_measure import MeasurementValue, MidMeasureMP, measure, find_post_processed_mcms from .mutual_info import MutualInfoMP, mutual_info @@ -298,3 +299,4 @@ def circuit(x): from .state import DensityMatrixMP, StateMP, density_matrix, state from .var import VarianceMP, var from .vn_entropy import VnEntropyMP, vn_entropy +from .vn_entanglement_entropy import VnEntanglementEntropyMP, vn_entanglement_entropy diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index ab28f487415..6b7352b23e1 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -46,6 +46,7 @@ class ObservableReturnTypes(Enum): State = "state" MidMeasure = "measure" VnEntropy = "vnentropy" + VnEntanglementEntropy = "vnentanglemententropy" MutualInfo = "mutualinfo" Shadow = "shadow" ShadowExpval = "shadowexpval" @@ -90,6 +91,9 @@ def __repr__(self): VnEntropy = ObservableReturnTypes.VnEntropy """Enum: An enumeration which represents returning Von Neumann entropy before measurements.""" +VnEntanglementEntropy = ObservableReturnTypes.VnEntanglementEntropy +"""Enum: An enumeration which represents returning Von Neumann entanglement entropy before measurements.""" + MutualInfo = ObservableReturnTypes.MutualInfo """Enum: An enumeration which represents returning the mutual information before measurements.""" diff --git a/pennylane/measurements/vn_entanglement_entropy.py b/pennylane/measurements/vn_entanglement_entropy.py new file mode 100644 index 00000000000..758db6e49c2 --- /dev/null +++ b/pennylane/measurements/vn_entanglement_entropy.py @@ -0,0 +1,183 @@ +# Copyright 2018-2024 Xanadu Quantum Technologies Inc. + +# 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. +# pylint: disable=protected-access +""" +This module contains the qml.vn_entanglement_entropy measurement. +""" +from copy import copy +from typing import Optional, Sequence + +import pennylane as qml +from pennylane.wires import Wires + +from .measurements import StateMeasurement, VnEntanglementEntropy + + +def vn_entanglement_entropy(wires0, wires1, log_base=None): + r"""Measures the `Von Neumann entanglement entropy `_ + of a quantum state: + + .. math:: + + S(\rho_A) = -\text{Tr}[\rho_A \log \rho_A] = -\text{Tr}[\rho_B \log \rho_B] = S(\rho_B) + + where :math:`S` is the Von Neumann entropy; :math:`\rho_A = \text{Tr}_B [\rho_{AB}]` and + :math:`\rho_B = \text{Tr}_A [\rho_{AB}]` are the reduced density matrices for each partition. + The Von Neumann entanglement entropy is a measure of the degree of quantum entanglement between + two subsystems constituting a pure bipartite quantum state. The entropy of entanglement is the + Von Neumann entropy of the reduced density matrix for any of the subsystems. If it is non-zero, + it indicates the two subsystems are entangled. + + Args: + wires0 (Sequence[int] or int): the wires of the first subsystem + wires1 (Sequence[int] or int): the wires of the second subsystem + log_base (float): Base for the logarithm. + + Returns: + VnEntanglementEntropyMP: measurement process instance + + **Example:** + + .. code-block:: python3 + + dev = qml.device("default.qubit", wires=2) + @qml.qnode(dev) + def circuit(x): + qml.RY(x, 0) + qml.Hadamard(0) + qml.CNOT([0, 1]) + return qml.vn_entanglement_entropy([0], [1]) + + Executing this QNode: + + >>> circuit(1.967) + 0.16389850379003218 + + It is also possible to get the gradient of the previous QNode: + + >>> param = np.array(np.pi/4, requires_grad=True) + >>> qml.grad(circuit)(param) + tensor(-0.62322524, requires_grad=True) + + .. note:: + + Calculating the derivative of :func:`~pennylane.vn_entanglement_entropy` is currently supported when + using the classical backpropagation differentiation method (``diff_method="backprop"``) + with a compatible device and finite differences (``diff_method="finite-diff"``). + + .. seealso:: :func:`~pennylane.vn_entropy` and :func:`pennylane.math.vn_entanglement_entropy` + """ + wires0 = qml.wires.Wires(wires0) + wires1 = qml.wires.Wires(wires1) + + # the subsystems cannot overlap + if not any(qml.math.is_abstract(w) for w in wires0 + wires1) and [ + wire for wire in wires0 if wire in wires1 + ]: + raise qml.QuantumFunctionError( + "Subsystems for computing entanglement entropy must not overlap." + ) + return VnEntanglementEntropyMP(wires=(wires0, wires1), log_base=log_base) + + +class VnEntanglementEntropyMP(StateMeasurement): + """Measurement process that computes the Von Neumann entanglement entropy between the provided wires. + Please refer to :func:`~pennylane.vn_entanglement_entropy` for detailed documentation. + + Args: + wires (Sequence[.Wires]): The wires the measurement process applies to. + id (str): custom label given to a measurement instance, can be useful for some applications + where the instance has to be identified + log_base (float): base for the logarithm + """ + + def _flatten(self): + metadata = (("wires", tuple(self.raw_wires)), ("log_base", self.log_base)) + return (None, None), metadata + + def __init__( + self, + wires: Optional[Sequence[Wires]] = None, + id: Optional[str] = None, + log_base: Optional[float] = None, + ): + self.log_base = log_base + super().__init__(wires=wires, id=id) + + # pylint: disable=arguments-differ + @classmethod + def _primitive_bind_call(cls, wires: Sequence, **kwargs): + if cls._wires_primitive is None: # pragma: no cover + # just a safety check + return type.__call__(cls, wires=wires, **kwargs) # pragma: no cover + return cls._wires_primitive.bind(*wires[0], *wires[1], n_wires0=len(wires[0]), **kwargs) + + def __repr__(self): + return f"VnEntanglementEntropy(wires0={self.raw_wires[0].tolist()}, wires1={self.raw_wires[1].tolist()}, log_base={self.log_base})" + + @property + def hash(self): + """int: returns an integer hash uniquely representing the measurement process""" + fingerprint = ( + self.__class__.__name__, + tuple(self.raw_wires[0].tolist()), + tuple(self.raw_wires[1].tolist()), + self.log_base, + ) + + return hash(fingerprint) + + @property + def return_type(self): + return VnEntanglementEntropy + + @property + def numeric_type(self): + return float + + def map_wires(self, wire_map: dict): + new_measurement = copy(self) + new_measurement._wires = [ + Wires([wire_map.get(wire, wire) for wire in wires]) for wires in self.raw_wires + ] + return new_measurement + + def shape( + self, shots: Optional[int] = None, num_device_wires: int = 0 + ): # pylint: disable=unused-argument + return () + + def process_state(self, state: Sequence[complex], wire_order: Wires): + density_matrix = qml.math.dm_from_state_vector(state) + return self.process_density_matrix(density_matrix=density_matrix, wire_order=wire_order) + + def process_density_matrix( + self, density_matrix: Sequence[complex], wire_order: Wires + ): # pylint: disable=unused-argument + return qml.math.vn_entanglement_entropy( + density_matrix, + indices0=list(self._wires[0]), + indices1=list(self._wires[1]), + c_dtype=density_matrix.dtype, + base=self.log_base, + ) + + +if VnEntanglementEntropyMP._wires_primitive is not None: + + @VnEntanglementEntropyMP._wires_primitive.def_impl + def _(*all_wires, n_wires0, **kwargs): + wires0 = all_wires[:n_wires0] + wires1 = all_wires[n_wires0:] + return type.__call__(VnEntanglementEntropyMP, wires=(wires0, wires1), **kwargs) diff --git a/tests/capture/test_measurements_capture.py b/tests/capture/test_measurements_capture.py index 7f39e7a83e9..b252a3da642 100644 --- a/tests/capture/test_measurements_capture.py +++ b/tests/capture/test_measurements_capture.py @@ -32,6 +32,7 @@ ShadowExpvalMP, StateMP, VarianceMP, + VnEntanglementEntropyMP, VnEntropyMP, ) @@ -149,6 +150,7 @@ def f(): lambda: ProbabilityMP(wires=qml.wires.Wires((0, 1)), eigvals=np.array([-1.0, -0.5, 0.5, 1.0])), lambda: qml.sample(wires=(3, 4)), lambda: qml.shadow_expval(np.array(2) * qml.X(0)), + lambda: qml.vn_entanglement_entropy(wires0=(1, 3), wires1=(2, 4), log_base=2), lambda: qml.vn_entropy(wires=(1, 2)), lambda: qml.purity(wires=(0, 1)), lambda: qml.mutual_info(wires0=(1, 3), wires1=(2, 4), log_base=2), @@ -578,7 +580,7 @@ def f(): @pytest.mark.parametrize("x64_mode", (True, False)) @pytest.mark.parametrize("mtype, kwargs", [(VnEntropyMP, {"log_base": 2}), (PurityMP, {})]) -def test_qinfo_measurements(mtype, kwargs, x64_mode): +def test_vn_entropy_purity(mtype, kwargs, x64_mode): """Test the capture of a vn entropy and purity measurement.""" initial_mode = jax.config.jax_enable_x64 @@ -609,24 +611,25 @@ def f(w1, w2): @pytest.mark.parametrize("x64_mode", (True, False)) -def test_MutualInfo(x64_mode): - """Test the capture of a vn entropy and purity measurement.""" +@pytest.mark.parametrize("mtype", [MutualInfoMP, VnEntanglementEntropyMP]) +def test_mutual_info_vn_entanglement_entropy(mtype, x64_mode): + """Test the capture of a mutual info and vn entanglement entropy measurement.""" initial_mode = jax.config.jax_enable_x64 jax.config.update("jax_enable_x64", x64_mode) def f(w1, w2): - return qml.mutual_info(wires0=[w1, 1], wires1=[w2, 3], log_base=2) + return mtype(wires=(qml.wires.Wires([w1, 1]), qml.wires.Wires([w2, 3])), log_base=2) jaxpr = jax.make_jaxpr(f)(0, 2) assert len(jaxpr.eqns) == 1 - assert jaxpr.eqns[0].primitive == MutualInfoMP._wires_primitive + assert jaxpr.eqns[0].primitive == mtype._wires_primitive assert jaxpr.eqns[0].params == {"log_base": 2, "n_wires0": 2} assert len(jaxpr.eqns[0].invars) == 4 mp = jaxpr.eqns[0].outvars[0].aval assert isinstance(mp, AbstractMeasurement) - assert mp._abstract_eval == MutualInfoMP._abstract_eval + assert mp._abstract_eval == mtype._abstract_eval shapes = _get_shapes_for( *jaxpr.out_avals, num_device_wires=4, shots=qml.measurements.Shots(None) diff --git a/tests/devices/test_default_mixed.py b/tests/devices/test_default_mixed.py index 86cdbf02430..bed20e39d3d 100644 --- a/tests/devices/test_default_mixed.py +++ b/tests/devices/test_default_mixed.py @@ -911,6 +911,7 @@ def test_identity_skipped(self, mocker): qml.probs(op=qml.Y(0)), qml.probs(op=qml.X(0) @ qml.Y(1)), qml.vn_entropy(wires=[0]), + qml.vn_entanglement_entropy(wires0=[1], wires1=[0]), qml.mutual_info(wires0=[1], wires1=[0]), qml.purity(wires=[1]), ], @@ -1202,19 +1203,21 @@ def circuit(): @pytest.mark.parametrize("prob", [0, 0.5, 1]) @pytest.mark.parametrize("nr_wires", [2, 3]) - def test_readout_vnentropy_and_mutualinfo(self, nr_wires, prob): - """Tests the output of qml.vn_entropy and qml.mutual_info are not affected by readout error""" + def test_readout_vnentropy_and_vnentanglemententropy_and_mutualinfo(self, nr_wires, prob): + """Tests the output of qml.vn_entropy, qml.vn_entanglement_entropy, and qml.mutual_info + are not affected by readout error""" dev = qml.device("default.mixed", wires=nr_wires, readout_prob=prob) @qml.qnode(dev) def circuit(): return ( qml.vn_entropy(wires=0, log_base=2), + qml.vn_entanglement_entropy(wires0=[0], wires1=[1], log_base=2), qml.mutual_info(wires0=[0], wires1=[1], log_base=2), ) res = circuit() - expected = np.array([0, 0]) + expected = np.array([0, 0, 0]) assert np.allclose(res, expected) @pytest.mark.parametrize("nr_wires", [2, 3]) diff --git a/tests/devices/test_qubit_device.py b/tests/devices/test_qubit_device.py index 00c1725cf3f..83d66ba59d0 100644 --- a/tests/devices/test_qubit_device.py +++ b/tests/devices/test_qubit_device.py @@ -425,6 +425,17 @@ def test_no_entropy_with_shot_vectors(self, mock_qubit_device_extract_stats): with pytest.raises(NotImplementedError, match="Returning the Von Neumann entropy"): dev.statistics(tape) + def test_vn_entanglement_entropy_with_shot_vectors(self, mock_qubit_device_extract_stats): + + dev = mock_qubit_device_extract_stats() + dev.shots = (10, 10) + tape = qml.tape.QuantumScript([], [qml.vn_entanglement_entropy(wires0=0, wires1=1)]) + + with pytest.raises( + NotImplementedError, match="Returning the Von Neumann entanglement entropy" + ): + dev.statistics(tape) + def test_mutual_info_with_shot_vectors(self, mock_qubit_device_extract_stats): dev = mock_qubit_device_extract_stats() diff --git a/tests/devices/test_qutrit_device.py b/tests/devices/test_qutrit_device.py index 8799d75234b..27ef3398a36 100644 --- a/tests/devices/test_qutrit_device.py +++ b/tests/devices/test_qutrit_device.py @@ -1216,6 +1216,13 @@ def test_state(self, mock_qutrit_device): with pytest.raises(NotImplementedError): dev.state() + def test_density_matrix(self, mock_qutrit_device): + """Test that density_matrix is unimplemented""" + dev = mock_qutrit_device() + + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): + dev.density_matrix(wires=0) + def test_vn_entropy(self, mock_qutrit_device): """Test that vn_entropy is unimplemented""" dev = mock_qutrit_device() @@ -1223,12 +1230,12 @@ def test_vn_entropy(self, mock_qutrit_device): with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): dev.vn_entropy(wires=0, log_base=3) - def test_density_matrix(self, mock_qutrit_device): - """Test that vn_entropy is unimplemented""" + def test_vn_entanglement_entropy(self, mock_qutrit_device): + """Test that vn_entanglement_entropy is unimplemented""" dev = mock_qutrit_device() with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): - dev.density_matrix(wires=0) + dev.vn_entanglement_entropy(0, 1, log_base=3) def test_mutual_info(self, mock_qutrit_device): """Test that mutual_info is unimplemented""" diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 3019da7ddba..3e86a61de32 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -41,6 +41,7 @@ StateMP, Variance, VarianceMP, + VnEntanglementEntropyMP, VnEntropyMP, expval, sample, @@ -184,6 +185,7 @@ class DummyMP(MeasurementProcess): VarianceMP(eigvals=[0.6, 0.7], wires=Wires(0)), VarianceMP(obs=mv), VnEntropyMP(wires=Wires("a"), log_base=3), + VnEntanglementEntropyMP(wires=(Wires("a"), Wires("b")), log_base=3), ] @@ -511,6 +513,7 @@ def test_has_decomposition_false_no_observable(self): CountsMP(wires=["a", 1]), StateMP(), VnEntropyMP(wires=["a", 1]), + VnEntanglementEntropyMP(wires=[["a", 1], ["b", 2]]), MutualInfoMP(wires=[["a", 1], ["b", 2]]), ProbabilityMP(wires=["a", 1]), ], @@ -699,6 +702,7 @@ class TestMeasurementProcess: (qml.state(), (8,)), (qml.density_matrix(wires=[0, 1]), (4, 4)), (qml.mutual_info(wires0=[0], wires1=[1]), ()), + (qml.vn_entanglement_entropy(wires0=[0], wires1=[1]), ()), (qml.vn_entropy(wires=[0, 1]), ()), ] @@ -711,6 +715,7 @@ class TestMeasurementProcess: (qml.sample(qml.PauliZ(0)), (10,)), (qml.sample(), (10, 3)), (qml.mutual_info(wires0=0, wires1=1), ()), + (qml.vn_entanglement_entropy(wires0=[0], wires1=[1]), ()), (qml.vn_entropy(wires=[0, 1]), ()), ] diff --git a/tests/measurements/test_vn_entanglement_entropy.py b/tests/measurements/test_vn_entanglement_entropy.py new file mode 100644 index 00000000000..c48e79c983c --- /dev/null +++ b/tests/measurements/test_vn_entanglement_entropy.py @@ -0,0 +1,413 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# 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. +"""Unit tests for the vn_entanglement_entropy module""" +import copy + +import numpy as np +import pytest + +import pennylane as qml +from pennylane.measurements import VnEntanglementEntropy, VnEntanglementEntropyMP +from pennylane.wires import Wires + +# pylint: disable=too-many-arguments, no-member + + +def expected_entropy_ising_xx(param): + """ + Return the analytical entropy for the IsingXX. + """ + eig_1 = (1 + np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2)) / 2 + eig_2 = (1 - np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2)) / 2 + eigs = [eig_1, eig_2] + eigs = [eig for eig in eigs if eig > 0] + + expected_entropy = eigs * np.log(eigs) + + expected_entropy = -np.sum(expected_entropy) + return expected_entropy + + +def expected_entropy_grad_ising_xx(param): + """ + Return the analytical gradient entropy for the IsingXX. + """ + eig_1 = (1 + np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2)) / 2 + eig_2 = (1 - np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2)) / 2 + eigs = [eig_1, eig_2] + eigs = np.maximum(eigs, 1e-08) + + return -( + (np.log(eigs[0]) + 1) + * (np.sin(param / 2) ** 3 * np.cos(param / 2) - np.sin(param / 2) * np.cos(param / 2) ** 3) + / np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2) + ) - ( + (np.log(eigs[1]) + 1) + * ( + np.sin(param / 2) + * np.cos(param / 2) + * (np.cos(param / 2) ** 2 - np.sin(param / 2) ** 2) + ) + / np.sqrt(1 - 4 * np.cos(param / 2) ** 2 * np.sin(param / 2) ** 2) + ) + + +class TestInitialization: + """Unit tests for the ``qml.vn_entanglement_entropy`` function.""" + + @pytest.mark.all_interfaces + @pytest.mark.parametrize( + "state_vector,expected", + [([1.0, 0.0, 0.0, 1.0] / qml.math.sqrt(2), qml.math.log(2)), ([1.0, 0.0, 0.0, 0.0], 0)], + ) + @pytest.mark.parametrize("interface", ["autograd", "jax", "tf", "torch"]) + def test_vn_entanglement_entropy(self, interface, state_vector, expected): + """Tests the output of qml.vn_entanglement_entropy""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface=interface) + def circuit(): + qml.StatePrep(state_vector, wires=[0, 1]) + return qml.vn_entanglement_entropy(wires0=0, wires1=1) + + assert qml.math.allclose(circuit(), expected) + + def test_queue(self): + """Test that the right measurement class is queued.""" + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev) + def circuit(): + return qml.vn_entanglement_entropy(wires0=0, wires1=1, log_base=2) + + circuit() + + assert isinstance(circuit.tape[0], VnEntanglementEntropyMP) + + def test_copy(self): + """Test that the ``__copy__`` method also copies the ``log_base`` information.""" + meas = qml.vn_entanglement_entropy(wires0=0, wires1=1, log_base=2) + meas_copy = copy.copy(meas) + assert meas_copy.log_base == 2 + assert meas_copy.wires == Wires([0, 1]) + + def test_properties(self): + """Test that the properties are correct.""" + meas = qml.vn_entanglement_entropy(wires0=0, wires1=1) + assert meas.numeric_type == float + assert meas.return_type == VnEntanglementEntropy + + @pytest.mark.parametrize("shots, shape", [(None, ()), (10, ())]) + def test_shape(self, shots, shape): + """Test the ``shape`` method.""" + meas = qml.vn_entanglement_entropy(wires0=0, wires1=1) + + assert meas.shape(shots, 1) == shape + + def test_overlapping_wires_error(self): + """Test that an error is raised if wires0 and wires1 have overlap""" + with pytest.raises( + qml.QuantumFunctionError, + match="Subsystems for computing entanglement entropy must not overlap", + ): + _ = qml.vn_entanglement_entropy(wires0=[0, 1], wires1=[1, 2]) + + +class TestIntegration: + """Integration tests for the vn_entanglement_entropy measurement function.""" + + parameters = np.linspace(0, 2 * np.pi, 10) + + devices = ["default.qubit", "default.mixed", "lightning.qubit"] + + wires_list = [ + [0, 1], + [1, 0], + ] + + base = [2, np.exp(1), 10] + + check_state = [True, False] + + diff_methods = ["backprop", "finite-diff"] + + @pytest.mark.parametrize("shots", [1000, [1, 10, 10, 1000]]) + def test_finite_shots_error(self, shots): + """Test an error is raised when using shot vectors with vn_entanglement_entropy.""" + dev = qml.device("default.qubit", wires=2, shots=shots) + + @qml.qnode(device=dev) + def circuit(x): + qml.Hadamard(wires=[0]) + qml.CRX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(wires0=[0], wires1=[1]) + + with pytest.raises( + qml.DeviceError, match="not accepted with finite shots on default.qubit" + ): + circuit(0.5) + + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("device", devices) + @pytest.mark.parametrize("base", base) + def test_IsingXX_qnode_entropy(self, param, wires, device, base): + """Test entropy for a QNode numpy.""" + + dev = qml.device(device, wires=2) + + @qml.qnode(dev) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + entropy = circuit_entropy(param) + + expected_entropy = expected_entropy_ising_xx(param) / np.log(base) + assert qml.math.allclose(entropy, expected_entropy) + + @pytest.mark.autograd + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("diff_method", diff_methods) + def test_IsingXX_qnode_entropy_grad(self, param, wires, base, diff_method): + """Test entropy for a QNode gradient with autograd.""" + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, diff_method=diff_method) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + grad_entropy = qml.grad(circuit_entropy)(param) + + # higher tolerance for finite-diff method + tol = 1e-8 if diff_method == "backprop" else 1e-5 + + grad_expected_entropy = expected_entropy_grad_ising_xx(param) / np.log(base) + assert qml.math.allclose(grad_entropy, grad_expected_entropy, atol=tol) + + @pytest.mark.torch + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("device", devices) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("interface", ["torch"]) + def test_IsingXX_qnode_torch_entropy(self, param, wires, device, base, interface): + """Test entropy for a QNode with torch interface.""" + import torch + + dev = qml.device(device, wires=2) + + @qml.qnode(dev, interface=interface) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + entropy = circuit_entropy(torch.tensor(param)) + + expected_entropy = expected_entropy_ising_xx(param) / np.log(base) + + assert qml.math.allclose(entropy, expected_entropy) + + @pytest.mark.torch + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("diff_method", diff_methods) + def test_IsingXX_qnode_entropy_grad_torch(self, param, wires, base, diff_method): + """Test entropy for a QNode gradient with torch.""" + import torch + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface="torch", diff_method=diff_method) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + grad_expected_entropy = expected_entropy_grad_ising_xx(param) / np.log(base) + + param = torch.tensor(param, dtype=torch.float64, requires_grad=True) + entropy = circuit_entropy(param) + entropy.backward() + grad_entropy = param.grad + + # higher tolerance for finite-diff method + tol = 1e-8 if diff_method == "backprop" else 1e-5 + + assert qml.math.allclose(grad_entropy, grad_expected_entropy, atol=tol) + + @pytest.mark.tf + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("device", devices) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("interface", ["tf"]) + def test_IsingXX_qnode_tf_entropy(self, param, wires, device, base, interface): + """Test entropy for a QNode with tf interface.""" + import tensorflow as tf + + dev = qml.device(device, wires=2) + + @qml.qnode(dev, interface=interface) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + entropy = circuit_entropy(tf.Variable(param)) + + expected_entropy = expected_entropy_ising_xx(param) / np.log(base) + + assert qml.math.allclose(entropy, expected_entropy) + + @pytest.mark.tf + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("diff_method", diff_methods) + @pytest.mark.parametrize("interface", ["tf"]) + def test_IsingXX_qnode_entropy_grad_tf(self, param, wires, base, diff_method, interface): + """Test entropy for a QNode gradient with tf.""" + import tensorflow as tf + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface=interface, diff_method=diff_method) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + param = tf.Variable(param) + with tf.GradientTape() as tape: + entropy = circuit_entropy(param) + + grad_entropy = tape.gradient(entropy, param) + + grad_expected_entropy = expected_entropy_grad_ising_xx(param) / np.log(base) + + # higher tolerance for finite-diff method + tol = 1e-8 if diff_method == "backprop" else 1e-5 + + assert qml.math.allclose(grad_entropy, grad_expected_entropy, atol=tol) + + @pytest.mark.jax + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("device", devices) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("interface", ["jax"]) + def test_IsingXX_qnode_jax_entropy(self, param, wires, device, base, interface): + """Test entropy for a QNode with jax interface.""" + import jax.numpy as jnp + + dev = qml.device(device, wires=2) + + @qml.qnode(dev, interface=interface) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + entropy = circuit_entropy(jnp.array(param)) + + expected_entropy = expected_entropy_ising_xx(param) / np.log(base) + + assert qml.math.allclose(entropy, expected_entropy) + + @pytest.mark.jax + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("diff_method", diff_methods) + @pytest.mark.parametrize("interface", ["jax"]) + def test_IsingXX_qnode_entropy_grad_jax(self, param, wires, base, diff_method, interface): + """Test entropy for a QNode gradient with Jax.""" + import jax + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface=interface, diff_method=diff_method) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + grad_entropy = jax.grad(circuit_entropy)(jax.numpy.array(param)) + grad_expected_entropy = expected_entropy_grad_ising_xx(param) / np.log(base) + + # higher tolerance for finite-diff method + tol = 1e-8 if diff_method == "backprop" else 1e-5 + + assert qml.math.allclose(grad_entropy, grad_expected_entropy, rtol=1e-04, atol=tol) + + @pytest.mark.jax + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("device", devices) + @pytest.mark.parametrize("interface", ["jax"]) + def test_IsingXX_qnode_jax_jit_entropy(self, param, wires, base, device, interface): + """Test entropy for a QNode with jax-jit interface.""" + import jax + import jax.numpy as jnp + + dev = qml.device(device, wires=2) + + @qml.qnode(dev, interface=interface) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + entropy = jax.jit(circuit_entropy)(jnp.array(param)) + + expected_entropy = expected_entropy_ising_xx(param) / np.log(base) + + assert qml.math.allclose(entropy, expected_entropy) + + @pytest.mark.jax + @pytest.mark.parametrize("wires", wires_list) + @pytest.mark.parametrize("param", parameters) + @pytest.mark.parametrize("base", base) + @pytest.mark.parametrize("diff_method", diff_methods) + @pytest.mark.parametrize("interface", ["jax-jit"]) + def test_IsingXX_qnode_entropy_grad_jax_jit(self, param, wires, base, diff_method, interface): + """Test entropy for a QNode gradient with Jax-jit.""" + import jax + + dev = qml.device("default.qubit", wires=2) + + @qml.qnode(dev, interface=interface, diff_method=diff_method) + def circuit_entropy(x): + qml.IsingXX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(*wires, log_base=base) + + grad_entropy = jax.jit(jax.grad(circuit_entropy))(jax.numpy.array(param)) + + grad_expected_entropy = expected_entropy_grad_ising_xx(param) / np.log(base) + + assert qml.math.allclose(grad_entropy, grad_expected_entropy, rtol=1e-04, atol=1e-05) + + def test_qnode_entropy_custom_wires(self): + """Test that entropy can be returned with custom wires.""" + # Note that this test will only work with devices that map custom wires to standard labels + # before execution. + dev = qml.device("default.qubit", wires=["a", 1]) + + @qml.qnode(dev) + def circuit_entropy(x): + qml.IsingXX(x, wires=["a", 1]) + return qml.vn_entanglement_entropy("a", 1) + + assert np.isclose(circuit_entropy(0.1), expected_entropy_ising_xx(0.1)) diff --git a/tests/test_return_types.py b/tests/test_return_types.py index fda205d4f15..823a13e2c24 100644 --- a/tests/test_return_types.py +++ b/tests/test_return_types.py @@ -133,6 +133,26 @@ def circuit(x): assert res[0].shape == () assert isinstance(res[0], (np.ndarray, np.float64)) + @pytest.mark.parametrize("device", devices) + def test_vn_entanglement_entropy(self, device, interface, shots): + """Return a single vn entanglement entropy.""" + dev = qml.device(device, wires=2, shots=shots) + + def circuit(x): + qml.Hadamard(wires=[0]) + qml.CRX(x, wires=[0, 1]) + return qml.vn_entanglement_entropy(wires0=[0], wires1=[1]) + + qnode = qml.QNode(circuit, dev) + qnode.construct([0.5], {}) + + if dev.shots: + pytest.skip("cannot return analytic measurements with finite shots.") + res = qml.execute(tapes=[qnode.tape], device=dev, gradient_fn=None, interface=interface) + + assert res[0].shape == () + assert isinstance(res[0], (np.ndarray, np.float64)) + @pytest.mark.parametrize("device", devices) def test_mutual_info(self, device, interface, shots): """Return a single mutual information.""" diff --git a/tests/test_return_types_legacy.py b/tests/test_return_types_legacy.py index 2f340af2bed..118697f79f6 100644 --- a/tests/test_return_types_legacy.py +++ b/tests/test_return_types_legacy.py @@ -1192,8 +1192,8 @@ def test_state_return_with_other_types(self): ): qml.execute(tapes=[tape], device=dev, gradient_fn=None) - def test_entropy_no_custom_wires(self): - """Test that entropy cannot be returned with custom wires.""" + def test_vn_entropy_no_custom_wires(self): + """Test that vn_entropy cannot be returned with custom wires.""" dev = qml.device("default.mixed", wires=["a", 1]) @@ -1208,6 +1208,22 @@ def test_entropy_no_custom_wires(self): ): qml.execute(tapes=[tape], device=dev, gradient_fn=None) + def test_vn_entanglement_entropy_no_custom_wires(self): + """Test that vn_entanglement_entropy cannot be returned with custom wires.""" + + dev = qml.device("default.mixed", wires=["a", 1]) + + with qml.queuing.AnnotatedQueue() as q: + qml.PauliX(wires="a") + qml.vn_entanglement_entropy(wires0=["a"], wires1=["b"]) + + tape = qml.tape.QuantumScript.from_queue(q) + with pytest.raises( + qml.QuantumFunctionError, + match="Returning the Von Neumann entanglement entropy is not supported when using custom wire labels", + ): + qml.execute(tapes=[tape], device=dev, gradient_fn=None) + def test_custom_wire_labels_error(self): """Tests that an error is raised when mutual information is measured with custom wire labels"""