Repository: PennyLaneAI/pennylane (python)

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.        ]])]
```


Pull request:
Add validation for noise channels parameters
**Context:** Noise channels do not validate parameter values

**Description of the Change:** Add validation for noise channel parameters, now it raises an `ValueError` when
  noise parameters are invalid.

**Benefits:** It no longer raises a `RuntimeWarning` when using invalid parameters on noise channels.

**Possible Drawbacks:**

**Related GitHub Issues:** Closes #1337 


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"""