From 087c1c89cba344a8dc4dd6062ceece8a4de7bc8e Mon Sep 17 00:00:00 2001 From: Gabriel Bottrill <78718539+Gabriel-Bottrill@users.noreply.github.com> Date: Wed, 5 Jun 2024 21:47:30 -0700 Subject: [PATCH] Improve variable names for QutritAmplitudeDamping channel (#5799) **Context:** The QutritAmplitudeDamping channel #5503 and #5757 variable names are named with numbers, when making the TritFlip channel #5784 I realized that it would be better to specify subspaces for clarity. This PR is meant to change the name of QutritAmplitudeDamping's variables. **Description of the Change:** Change the name of the QutritAmplitudeDamping channel's variables from gamma_1, gamma_2, and gamma_3 to gamma_01, gamma_02, and gamma_12, respectively. **Benefits:** Makes the code more clear for which variables affect which subspace, reducing the need to check the docs. **Possible Drawbacks:** Changes the name of variables of development code. **Related GitHub Issues:** N/A --------- Co-authored-by: Gabriel Bottrill Co-authored-by: Olivia Di Matteo <2068515+glassnotes@users.noreply.github.com> --- doc/releases/changelog-dev.md | 1 + pennylane/ops/qutrit/channel.py | 56 +++++++------- tests/ops/qutrit/test_qutrit_channel_ops.py | 84 ++++++++++----------- 3 files changed, 71 insertions(+), 70 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 2e1c1b94628..4e8402c3c98 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -183,6 +183,7 @@ * `qml.QutritAmplitudeDamping` channel has been added, allowing for noise processes modelled by amplitude damping to be simulated on the `default.qutrit.mixed` device. [(#5503)](https://github.com/PennyLaneAI/pennylane/pull/5503) [(#5757)](https://github.com/PennyLaneAI/pennylane/pull/5757) + [(#5799)](https://github.com/PennyLaneAI/pennylane/pull/5799)

Breaking changes 💔

diff --git a/pennylane/ops/qutrit/channel.py b/pennylane/ops/qutrit/channel.py index 82f5d9c4407..d124b8f0c2a 100644 --- a/pennylane/ops/qutrit/channel.py +++ b/pennylane/ops/qutrit/channel.py @@ -234,39 +234,39 @@ class QutritAmplitudeDamping(Channel): .. math:: K_0 = \begin{bmatrix} 1 & 0 & 0\\ - 0 & \sqrt{1-\gamma_1} & 0 \\ - 0 & 0 & \sqrt{1-(\gamma_2+\gamma_3)} + 0 & \sqrt{1-\gamma_{10}} & 0 \\ + 0 & 0 & \sqrt{1-(\gamma_{20}+\gamma_{21})} \end{bmatrix} .. math:: K_1 = \begin{bmatrix} - 0 & \sqrt{\gamma_1} & 0 \\ + 0 & \sqrt{\gamma_{10}} & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix}, \quad K_2 = \begin{bmatrix} - 0 & 0 & \sqrt{\gamma_2} \\ + 0 & 0 & \sqrt{\gamma_{20}} \\ 0 & 0 & 0 \\ 0 & 0 & 0 \end{bmatrix}, \quad K_3 = \begin{bmatrix} 0 & 0 & 0 \\ - 0 & 0 & \sqrt{\gamma_3} \\ + 0 & 0 & \sqrt{\gamma_{21}} \\ 0 & 0 & 0 \end{bmatrix} - where :math:`\gamma_1, \gamma_2, \gamma_3 \in [0, 1]` are the amplitude damping + where :math:`\gamma_{10}, \gamma_{20}, \gamma_{21} \in [0, 1]` are the amplitude damping probabilities for subspaces (0,1), (0,2), and (1,2) respectively. .. note:: - When :math:`\gamma_3=0` then Kraus operators :math:`\{K_0, K_1, K_2\}` are adapted from + When :math:`\gamma_{21}=0` then Kraus operators :math:`\{K_0, K_1, K_2\}` are adapted from [`1 `_] (Eq. 8). The Kraus operator :math:`K_3` represents the :math:`|2 \rangle \rightarrow |1 \rangle` transition which is more likely on some devices [`2 `_] (Sec II.A). - To maintain normalization :math:`\gamma_2 + \gamma_3 \leq 1`. + To maintain normalization :math:`\gamma_{20} + \gamma_{21} \leq 1`. **Details:** @@ -275,9 +275,9 @@ class QutritAmplitudeDamping(Channel): * Number of parameters: 3 Args: - gamma_1 (float): :math:`|1 \rangle \rightarrow |0 \rangle` amplitude damping probability. - gamma_2 (float): :math:`|2 \rangle \rightarrow |0 \rangle` amplitude damping probability. - gamma_3 (float): :math:`|2 \rangle \rightarrow |1 \rangle` amplitude damping probability. + gamma_10 (float): :math:`|1 \rangle \rightarrow |0 \rangle` amplitude damping probability. + gamma_20 (float): :math:`|2 \rangle \rightarrow |0 \rangle` amplitude damping probability. + gamma_21 (float): :math:`|2 \rangle \rightarrow |1 \rangle` amplitude damping probability. wires (Sequence[int] or int): the wire the channel acts on id (str or None): String representing the operation (optional) """ @@ -286,25 +286,25 @@ class QutritAmplitudeDamping(Channel): num_wires = 1 grad_method = "F" - def __init__(self, gamma_1, gamma_2, gamma_3, wires, id=None): + def __init__(self, gamma_10, gamma_20, gamma_21, wires, id=None): # Verify input - for gamma in (gamma_1, gamma_2, gamma_3): + for gamma in (gamma_10, gamma_20, gamma_21): if not math.is_abstract(gamma): if not 0.0 <= gamma <= 1.0: raise ValueError("Each probability must be in the interval [0,1]") - if not (math.is_abstract(gamma_2) or math.is_abstract(gamma_3)): - if not 0.0 <= gamma_2 + gamma_3 <= 1.0: - raise ValueError(r"\gamma_2+\gamma_3 must be in the interval [0,1]") - super().__init__(gamma_1, gamma_2, gamma_3, wires=wires, id=id) + if not (math.is_abstract(gamma_20) or math.is_abstract(gamma_21)): + if not 0.0 <= gamma_20 + gamma_21 <= 1.0: + raise ValueError(r"\gamma_{20}+\gamma_{21} must be in the interval [0,1]") + super().__init__(gamma_10, gamma_20, gamma_21, wires=wires, id=id) @staticmethod - def compute_kraus_matrices(gamma_1, gamma_2, gamma_3): # pylint:disable=arguments-differ + def compute_kraus_matrices(gamma_10, gamma_20, gamma_21): # pylint:disable=arguments-differ r"""Kraus matrices representing the ``QutritAmplitudeDamping`` channel. Args: - gamma_1 (float): :math:`|1\rangle \rightarrow |0\rangle` amplitude damping probability. - gamma_2 (float): :math:`|2\rangle \rightarrow |0\rangle` amplitude damping probability. - gamma_3 (float): :math:`|2\rangle \rightarrow |1\rangle` amplitude damping probability. + gamma_10 (float): :math:`|1\rangle \rightarrow |0\rangle` amplitude damping probability. + gamma_20 (float): :math:`|2\rangle \rightarrow |0\rangle` amplitude damping probability. + gamma_21 (float): :math:`|2\rangle \rightarrow |1\rangle` amplitude damping probability. Returns: list(array): list of Kraus matrices @@ -328,15 +328,15 @@ def compute_kraus_matrices(gamma_1, gamma_2, gamma_3): # pylint:disable=argumen ] """ K0 = math.diag( - [1, math.sqrt(1 - gamma_1 + math.eps), math.sqrt(1 - gamma_2 - gamma_3 + math.eps)] + [1, math.sqrt(1 - gamma_10 + math.eps), math.sqrt(1 - gamma_20 - gamma_21 + math.eps)] ) - K1 = math.sqrt(gamma_1 + math.eps) * math.convert_like( - math.cast_like(math.array([[0, 1, 0], [0, 0, 0], [0, 0, 0]]), gamma_1), gamma_1 + K1 = math.sqrt(gamma_10 + math.eps) * math.convert_like( + math.cast_like(math.array([[0, 1, 0], [0, 0, 0], [0, 0, 0]]), gamma_10), gamma_10 ) - K2 = math.sqrt(gamma_2 + math.eps) * math.convert_like( - math.cast_like(math.array([[0, 0, 1], [0, 0, 0], [0, 0, 0]]), gamma_2), gamma_2 + K2 = math.sqrt(gamma_20 + math.eps) * math.convert_like( + math.cast_like(math.array([[0, 0, 1], [0, 0, 0], [0, 0, 0]]), gamma_20), gamma_20 ) - K3 = math.sqrt(gamma_3 + math.eps) * math.convert_like( - math.cast_like(math.array([[0, 0, 0], [0, 0, 1], [0, 0, 0]]), gamma_3), gamma_3 + K3 = math.sqrt(gamma_21 + math.eps) * math.convert_like( + math.cast_like(math.array([[0, 0, 0], [0, 0, 1], [0, 0, 0]]), gamma_21), gamma_21 ) return [K0, K1, K2, K3] diff --git a/tests/ops/qutrit/test_qutrit_channel_ops.py b/tests/ops/qutrit/test_qutrit_channel_ops.py index 4b2612815ce..e0be689b688 100644 --- a/tests/ops/qutrit/test_qutrit_channel_ops.py +++ b/tests/ops/qutrit/test_qutrit_channel_ops.py @@ -175,32 +175,32 @@ class TestQutritAmplitudeDamping: """Tests for the qutrit quantum channel QutritAmplitudeDamping""" def test_gamma_zero(self, tol): - """Test gamma_1=gamma_2=0 gives correct Kraus matrices""" + """Test gamma_10=gamma_20=0 gives correct Kraus matrices""" kraus_mats = qml.QutritAmplitudeDamping(0, 0, 0, wires=0).kraus_matrices() assert np.allclose(kraus_mats[0], np.eye(3), atol=tol, rtol=0) for kraus_mat in kraus_mats[1:]: assert np.allclose(kraus_mat, np.zeros((3, 3)), atol=tol, rtol=0) - @pytest.mark.parametrize("gamma1,gamma2,gamma3", ((0.1, 0.2, 0.3), (0.75, 0.75, 0.25))) - def test_gamma_arbitrary(self, gamma1, gamma2, gamma3, tol): + @pytest.mark.parametrize("gamma_10,gamma_20,gamma_21", ((0.1, 0.2, 0.3), (0.75, 0.75, 0.25))) + def test_gamma_arbitrary(self, gamma_10, gamma_20, gamma_21, tol): """Test the correct Kraus matrices are returned.""" - K_0 = np.diag((1, np.sqrt(1 - gamma1), np.sqrt(1 - gamma2 - gamma3))) + K_0 = np.diag((1, np.sqrt(1 - gamma_10), np.sqrt(1 - gamma_20 - gamma_21))) K_1 = np.zeros((3, 3)) - K_1[0, 1] = np.sqrt(gamma1) + K_1[0, 1] = np.sqrt(gamma_10) K_2 = np.zeros((3, 3)) - K_2[0, 2] = np.sqrt(gamma2) + K_2[0, 2] = np.sqrt(gamma_20) K_3 = np.zeros((3, 3)) - K_3[1, 2] = np.sqrt(gamma3) + K_3[1, 2] = np.sqrt(gamma_21) expected = [K_0, K_1, K_2, K_3] - damping_channel = qml.QutritAmplitudeDamping(gamma1, gamma2, gamma3, wires=0) + damping_channel = qml.QutritAmplitudeDamping(gamma_10, gamma_20, gamma_21, wires=0) assert np.allclose(damping_channel.kraus_matrices(), expected, atol=tol, rtol=0) @pytest.mark.parametrize( - "gamma1,gamma2,gamma3", + "gamma_10,gamma_20,gamma_21", ( (1.5, 0.0, 0.0), (0.0, 1.0 + math.eps, 0.0), @@ -208,56 +208,56 @@ def test_gamma_arbitrary(self, gamma1, gamma2, gamma3, tol): (0.0, 0.33, 0.67 + math.eps), ), ) - def test_gamma_invalid_parameter(self, gamma1, gamma2, gamma3): - """Ensures that error is thrown when gamma_1, gamma_2, gamma_3, or (gamma_2 + gamma_3) are outside [0,1]""" + def test_gamma_invalid_parameter(self, gamma_10, gamma_20, gamma_21): + """Ensures that error is thrown when gamma_10, gamma_20, gamma_21, or (gamma_20 + gamma_21) are outside [0,1]""" with pytest.raises(ValueError, match="must be in the interval"): - channel.QutritAmplitudeDamping(gamma1, gamma2, gamma3, wires=0).kraus_matrices() + channel.QutritAmplitudeDamping(gamma_10, gamma_20, gamma_21, wires=0).kraus_matrices() @staticmethod - def expected_jac_fn(gamma_1, gamma_2, gamma_3): + def expected_jac_fn(gamma_10, gamma_20, gamma_21): """Gets the expected Jacobian of Kraus matrices""" partial_1 = [math.zeros((3, 3)) for _ in range(4)] - partial_1[0][1, 1] = -1 / (2 * math.sqrt(1 - gamma_1)) - partial_1[1][0, 1] = 1 / (2 * math.sqrt(gamma_1)) + partial_1[0][1, 1] = -1 / (2 * math.sqrt(1 - gamma_10)) + partial_1[1][0, 1] = 1 / (2 * math.sqrt(gamma_10)) partial_2 = [math.zeros((3, 3)) for _ in range(4)] - partial_2[0][2, 2] = -1 / (2 * math.sqrt(1 - gamma_2 - gamma_3)) - partial_2[2][0, 2] = 1 / (2 * math.sqrt(gamma_2)) + partial_2[0][2, 2] = -1 / (2 * math.sqrt(1 - gamma_20 - gamma_21)) + partial_2[2][0, 2] = 1 / (2 * math.sqrt(gamma_20)) partial_3 = [math.zeros((3, 3)) for _ in range(4)] - partial_3[0][2, 2] = -1 / (2 * math.sqrt(1 - gamma_2 - gamma_3)) - partial_3[3][1, 2] = 1 / (2 * math.sqrt(gamma_3)) + partial_3[0][2, 2] = -1 / (2 * math.sqrt(1 - gamma_20 - gamma_21)) + partial_3[3][1, 2] = 1 / (2 * math.sqrt(gamma_21)) return [partial_1, partial_2, partial_3] @staticmethod - def kraus_fn(gamma_1, gamma_2, gamma_3): + def kraus_fn(gamma_10, gamma_20, gamma_21): """Gets the Kraus matrices of QutritAmplitudeDamping channel, used for differentiation.""" - damping_channel = qml.QutritAmplitudeDamping(gamma_1, gamma_2, gamma_3, wires=0) + damping_channel = qml.QutritAmplitudeDamping(gamma_10, gamma_20, gamma_21, wires=0) return math.stack(damping_channel.kraus_matrices()) @pytest.mark.autograd def test_kraus_jac_autograd(self): """Tests Jacobian of Kraus matrices using autograd.""" - gamma_1 = pnp.array(0.43, requires_grad=True) - gamma_2 = pnp.array(0.12, requires_grad=True) - gamma_3 = pnp.array(0.35, requires_grad=True) + gamma_10 = pnp.array(0.43, requires_grad=True) + gamma_20 = pnp.array(0.12, requires_grad=True) + gamma_21 = pnp.array(0.35, requires_grad=True) - jac = qml.jacobian(self.kraus_fn)(gamma_1, gamma_2, gamma_3) - assert math.allclose(jac, self.expected_jac_fn(gamma_1, gamma_2, gamma_3)) + jac = qml.jacobian(self.kraus_fn)(gamma_10, gamma_20, gamma_21) + assert math.allclose(jac, self.expected_jac_fn(gamma_10, gamma_20, gamma_21)) @pytest.mark.torch def test_kraus_jac_torch(self): """Tests Jacobian of Kraus matrices using PyTorch.""" import torch - gamma_1 = torch.tensor(0.43, requires_grad=True) - gamma_2 = torch.tensor(0.12, requires_grad=True) - gamma_3 = torch.tensor(0.35, requires_grad=True) + gamma_10 = torch.tensor(0.43, requires_grad=True) + gamma_20 = torch.tensor(0.12, requires_grad=True) + gamma_21 = torch.tensor(0.35, requires_grad=True) - jac = torch.autograd.functional.jacobian(self.kraus_fn, (gamma_1, gamma_2, gamma_3)) + jac = torch.autograd.functional.jacobian(self.kraus_fn, (gamma_10, gamma_20, gamma_21)) expected = self.expected_jac_fn( - gamma_1.detach().numpy(), gamma_2.detach().numpy(), gamma_3.detach().numpy() + gamma_10.detach().numpy(), gamma_20.detach().numpy(), gamma_21.detach().numpy() ) for res_partial, exp_partial in zip(jac, expected): @@ -268,23 +268,23 @@ def test_kraus_jac_tf(self): """Tests Jacobian of Kraus matrices using TensorFlow.""" import tensorflow as tf - gamma_1 = tf.Variable(0.43) - gamma_2 = tf.Variable(0.12) - gamma_3 = tf.Variable(0.35) + gamma_10 = tf.Variable(0.43) + gamma_20 = tf.Variable(0.12) + gamma_21 = tf.Variable(0.35) with tf.GradientTape() as tape: - out = self.kraus_fn(gamma_1, gamma_2, gamma_3) - jac = tape.jacobian(out, (gamma_1, gamma_2, gamma_3)) - assert math.allclose(jac, self.expected_jac_fn(gamma_1, gamma_2, gamma_3)) + out = self.kraus_fn(gamma_10, gamma_20, gamma_21) + jac = tape.jacobian(out, (gamma_10, gamma_20, gamma_21)) + assert math.allclose(jac, self.expected_jac_fn(gamma_10, gamma_20, gamma_21)) @pytest.mark.jax def test_kraus_jac_jax(self): """Tests Jacobian of Kraus matrices using JAX.""" import jax - gamma_1 = jax.numpy.array(0.43) - gamma_2 = jax.numpy.array(0.12) - gamma_3 = jax.numpy.array(0.35) + gamma_10 = jax.numpy.array(0.43) + gamma_20 = jax.numpy.array(0.12) + gamma_21 = jax.numpy.array(0.35) - jac = jax.jacobian(self.kraus_fn, argnums=[0, 1, 2])(gamma_1, gamma_2, gamma_3) - assert math.allclose(jac, self.expected_jac_fn(gamma_1, gamma_2, gamma_3)) + jac = jax.jacobian(self.kraus_fn, argnums=[0, 1, 2])(gamma_10, gamma_20, gamma_21) + assert math.allclose(jac, self.expected_jac_fn(gamma_10, gamma_20, gamma_21))