Repository: PennyLaneAI/pennylane

Issue:
[BUG] pennylane.tape.Unwarp does not unwrap Hamiltonian coefficients
### Expected behavior

`qml.tape.unwrap` convert all ArrayBox in a tape to usual arrays.

### Actual behavior

Coefficients of the `Hamiltonian` object instance (accessible via `ham.coeffs`) are not unwrapped.

### Additional information

_No response_

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.26.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /home/chae-yeun/work/Xanadu/devel-py39/lib/python3.9/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, retworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning

Platform info:           Linux-5.14.0-1042-oem-x86_64-with-glibc2.35
Python version:          3.9.13
Numpy version:           1.23.2
Scipy version:           1.9.0
Installed devices:
- lightning.qubit (PennyLane-Lightning-0.26.0.dev1)
- default.gaussian (PennyLane-0.26.0.dev0)
- default.mixed (PennyLane-0.26.0.dev0)
- default.qubit (PennyLane-0.26.0.dev0)
- default.qubit.autograd (PennyLane-0.26.0.dev0)
- default.qubit.jax (PennyLane-0.26.0.dev0)
- default.qubit.tf (PennyLane-0.26.0.dev0)
- default.qubit.torch (PennyLane-0.26.0.dev0)
- default.qutrit (PennyLane-0.26.0.dev0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.


Discussion:
@chaeyeunpark  Can we close this issue now? I see there is a merged pull request.
@albi3ro The PR linked to this issue is an ad-hoc fix for Lightning. This issue for PennyLane is still active.

Resolving patch:
diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py
--- a/pennylane/ops/functions/bind_new_parameters.py
+++ b/pennylane/ops/functions/bind_new_parameters.py
@@ -47,7 +47,7 @@ def bind_new_parameters(op: Operator, params: Sequence[TensorLike]) -> Operator:
     """
     try:
         return op.__class__(*params, wires=op.wires, **copy.deepcopy(op.hyperparameters))
-    except TypeError:
+    except (TypeError, ValueError):
         # operation is doing something different with its call signature.
         new_op = copy.deepcopy(op)
         new_op.data = tuple(params)
@@ -168,7 +168,10 @@ def bind_new_parameters_pow(op: Pow, params: Sequence[TensorLike]):
 
 @bind_new_parameters.register
 def bind_new_parameters_hamiltonian(op: qml.Hamiltonian, params: Sequence[TensorLike]):
-    return qml.Hamiltonian(params, op.ops)
+    new_H = qml.Hamiltonian(params, op.ops)
+    if op.grouping_indices is not None:
+        new_H.grouping_indices = op.grouping_indices
+    return new_H
 
 
 @bind_new_parameters.register
diff --git a/pennylane/transforms/convert_to_numpy_parameters.py b/pennylane/transforms/convert_to_numpy_parameters.py
--- a/pennylane/transforms/convert_to_numpy_parameters.py
+++ b/pennylane/transforms/convert_to_numpy_parameters.py
@@ -15,34 +15,32 @@
 This file contains preprocessings steps that may be called internally
 during execution.
 """
-import copy
-
 import pennylane as qml
 from pennylane import math
 from pennylane.tape import QuantumScript
 
 
+# pylint: disable=no-member
 def _convert_op_to_numpy_data(op: qml.operation.Operator) -> qml.operation.Operator:
     if math.get_interface(*op.data) == "numpy":
         return op
     # Use operator method to change parameters when it become available
-    copied_op = copy.copy(op)
-    copied_op.data = math.unwrap(op.data)
-    return copied_op
+    return qml.ops.functions.bind_new_parameters(op, math.unwrap(op.data))
 
 
+# pylint: disable=no-member
 def _convert_measurement_to_numpy_data(
     m: qml.measurements.MeasurementProcess,
 ) -> qml.measurements.MeasurementProcess:
-    if m.obs is None or math.get_interface(*m.obs.data) == "numpy":
+    if m.obs is None:
+        if m.eigvals() is None or math.get_interface(m.eigvals()) == "numpy":
+            return m
+        return type(m)(wires=m.wires, eigvals=math.unwrap(m.eigvals()))
+
+    if math.get_interface(*m.obs.data) == "numpy":
         return m
-    # Use measurement method to change parameters when it becomes available
-    copied_m = copy.copy(m)
-    if isinstance(copied_m.obs, qml.operation.Tensor):
-        copied_m.obs.data = math.unwrap([o.data for o in m.obs.obs])
-    else:
-        copied_m.obs.data = math.unwrap(m.obs.data)
-    return copied_m
+    new_obs = qml.ops.functions.bind_new_parameters(m.obs, math.unwrap(m.obs.data))
+    return type(m)(obs=new_obs)
 
 
 # pylint: disable=protected-access


Test patch:
diff --git a/tests/interfaces/test_autograd_qnode.py b/tests/interfaces/test_autograd_qnode.py
--- a/tests/interfaces/test_autograd_qnode.py
+++ b/tests/interfaces/test_autograd_qnode.py
@@ -87,7 +87,7 @@ def circuit(x, y):
 
         def mock_apply(*args, **kwargs):
             for op in args[0]:
-                param_data.extend(op.data.copy())
+                param_data.extend(op.data)
 
         mocker.patch.object(dev, "apply", side_effect=mock_apply)
         circuit(x, y)
diff --git a/tests/ops/functions/test_bind_new_parameters.py b/tests/ops/functions/test_bind_new_parameters.py
--- a/tests/ops/functions/test_bind_new_parameters.py
+++ b/tests/ops/functions/test_bind_new_parameters.py
@@ -207,6 +207,15 @@ def test_hamiltonian(H, new_coeffs, expected_H):
     assert new_H is not H
 
 
+def test_hamiltonian_grouping_indices():
+    """Test that bind_new_parameters with a Hamiltonian preserves the grouping indices."""
+    H = qml.Hamiltonian([1.0, 2.0], [qml.PauliX(0), qml.PauliX(1)])
+    H.compute_grouping()
+    new_H = bind_new_parameters(H, [2.3, 3.4])
+    assert H.grouping_indices == new_H.grouping_indices
+    assert new_H.data == (2.3, 3.4)
+
+
 @pytest.mark.parametrize(
     "op, new_params, expected_op",
     [
diff --git a/tests/transforms/test_convert_to_numpy_parameters.py b/tests/transforms/test_convert_to_numpy_parameters.py
--- a/tests/transforms/test_convert_to_numpy_parameters.py
+++ b/tests/transforms/test_convert_to_numpy_parameters.py
@@ -44,11 +44,16 @@ def test_convert_arrays_to_numpy(framework, shots):
 
     numpy_data = np.array(0.62)
 
-    ops = [qml.RX(x, 0), qml.RY(y, 1), qml.CNOT((0, 1)), qml.RZ(numpy_data, 0)]
+    ops = [
+        qml.StatePrep(state, 0),
+        qml.RX(x, 0),
+        qml.RY(y, 1),
+        qml.CNOT((0, 1)),
+        qml.RZ(numpy_data, 0),
+    ]
     m = [qml.state(), qml.expval(qml.Hermitian(M, 0))]
-    prep = [qml.StatePrep(state, 0)]
 
-    qs = qml.tape.QuantumScript(ops, m, prep, shots=shots)
+    qs = qml.tape.QuantumScript(ops, m, shots=shots)
     new_qs = convert_to_numpy_parameters(qs)
 
     # check ops that should be unaltered
@@ -111,3 +116,24 @@ def test_unwraps_tensor_observables():
 
     unwrapped_m = _convert_measurement_to_numpy_data(m)
     assert qml.math.get_interface(*unwrapped_m.obs.data) == "numpy"
+
+
+@pytest.mark.torch
+def test_unwraps_mp_eigvals():
+    """Test that a measurememnt process with autograd eigvals unwraps them to numpy."""
+    import torch
+
+    eigvals = torch.tensor([0.5, 0.5])
+    m = qml.measurements.ExpectationMP(eigvals=eigvals, wires=qml.wires.Wires(0))
+
+    unwrapped_m = _convert_measurement_to_numpy_data(m)
+    assert qml.math.get_interface(unwrapped_m.eigvals) == "numpy"
+
+
+def test_mp_numpy_eigvals():
+    """Test that a measurement process with numpy eigvals is returned unchanged."""
+    eigvals = np.array([0.5, 0.5])
+    m = qml.measurements.ExpectationMP(eigvals=eigvals, wires=qml.wires.Wires(0))
+
+    unwrapped_m = _convert_measurement_to_numpy_data(m)
+    assert m is unwrapped_m