Repository: PennyLaneAI/pennylane

Issue:
Noise channels do not validate parameter values, leading to errors
#### Issue description

Description of the issue - include code snippets and screenshots here
if relevant. You may use the following template below

* *Expected behavior:* Raise an error showing that the probability (or sum of probabilities) are not compatible with the expected channel behaviour.

* *Actual behavior:* Raises a `RuntimeWarning` for an invalid value encountered in sqrt.

* *Reproduces how often:* 100%

* *System information:* 
* Name: PennyLane
Version: 0.16.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location: /home/nahum/Documents/pennylane-training/pen-env/lib/python3.6/site-packages
Requires: scipy, toml, appdirs, autoray, semantic-version, networkx, autograd, numpy
Required-by: PennyLane-qiskit
Platform info:           Linux-5.4.0-73-generic-x86_64-with-debian-buster-sid
Python version:          3.6.8
Numpy version:           1.19.5
Scipy version:           1.5.4
Installed devices:
- default.gaussian (PennyLane-0.16.0.dev0)
- default.mixed (PennyLane-0.16.0.dev0)
- default.qubit (PennyLane-0.16.0.dev0)
- default.qubit.autograd (PennyLane-0.16.0.dev0)
- default.qubit.jax (PennyLane-0.16.0.dev0)
- default.qubit.tf (PennyLane-0.16.0.dev0)
- default.tensor (PennyLane-0.16.0.dev0)
- default.tensor.tf (PennyLane-0.16.0.dev0)
- qiskit.aer (PennyLane-qiskit-0.15.0)
- qiskit.basicaer (PennyLane-qiskit-0.15.0)
- qiskit.ibmq (PennyLane-qiskit-0.15.0)

#### Source code and tracebacks
```python
from pennylane.ops.channel import AmplitudeDamping

print(AmplitudeDamping(2, wires=0).kraus_matrices)
```

```
pennylane/ops/channel.py:61: RuntimeWarning: invalid value encountered in sqrt
  K0 = np.diag([1, np.sqrt(1 - gamma)])
[array([[ 1.,  0.],
       [ 0., nan]]), array([[0.        , 1.41421356],
       [0.        , 0.        ]])]
```


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1357](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (3c40869) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/a9a914d9b6e475374dba1e91a22f2635b97df913?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a9a914d) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@           Coverage Diff           @@
##           master    #1357   +/-   ##
=======================================
  Coverage   98.27%   98.27%           
=======================================
  Files         150      150           
  Lines       11256    11276   +20     
=======================================
+ Hits        11062    11082   +20     
  Misses        194      194           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/channel.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1357/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9jaGFubmVsLnB5) | `100.00% <100.00%> (ø)` | |

------

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

nahumsa: Hi @ixfoduap! 

Thanks for pointing that out! I have a question regarding this test, it would be an end-to-end test just like the example, or just computing the gradient for a given example would be enough?
ixfoduap: I would like to test that the gradient of a simple circuit is computed correctly. Actually maybe such tests are already done for some of the channels. If that's the case, then I guess no more code needs to be written. Basically I just want to make sure that we don't ship breaking code.

