Repository: PennyLaneAI/pennylane
Pull request #6803 (2025-01-09 21:41:20)

Add `QNode.update` method
**Context:**

There’s often a need to tweak an existing workflow to test new configurations, whether that involves a different:
- device
- differentiation method
- interface
- or any other general configuration setting.

**Description of the Change:**

Introduces an `update` method as an instance method for `QNode`, allowing the creation of a new instance with modified constructor arguments.

**Benefits:**

This change enhances:
- Flexibility when running workflows
- Test suite efficiency by reducing the code required to switch configurations during tests

**Possible Drawbacks:**  None identified.

[sc-81362]

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/6803?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.60%. Comparing base [(`5efeffb`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/5efeffbcbbd89e9ea90b3e4cf4c3a043e73cf053?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`62962b7`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/62962b7fb37f15ec67678dcd025453de84eae987?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 1 commits behind head on master.

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


```diff
@@           Coverage Diff           @@
##           master    #6803   +/-   ##
=======================================
  Coverage   99.60%   99.60%           
=======================================
  Files         476      476           
  Lines       45232    45243   +11     
=======================================
+ Hits        45055    45066   +11     
  Misses        177      177           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6803?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).

andrijapau: @albi3ro 

> My remaining question is about advertising. What would be the best way to let people know they can do this now? 

Good question. As you already mentioned, I think having some hands-on time to understand in what circumstances in which the method could improve a workflow would be beneficial. For now, I can add a sub-section titled "Updating a quantum node" within the "Creating a quantum node" section in the documentation. I think that would be a good starting point for users.

Patch:
diff --git a/doc/introduction/circuits.rst b/doc/introduction/circuits.rst
index 2dc13da2f..9a587d321 100644
--- a/doc/introduction/circuits.rst
+++ b/doc/introduction/circuits.rst
@@ -242,6 +242,28 @@ or the :func:`~.pennylane.draw_mpl` transform:
 
 .. _intro_vcirc_decorator:
 
+Re-configuring QNode settings
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+There is often a need to modify an existing QNode setup to test a new configuration. This includes,
+but is not limited to, executing on a different quantum device, using a new differentiation method or 
+machine learning interface, etc. The :meth:`~.pennylane.QNode.update` method provides a convenient
+way to make these adjustments. To update one or more QNode settings, simply give a new value to the 
+QNode keyword argument you want to change (e.g., `mcm_method=...`, `diff_method=...`, etc.). Only arguments
+used to instantiate a :class:`~.pennylane.QNode` can be updated, objects like the transform program cannot be updated 
+using this method.
+
+For instance, to use a different quantum device, the configuration can be updated with,
+
+>>> new_dev = qml.device('lightning.qubit', wires=dev_unique_wires.wires)
+>>> new_circuit = circuit.update(device = new_dev)
+>>> print(new_circuit.device.name)
+lightning.qubit
+>>> print(qml.draw(new_circuit)(np.pi/4, 0.7))
+aux: ───────────╭●─┤     
+ q1: ──RZ(0.79)─╰X─┤     
+ q2: ──RY(0.70)────┤  <Z>
+
 The QNode decorator
 -------------------
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 3c335a944..659407fc5 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,6 +6,29 @@
 
 <h3>Improvements 🛠</h3>
 
+* `QNode` objects now have an `update` method that allows for re-configuring settings like `diff_method`, `mcm_method`, and more. This allows for easier on-the-fly adjustments to workflows. Any arguments not specified will retain their original value.
+  [(#6803)](https://github.com/PennyLaneAI/pennylane/pull/6803)
+
+  After constructing a `QNode`,
+  ```python
+  import pennylane as qml
+
+  @qml.qnode(device=qml.device("default.qubit"))
+  def circuit():
+    qml.H(0)
+    qml.CNOT([0,1])
+    return qml.probs()
+  ```
+  its settings can be modified with `update`, which returns a new `QNode` object. Here is an example
+  of updating a QNode's `diff_method`:
+  ```pycon
+  >>> print(circuit.diff_method)
+  best
+  >>> new_circuit = circuit.update(diff_method="parameter-shift")
+  >>> print(new_circuit.diff_method)
+  'parameter-shift'
+  ```
+  
 * Finite shot and parameter-shift executions on `default.qubit` can now
   be natively jitted end-to-end, leading to performance improvements.
   Devices can now configure whether or not ML framework data is sent to them
diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py
index 1db50d2d0..723d58049 100644
--- a/pennylane/workflow/qnode.py
+++ b/pennylane/workflow/qnode.py
@@ -508,6 +508,8 @@ class QNode:
         mcm_method: Literal[None, "deferred", "one-shot", "tree-traversal"] = None,
         **gradient_kwargs,
     ):
+        self._init_args = locals()
+        del self._init_args["self"]
 
         if logger.isEnabledFor(logging.DEBUG):
             logger.debug(
@@ -626,6 +628,71 @@ class QNode:
         """
         self._transform_program.push_back(transform_container=transform_container)
 
+    def update(self, **kwargs) -> "QNode":
+        """Returns a new QNode instance but with updated settings (e.g., a different `diff_method`). Any settings not specified will retain their original value.
+
+        .. note::
+            The QNode`s transform program cannot be updated using this method.
+
+        Keyword Args:
+            **kwargs: The provided keyword arguments must match that of :meth:`QNode.__init__`.
+                The list of supported gradient keyword arguments can be found at ``qml.gradients.SUPPORTED_GRADIENT_KWARGS``.
+
+        Returns:
+            qnode (QNode): new QNode with updated settings
+
+
+        Raises:
+            ValueError: if provided keyword arguments are invalid
+
+        **Example**
+
+        Let's begin by defining a ``QNode`` object,
+
+        .. code-block:: python
+
+            dev = qml.device("default.qubit")
+
+            @qml.qnode(dev, diff_method="parameter-shift")
+            def circuit(x):
+                qml.RZ(x, wires=0)
+                qml.CNOT(wires=[0, 1])
+                qml.RY(x, wires=1)
+                return qml.expval(qml.PauliZ(1))
+
+        If we wish to try out a new configuration without having to repeat the
+        boiler plate above, we can use the ``QNode.update`` method. For example,
+        we can update the differentiation method and execution arguments,
+
+        >>> new_circuit = circuit.update(diff_method="adjoint", device_vjp=True)
+        >>> print(new_circuit.diff_method)
+        adjoint
+        >>> print(new_circuit.execute_kwargs["device_vjp"])
+        True
+
+        Similarly, if we wish to re-configure the interface used for execution,
+
+        >>> new_circuit= circuit.update(interface="torch")
+        >>> new_circuit(1)
+        tensor(0.5403, dtype=torch.float64)
+        """
+        if not kwargs:
+            valid_params = (
+                set(self._init_args.copy().pop("gradient_kwargs"))
+                | qml.gradients.SUPPORTED_GRADIENT_KWARGS
+            )
+            raise ValueError(
+                f"Must specify at least one configuration property to update. Valid properties are: {valid_params}."
+            )
+        original_init_args = self._init_args.copy()
+        gradient_kwargs = original_init_args.pop("gradient_kwargs")
+        original_init_args.update(gradient_kwargs)
+        original_init_args.update(kwargs)
+        updated_qn = QNode(**original_init_args)
+        # pylint: disable=protected-access
+        updated_qn._transform_program = qml.transforms.core.TransformProgram(self.transform_program)
+        return updated_qn
+
     # pylint: disable=too-many-return-statements, unused-argument
     @staticmethod
     @debug_logger


Test patch:
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index 7a58b4762..c60ae352e 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -92,6 +92,123 @@ def test_copy():
     assert copied_qn.diff_method == qn.diff_method
 
 
+class TestUpdate:
+    """Tests the update instance method of QNode"""
+
+    def test_new_object_creation(self):
+        """Test that a new object is created rather than mutated"""
+        dev = qml.device("default.qubit")
+        dummy_qn = qml.QNode(dummyfunc, dev)
+        updated_qn = dummy_qn.update(device=dummy_qn.device)
+        assert updated_qn is not dummy_qn
+
+    def test_empty_update(self):
+        """Test that providing no update parameters throws an error."""
+        dev = qml.device("default.qubit")
+        dummy_qn = qml.QNode(dummyfunc, dev)
+        with pytest.raises(
+            ValueError, match="Must specify at least one configuration property to update."
+        ):
+            dummy_qn.update()
+
+    def test_update_args(self):
+        """Test that arguments of QNode can be updated"""
+        dev = qml.device("default.qubit")
+        dummy_qn = qml.QNode(dummyfunc, dev)
+
+        def circuit(x):
+            qml.RZ(x, wires=0)
+            qml.CNOT(wires=[0, 1])
+            qml.RY(x, wires=1)
+            return qml.expval(qml.PauliZ(1))
+
+        new_circuit = dummy_qn.update(func=circuit)
+        assert new_circuit.func is circuit
+
+    @pytest.mark.torch
+    def test_update_kwargs(self):
+        """Test that keyword arguments can be updated"""
+        dev = qml.device("default.qubit")
+        dummy_qn = qml.QNode(dummyfunc, dev)
+
+        def circuit(x):
+            qml.RZ(x, wires=0)
+            qml.CNOT(wires=[0, 1])
+            qml.RY(x, wires=1)
+            return qml.expval(qml.PauliZ(1))
+
+        assert dummy_qn.interface == "auto"
+        new_circuit = dummy_qn.update(func=circuit).update(interface="torch")
+        assert qml.math.get_interface(new_circuit(1)) == "torch"
+
+    def test_update_gradient_kwargs(self):
+        """Test that gradient kwargs are updated correctly"""
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev, atol=1)
+        def circuit(x):
+            qml.RZ(x, wires=0)
+            qml.CNOT(wires=[0, 1])
+            qml.RY(x, wires=1)
+            return qml.expval(qml.PauliZ(1))
+
+        assert len(circuit.gradient_kwargs) == 1
+        assert list(circuit.gradient_kwargs.keys()) == ["atol"]
+
+        new_atol_circuit = circuit.update(atol=2)
+        assert len(new_atol_circuit.gradient_kwargs) == 1
+        assert list(new_atol_circuit.gradient_kwargs.keys()) == ["atol"]
+        assert new_atol_circuit.gradient_kwargs["atol"] == 2
+
+        new_kwarg_circuit = circuit.update(h=1)
+        assert len(new_kwarg_circuit.gradient_kwargs) == 2
+        assert list(new_kwarg_circuit.gradient_kwargs.keys()) == ["atol", "h"]
+        assert new_kwarg_circuit.gradient_kwargs["atol"] == 1
+        assert new_kwarg_circuit.gradient_kwargs["h"] == 1
+
+        with pytest.warns(
+            UserWarning,
+            match="Received gradient_kwarg blah, which is not included in the list of standard qnode gradient kwargs.",
+        ):
+            circuit.update(blah=1)
+
+    def test_update_multiple_arguments(self):
+        """Test that multiple parameters can be updated at once."""
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev)
+        def circuit(x):
+            qml.RZ(x, wires=0)
+            qml.CNOT(wires=[0, 1])
+            qml.RY(x, wires=1)
+            return qml.expval(qml.PauliZ(1))
+
+        assert circuit.diff_method == "best"
+        assert not circuit.execute_kwargs["device_vjp"]
+        new_circuit = circuit.update(diff_method="adjoint", device_vjp=True)
+        assert new_circuit.diff_method == "adjoint"
+        assert new_circuit.execute_kwargs["device_vjp"]
+
+    def test_update_transform_program(self):
+        """Test that the transform program is properly preserved"""
+        dev = qml.device("default.qubit", wires=2)
+
+        @qml.transforms.combine_global_phases
+        @qml.qnode(dev, atol=1)
+        def circuit(x):
+            qml.RZ(x, wires=0)
+            qml.GlobalPhase(phi=1)
+            qml.CNOT(wires=[0, 1])
+            qml.RY(x, wires=1)
+            return qml.expval(qml.PauliZ(1))
+
+        assert qml.transforms.combine_global_phases in circuit.transform_program
+        assert len(circuit.transform_program) == 1
+        new_circuit = circuit.update(diff_method="parameter-shift")
+        assert new_circuit.diff_method == "parameter-shift"
+        assert circuit.transform_program == new_circuit.transform_program
+
+
 class TestInitialization:
     """Testing the initialization of the qnode."""