Repository: PennyLaneAI/pennylane

Issue:
Add `max_entropy` function
### Feature details

It would be nice to add support for $H_{max}(ρ)=\log(rank(ρ))$ [max entropy](http://www.scholarpedia.org/article/Quantum_entropies#Max_entropy_.28Hartley_entropy.29)  in the quantum information module. This can be achieved by implementing a separate function in `pennylane/math/quantum.py` that takes as input either a state vector or a density matrix, plus additional arguments such as the logarithm base, and outputs a scalar.

Of course, we would want the function to be fully differentiable as well.

### Implementation

**Steps to take:**
1. Implement a function `max_entropy()` in `pennylane/math/quantum.py` that takes as input a state vector or a density matrix, plus any additional arguments you see fit.
2. Add tests for the function in `tests/math/test_entropies_math.py`.

**Notes:**
1. For both of these steps, you can refer to the implementations of the other quantum information functions.
2. Other quantum information quantities have additional support for i) measurement processes and ii) QNode transforms. In this case neither of these are required.
3. Differentiability is required. Testing must cover differentiability for all interfaces (autograd, TF, torch, and JAX). (Note that tests for differentiability for the other entropies, such as `vn_entropy`, are instead done in `tests/qinfo/test_entropies.py`. Since you're not implementing a QNode transform, it's fine to place these tests in `tests/math/test_entropies_math.py` instead.)

### How important would you say this feature is?

1: Not important. Would be nice to have.

### Additional information

_No response_


Discussion:
This issue is for technical assessment, pls don't tackle it. You can solve any `first good issue` instead.

Resolving patch:
diff --git a/pennylane/math/__init__.py b/pennylane/math/__init__.py
--- a/pennylane/math/__init__.py
+++ b/pennylane/math/__init__.py
@@ -67,6 +67,7 @@
     sqrt_matrix,
     fidelity,
     relative_entropy,
