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"""