parameterize random_psd_operator with wishart option#1390
parameterize random_psd_operator with wishart option#1390aman-coder03 wants to merge 9 commits intovprusso:masterfrom
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1390 +/- ##
======================================
Coverage 98.3% 98.3%
======================================
Files 202 202
Lines 5212 5221 +9
Branches 1196 1200 +4
======================================
+ Hits 5124 5133 +9
Misses 46 46
Partials 42 42 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
@aman-coder03 the code coverage went down with your changes. Please ensure you have tests that cover the areas that you modified. Code coverage report can be found here: |
|
i have covered all the areas now @vprusso |
|
|
||
| def test_random_psd_operator_wishart(): | ||
| """Test Wishart distribution generates PSD matrix.""" | ||
| mat = random_psd_operator(4, distribution="wishart") | ||
| assert mat.shape == (4, 4) | ||
| assert is_positive_semidefinite(mat) | ||
|
|
||
|
|
||
| def test_random_psd_operator_invalid_distribution(): | ||
| """Test invalid distribution raises ValueError.""" | ||
| with pytest.raises(ValueError): | ||
| random_psd_operator(4, distribution="invalid") | ||
|
|
||
| def test_random_psd_operator_invalid_dim_type(): | ||
| """Test invalid dim type raises ValueError.""" | ||
| with pytest.raises(ValueError): | ||
| random_psd_operator("4") | ||
|
|
||
|
|
||
| def test_random_psd_operator_invalid_dim_negative(): | ||
| """Test negative dim raises ValueError.""" | ||
| with pytest.raises(ValueError): | ||
| random_psd_operator(-2) | ||
|
|
||
|
|
||
| def test_random_psd_operator_wishart_real_branch(): | ||
| """Test Wishart distribution with real sampling branch.""" | ||
| mat = random_psd_operator(4, is_real=True, distribution="wishart") | ||
| assert mat.shape == (4, 4) | ||
| assert is_positive_semidefinite(mat) |
There was a problem hiding this comment.
There should be a way to use the pytest.parameterize decorator to consolidate these tests so that they are not all written as separate functions. You can refer to how this is used elsewhere in the repo for context/reference.
|
@vprusso i have updated the tests, please have a look! |
| @pytest.mark.parametrize( | ||
| "dim, distribution", | ||
| [ | ||
| ("4", "uniform"), # invalid dim type |
There was a problem hiding this comment.
Please keep the comment style consistent with other tests, e.g.
| ("4", "uniform"), # invalid dim type | |
| # Invalid dim type. | |
| ("4", "uniform"), |
etc.
toqito/rand/random_psd_operator.py
Outdated
| raise ValueError( | ||
| "Invalid distribution. Supported options are 'uniform' and 'wishart'." | ||
| ) |
There was a problem hiding this comment.
Linter is set for 120 char-width:
| raise ValueError( | |
| "Invalid distribution. Supported options are 'uniform' and 'wishart'." | |
| ) | |
| raise ValueError("Invalid distribution. Supported options are 'uniform' and 'wishart'.") |
|
i have implemented the suggestions @vprusso |
|
Any reason for not allowing greater customizability in the "wishart" option? The Wishart distribution is parametrized by a scale matrix |
@aman-coder03 FYI These are all good suggestions. Thanks, @Newtech66 ! |
|
@Newtech66 @vprusso thanks for your suggestions!! |
|
@vprusso if there are no additional changes, can we merge this PR? |
@aman-coder03 would it not be possible to address this comment as well, @aman-coder03 ? |
|
You’re right that the current implementation corresponds to the identity-scale Wishart case with That said, I agree that supporting optional Thanks again for the helpful feedback. I will push an update shortly |
| "is_real", | ||
| [True, False], | ||
| ) | ||
| def test_random_psd_operator_wishart(is_real): |
There was a problem hiding this comment.
Why not include this test to parameterize over the other distributions as well? Indeed, this could just be rolled into the initial test that's the first test in this file (instead of creating a whole new separate and fractured one here), right?
toqito/rand/random_psd_operator.py
Outdated
There was a problem hiding this comment.
| seeded = random_psd_operator(2, is_real=True, seed=42) | |
| seeded |
| from toqito.matrix_props import is_positive_semidefinite | ||
| is_positive_semidefinite(real_psd_mat) |
There was a problem hiding this comment.
| from toqito.matrix_props import is_positive_semidefinite | |
| is_positive_semidefinite(real_psd_mat) |
| real_psd_mat = random_psd_operator(2, is_real=True) | ||
|
|
||
| real_psd_mat |
There was a problem hiding this comment.
| real_psd_mat = random_psd_operator(2, is_real=True) | |
| real_psd_mat |
| complex_psd_mat = random_psd_operator(2) | ||
|
|
||
| complex_psd_mat |
There was a problem hiding this comment.
| complex_psd_mat = random_psd_operator(2) | |
| complex_psd_mat |
toqito/rand/random_psd_operator.py
Outdated
There was a problem hiding this comment.
There isn't anything in this description about the uniform and wishart distributions.
toqito/rand/random_psd_operator.py
Outdated
| if scale is None: | ||
| scale = np.eye(dim) | ||
|
|
||
| if df is None: |
There was a problem hiding this comment.
df is generally used to indicate a dataframe which is not what this is here.
toqito/rand/random_psd_operator.py
Outdated
|
|
||
| rand_mat = (rand_mat.conj().T + rand_mat) / 2 | ||
| eigenvals, eigenvecs = np.linalg.eigh(rand_mat) | ||
| Q, _ = np.linalg.qr(eigenvecs) |
There was a problem hiding this comment.
Upper case letters are not appropriate for variable names according to PEP-8
|
@vprusso can you please have a look at the PR, according to your convenience! |
| scale: np.ndarray | None = None, | ||
| num_degrees: int | None = None, | ||
| ) -> np.ndarray: | ||
| r"""Generate a random positive semidefinite operator. |
There was a problem hiding this comment.
The docstring still uses the old Sphinx/RST format (.. jupyter-execute::, :param:, :return:,
Examples\n========). The rest of the codebase — including the closest precedent random_density_matrix.py —
has been migrated to Google-style with markdown-exec blocks. Compare:
PR (outdated):
Examples
========
.. jupyter-execute::
from toqito.rand import random_psd_operator
:param dim: ...
:return: ...
Expected (matches random_density_matrix.py):
Examples:
python exec="1" source="above" session="psd_operator" from toqito.rand import random_psd_operator ...
Args:
dim: ...
Returns:
...
The current master version of this file already uses the modern format. This PR would revert the docstring to
the old format, which would break the docs build.
| :param is_real: Boolean denoting whether the returned matrix will have all real entries or not. | ||
| Default is :code:`False`. | ||
| :param seed: A seed used to instantiate numpy's random number generator. | ||
| :param distribution: The sampling strategy to use. Either ``"uniform"`` (default) or ``"wishart"``. |
There was a problem hiding this comment.
Missing docstring examples for the new feature
random_density_matrix.py includes an explicit example showing the distance_metric="bures" option (lines
71–80). This PR adds distribution, scale, and num_degrees parameters but the docstring has no example
demonstrating Wishart usage. Should add at least one example like:
```python exec="1" source="above" session="psd_operator"
from toqito.rand import random_psd_operator
wishart_mat = random_psd_operator(3, distribution="wishart", num_degrees=5)
print(wishart_mat)
```
| assert_allclose(matrix, expected_mat) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( |
There was a problem hiding this comment.
dim=0 should probably not be allowed
The validation allows dim=0 (dim < 0 rejects negatives, but zero passes). A 0×0 matrix is technically valid
but useless and likely a user error. The existing code on master doesn't validate dim at all, and neither
does random_density_matrix.py, so at minimum this should be consistent — but if validation is being added,
dim < 1 would be more useful than dim < 0.
| This function generates a random positive semidefinite operator by constructing a Hermitian matrix, | ||
| based on the fact that a Hermitian matrix can have real eigenvalues. | ||
| This function generates a random PSD operator using one of two sampling strategies: ``"uniform"`` | ||
| constructs a Hermitian matrix via random sampling and eigendecomposition, while ``"wishart"`` samples |
There was a problem hiding this comment.
When distribution="wishart" with a user-supplied scale matrix, there's no check that:
- scale is square and has shape (dim, dim)
- scale is positive semidefinite (required for a valid Wishart distribution)
np.random.Generator.multivariate_normal will raise a LinAlgError on an invalid covariance matrix, but the
error message would be confusing. A clear ValueError upfront would be better, e.g.:
if scale.shape != (dim, dim):
raise ValueError(f"scale must be a {dim}x{dim} matrix, got {scale.shape}.")
| if distribution == "wishart": | ||
| if scale is None: | ||
| scale = np.eye(dim) | ||
| if num_degrees is None: |
There was a problem hiding this comment.
num_degrees (degrees of freedom) must be >= dim for the Wishart distribution to be non-singular, and at
minimum >= 1. Passing num_degrees=0 or a negative value will produce a 0-column x_mat or crash. Should
validate:
if num_degrees < 1:
raise ValueError("num_degrees must be a positive integer.")
| is_real: bool = False, | ||
| seed: int | None = None, | ||
| distribution: str = "uniform", | ||
| scale: np.ndarray | None = None, |
There was a problem hiding this comment.
If a user passes scale=some_matrix with distribution="uniform", those parameters are silently ignored. This could mask bugs. Consider raising a warning or ValueError, similar to how well-designed APIs prevent contradictory arguments.
| if num_degrees is None: | ||
| num_degrees = dim | ||
| if is_real: | ||
| x_mat = gen.multivariate_normal(np.zeros(dim), scale, size=num_degrees).T |
There was a problem hiding this comment.
The complex Wishart implementation is:
x_mat = (
gen.multivariate_normal(np.zeros(dim), scale, size=num_degrees).T
+ 1j * gen.multivariate_normal(np.zeros(dim), scale, size=num_degrees).T
)
This draws two independent Gaussian matrices for real and imaginary parts, each with covariance scale. This
means the resulting complex Wishart matrix x_mat @ x_mat.conj().T follows a complex Wishart distribution with
scale 2 * scale (not scale), since each entry has variance scale_ii + scale_ii. This is a subtle but
meaningful statistical point that should at least be documented, or the scaling should be adjusted (e.g.,
divide each component by sqrt(2)).
| (-2, "uniform"), | ||
| ], | ||
| ) | ||
| def test_random_psd_operator_invalid_dim(dim, distribution): |
There was a problem hiding this comment.
The test_random_psd_operator_with_seed tests only check the uniform distribution. There should be at least one seeded test for distribution="wishart" to catch regressions if the sampling logic changes.
Description
closes #1275
this PR parameterizes the distribution used in
random_psd_operatorby introducing a newdistributionargument. This allows users to select alternative sampling strategies when generating random positive semidefinite (PSD) operatorsthe default behavior remains unchanged to preserve backward compatibility
Changes
distribution: str = "uniform"parameter torandom_psd_operator"uniform") for full backward compatibility"wishart"option using Gaussian sampling (X @ X.conj().T) to generate PSD operatorsChecklist
rufffor errors related to code style and formattingpytest(targeted tests for modified module pass locally)