This doesn't have to appear as an actual integration test, but I would be more comfortable if you could run the examples from the tutorial from a pennylane version from this branch and check that everything goes well.
nahumsa: I think that this test is covered [here](https://github.com/PennyLaneAI/pennylane/blob/12faeca6f7dcd6fcba868e7ae27907d693025476/tests/ops/test_channel_ops.py#L148-L164):

``` python
    @pytest.mark.parametrize("angle", np.linspace(0, 2 * np.pi, 7))
    def test_grad_bitflip(self, angle, tol):
        """Test that analytical gradient is computed correctly for different states. Channel
        grad recipes are independent of channel parameter"""

        dev = qml.device("default.mixed", wires=1)
        prob = 0.5

        @qml.qnode(dev)
        def circuit(p):
            qml.RX(angle, wires=0)
            qml.BitFlip(p, wires=0)
            return qml.expval(qml.PauliZ(0))

        gradient = np.squeeze(qml.grad(circuit)(prob))
        assert gradient == circuit(1) - circuit(0)
        assert np.allclose(gradient, (-2 * np.cos(angle)))
```

Please, let me know if it needs more tests about this.
 

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index bf107144e..4a3c53850 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 <h3>New features since last release</h3>
 
+* Added validation for noise channel parameters. Invalid noise parameters now
+  raise a `ValueError`. [(#1357)](https://github.com/PennyLaneAI/pennylane/pull/1357)
+
 * PennyLane now has a `fourier` module, which hosts a [growing library 
   of methods](https://pennylane.readthedocs.io/en/stable/code/qml_fourier.html) 
   that help with investigating the Fourier representation of functions 
diff --git a/pennylane/ops/channel.py b/pennylane/ops/channel.py
index 630c5910b..422568197 100644
--- a/pennylane/ops/channel.py
+++ b/pennylane/ops/channel.py
@@ -58,6 +58,10 @@ class AmplitudeDamping(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma = params[0]
+
+        if not 0.0 <= gamma <= 1.0:
+            raise ValueError("gamma must be between [0,1].")
+
         K0 = np.diag([1, np.sqrt(1 - gamma)])
         K1 = np.sqrt(gamma) * np.array([[0, 1], [0, 0]])
         return [K0, K1]
@@ -115,6 +119,13 @@ class GeneralizedAmplitudeDamping(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma, p = params
+
+        if not 0.0 <= gamma <= 1.0:
+            raise ValueError("gamma must be between [0,1].")
+
+        if not 0.0 <= p <= 1.0:
+            raise ValueError("p must be between [0,1].")
+
         K0 = np.sqrt(p) * np.diag([1, np.sqrt(1 - gamma)])
         K1 = np.sqrt(p) * np.sqrt(gamma) * np.array([[0, 1], [0, 0]])
         K2 = np.sqrt(1 - p) * np.diag([np.sqrt(1 - gamma), 1])
@@ -161,6 +172,10 @@ class PhaseDamping(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma = params[0]
+
+        if not 0.0 <= gamma <= 1.0:
+            raise ValueError("gamma must be between [0,1].")
+
         K0 = np.diag([1, np.sqrt(1 - gamma)])
         K1 = np.diag([0, np.sqrt(gamma)])
         return [K0, K1]
@@ -217,6 +232,10 @@ class DepolarizingChannel(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
+
+        if not 0.0 <= p <= 1.0:
+            raise ValueError("p must be between [0,1]")
+
         K0 = np.sqrt(1 - p) * np.eye(2)
         K1 = np.sqrt(p / 3) * np.array([[0, 1], [1, 0]])
         K2 = np.sqrt(p / 3) * np.array([[0, -1j], [1j, 0]])
@@ -262,6 +281,10 @@ class BitFlip(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
+
+        if not 0.0 <= p <= 1.0:
+            raise ValueError("p must be between [0,1]")
+
         K0 = np.sqrt(1 - p) * np.eye(2)
         K1 = np.sqrt(p) * np.array([[0, 1], [1, 0]])
         return [K0, K1]
@@ -324,6 +347,16 @@ class ResetError(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         p_0, p_1 = params[0], params[1]
+
+        if not 0.0 <= p_0 <= 1.0:
+            raise ValueError("p_0 must be between [0,1]")
+
+        if not 0.0 <= p_1 <= 1.0:
+            raise ValueError("p_1 must be between [0,1]")
+
+        if not 0.0 <= p_0 + p_1 <= 1.0:
+            raise ValueError("p_0 + p_1 must be between [0,1]")
+
         K0 = np.sqrt(1 - p_0 - p_1) * np.eye(2)
         K1 = np.sqrt(p_0) * np.array([[1, 0], [0, 0]])
         K2 = np.sqrt(p_0) * np.array([[0, 1], [0, 0]])
@@ -370,6 +403,10 @@ class PhaseFlip(Channel):
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
+
+        if not 0.0 <= p <= 1.0:
+            raise ValueError("p must be between [0,1]")
+
         K0 = np.sqrt(1 - p) * np.eye(2)
         K1 = np.sqrt(p) * np.array([[1, 0], [0, -1]])
         return [K0, K1]


Test patch:
diff --git a/tests/ops/test_channel_ops.py b/tests/ops/test_channel_ops.py
index d4b85e321..66255586f 100644
--- a/tests/ops/test_channel_ops.py
+++ b/tests/ops/test_channel_ops.py
@@ -73,6 +73,10 @@ class TestAmplitudeDamping:
         ]
         assert np.allclose(op(0.1, wires=0).kraus_matrices, expected, atol=tol, rtol=0)
 
+    def test_gamma_invalid_parameter(self):
+        with pytest.raises(ValueError, match="gamma must be between"):
+            channel.AmplitudeDamping(1.5, wires=0).kraus_matrices
+
 
 class TestGeneralizedAmplitudeDamping:
     """Tests for the quantum channel GeneralizedAmplitudeDamping"""
@@ -95,6 +99,14 @@ class TestGeneralizedAmplitudeDamping:
         expected_K3 = np.array([[0.0, 0.0], [0.2236068, 0.0]])
         assert np.allclose(op(0.1, 0.5, wires=0).kraus_matrices[3], expected_K3, atol=tol, rtol=0)
 
+    def test_gamma_invalid_parameter(self):
+        with pytest.raises(ValueError, match="gamma must be between"):
+            channel.GeneralizedAmplitudeDamping(1.5, 0.0, wires=0).kraus_matrices
+
+    def test_p_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p must be between"):
+            channel.GeneralizedAmplitudeDamping(0.0, 1.5, wires=0).kraus_matrices
+
 
 class TestPhaseDamping:
     """Tests for the quantum channel PhaseDamping"""
@@ -114,6 +126,10 @@ class TestPhaseDamping:
         ]
         assert np.allclose(op(0.1, wires=0).kraus_matrices, expected, atol=tol, rtol=0)
 
+    def test_gamma_invalid_parameter(self):
+        with pytest.raises(ValueError, match="gamma must be between"):
+            channel.PhaseDamping(1.5, wires=0).kraus_matrices
+
 
 class TestBitFlip:
     """Tests for the quantum channel BitFlipChannel"""
@@ -147,6 +163,10 @@ class TestBitFlip:
         assert gradient == circuit(1) - circuit(0)
         assert np.allclose(gradient, (-2 * np.cos(angle)))
 
+    def test_p_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p must be between"):
+            channel.BitFlip(1.5, wires=0).kraus_matrices
+
 
 class TestPhaseFlip:
     """Test that various values of p give correct Kraus matrices"""
@@ -180,6 +200,10 @@ class TestPhaseFlip:
         assert gradient == circuit(1) - circuit(0)
         assert np.allclose(gradient, 0.0)
 
+    def test_p_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p must be between"):
+            channel.PhaseFlip(1.5, wires=0).kraus_matrices
+
 
 class TestDepolarizingChannel:
     """Tests for the quantum channel DepolarizingChannel"""
@@ -215,6 +239,10 @@ class TestDepolarizingChannel:
         assert gradient == circuit(1) - circuit(0)
         assert np.allclose(gradient, -(4 / 3) * np.cos(angle))
 
+    def test_p_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p must be between"):
+            channel.DepolarizingChannel(1.5, wires=0).kraus_matrices
+
 
 class TestResetError:
     """Tests for the quantum channel ResetError"""
@@ -239,6 +267,18 @@ class TestResetError:
         expected_K4 = np.sqrt(p_1) * np.array([[0, 0], [0, 1]])
         assert np.allclose(op(p_0, p_1, wires=0).kraus_matrices[4], expected_K4, atol=tol, rtol=0)
 
+    def test_p0_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p_0 must be between"):
+            channel.ResetError(1.5, 0.0, wires=0).kraus_matrices
+
+    def test_p1_invalid_parameter(self):
+        with pytest.raises(ValueError, match="p_1 must be between"):
+            channel.ResetError(0.0, 1.5, wires=0).kraus_matrices
+
+    def test_p0_p1_sum_not_normalized(self):
+        with pytest.raises(ValueError, match="must be between"):
+            channel.ResetError(1.0, 1.0, wires=0).kraus_matrices
+
 
 class TestQubitChannel:
     """Tests for the quantum channel QubitChannel"""