Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1543fc3
parameterize random_psd_operator with wishart option
aman-coder03 Jan 23, 2026
0a76570
adding tests
aman-coder03 Jan 23, 2026
4386100
parametrize tests
aman-coder03 Jan 23, 2026
033ebac
implementing the feedback
aman-coder03 Jan 24, 2026
6582e49
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Feb 5, 2026
69dc08e
enhance wishart sampling with scale matrix and df parameterization
aman-coder03 Feb 5, 2026
2e0f4c7
enhance random_psd_operator with parameterized Wishart distribution s…
aman-coder03 Feb 24, 2026
04ee607
reordering
aman-coder03 Feb 24, 2026
b178239
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Feb 24, 2026
68371dc
address reviewer feedback:docstring format, validation, wishart fixes…
aman-coder03 Mar 3, 2026
d9f6407
Merge branch 'feature/random-psd-parameterization' of https://github.…
aman-coder03 Mar 3, 2026
2b1e616
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Mar 3, 2026
3190b3e
fix invalid non-PSD scale matrix shape in test
aman-coder03 Mar 3, 2026
3821e99
Merge branch 'feature/random-psd-parameterization' of https://github.…
aman-coder03 Mar 3, 2026
c43e400
fix docstring format, add wishart expected values, rank-deficient war…
aman-coder03 Mar 6, 2026
a11999f
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Mar 14, 2026
789b245
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Mar 19, 2026
3e1cb51
address reviewer feedback
aman-coder03 Mar 19, 2026
4d40650
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Mar 19, 2026
0914f38
Merge branch 'master' into feature/random-psd-parameterization
aman-coder03 Apr 7, 2026
378bb08
fix review comments
aman-coder03 Apr 11, 2026
d4dfbeb
Merge branch 'feature/random-psd-parameterization' of https://github.…
aman-coder03 Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions toqito/rand/random_psd_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ def random_psd_operator(
dim: int,
is_real: bool = False,
seed: int | None = None,
distribution: str = "uniform",
scale: np.ndarray | None = None,
Copy link
Copy Markdown
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.

num_degrees: int | None = None,
) -> np.ndarray:
r"""Generate a random positive semidefinite operator.
Copy link
Copy Markdown
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.


A positive semidefinite operator is a Hermitian operator that has only real and non-negative eigenvalues.
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
Copy Markdown
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}.")

from the Wishart distribution parameterized by a scale matrix and degrees of freedom.

Examples
========
Expand Down Expand Up @@ -54,6 +58,7 @@ def random_psd_operator(
.. jupyter-execute::

from toqito.matrix_props import is_positive_semidefinite

is_positive_semidefinite(real_psd_mat)
Copy link
Copy Markdown
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)



Expand All @@ -65,8 +70,6 @@ def random_psd_operator(

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

seeded


References
==========
Expand All @@ -77,21 +80,41 @@ def random_psd_operator(
: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
Copy Markdown
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)
  ```

:param scale: Scale matrix for the Wishart distribution. Defaults to the identity matrix if not provided.
Only used when ``distribution="wishart"``.
:param num_degrees: Degrees of freedom for the Wishart distribution. Defaults to ``dim`` if not provided.
Only used when ``distribution="wishart"``.
:return: A :code:`dim` x :code:`dim` random positive semidefinite matrix.

"""
# Generate a random matrix of dimension dim x dim.
gen = np.random.default_rng(seed=seed)
rand_mat = gen.random((dim, dim))
if not isinstance(dim, int) or dim < 0:
raise ValueError("dim must be a non-negative integer.")

# If is_real is False, add an imaginary component to the matrix.
if not is_real:
rand_mat = rand_mat + 1j * gen.random((dim, dim))

# Constructing a Hermitian matrix.
rand_mat = (rand_mat.conj().T + rand_mat) / 2
eigenvals, eigenvecs = np.linalg.eigh(rand_mat)

Q, R = np.linalg.qr(eigenvecs)
gen = np.random.default_rng(seed=seed)

