Repository: vprusso/toqito

Issue:
Add tests for toqito/state_props/entanglement_of_formation.py: 71, 66->77
https://github.com/vprusso/toqito/blob/2b1e36a646af941d4fa900d49d7339eccee1a1c8/toqito/state_props/entanglement_of_formation.py#L70-L74

https://github.com/vprusso/toqito/blob/2b1e36a646af941d4fa900d49d7339eccee1a1c8/toqito/state_props/entanglement_of_formation.py#L66-L77

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/vprusso/toqito/pull/371?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vincent+Russo) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`2b1e36a`)](https://app.codecov.io/gh/vprusso/toqito/commit/2b1e36a646af941d4fa900d49d7339eccee1a1c8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vincent+Russo) 98.2% compared to head [(`a294699`)](https://app.codecov.io/gh/vprusso/toqito/pull/371?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vincent+Russo) 98.2%.


<details><summary>Additional details and impacted files</summary>


```diff
@@          Coverage Diff           @@
##           master    #371   +/-   ##
======================================
  Coverage    98.2%   98.2%           
======================================
  Files         301     301           
  Lines        6560    6558    -2     
  Branches     1025    1024    -1     
======================================
  Hits         6446    6446           
+ Misses         78      77    -1     
+ Partials       36      35    -1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/vprusso/toqito/pull/371?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vincent+Russo).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Vincent+Russo).

purva-thakre: @vprusso I am not sure what I am doing wrong here. Maybe you could catch it?

The following lines show up as uncovered even though I do believe the tests for them have been added in this PR. 

https://github.com/vprusso/toqito/blob/e003e8452cbf76c020e879f8bc1912301bc26777/toqito/state_props/entanglement_of_formation.py#L66-L72



Patch:
diff --git a/.coveragerc b/.coveragerc
index d3441069..bbafacbd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -10,6 +10,9 @@ show_missing = True
 precision = 2
 omit =
         *__init__*
+        */tests/*
+        *tests/*
+        */tests*
         *tests*
         virtualenvs/*
 
diff --git a/pyproject.toml b/pyproject.toml
index fb51940b..2d01c250 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,7 @@ ruff = "0.1.8"
 pylint = "3.0.3"
 pytest = "7.4.3"
 pytest-cov = "4.1.0"
-coverage = "7.3.2"
+coverage = "7.3.3"
 mypy = "1.7.1"
 setuptools = ">65.5.1"
 sphinx = "7.2.6"
diff --git a/toqito/matrix_ops/vectors_from_gram_matrix.py b/toqito/matrix_ops/vectors_from_gram_matrix.py
index 19464856..67fd6be8 100644
--- a/toqito/matrix_ops/vectors_from_gram_matrix.py
+++ b/toqito/matrix_ops/vectors_from_gram_matrix.py
@@ -1,5 +1,6 @@
 """Vectors associated to Gram matrix."""
 import numpy as np
+import scipy
 
 
 def vectors_from_gram_matrix(gram: np.ndarray) -> list[np.ndarray]:
@@ -49,4 +50,4 @@ def vectors_from_gram_matrix(gram: np.ndarray) -> list[np.ndarray]:
     except np.linalg.LinAlgError:
         print("Matrix is not positive semidefinite. Using eigendecomposition as alternative.")
         d, v = np.linalg.eig(gram)
-        return [np.sqrt(np.diag(d)) @ v[i].conj().T for i in range(dim)]
+        return [scipy.linalg.sqrtm(np.diag(d)) @ v[i].conj().T for i in range(dim)]
diff --git a/toqito/matrix_props/is_projection.py b/toqito/matrix_props/is_projection.py
index 5926086d..66e4db57 100644
--- a/toqito/matrix_props/is_projection.py
+++ b/toqito/matrix_props/is_projection.py
@@ -1,7 +1,7 @@
 """Is matrix a projection matrix."""
 import numpy as np
 
-from toqito.matrix_props import is_positive_semidefinite, is_square
+from toqito.matrix_props import is_square
 
 
 def is_projection(mat: np.ndarray, rtol: float = 1e-05, atol: float = 1e-08) -> bool:
@@ -64,7 +64,4 @@ def is_projection(mat: np.ndarray, rtol: float = 1e-05, atol: float = 1e-08) ->
     """
     if not is_square(mat):
         return False
-
-    if not is_positive_semidefinite(mat):
-        return False
     return np.allclose(np.linalg.matrix_power(mat, 2), mat, rtol=rtol, atol=atol)
diff --git a/toqito/perms/symmetric_projection.py b/toqito/perms/symmetric_projection.py
index 967cfe44..8263e97d 100644
--- a/toqito/perms/symmetric_projection.py
+++ b/toqito/perms/symmetric_projection.py
@@ -40,7 +40,7 @@ def symmetric_projection( dim: int, p_val: int = 2, partial: bool = False ) -> [
     Using :code:`toqito`, we can see this gives the proper result.
 
     >>> from toqito.perms import symmetric_projection
-    >>> symmetric_projection(2, 1).todense()
+    >>> symmetric_projection(2, 1)
     [[1., 0.],
      [0., 1.]]
 
@@ -57,7 +57,7 @@ def symmetric_projection( dim: int, p_val: int = 2, partial: bool = False ) -> [
     Using :code:`toqito` we can see this gives the proper result.
 
     >>> from toqito.perms import symmetric_projection
-    >>> symmetric_projection(dim=2).todense()
+    >>> symmetric_projection(dim=2)
     [[1. , 0. , 0. , 0. ],
      [0. , 0.5, 0.5, 0. ],
      [0. , 0.5, 0.5, 0. ],
diff --git a/toqito/state_props/entanglement_of_formation.py b/toqito/state_props/entanglement_of_formation.py
index 4791dd3b..8af9d0ba 100644
--- a/toqito/state_props/entanglement_of_formation.py
+++ b/toqito/state_props/entanglement_of_formation.py
@@ -57,7 +57,7 @@ def entanglement_of_formation(rho: np.ndarray, dim: list[int] | int = None) -> f
     """
     dim_x, dim_y = rho.shape
     round_dim = int(np.round(np.sqrt(max(dim_x, dim_y))))
-    eps = np.finfo(float).eps
+
 
     if dim is None:
         dim = round_dim
@@ -67,11 +67,6 @@ def entanglement_of_formation(rho: np.ndarray, dim: list[int] | int = None) -> f
         dim = np.array(  # pylint: disable=redefined-variable-type
             [dim, max(dim_x, dim_y) / dim], dtype=int
         )
-        if abs(dim[1] - np.round(dim[1])) >= 2 * max(dim_x, dim_y) * eps:
-            raise ValueError(
-                "Invalid dimension: If `dim` is provided as a "
-                "scalar, `dim` must evenly divide `len(rho)`."
-            )
         dim[1] = np.round(dim[1])
 
     if np.prod(dim) != max(dim_x, dim_y):
@@ -106,7 +101,7 @@ def entanglement_of_formation(rho: np.ndarray, dim: list[int] | int = None) -> f
 
             return -rho_c1 * rho_c1_log2 - rho_c2 * rho_c2_log2
         raise ValueError(
-            "Invalid dimension: It is presently only known how to compute "
+            "Invalid input: It is presently only known how to compute "
             "the entanglement-of-formation for two-qubit states and pure "
             "states."
         )
diff --git a/toqito/state_props/has_symmetric_extension.py b/toqito/state_props/has_symmetric_extension.py
index 397f617a..7f3ce19b 100644
--- a/toqito/state_props/has_symmetric_extension.py
+++ b/toqito/state_props/has_symmetric_extension.py
@@ -45,7 +45,7 @@ def has_symmetric_extension(
     ...                 [0, 1/2, 1, 0],
     ...                 [-1, 0, 0, 1]])
     >>> # Show the closed-form equation holds
-    >>> np.trace(partial_trace(rho, 1)**2) >= np.trace(rho**2) - 4 * np.sqrt(np.linalg.det(rho))
+    >>> np.trace(np.linalg.matrix_power(partial_trace(rho, 1), 2)) >= np.trace(rho**2) - 4 * np.sqrt(np.linalg.det(rho))
     True
     >>> # Now show that the `has_symmetric_extension` function recognizes this case.
     >>> has_symmetric_extension(rho)
diff --git a/toqito/state_props/in_separable_ball.py b/toqito/state_props/in_separable_ball.py
index e920500f..004f4adf 100644
--- a/toqito/state_props/in_separable_ball.py
+++ b/toqito/state_props/in_separable_ball.py
@@ -44,7 +44,7 @@ def in_separable_ball(mat: np.ndarray) -> bool:
     >>> import numpy as np
     >>>
     >>> U = random_unitary(4)
-    >>> lam = np.array([1.01, 1, 0,.99, 0]) / 3
+    >>> lam = np.array([1.01, 1, 0.99, 0]) / 3
     >>> rho = U @ np.diag(lam) @ U.conj().T
     >>> in_separable_ball(rho)
     False
diff --git a/toqito/state_props/is_mutually_unbiased_basis.py b/toqito/state_props/is_mutually_unbiased_basis.py
index 9101eba3..f63d3f6c 100644
--- a/toqito/state_props/is_mutually_unbiased_basis.py
+++ b/toqito/state_props/is_mutually_unbiased_basis.py
@@ -50,7 +50,7 @@ def is_mutually_unbiased_basis(vectors: list[np.ndarray | list[float | Any]]) ->
     >>> mub_1 = [e_0, e_1]
     >>> mub_2 = [1 / np.sqrt(2) * (e_0 + e_1), 1 / np.sqrt(2) * (e_0 - e_1)]
     >>> mub_3 = [1 / np.sqrt(2) * (e_0 + 1j * e_1), 1 / np.sqrt(2) * (e_0 - 1j * e_1)]
-    >>> mubs = [mub_1, mub_2, mub_3]
+    >>> mubs = np.array([mub_1, mub_2, mub_3])
     >>> is_mutually_unbiased_basis(mubs)
     True
 
diff --git a/toqito/state_props/schmidt_rank.py b/toqito/state_props/schmidt_rank.py
index 57f6e08d..26586b86 100644
--- a/toqito/state_props/schmidt_rank.py
+++ b/toqito/state_props/schmidt_rank.py
@@ -42,18 +42,18 @@ def schmidt_rank(rho: np.ndarray, dim: int | list[int] | np.ndarray = None) -> f
 
     >>> from toqito.states import bell
     >>> from toqito.state_props import schmidt_rank
-    >>> rho = bell(0).conj().T * bell(0)
+    >>> rho = bell(0) @ bell(0).conj().T
     >>> schmidt_rank(rho)
-    2
+    4
 
     Computing the Schmidt rank of the entangled singlet state should yield a value greater than
     :math:`1`.
 
     >>> from toqito.states import bell
     >>> from toqito.state_props import schmidt_rank
-    >>> u = bell(2).conj().T * bell(2)
+    >>> u = bell(2) @ bell(2).conj().T
     >>> schmidt_rank(u)
-    2
+    4
 
     Computing the Schmidt rank of a separable state should yield a value equal to :math:`1`.
 
@@ -67,7 +67,7 @@ def schmidt_rank(rho: np.ndarray, dim: int | list[int] | np.ndarray = None) -> f
     >>> e_11 = np.kron(e_1, e_1)
     >>>
     >>> rho = 1 / 2 * (e_00 - e_01 - e_10 + e_11)
-    >>> rho = rho.conj().T * rho
+    >>> rho = rho @ rho.conj().T
     >>> schmidt_rank(rho)
     1
 
diff --git a/toqito/state_props/sk_vec_norm.py b/toqito/state_props/sk_vec_norm.py
index aff248bc..f6b7462c 100644
--- a/toqito/state_props/sk_vec_norm.py
+++ b/toqito/state_props/sk_vec_norm.py
@@ -59,8 +59,6 @@ def sk_vector_norm(rho: np.ndarray, k: int = 1, dim: int | list[int] = None) ->
     # Allow the user to enter in a single integer for dimension.
     if isinstance(dim, int):
         dim = np.array([dim, dim_xy / dim])  # pylint: disable=redefined-variable-type
-        if np.abs(dim[1] - np.round(dim[1])) >= 2 * dim_xy * np.finfo(float).eps:
-            raise ValueError("If `dim` is a scalar, it must evenly divide the length of the state.")
         dim[1] = int(np.round(dim[1]))
 
     # It's faster to just compute the norm of `rho` directly if that will give


Test patch:
diff --git a/.github/workflows/build-test-actions.yml b/.github/workflows/build-test-actions.yml
index 26265760..3ed556c2 100644
--- a/.github/workflows/build-test-actions.yml
+++ b/.github/workflows/build-test-actions.yml
@@ -17,7 +17,7 @@ jobs:
       matrix:
           # Add versions here to expand support matrix
         python-version: ["3.10", "3.11"]
-        os: [ubuntu-latest]
+        os: [ubuntu-latest, macos-latest]
     runs-on: ${{ matrix.os }}
     env:
       PYTHON: ${{ matrix.python-version }}
@@ -29,9 +29,16 @@ jobs:
         uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
+      - name: Install blas, lapack for Linux
+        if: runner.os == 'Linux'
         run: |
           sudo apt-get install -y libblas-dev liblapack-dev
+      - name: Install blas, lapack for macOS
+        if: runner.os == 'macOS'
+        run: |
+          brew install openblas lapack
+      - name: Install Toqito + dependencies
+        run: |
           python -m pip install --upgrade pip
           pip install poetry
           poetry install --no-interaction
@@ -44,7 +51,7 @@ jobs:
       matrix:
         # Add versions here to expand support matrix
         python-version: ["3.10", "3.11"]
-        os: [ubuntu-latest]
+        os: [ubuntu-latest, macos-latest]
     runs-on: ${{ matrix.os }}
         # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#using-a-matrix-strategy
     env:
@@ -55,9 +62,16 @@ jobs:
         uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
+      - name: Install blas, lapack for Linux
+        if: runner.os == 'Linux'
         run: |
           sudo apt-get install -y libblas-dev liblapack-dev
+      - name: Install blas, lapack for macOS
+        if: runner.os == 'macOS'
+        run: |
+          brew install openblas lapack
+      - name: Install Toqito + dependencies
+        run: |
           python -m pip install --upgrade pip
           pip install poetry
           poetry install --no-interaction
diff --git a/toqito/matrix_props/tests/test_is_projection.py b/toqito/matrix_props/tests/test_is_projection.py
index 7116edf0..28df9422 100644
--- a/toqito/matrix_props/tests/test_is_projection.py
+++ b/toqito/matrix_props/tests/test_is_projection.py
@@ -5,9 +5,9 @@ from toqito.matrix_props import is_projection
 
 
 def test_is_projection():
-    """Check that non-Hermitian projection matrix returns False."""
+    """Check that non-symmetric projection matrix returns True."""
     mat = np.array([[0, 1], [0, 1]])
-    np.testing.assert_equal(is_projection(mat), False)
+    np.testing.assert_equal(is_projection(mat), True)
 
 
 def test_is_projection_2():
diff --git a/toqito/perms/tests/test_antisymmetric_projection.py b/toqito/perms/tests/test_antisymmetric_projection.py
index 5e3fbd93..70abd11d 100644
--- a/toqito/perms/tests/test_antisymmetric_projection.py
+++ b/toqito/perms/tests/test_antisymmetric_projection.py
@@ -1,4 +1,6 @@
 """Test antisymmetric_projection."""
+import platform
+
 import numpy as np
 import pytest
 
@@ -15,7 +17,9 @@ anti_proj_3_3_partial[15] = -0.40824829
 anti_proj_3_3_partial[19] = -0.40824829
 anti_proj_3_3_partial[21] = 0.40824829
 
-
+# https://docs.python.org/3/library/platform.html
+# Darwin is the system name for macOS
+@pytest.mark.skipif(platform.system() == "Darwin", reason="3-3-True-expected_result3 fails for macOS")
 @pytest.mark.parametrize("dim, p_param, partial, expected_result", [
     # Dimension is 2 and p is equal to 1.
     (2, 1, False, np.array([[1, 0], [0, 1]])),
diff --git a/toqito/state_props/tests/test_entanglement_of_formation.py b/toqito/state_props/tests/test_entanglement_of_formation.py
index 8bd8d2a8..73c5a6cd 100644
--- a/toqito/state_props/tests/test_entanglement_of_formation.py
+++ b/toqito/state_props/tests/test_entanglement_of_formation.py
@@ -15,25 +15,41 @@ e_0, e_1 = np.array([[1], [0]]), np.array([[0], [1]])
         (bell(0) @ bell(0).conj().T, None, 1),
         # The entanglement-of-formation on a maximally mixed.
         (max_mixed(4, False) @ max_mixed(4, False).conj().T, None, 0),
+        # The entanglement-of-formation on a Bell state with int dim
+        (bell(0) @ bell(0).conj().T, 2, 1),
+        # The entanglement-of-formation on a maximally mixed with int dim
+        (max_mixed(4, False) @ max_mixed(4, False).conj().T, 1, 0),
+        # The entanglement-of-formation on a Bell state with list dim
+        (bell(0) @ bell(0).conj().T, [2, 2], 1),
+        # The entanglement-of-formation on a maximally mixed with list dim
+        (max_mixed(4, False) @ max_mixed(4, False).conj().T, [2, 2], 0),
     ],
 )
 def test_entanglement_of_formation(rho, dim, expected_result):
     """Test function works as expected for a valid input."""
-    np.testing.assert_equal(np.isclose(entanglement_of_formation(rho, dim), expected_result), True)
+    assert np.isclose(entanglement_of_formation(rho, dim), expected_result)
 
 
 @pytest.mark.parametrize(
-    "rho, dim",
+    "rho, dim, error_msg",
     [
         # Invalid local dimension for entanglement_of_formation.
-        (np.identity(4), 3),
+        (np.identity(4), 3, "Invalid dimension: Please provide local dimensions that match the size of `rho`."),
         # Not presently known how to calculate for mixed states.
-        (3 / 4 * e_0 @ e_0.conj().T + 1 / 4 * e_1 @ e_1.conj().T, None),
+        (3 / 4 * e_0 @ e_0.conj().T + 1 / 4 * e_1 @ e_1.conj().T, None,
+            "Invalid input: It is presently only known how to compute "
+            "the entanglement-of-formation for two-qubit states and pure "
+            "states."),
         # Invalid non-square matrix for entanglement_of_formation.
-        (np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]), None),
+        (
+            np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]), None,
+            "Invalid dimension: `rho` must be either a vector or square matrix."),
+        # The entanglement-of-formation on a maximally mixed with list dim
+        (max_mixed(4, False) @ max_mixed(4, False).conj().T, [1, 1],
+        "Invalid dimension: Please provide local dimensions that match the size of `rho`.")
     ],
 )
-def test_entanglement_of_formation_invalid(rho, dim):
+def test_entanglement_of_formation_invalid(rho, dim, error_msg):
     """Ensures that an integer above 4 is error-checked."""
-    with np.testing.assert_raises(ValueError):
+    with pytest.raises(ValueError, match = error_msg):
         entanglement_of_formation(rho, dim)
diff --git a/toqito/state_props/tests/test_sk_vec_norm.py b/toqito/state_props/tests/test_sk_vec_norm.py
index 672fe93b..39107080 100644
--- a/toqito/state_props/tests/test_sk_vec_norm.py
+++ b/toqito/state_props/tests/test_sk_vec_norm.py
@@ -11,5 +11,19 @@ def test_sk_norm_maximally_entagled_state(n, k):
     """The S(k)-norm of the maximally entagled state."""
     v_vec = max_entangled(n)
     res = sk_vector_norm(v_vec, k=k)
+    assert np.isclose(res, np.sqrt(k / n))
 
-    np.testing.assert_equal(np.isclose(res, np.sqrt(k / n)), True)
+
+@pytest.mark.parametrize("n, k, dim", [(4, 2, 1), (4, 2, [1]), (5, 2, 1), (5, 2, [1])])
+def test_sk_norm_maximally_entagled_state_with_dim(n, k, dim):
+    """The S(k)-norm of the maximally entagled state where k> input_dim."""
+    v_vec = max_entangled(n)
+    res = sk_vector_norm(v_vec, k=k, dim=dim)
+    assert np.isclose(res, 1.0)
+
+@pytest.mark.parametrize("n, k, expected_result", [(4, 2, 0.7), (5, 2, 0.63)])
+def test_sk_norm_maximally_entagled_state_with_none_dim(n, k, expected_result):
+    """The S(k)-norm of the maximally entagled state dim = None."""
+    v_vec = max_entangled(n)
+    res = sk_vector_norm(v_vec, k=k, dim=None)
+    assert np.isclose(res, expected_result, atol=0.01)