Skip to content

parameterize random_psd_operator with wishart option#1390

Open
aman-coder03 wants to merge 9 commits intovprusso:masterfrom
aman-coder03:feature/random-psd-parameterization
Open

parameterize random_psd_operator with wishart option#1390
aman-coder03 wants to merge 9 commits intovprusso:masterfrom
aman-coder03:feature/random-psd-parameterization

Conversation

@aman-coder03
Copy link

Description

closes #1275

this PR parameterizes the distribution used in random_psd_operator by introducing a new distribution argument. This allows users to select alternative sampling strategies when generating random positive semidefinite (PSD) operators
the default behavior remains unchanged to preserve backward compatibility

Changes

  • Added distribution: str = "uniform" parameter to random_psd_operator
  • Preserved existing behavior as the default ("uniform") for full backward compatibility
  • Added "wishart" option using Gaussian sampling (X @ X.conj().T) to generate PSD operators
  • Added validation for invalid distribution values
  • Added unit tests for Wishart sampling and invalid distribution handling

Checklist

  • Use ruff for errors related to code style and formatting
  • Verify all previous and newly added unit tests pass in pytest (targeted tests for modified module pass locally)
  • Check the documentation build does not lead to any failures

@codecov
Copy link

codecov bot commented Jan 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.3%. Comparing base (b9865b8) to head (4386100).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vprusso
Copy link
Owner

vprusso commented Jan 23, 2026

@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:

https://app.codecov.io/gh/vprusso/toqito/pull/1390

@aman-coder03
Copy link
Author

i have covered all the areas now @vprusso
thanks!

Comment on lines +82 to +111

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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

@aman-coder03 aman-coder03 Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure @vprusso i will check and update shortly!

@aman-coder03
Copy link
Author

@vprusso i have updated the tests, please have a look!

@pytest.mark.parametrize(
"dim, distribution",
[
("4", "uniform"), # invalid dim type
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the comment style consistent with other tests, e.g.

Suggested change
("4", "uniform"), # invalid dim type
# Invalid dim type.
("4", "uniform"),

etc.

Comment on lines +113 to +115
raise ValueError(
"Invalid distribution. Supported options are 'uniform' and 'wishart'."
)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter is set for 120 char-width:

Suggested change
raise ValueError(
"Invalid distribution. Supported options are 'uniform' and 'wishart'."
)
raise ValueError("Invalid distribution. Supported options are 'uniform' and 'wishart'.")

@aman-coder03
Copy link
Author

i have implemented the suggestions @vprusso

@Newtech66
Copy link
Contributor

Newtech66 commented Jan 24, 2026

Any reason for not allowing greater customizability in the "wishart" option? The Wishart distribution is parametrized by a scale matrix $V$ and a parameter $n$ as given here. The current implementation amounts to setting $V$ to the identity matrix and $n=$dim. Also, why not use scipy.linalg.wishart?

@vprusso
Copy link
Owner

vprusso commented Jan 24, 2026

Any reason for not allowing greater customizability in the "wishart" option? The Wishart distribution is parametrized by a scale matrix V and a parameter n as given here. The current implementation amounts to setting V to the identity matrix and n = dim. Also, why not use scipy.linalg.wishart?

@aman-coder03 FYI These are all good suggestions. Thanks, @Newtech66 !

@aman-coder03
Copy link
Author

aman-coder03 commented Jan 24, 2026

@Newtech66 @vprusso thanks for your suggestions!!
my initial goal was to introduce parameterization in a minimal and backward compatible way to address the eigenvalue spread concern, without expanding the function signature too much in a single PR
i agree that allowing users to specify the scale matrix and degrees of freedom would make the implementation more faithful to the general Wishart definition
Using scipy.stats.wishart is also a reasonable option
if there is interest, i would be happy to extend this further to support a fully parameterized Wishart distribution in a follow up PR

@aman-coder03
Copy link
Author

@vprusso if there are no additional changes, can we merge this PR?

@vprusso
Copy link
Owner

vprusso commented Feb 5, 2026

Any reason for not allowing greater customizability in the "wishart" option? The Wishart distribution is parametrized by a scale matrix V and a parameter n as given here. The current implementation amounts to setting V to the identity matrix and n = dim. Also, why not use scipy.linalg.wishart?

@aman-coder03 would it not be possible to address this comment as well, @aman-coder03 ?

@aman-coder03
Copy link
Author

You’re right that the current implementation corresponds to the identity-scale Wishart case with df = dim. My initial goal was to introduce distribution parameterization in a minimal and backward-compatible way.

That said, I agree that supporting optional scale and df parameters would make the implementation more complete. I’ll extend this PR to include a fully parameterized Wishart option with proper validation and tests.

Thanks again for the helpful feedback. I will push an update shortly

@aman-coder03 aman-coder03 requested a review from vprusso February 9, 2026 14:11
"is_real",
[True, False],
)
def test_random_psd_operator_wishart(is_real):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines 69 to 71
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
seeded = random_psd_operator(2, is_real=True, seed=42)
seeded

Comment on lines 59 to 60
from toqito.matrix_props import is_positive_semidefinite
is_positive_semidefinite(real_psd_mat)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from toqito.matrix_props import is_positive_semidefinite
is_positive_semidefinite(real_psd_mat)

Comment on lines 50 to 52
real_psd_mat = random_psd_operator(2, is_real=True)

real_psd_mat
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
real_psd_mat = random_psd_operator(2, is_real=True)
real_psd_mat

Comment on lines 30 to 32
complex_psd_mat = random_psd_operator(2)

complex_psd_mat
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
complex_psd_mat = random_psd_operator(2)
complex_psd_mat

Comment on lines 16 to 18
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't anything in this description about the uniform and wishart distributions.

if scale is None:
scale = np.eye(dim)

if df is None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

df is generally used to indicate a dataframe which is not what this is here.


rand_mat = (rand_mat.conj().T + rand_mat) / 2
eigenvals, eigenvecs = np.linalg.eigh(rand_mat)
Q, _ = np.linalg.qr(eigenvecs)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upper case letters are not appropriate for variable names according to PEP-8

@aman-coder03 aman-coder03 requested a review from vprusso February 25, 2026 16:52
@aman-coder03
Copy link
Author

@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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"``.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eigenvalue spread of random_psd_operator is very skewed

3 participants