Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] `qml.ctrl` does not modify `parameter_frequencies` correctly
### Expected behavior

`qml.ctrl(op)` can be differentiated with the parameter-shift rule if `op` can be differentiated with the parameter-shift rule.
The differentiation yields the correct derivative.

### Actual behavior

The differentiation of `qml.ctrl(op)` will be wrong whenever the generator of the original op is a `SparseHamiltonian` and has more than 1 eigenvalue.


### Additional information

This is because `qml.eigvals` is used to compute the `parameter_frequencies` of `qml.ctrl(op)` and `qml.eigvals` has a kwarg `k`, which defaults to 1 and determines the number of eigenvalues that are being computed for `SparseHamiltonian` inputs.

### Source code

```shell
>>> @qml.qnode(qml.device("default.qubit"))
>>> def circuit(x):
...     qml.Hadamard(0)
...     qml.Hadamard(1)
...     qml.Hadamard(2)
...     qml.ctrl(qml.DoubleExcitationMinus(x, [1, 2, 3, 4]), 0)
...     return qml.expval(qml.dot([1, 1], [qml.PauliY(0), qml.PauliZ(3)]))

>>> x = pnp.array(0.7, requires_grad=True)
>>> circuit(x)
>>> print(qml.grad(circuit)(x))
>>> print(qml.gradients.param_shift(circuit)(x))

-0.43279197822247817
-0.3522647673177669
```


### Tracebacks

_No response_

### System information

```shell
pl dev
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Pull request:
Fix `parameter_frequencies` of `qml.ctrl(op)` if `op.generator()` is `SparseHamiltonian`
**Context:**
The `parameter_frequencies` of `qml.ctrl(op)` are obtained using `qml.eigvals`. If `op.generator` is a `SparseHamiltonian`, `qml.eigvals` only returns one eigenvalue because of the default setting of the kwarg `k`.

**edit:** The same problem appears for the default `qml.operation.parameter_frequencies`.

**Description of the Change:**
This PR explicitly sets the `k` kwarg, in order to obtain all eigenvalues of the generator, even if it is a `SparseHamiltonian`.

**edit:** Also sets `k` in `qml.operation.parameter_frequencies`

**Benefits:**
parameter-shift rule for `qml.ctrl(op)` works, also for `op` being a `DoubleExcitation`, e.g.

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**
Fixed #4898 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index a8c8dc702..67ae41bca 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -304,6 +304,10 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* Fixed a bug where the parameter-shift rule of `qml.ctrl(op)` was wrong if `op` had a generator
+  that has two or more eigenvalues and is stored as a `SparseHamiltonian`.
+  [(#4899)](https://github.com/PennyLaneAI/pennylane/pull/4899)
+
 * Fix a bug where trainable parameters in the post-processing of finite diff were incorrect for Jax when applying
   the transform directly on a ``QNode``.
   [(#4879)](https://github.com/PennyLaneAI/pennylane/pull/4879)
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 4987b4ff9..18b112923 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1760,7 +1760,7 @@ class Operation(Operator):
                 warnings.filterwarnings(
                     action="ignore", message=r".+ eigenvalues will be computed numerically\."
                 )
-                eigvals = qml.eigvals(gen)
+                eigvals = qml.eigvals(gen, k=2**self.num_wires)
 
             eigvals = tuple(np.round(eigvals, 8))
             return [qml.gradients.eigvals_to_frequencies(eigvals)]
diff --git a/pennylane/ops/op_math/controlled.py b/pennylane/ops/op_math/controlled.py
index 1296f9ae8..f47c1944b 100644
--- a/pennylane/ops/op_math/controlled.py
+++ b/pennylane/ops/op_math/controlled.py
@@ -696,7 +696,7 @@ class ControlledOp(Controlled, operation.Operation):
                 warnings.filterwarnings(
                     action="ignore", message=r".+ eigenvalues will be computed numerically\."
                 )
-                base_gen_eigvals = qml.eigvals(base_gen)
+                base_gen_eigvals = qml.eigvals(base_gen, k=2**self.base.num_wires)
 
             # The projectors in the full generator add a eigenvalue of `0` to
             # the eigenvalues of the base generator.


Test patch:
diff --git a/tests/ops/op_math/test_controlled.py b/tests/ops/op_math/test_controlled.py
index 049f7c1ed..e82d7594f 100644
--- a/tests/ops/op_math/test_controlled.py
+++ b/tests/ops/op_math/test_controlled.py
@@ -544,12 +544,13 @@ class TestOperationProperties:
             (qml.RX(1.23, wires=0), [(0.5, 1.0)]),
             (qml.PhaseShift(-2.4, wires=0), [(1,)]),
             (qml.IsingZZ(-9.87, (0, 1)), [(0.5, 1.0)]),
+            (qml.DoubleExcitationMinus(0.7, [0, 1, 2, 3]), [(0.5, 1.0)]),
         ],
     )
     def test_parameter_frequencies(self, base, expected):
         """Test parameter-frequencies against expected values."""
 
-        op = Controlled(base, (3, 4))
+        op = Controlled(base, (4, 5))
         assert op.parameter_frequencies == expected
 
     def test_parameter_frequencies_no_generator_error(self):
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 4f10c556b..06e9f0276 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -836,6 +836,16 @@ class TestOperationConstruction:
         for i in range(num_param):
             assert f[i] == (0.2, x[i])
 
+    def test_frequencies_sparse_generator(self):
+        """Test that the parameter frequencies are correctly deduced from a generator
+        that is a ``SparseHamiltonian``."""
+        DummyOp = copy.copy(qml.DoubleExcitationPlus)
+        DummyOp.parameter_frequencies = qml.operation.Operation.parameter_frequencies
+
+        op = DummyOp(0.7, [0, 1, 2, 3])
+        assert isinstance(op.generator(), qml.SparseHamiltonian)
+        assert op.parameter_frequencies == [(1.0,)]
+
     def test_no_wires_passed(self):
         """Test exception raised if no wires are passed"""