+    max_entropy,
 )
 
 from .utils import (
@@ -121,6 +122,7 @@ def __getattr__(name):
     "is_abstract",
     "is_independent",
     "marginal_prob",
+    "max_entropy",
     "mutual_info",
     "ones_like",
     "purity",
diff --git a/pennylane/math/quantum.py b/pennylane/math/quantum.py
--- a/pennylane/math/quantum.py
+++ b/pennylane/math/quantum.py
@@ -1014,7 +1014,7 @@ def _check_density_matrix(density_matrix):
         if not allclose(density_matrix, conj_trans):
             raise ValueError("The matrix is not Hermitian.")
         # Check if positive semi-definite
-        evs = np.linalg.eigvalsh(density_matrix)
+        evs, _ = qml.math.linalg.eigh(density_matrix)
         evs = np.real(evs)
         evs_non_negative = [ev for ev in evs if ev >= 0.0]
         if len(evs) != len(evs_non_negative):
@@ -1032,3 +1032,97 @@ def _check_state_vector(state_vector):
     if not is_abstract(norm):
         if not allclose(norm, 1.0, atol=1e-10):
             raise ValueError("Sum of amplitudes-squared does not equal one.")
+
+
+def max_entropy(state, indices, base=None, check_state=False, c_dtype="complex128"):
+    r"""Compute the maximum entropy from a state vector or density matrix on a given subsystem. It supports all
+    interfaces (Numpy, Autograd, Torch, Tensorflow and Jax).
+
+    .. math::
+        S_{\text{max}}( \rho ) = \log( \text{rank} ( \rho ))
+
+    Args:
+        state (tensor_like): ``(2**N)`` state vector or ``(2**N, 2**N)`` density matrix.
+        indices (list(int)): List of indices in the considered subsystem.
+        base (float): Base for the logarithm. If None, the natural logarithm is used.
+        check_state (bool): If True, the function will check the state validity (shape and norm).
+        c_dtype (str): Complex floating point precision type.
+
+    Returns:
+        float: The maximum entropy of the considered subsystem.
+
+    **Example**
+
+    The maximum entropy of a subsystem for any state vector can be obtained. Here is an example for the
+    maximally entangled state, where the subsystem entropy is maximal (default base for log is exponential).
+
+
+    >>> x = [1, 0, 0, 1] / np.sqrt(2)
+    >>> max_entropy(x, indices=[0])
+    0.6931472
+
+    The logarithm base can be switched to 2 for example.
+
+    >>> max_entropy(x, indices=[0], base=2)
+    1.0
+
+    The maximum entropy can be obtained by providing a quantum state as a density matrix, for example:
+
+    >>> y = [[1/2, 0, 0, 1/2], [0, 0, 0, 0], [0, 0, 0, 0], [1/2, 0, 0, 1/2]]
+    >>> max_entropy(x, indices=[0])
+    0.6931472
+
+    The maximum entropy is always greater or equal to the Von Neumann entropy. In this maximally
+    entangled example, they are equal:
+
+    >>> vn_entropy(x, indices=[0])
+    0.6931472
+
+    However, in general, the Von Neumann entropy is lower:
+
+    >>> x = [np.cos(np.pi/8), 0, 0, -1j*np.sin(np.pi/8)]
+    >>> vn_entropy(x, indices=[1])
+    0.4164955
+    >>> max_entropy(x, indices=[1])
+    0.6931472
+
+    """
+    density_matrix = reduced_dm(state, indices, check_state, c_dtype)
+    maximum_entropy = _compute_max_entropy(density_matrix, base)
+
+    return maximum_entropy
+
+
+def _compute_max_entropy(density_matrix, base):
+    """Compute the maximum entropy from a density matrix
+
+    Args:
+        density_matrix (tensor_like): ``(2**N, 2**N)`` tensor density matrix for an integer `N`.
+        base (float, int): Base for the logarithm. If None, the natural logarithm is used.
+
+    Returns:
+        float: Maximum entropy of the density matrix.
+
+    **Example**
+
+    >>> x = [[1/2, 0], [0, 1/2]]
+    >>> _compute_max_entropy(x)
+    0.6931472
+
+    >>> x = [[1/2, 0], [0, 1/2]]
+    >>> _compute_max_entropy(x, base=2)
+    1.0
+
+    """
+    # Change basis if necessary
+    if base:
+        div_base = np.log(base)
+    else:
+        div_base = 1
+
+    evs, _ = qml.math.linalg.eigh(density_matrix)
+    evs = qml.math.real(evs)
+    rank = qml.math.sum(evs / qml.math.where(evs > 1e-8, evs, 1.0))
+    maximum_entropy = qml.math.log(rank) / div_base
+
+    return maximum_entropy


Test patch:
diff --git a/tests/math/test_entropies_math.py b/tests/math/test_entropies_math.py
--- a/tests/math/test_entropies_math.py
+++ b/tests/math/test_entropies_math.py
@@ -305,3 +305,159 @@ def test_size_mismatch(self, state0, state1):
 
         with pytest.raises(qml.QuantumFunctionError, match=msg):
             qml.math.relative_entropy(state0, state1)
+
+
+class TestMaxEntropy:
+    """Tests for computing the maximum entropy of a given state."""
+
+    state_vector = [([1, 0, 0, 1] / np.sqrt(2), False), ([1, 0, 0, 0], True)]
+
+    single_wires_list = [
+        [0],
+        [1],
+    ]
+
+    base = [2, np.exp(1), 10]
+
+    check_state = [True, False]
+
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("state_vector,pure", state_vector)
+    @pytest.mark.parametrize("check_state", check_state)
+    @pytest.mark.parametrize("interface", [None, "autograd", "jax", "tensorflow", "torch"])
+    def test_state_vector_max_entropy_without_base(
+        self, state_vector, wires, check_state, pure, interface
+    ):
+        """Test maximum entropy for different state vectors without base for log."""
+        if interface:
+            state_vector = qml.math.asarray(state_vector, like=interface)
+
+        entropy = qml.math.max_entropy(state_vector, wires, check_state=check_state)
+
+        if pure:
+            expected_max_entropy = 0
+        else:
+            expected_max_entropy = np.log(2)
+        assert qml.math.allclose(entropy, expected_max_entropy)
+
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("state_vector,pure", state_vector)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    @pytest.mark.parametrize("interface", [None, "autograd", "jax", "tensorflow", "torch"])
+    def test_state_vector_max_entropy(
+        self, state_vector, wires, base, check_state, pure, interface
+    ):
+        """Test maximum entropy for different state vectors and log bases."""
+        if interface:
+            state_vector = qml.math.asarray(state_vector, like=interface)
+        entropy = qml.math.max_entropy(state_vector, wires, base, check_state)
+
+        if pure:
+            expected_max_entropy = 0
+        else:
+            expected_max_entropy = np.log(2) / np.log(base)
+
+        assert qml.math.allclose(entropy, expected_max_entropy)
+
+    density_matrices = [
+        ([[1 / 2, 0, 0, 1 / 2], [0, 0, 0, 0], [0, 0, 0, 0], [1 / 2, 0, 0, 1 / 2]], False),
+        ([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], True),
+    ]
+
+    @pytest.mark.parametrize("density_matrix,pure", density_matrices)
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    @pytest.mark.parametrize("interface", [None, "autograd", "jax", "tensorflow", "torch"])
+    def test_density_matrices_max_entropy(
+        self, density_matrix, pure, wires, base, check_state, interface
+    ):
+        """Test maximum entropy for different density matrices."""
+        if interface:
+            density_matrix = qml.math.asarray(density_matrix, like=interface)
+        entropy = qml.math.max_entropy(density_matrix, wires, base, check_state)
+
+        if pure:
+            expected_max_entropy = 0
+        else:
+            expected_max_entropy = np.log(2) / np.log(base)
+
+        assert qml.math.allclose(entropy, expected_max_entropy)
+
+    parameters = [
+        [1, 0, 0, 1] / np.sqrt(2),
+        [[1 / 2, 0, 0, 1 / 2], [0, 0, 0, 0], [0, 0, 0, 0], [1 / 2, 0, 0, 1 / 2]],
+    ]
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("params", parameters)
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    def test_max_entropy_grad(self, params, wires, base, check_state):
+        """Test `max_entropy` differentiability with autograd."""
+        params = np.tensor(params)
+
+        gradient = qml.grad(qml.math.max_entropy)(params, wires, base, check_state)
+
+        assert qml.math.allclose(gradient, 0.0)
+
+    @pytest.mark.torch
+    @pytest.mark.parametrize("params", parameters)
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    def test_max_entropy_grad_torch(self, params, wires, base, check_state):
+        """Test `max_entropy` differentiability with torch interface."""
+        import torch
+
+        params = torch.tensor(params, requires_grad=True)
+
+        max_entropy = qml.math.max_entropy(params, wires, base, check_state)
+        max_entropy.backward()
+        gradient = params.grad
+
+        assert qml.math.allclose(gradient, 0.0)
+
+    @pytest.mark.tf
+    @pytest.mark.parametrize("params", parameters)
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    def test_max_entropy_grad_tf(self, params, wires, base, check_state):
+        """Test `max_entropy` differentiability with tensorflow interface."""
+        import tensorflow as tf
+
+        params = tf.Variable(params)
+
+        with tf.GradientTape() as tape:
+            max_entropy = qml.math.max_entropy(params, wires, base, check_state)
+
+        gradient = tape.gradient(max_entropy, params)
+
+        assert qml.math.allclose(gradient, 0.0)
+
+    @pytest.mark.jax
+    @pytest.mark.parametrize("params", parameters)
+    @pytest.mark.parametrize("wires", single_wires_list)
+    @pytest.mark.parametrize("base", base)
+    @pytest.mark.parametrize("check_state", check_state)
+    @pytest.mark.parametrize("jit", [False, True])
+    def test_max_entropy_grad_jax(self, params, wires, base, check_state, jit):
+        """Test `max_entropy` differentiability with jax."""
+        import jax
+        import jax.numpy as jnp
+
+        params = jnp.array(params)
+
+        max_entropy_grad = jax.grad(qml.math.max_entropy)
+
+        if jit:
+            gradient = jax.jit(max_entropy_grad, static_argnums=[1, 2, 3])(
+                params, tuple(wires), base, check_state
+            )
+        else:
+            gradient = max_entropy_grad(params, wires, base, check_state)
+
+        assert qml.math.allclose(gradient, 0.0)