Repository: PennyLaneAI/pennylane

Issue:
[BUG] `null.qubit` no longer plays nicely with custom operations
### Expected behavior

``` py
import pennylane as qml
from pennylane.resource.error import ErrorOperation

class IssueErrorOp(ErrorOperation):
    def __init__(self, phase, wires):
        self.phase = phase
        super().__init__(phase, wires=wires)
    def error(self):
        return 1.0

@qml.qnode(qml.device('null.qubit', wires=2))
def circuit():
    IssueErrorOp(1.0, [0])
    return qml.state()

>>> circuit()
[0.0]
```

`null.qubit` should allow for adding custom operators to the workflow.

### Actual behavior

``` py
import pennylane as qml
from pennylane.resource.error import ErrorOperation

class IssueErrorOp(ErrorOperation):
    def __init__(self, phase, wires):
        self.phase = phase
        super().__init__(phase, wires=wires)
    def error(self):
        return 1.0

@qml.qnode(qml.device('null.qubit', wires=2))
def circuit():
    IssueErrorOp(1.0, [0])
    return qml.state()

>>> circuit()
DeviceError: Operator ReplyErrorOp(1.0, wires=[0]) not supported on null.qubit and does not provide a decomposition.
```

### Additional information

This issue exists on the master and possibly comes from using `DefaultQubit`'s `preprocess` for `null.qubit` as well. 


### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.36.0.dev0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages
Editable project location: /Users/utkarsh/Xanadu/Codes/SourceCodes/pennylane
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: amazon-braket-pennylane-plugin, PennyLane-Catalyst, PennyLane-Cirq, PennyLane-qiskit, PennyLane_Lightning, quantum-datasets

Platform info:           macOS-13.6.4-arm64-arm-64bit
Python version:          3.10.4
Numpy version:           1.26.4
Scipy version:           1.12.0
Installed devices:
- default.clifford (PennyLane-0.36.0.dev0)
- default.gaussian (PennyLane-0.36.0.dev0)
- default.mixed (PennyLane-0.36.0.dev0)
- default.qubit (PennyLane-0.36.0.dev0)
- default.qubit.autograd (PennyLane-0.36.0.dev0)
- default.qubit.jax (PennyLane-0.36.0.dev0)
- default.qubit.legacy (PennyLane-0.36.0.dev0)
- default.qubit.tf (PennyLane-0.36.0.dev0)
- default.qubit.torch (PennyLane-0.36.0.dev0)
- default.qutrit (PennyLane-0.36.0.dev0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Discussion:
github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5582?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.69%. Comparing base [(`9151477`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9151477d0ae946fefd74f2634188a1ea5b6a5ca8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`1fff3e6`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5582?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 9 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5582      +/-   ##
==========================================
- Coverage   99.69%   99.69%   -0.01%     
==========================================
  Files         410      412       +2     
  Lines       38278    38159     -119     
==========================================
- Hits        38161    38041     -120     
- Misses        117      118       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5582?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index a1d7cae39..7d475e151 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -543,6 +543,9 @@
 
 <h3>Bug fixes 🐛</h3>
 
+* `null.qubit` now automatically supports any operation without a decomposition.
+  [(#5582)](https://github.com/PennyLaneAI/pennylane/pull/5582)
+
 * Fixed a bug where the shape and type of derivatives obtained by applying a gradient transform to
   a QNode differed, based on whether the QNode uses classical coprocessing.
   [(#4945)](https://github.com/PennyLaneAI/pennylane/pull/4945)
diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py
index 8b62afd47..9dd08dd36 100644
--- a/pennylane/devices/null_qubit.py
+++ b/pennylane/devices/null_qubit.py
@@ -45,6 +45,7 @@ from pennylane.measurements import (
 
 from . import Device, DefaultQubit
 from .execution_config import ExecutionConfig, DefaultExecutionConfig
+from .preprocess import decompose
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.NullHandler())
@@ -277,10 +278,28 @@ class NullQubit(Device):
         """No-op property to allow for borrowing DefaultQubit.preprocess without AttributeErrors"""
         return None
 
+    # pylint: disable=cell-var-from-loop
     def preprocess(
         self, execution_config=DefaultExecutionConfig
     ) -> Tuple[TransformProgram, ExecutionConfig]:
         program, _ = DefaultQubit.preprocess(self, execution_config)
+        for t in program:
+            if t.transform == decompose.transform:
+                original_stopping_condition = t.kwargs["stopping_condition"]
+
+                def new_stopping_condition(op):
+                    return (not op.has_decomposition) or original_stopping_condition(op)
+
+                t.kwargs["stopping_condition"] = new_stopping_condition
+
+                original_shots_stopping_condition = t.kwargs.get("stopping_condition_shots", None)
+                if original_shots_stopping_condition:
+
+                    def new_shots_stopping_condition(op):
+                        return (not op.has_decomposition) or original_shots_stopping_condition(op)
+
+                    t.kwargs["stopping_condition_shots"] = new_shots_stopping_condition
+
         updated_values = {}
         if execution_config.gradient_method in ["best", "adjoint"]:
             updated_values["gradient_method"] = "device"


Test patch:
diff --git a/tests/devices/test_null_qubit.py b/tests/devices/test_null_qubit.py
index 507fc9f2e..369469182 100644
--- a/tests/devices/test_null_qubit.py
+++ b/tests/devices/test_null_qubit.py
@@ -64,6 +64,28 @@ def test_debugger_attribute():
     assert dev._debugger is None
 
 
+@pytest.mark.parametrize("shots", (None, 10))
+def test_supports_operator_without_decomp(shots):
+    """Test that null.qubit automatically supports any operation without a decomposition."""
+
+    # pylint: disable=too-few-public-methods
+    class MyOp(qml.operation.Operator):
+        pass
+
+    tape = qml.tape.QuantumScript([MyOp(wires=(0, 1))], [qml.expval(qml.Z(0))], shots=shots)
+    dev = NullQubit()
+
+    program, _ = dev.preprocess()
+    batch, _ = program((tape,))
+
+    assert isinstance(batch[0][0], MyOp)
+
+    with dev.tracker:
+        _ = dev.execute(batch)
+
+    assert dev.tracker.latest["resources"].gate_types["MyOp"] == 1
+
+
 def test_tracking():
     """Test some tracking values for null.qubit"""