return Q @ np.diag(np.abs(eigenvals)) @ Q.conj().T
if distribution == "uniform":
rand_mat = gen.random((dim, dim))
if not is_real:
rand_mat = rand_mat + 1j * gen.random((dim, dim))
rand_mat = (rand_mat.conj().T + rand_mat) / 2
eigenvals, eigenvecs = np.linalg.eigh(rand_mat)
q_mat, _ = np.linalg.qr(eigenvecs)
return q_mat @ np.diag(np.abs(eigenvals)) @ q_mat.conj().T

if distribution == "wishart":
if scale is None:
scale = np.eye(dim)
if num_degrees is None:
Copy link
Copy Markdown
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.")

num_degrees = dim
if is_real:
x_mat = gen.multivariate_normal(np.zeros(dim), scale, size=num_degrees).T
Copy link
Copy Markdown
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)).

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

Choose a reason for hiding this comment

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

Minor: this calls multivariate_normal twice per invocation. You can generate the complex Wishart sample more directly by drawing a single white complex Gaussian and left-multiplying by the Cholesky factor of scale:

L = np.linalg.cholesky(scale)
z = (gen.standard_normal((dim, num_degrees))
     + 1j * gen.standard_normal((dim, num_degrees))) / np.sqrt(2)
x_mat = L @ z

Same distribution (E[x_mat x_mat^H] = num_degrees * scale), one RNG path, and it keeps the real and complex branches structurally similar. Not blocking — current code is correct.

)
return x_mat @ x_mat.conj().T

raise ValueError("Invalid distribution. Supported options are 'uniform' and 'wishart'.")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Consider typing distribution as Literal["uniform", "wishart"] in the signature so type checkers and IDE autocomplete can surface the valid options. The runtime ValueError here is still the right safety net for untyped callers.

52 changes: 36 additions & 16 deletions toqito/rand/tests/test_random_psd_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@


@pytest.mark.parametrize(
"dim, is_real",
"dim, is_real, distribution",
[
# Test with a matrix of dimension 2.
(2, True),
# Test with a matrix of dimension 4.
(4, False),
# Test with a matrix of dimension 5.
(5, False),
# Test with a matrix of dimension 10.
(10, True),
# Test with a matrix of dimension 2, real, uniform.
(2, True, "uniform"),
# Test with a matrix of dimension 4, complex, uniform.
(4, False, "uniform"),
# Test with a matrix of dimension 5, complex, uniform.
(5, False, "uniform"),
# Test with a matrix of dimension 10, real, uniform.
(10, True, "uniform"),
# Test with a matrix of dimension 4, complex, wishart.
(4, False, "wishart"),
# Test with a matrix of dimension 4, real, wishart.
(4, True, "wishart"),
],
)
def test_random_psd_operator(dim, is_real):
def test_random_psd_operator(dim, is_real, distribution):
"""Test for random_psd_operator function."""
# Generate a random positive semidefinite operator.
rand_psd_operator = random_psd_operator(dim, is_real)

# Ensure the matrix has the correct shape.
rand_psd_operator = random_psd_operator(dim, is_real, distribution=distribution)
assert_equal(rand_psd_operator.shape, (dim, dim))

# Check if the matrix is positive semidefinite.
assert is_positive_semidefinite(rand_psd_operator)


Expand Down Expand Up @@ -79,3 +78,24 @@ def test_random_psd_operator_with_seed(dim, is_real, seed, expected_mat):
"""Test that random_psd_operator function returns the expected output when seeded."""
matrix = random_psd_operator(dim, is_real, seed)
assert_allclose(matrix, expected_mat)


@pytest.mark.parametrize(
Copy link
Copy Markdown
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.

"dim, distribution",
[
# Invalid dim type.
("4", "uniform"),
# Negative dim.
(-2, "uniform"),
],
)
def test_random_psd_operator_invalid_dim(dim, distribution):
Copy link
Copy Markdown
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.

"""Test that invalid dim raises ValueError."""
with pytest.raises(ValueError):
random_psd_operator(dim, distribution=distribution)


def test_random_psd_operator_invalid_distribution():
"""Test that invalid distribution raises ValueError."""
with pytest.raises(ValueError):
random_psd_operator(4, distribution="invalid")