Repository: PennyLaneAI/pennylane
Pull request #1196 (2021-04-08 12:04:26)

Informative error message for removal of attribute `analytic` in devices
**Context:**

The merged PR #1079 removes the keyword argument `analytic` in favour of setting `shots=None`. However this change will break old code and users will be left with the error that `analytic` is an unexpected keyword argument. To make it easy for users I added an error message.

**Description of the Change:**

Added the keyword argument `analytic=None` and an explicit check if it is set to `True`. Setting `analytic=True` raises an informative error message - "The attribute `analytic` has been replaced by `shots=None`. Please use `shots=None` instead of `analytic=True`."

We could remove this in future versions when all the documentation is updated and the change is assimilated.

**Benefits:**

Users can easily know what to change without needing to search for the error.

**Possible Drawbacks:**

@josh146  suggested using a **kwargs and checking the keys to see if `analytic` is present. But that would also mean that we allow the users to input other keyword arguments which would silently do nothing. Therefore I use the explicit argument `analytic` instead of using **kwargs.

**Related GitHub Issues:**
PR #1079

codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=h1) Report
> Merging [#1196](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=desc) (b85e343) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/ab1372566ddd8c4e11cd40bd658dda156eabe2af?el=desc) (ab13725) will **decrease** coverage by `1.09%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1196      +/-   ##
==========================================
- Coverage   99.09%   98.00%   -1.10%     
==========================================
  Files         142      145       +3     
  Lines       10439    10965     +526     
==========================================
+ Hits        10345    10746     +401     
- Misses         94      219     +125     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL19kZXZpY2UucHk=) | `95.41% <40.00%> (-4.15%)` | :arrow_down: |
| [pennylane/devices/default\_mixed.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9taXhlZC5weQ==) | `98.03% <40.00%> (-1.97%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_autograd.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9hdXRvZ3JhZC5weQ==) | `95.16% <40.00%> (-4.84%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `90.14% <40.00%> (-5.32%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_tf.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF90Zi5weQ==) | `87.34% <40.00%> (-5.81%)` | :arrow_down: |
| [pennylane/devices/default\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdC5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/tape/operation\_recorder.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RhcGUvb3BlcmF0aW9uX3JlY29yZGVyLnB5) | `64.51% <0.00%> (-35.49%)` | :arrow_down: |
| [pennylane/measure.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21lYXN1cmUucHk=) | `91.76% <0.00%> (-8.24%)` | :arrow_down: |
| [pennylane/circuit\_drawer/grid.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2NpcmN1aXRfZHJhd2VyL2dyaWQucHk=) | `88.70% <0.00%> (-7.72%)` | :arrow_down: |
| [pennylane/grouping/optimize\_measurements.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2dyb3VwaW5nL29wdGltaXplX21lYXN1cmVtZW50cy5weQ==) | `92.30% <0.00%> (-7.70%)` | :arrow_down: |
| ... and [29 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=footer). Last update [ab13725...b85e343](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1196?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

quantshah: @josh146 Thanks for rebasing the master branch. This is ready for review. I think the test coverage will fail due to not testing of the abstract class. I guess it will be ignored?
josh146: Thanks @quantshah! This is great.

> I think the test coverage will fail due to not testing of the abstract class. I guess it will be ignored?

I just had a thought while looking at the changes --- since the error message is defined in the parent abstract class, does it need to be defined in the inheriting devices as well? 
quantshah: @josh146 Thanks for the comment. Indeed it was rather unnecessary to add the error message to all subclasses. I forgot basic object-oriented programming concepts :p. Now the changes are ready to be reviewed again. The error message is only in the two base classes `Device` and `QubitDevice` with the subclasses just having the keyword `analytic` passed to the `super` method. I hope this also fixes the code coverage issue so I get all the green ticks from CI. 

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index faf52cdd8..1c10aa341 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -658,6 +658,9 @@
 
 <h3>Breaking changes</h3>
 
+* Adds an informative error message for removal of the `analytic` keyword in devices. Users are directed to use `shots=None` instead.
+  [(#1196)](https://github.com/PennyLaneAI/pennylane/pull/1196)
+
 * A deprecation warning is now raised when loading content from the `qnn` module. In release 
   `0.16.0`, the `qnn` module will no-longer be automatically loaded due to its dependency on
   TensorFlow and Torch. Instead, users will need to do `from pennylane import qnn`.
@@ -776,9 +779,8 @@
 
 This release contains contributions from (in alphabetical order):
 
-Juan Miguel Arrazola, Thomas Bromley, Olivia Di Matteo, Kyle Godbey, Diego Guala, Josh Izaac,
-Christina Lee, Daniel Polatajko, Chase Roberts, Sankalp Sanand, Pritish Sehzpaul, Maria Schuld,
-Antal Száva.
+Shahnawaz Ahmed, Juan Miguel Arrazola, Thomas Bromley, Olivia Di Matteo, Kyle Godbey, Diego Guala, Josh Izaac,
+Daniel Polatajko, Chase Roberts, Sankalp Sanand, Pritish Sehzpaul, Maria Schuld, Antal Száva.
 
 # Release 0.14.1 (current release)
 
diff --git a/pennylane/_device.py b/pennylane/_device.py
index e8f02a4ec..631a37465 100644
--- a/pennylane/_device.py
+++ b/pennylane/_device.py
@@ -118,10 +118,15 @@ class Device(abc.ABC):
     _circuits = {}  #: dict[str->Circuit]: circuit templates associated with this API class
     _asarray = staticmethod(np.asarray)
 
-    def __init__(self, wires=1, shots=1000):
+    def __init__(self, wires=1, shots=1000, *, analytic=None):
 
         self.shots = shots
 
+        if analytic is not None:
+            msg = "The analytic argument has been replaced by shots=None. "
+            msg += "Please use shots=None instead of analytic=True."
+            raise DeviceError(msg)
+
         if not isinstance(wires, Iterable):
             # interpret wires as the number of consecutive wires
             wires = range(wires)
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index 057be9b9c..9ae321caa 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -111,8 +111,8 @@ class QubitDevice(Device):
 
     observables = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Hermitian", "Identity"}
 
-    def __init__(self, wires=1, shots=None, cache=0):
-        super().__init__(wires=wires, shots=shots)
+    def __init__(self, wires=1, shots=None, cache=0, analytic=None):
+        super().__init__(wires=wires, shots=shots, analytic=analytic)
 
         self._samples = None
         """None or array[int]: stores the samples generated by the device
diff --git a/pennylane/devices/default_gaussian.py b/pennylane/devices/default_gaussian.py
index a3f0c11f5..729ecd14c 100644
--- a/pennylane/devices/default_gaussian.py
+++ b/pennylane/devices/default_gaussian.py
@@ -680,8 +680,8 @@ class DefaultGaussian(Device):
 
     _circuits = {}
 
-    def __init__(self, wires, *, shots=None, hbar=2):
-        super().__init__(wires, shots)
+    def __init__(self, wires, *, shots=None, hbar=2, analytic=None):
+        super().__init__(wires, shots, analytic=analytic)
         self.eng = None
         self.hbar = hbar
 
diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py
index 237dcbb11..2807af16f 100644
--- a/pennylane/devices/default_mixed.py
+++ b/pennylane/devices/default_mixed.py
@@ -100,13 +100,14 @@ class DefaultMixed(QubitDevice):
         "DoubleExcitationMinus",
     }
 
-    def __init__(self, wires, *, shots=None, cache=0):
+    def __init__(self, wires, *, shots=None, cache=0, analytic=None):
         if isinstance(wires, int) and wires > 23:
             raise ValueError(
                 "This device does not currently support computations on more than 23 wires"
             )
+
         # call QubitDevice init
-        super().__init__(wires, shots, cache=cache)
+        super().__init__(wires, shots, cache=cache, analytic=analytic)
 
         # Create the initial state.
         self._state = self._create_basis_state(0)
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 0bbf815b1..35bfcddfb 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -130,9 +130,8 @@ class DefaultQubit(QubitDevice):
 
     observables = {"PauliX", "PauliY", "PauliZ", "Hadamard", "Hermitian", "Identity"}
 
-    def __init__(self, wires, *, shots=None, cache=0):
-        # call QubitDevice init
-        super().__init__(wires, shots, cache=cache)
+    def __init__(self, wires, *, shots=None, cache=0, analytic=None):
+        super().__init__(wires, shots, cache=cache, analytic=analytic)
 
         # Create the initial state. Internally, we store the
         # state as an array of dimension [2]*wires.
diff --git a/pennylane/devices/default_qubit_autograd.py b/pennylane/devices/default_qubit_autograd.py
index 1815eaa3a..6476b66de 100644
--- a/pennylane/devices/default_qubit_autograd.py
+++ b/pennylane/devices/default_qubit_autograd.py
@@ -127,8 +127,8 @@ class DefaultQubitAutograd(DefaultQubit):
 
         return res
 
-    def __init__(self, wires, *, shots=None):
-        super().__init__(wires, shots=shots, cache=0)
+    def __init__(self, wires, *, shots=None, analytic=None):
+        super().__init__(wires, shots=shots, cache=0, analytic=analytic)
 
         # prevent using special apply methods for these gates due to slowdown in Autograd
         # implementation
diff --git a/pennylane/devices/default_qubit_jax.py b/pennylane/devices/default_qubit_jax.py
index a905c7567..2139e2cf3 100644
--- a/pennylane/devices/default_qubit_jax.py
+++ b/pennylane/devices/default_qubit_jax.py
@@ -19,6 +19,7 @@ reference plugin.
 from pennylane.operation import DiagonalOperation
 from pennylane.devices import DefaultQubit
 from pennylane.devices import jax_ops
+
 import numpy as np
 
 try:
@@ -174,8 +175,8 @@ class DefaultQubitJax(DefaultQubit):
     _roll = staticmethod(jnp.roll)
     _stack = staticmethod(jnp.stack)
 
-    def __init__(self, wires, *, shots=None, prng_key=None):
-        super().__init__(wires, shots=shots, cache=0)
+    def __init__(self, wires, *, shots=None, prng_key=None, analytic=None):
+        super().__init__(wires, shots=shots, cache=0, analytic=analytic)
 
         # prevent using special apply methods for these gates due to slowdown in jax
         # implementation
diff --git a/pennylane/devices/default_qubit_tf.py b/pennylane/devices/default_qubit_tf.py
index aa00f3f23..694a8b953 100644
--- a/pennylane/devices/default_qubit_tf.py
+++ b/pennylane/devices/default_qubit_tf.py
@@ -178,8 +178,8 @@ class DefaultQubitTF(DefaultQubit):
 
         return res
 
-    def __init__(self, wires, *, shots=None):
-        super().__init__(wires, shots=shots, cache=0)
+    def __init__(self, wires, *, shots=None, analytic=None):
+        super().__init__(wires, shots=shots, cache=0, analytic=analytic)
 
         # prevent using special apply method for this gate due to slowdown in TF implementation
         del self._apply_ops["CZ"]


Test patch:
diff --git a/tests/devices/test_default_gaussian.py b/tests/devices/test_default_gaussian.py
index 41cc2d2a0..92e5e7bce 100644
--- a/tests/devices/test_default_gaussian.py
+++ b/tests/devices/test_default_gaussian.py
@@ -23,6 +23,7 @@ import numpy as np
 import numpy.random
 
 import pennylane as qml
+from pennylane import DeviceError
 from pennylane.wires import Wires
 from pennylane.devices.default_gaussian import (
     fock_prob,
@@ -77,6 +78,17 @@ def gaussian_device_3_wires():
 
 gaussian_dev = gaussian_device_2_wires  # alias
 
+def test_analytic_deprecation():
+    """Tests if the kwarg `analytic` is used and displays error message.
+    """
+    msg = "The analytic argument has been replaced by shots=None. "
+    msg += "Please use shots=None instead of analytic=True."
+
+    with pytest.raises(
+                DeviceError,
+                match=msg,
+        ):
+          qml.device("default.gaussian", wires=1, shots=1, analytic=True)
 
 
 class TestExceptions:
diff --git a/tests/devices/test_default_mixed.py b/tests/devices/test_default_mixed.py
index e70329e14..484fcf42f 100644
--- a/tests/devices/test_default_mixed.py
+++ b/tests/devices/test_default_mixed.py
@@ -729,3 +729,15 @@ class TestInit:
         """Tests that an error is raised if the device is initialized with more than 23 wires"""
         with pytest.raises(ValueError, match="This device does not currently"):
             qml.device("default.mixed", wires=24)
+
+    def test_analytic_deprecation(self):
+        """Tests if the kwarg `analytic` is used and displays error message.
+        """
+        msg = "The analytic argument has been replaced by shots=None. "
+        msg += "Please use shots=None instead of analytic=True."
+
+        with pytest.raises(
+                    DeviceError,
+                    match=msg,
+            ):
+              qml.device("default.mixed", wires=1, shots=1, analytic=True)
diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py
index b6f6f8f98..6ac9a75d3 100644
--- a/tests/devices/test_default_qubit.py
+++ b/tests/devices/test_default_qubit.py
@@ -98,6 +98,19 @@ def include_inverses_with_test_data(test_data):
     return test_data + [(item[0] + ".inv", item[1], item[2]) for item in test_data]
 
 
+def test_analytic_deprecation():
+    """Tests if the kwarg `analytic` is used and displays error message.
+    """
+    msg = "The analytic argument has been replaced by shots=None. "
+    msg += "Please use shots=None instead of analytic=True."
+
+    with pytest.raises(
+                DeviceError,
+                match=msg,
+        ):
+          qml.device("default.qubit", wires=1, shots=1, analytic=True)
+
+
 class TestApply:
     """Tests that operations and inverses of certain operations are applied correctly or that the proper
     errors are raised.
diff --git a/tests/devices/test_default_qubit_autograd.py b/tests/devices/test_default_qubit_autograd.py
index 27fcbcb16..8324181b7 100644
--- a/tests/devices/test_default_qubit_autograd.py
+++ b/tests/devices/test_default_qubit_autograd.py
@@ -19,6 +19,20 @@ import pytest
 import pennylane as qml
 from pennylane import numpy as np
 from pennylane.devices.default_qubit_autograd import DefaultQubitAutograd
+from pennylane import DeviceError
+
+
+def test_analytic_deprecation():
+    """Tests if the kwarg `analytic` is used and displays error message.
+    """
+    msg = "The analytic argument has been replaced by shots=None. "
+    msg += "Please use shots=None instead of analytic=True."
+
+    with pytest.raises(
+                DeviceError,
+                match=msg,
+        ):
+          qml.device("default.qubit.autograd", wires=1, shots=1, analytic=True)
 
 
 class TestQNodeIntegration:
diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py
index 31616a8a2..0b367f349 100644
--- a/tests/devices/test_default_qubit_jax.py
+++ b/tests/devices/test_default_qubit_jax.py
@@ -5,6 +5,20 @@ jnp = jax.numpy
 import numpy as np
 import pennylane as qml
 from pennylane.devices.default_qubit_jax import DefaultQubitJax
+from pennylane import DeviceError
+
+
+def test_analytic_deprecation():
+    """Tests if the kwarg `analytic` is used and displays error message.
+    """
+    msg = "The analytic argument has been replaced by shots=None. "
+    msg += "Please use shots=None instead of analytic=True."
+
+    with pytest.raises(
+                DeviceError,
+                match=msg,
+        ):
+          qml.device("default.qubit.jax", wires=1, shots=1, analytic=True)
 
 
 class TestQNodeIntegration:
diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py
index 032621a9d..2c567fbc6 100644
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -22,6 +22,7 @@ import pytest
 tf = pytest.importorskip("tensorflow", minversion="2.0")
 
 import pennylane as qml
+from pennylane import DeviceError
 from pennylane.wires import Wires
 from pennylane.devices.default_qubit_tf import DefaultQubitTF
 from gate_data import (
@@ -111,6 +112,24 @@ def init_state(scope="session"):
     return _init_state
 
 
+#####################################################
+# Initialization test
+#####################################################
+
+
+def test_analytic_deprecation():
+    """Tests if the kwarg `analytic` is used and displays error message.
+    """
+    msg = "The analytic argument has been replaced by shots=None. "
+    msg += "Please use shots=None instead of analytic=True."
+
+    with pytest.raises(
+                DeviceError,
+                match=msg,
+        ):
+          qml.device("default.qubit.tf", wires=1, shots=1, analytic=True)
+
+
 #####################################################
 # Device-level integration tests
 #####################################################