Repository: vprusso/toqito

Issue:
Feature: Choi rank
Write a function that calculates the "Choi rank". 

Refer to page 79 of https://cs.uwaterloo.ca/~watrous/TQI/TQI.pdf for the definition of the Choi rank. 

In short, the Choi rank is the rank of the Choi representation of a quantum channel. The input to the function should be either a set of Kraus operators or a Choi matrix. If the former, the Kraus operators should be converted to a Choi matrix. Calculating the matrix rank of the resulting Choi matrix would yield the desired "Choi rank". 

This task would also require adding test coverage for the function along with ensuring it is included in the docs. Refer to the style guide and code contributing guidelines for more information.

Comments:

Patch:
diff --git a/docs/channels.rst b/docs/channels.rst
index f0a73b7..22cc7f0 100644
--- a/docs/channels.rst
+++ b/docs/channels.rst
@@ -64,3 +64,4 @@ Properties of Quantum Channels
     toqito.channel_props.is_herm_preserving
     toqito.channel_props.is_positive
     toqito.channel_props.is_unital
+    toqito.channel_props.choi_rank
diff --git a/toqito/channel_props/__init__.py b/toqito/channel_props/__init__.py
index 74eab7f..66750f4 100644
--- a/toqito/channel_props/__init__.py
+++ b/toqito/channel_props/__init__.py
@@ -3,3 +3,4 @@ from toqito.channel_props.is_herm_preserving import is_herm_preserving
 from toqito.channel_props.is_completely_positive import is_completely_positive
 from toqito.channel_props.is_positive import is_positive
 from toqito.channel_props.is_unital import is_unital
+from toqito.channel_props.choi_rank import choi_rank
diff --git a/toqito/channel_props/choi_rank.py b/toqito/channel_props/choi_rank.py
new file mode 100644
index 0000000..dcc6b18
--- /dev/null
+++ b/toqito/channel_props/choi_rank.py
@@ -0,0 +1,89 @@
+"""Calculate the Choi rank of a channel."""
+from typing import List, Union
+
+import numpy as np
+from toqito.channel_ops import kraus_to_choi
+
+
+def choi_rank(phi: Union[np.ndarray, List[List[np.ndarray]]]) -> int:
+    r"""
+    Calculate the rank of the Choi representation of a quantum channel.
+
+    Examples
+    ==========
+
+    The transpose map can be written either in Choi representation (as a
+    SWAP operator) or in Kraus representation. If we choose the latter, it
+    will be given by the following matrices:
+
+    .. math::
+        \begin{equation}
+            \begin{aligned}
+                \frac{1}{\sqrt{2}}
+                \begin{pmatrix}
+                    0 & i \\ -i & 0
+                \end{pmatrix}, &\quad
+                \frac{1}{\sqrt{2}}
+                \begin{pmatrix}
+                    0 & 1 \\
+                    1 & 0
+                \end{pmatrix}, \\
+                \begin{pmatrix}
+                    1 & 0 \\
+                    0 & 0
+                \end{pmatrix}, &\quad
+                \begin{pmatrix}
+                    0 & 0 \\
+                    0 & 1
+                \end{pmatrix}.
+            \end{aligned}
+        \end{equation}
+
+    and can be generated in :code:`toqito` with the following list:
+
+    >>> import numpy as np
+    >>> kraus_1 = np.array([[1, 0], [0, 0]])
+    >>> kraus_2 = np.array([[1, 0], [0, 0]]).conj().T
+    >>> kraus_3 = np.array([[0, 1], [0, 0]])
+    >>> kraus_4 = np.array([[0, 1], [0, 0]]).conj().T
+    >>> kraus_5 = np.array([[0, 0], [1, 0]])
+    >>> kraus_6 = np.array([[0, 0], [1, 0]]).conj().T
+    >>> kraus_7 = np.array([[0, 0], [0, 1]])
+    >>> kraus_8 = np.array([[0, 0], [0, 1]]).conj().T
+    >>> kraus_ops = [
+    >>>     [kraus_1, kraus_2],
+    >>>     [kraus_3, kraus_4],
+    >>>     [kraus_5, kraus_6],
+    >>>     [kraus_7, kraus_8],
+    >>> ]
+
+    To calculate its Choi rank, we proceed in the following way:
+
+    >>> from toqito.channel_props import choi_rank
+    >>> choi_rank(kraus_ops)
+    4
+
+    We can the verify the associated Choi representation (the SWAP gate)
+    gets the same Choi rank:
+
+    >>> choi_matrix = np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]])
+    >>> choi_rank(choi_matrix)
+    4
+
+    References
+    ==========
+
+    .. [WatDepo18] Watrous, John.
+        "The theory of quantum information."
+        Section: "2.2 Quantum Channels".
+        Cambridge University Press, 2018.
+
+    :param phi: Either a Choi matrix or a list of Kraus operators
+    :return: The Choi rank of the provided channel representation.
+    """
+    if isinstance(phi, list):
+        phi = kraus_to_choi(phi)
+    elif not isinstance(phi, np.ndarray):
+        raise ValueError("Not a valid Choi matrix.")
+
+    return np.linalg.matrix_rank(phi)


Test patch:
diff --git a/tests/test_channel_props/test_choi_rank.py b/tests/test_channel_props/test_choi_rank.py
new file mode 100644
index 0000000..f6dddef
--- /dev/null
+++ b/tests/test_channel_props/test_choi_rank.py
@@ -0,0 +1,44 @@
+"""Tests for choi_rank."""
+import numpy as np
+import pytest
+
+from toqito.channel_props import choi_rank
+
+
+def test_choi_rank_list_kraus():
+    """Verify that a list of Kraus operators gives correct Choi rank"""
+    kraus_1 = np.array([[1, 0], [0, 0]])
+    kraus_2 = np.array([[1, 0], [0, 0]]).conj().T
+    kraus_3 = np.array([[0, 1], [0, 0]])
+    kraus_4 = np.array([[0, 1], [0, 0]]).conj().T
+    kraus_5 = np.array([[0, 0], [1, 0]])
+    kraus_6 = np.array([[0, 0], [1, 0]]).conj().T
+    kraus_7 = np.array([[0, 0], [0, 1]])
+    kraus_8 = np.array([[0, 0], [0, 1]]).conj().T
+
+    kraus_ops = [
+        [kraus_1, kraus_2],
+        [kraus_3, kraus_4],
+        [kraus_5, kraus_6],
+        [kraus_7, kraus_8],
+    ]
+    np.testing.assert_equal(choi_rank(kraus_ops), 4)
+
+
+def test_choi_rank_choi_matrix():
+    """Verify Choi matrix of the swap operator map gives correct Choi rank."""
+    choi_matrix = np.array([[1, 0, 0, 0], [0, 0, 1, 0],
+                            [0, 1, 0, 0], [0, 0, 0, 1]])
+    np.testing.assert_equal(choi_rank(choi_matrix), 4)
+
+
+def test_choi_bad_input():
+    """Verify that a bad input (such as a string which still passes
+    with `numpy.linalg.matrix_rank`) raises an error"""
+    with pytest.raises(ValueError, match="Not a valid"):
+        bad_input = 'string'
+        choi_rank(bad_input)
+
+
+if __name__ == "__main__":
+    np.testing.run_module_suite()