From 02b7aa4c808657c0a3bd22d7cd3641f9f6fb19b4 Mon Sep 17 00:00:00 2001 From: Laasya-73 <77721581+Laasya-73@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:53:19 -0500 Subject: [PATCH 1/4] Add summary statistics to ProductMeasure --- qmcpy/true_measure/product_measure.py | 83 +++++++++++++++++++++++ test/test_product_measure.py | 96 +++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/qmcpy/true_measure/product_measure.py b/qmcpy/true_measure/product_measure.py index b644666a3..dc2317f32 100644 --- a/qmcpy/true_measure/product_measure.py +++ b/qmcpy/true_measure/product_measure.py @@ -1,4 +1,5 @@ import numpy as np +from scipy import sparse from .abstract_true_measure import AbstractTrueMeasure from ..discrete_distribution.abstract_discrete_distribution import ( @@ -46,6 +47,9 @@ class ProductMeasure(AbstractTrueMeasure): Notes ----- + For independent marginal blocks, means, variances, and standard deviations + are concatenated in marginal order, while covariance is block diagonal. + Exact product weights are supported for direct marginal true measures. For recursively composed marginal measures, sampling is supported through QMCPy's recursive transform helper, but exact final-space product weights @@ -177,6 +181,85 @@ def __init__(self, sampler, marginals): super(ProductMeasure, self).__init__() + for statistic in ( + "mean", + "variance", + "standard_deviation", + "covariance", + ): + if all(hasattr(marginal, statistic) for marginal in self.marginals): + self.parameters.append(statistic) + + def _marginal_statistic(self, marginal, marginal_index, statistic): + """Return a statistic or identify the marginal that does not provide it.""" + try: + return getattr(marginal, statistic) + except AttributeError as error: + raise AttributeError( + f"ProductMeasure marginal {marginal_index} " + f"({type(marginal).__name__}) does not provide {statistic}." + ) from error + + def _concatenate_marginal_statistic(self, statistic): + """Concatenate a coordinate-wise statistic in marginal order.""" + values = [] + for marginal_index, marginal in enumerate(self.marginals): + value = self._marginal_statistic( + marginal, marginal_index, statistic + ) + value = np.atleast_1d(np.asarray(value)) + if value.shape != (marginal.d,): + raise DimensionError( + f"ProductMeasure marginal {marginal_index} " + f"({type(marginal).__name__}) {statistic} must have shape " + f"({marginal.d},), got {value.shape}." + ) + values.append(value) + + combined = self._read_only_array(np.concatenate(values)) + return self._scalar_if_univariate(combined) + + @property + def mean(self): + return self._concatenate_marginal_statistic("mean") + + @property + def variance(self): + return self._concatenate_marginal_statistic("variance") + + @property + def standard_deviation(self): + return self._concatenate_marginal_statistic("standard_deviation") + + @property + def covariance(self): + blocks = [] + for marginal_index, marginal in enumerate(self.marginals): + block = self._marginal_statistic( + marginal, marginal_index, "covariance" + ) + if sparse.issparse(block): + block = block.toarray() + block = np.atleast_2d(np.asarray(block)) + expected_shape = (marginal.d, marginal.d) + if block.shape != expected_shape: + raise DimensionError( + f"ProductMeasure marginal {marginal_index} " + f"({type(marginal).__name__}) covariance must have shape " + f"{expected_shape}, got {block.shape}." + ) + blocks.append(block) + + covariance = np.zeros( + (self.d, self.d), dtype=np.result_type(*[block.dtype for block in blocks]) + ) + start = 0 + for block in blocks: + stop = start + block.shape[0] + covariance[start:stop, start:stop] = block + start = stop + return self._read_only_array(covariance) + @staticmethod def _expand_bounds(bounds, dimension, name): """ diff --git a/test/test_product_measure.py b/test/test_product_measure.py index c91b313bd..9e217fd6a 100644 --- a/test/test_product_measure.py +++ b/test/test_product_measure.py @@ -48,6 +48,102 @@ def test_product_measure_replication_shape(): assert x.shape == (r, n, 2) +def test_product_measure_statistics_for_multiple_1d_marginals(): + marginals = [ + Uniform(DummySampler(1), lower_bound=8.0, upper_bound=12.0), + Uniform(DummySampler(1), lower_bound=-1.0, upper_bound=5.0), + ] + tm = ProductMeasure(sampler=DigitalNetB2(2, seed=23), marginals=marginals) + + np.testing.assert_allclose(tm.mean, [10.0, 2.0]) + np.testing.assert_allclose(tm.variance, [4.0 / 3.0, 3.0]) + np.testing.assert_allclose( + tm.standard_deviation, [np.sqrt(4.0 / 3.0), np.sqrt(3.0)] + ) + np.testing.assert_allclose(tm.covariance, np.diag([4.0 / 3.0, 3.0])) + + assert tm.mean.shape == (2,) + assert tm.variance.shape == (2,) + assert tm.standard_deviation.shape == (2,) + assert tm.covariance.shape == (2, 2) + for statistic in ("mean", "variance", "standard_deviation", "covariance"): + assert not getattr(tm, statistic).flags.writeable + + +def test_product_measure_normalizes_scalar_1d_statistics(): + marginals = [ + Uniform(DummySampler(1), lower_bound=8.0, upper_bound=12.0), + Gaussian(DummySampler(1), mean=2.0, covariance=9.0), + ] + for marginal in marginals: + assert isinstance(marginal.mean, float) + assert isinstance(marginal.variance, float) + assert isinstance(marginal.standard_deviation, float) + + tm = ProductMeasure(sampler=DigitalNetB2(2, seed=29), marginals=marginals) + + np.testing.assert_allclose(tm.mean, [10.0, 2.0]) + np.testing.assert_allclose(tm.variance, [4.0 / 3.0, 9.0]) + np.testing.assert_allclose( + tm.standard_deviation, [np.sqrt(4.0 / 3.0), 3.0] + ) + np.testing.assert_allclose(tm.covariance, np.diag([4.0 / 3.0, 9.0])) + assert tm.mean.shape == (2,) + assert tm.variance.shape == (2,) + assert tm.standard_deviation.shape == (2,) + assert tm.covariance.shape == (2, 2) + + +def test_product_measure_statistics_preserve_order_and_covariance_blocks(): + marginals = [ + Uniform(DummySampler(1), lower_bound=8.0, upper_bound=12.0), + Gaussian( + DummySampler(2), + mean=[2.0, 5.0], + covariance=[[2.0, 0.5], [0.5, 3.0]], + ), + ] + tm = ProductMeasure(sampler=DigitalNetB2(3, seed=31), marginals=marginals) + expected_covariance = np.array( + [ + [4.0 / 3.0, 0.0, 0.0], + [0.0, 2.0, 0.5], + [0.0, 0.5, 3.0], + ] + ) + + np.testing.assert_allclose(tm.mean, [10.0, 2.0, 5.0]) + np.testing.assert_allclose(tm.variance, [4.0 / 3.0, 2.0, 3.0]) + np.testing.assert_allclose( + tm.standard_deviation, + [np.sqrt(4.0 / 3.0), np.sqrt(2.0), np.sqrt(3.0)], + ) + np.testing.assert_allclose(tm.covariance, expected_covariance) + + assert tm.mean.shape == (3,) + assert tm.variance.shape == (3,) + assert tm.standard_deviation.shape == (3,) + assert tm.covariance.shape == (3, 3) + assert np.array_equal(tm.covariance[:1, 1:], np.zeros((1, 2))) + assert np.array_equal(tm.covariance[1:, :1], np.zeros((2, 1))) + + +def test_product_measure_missing_marginal_statistic_is_identified(): + marginals = [ + ZeroInflatedExpUniform(DummySampler(1), p_zero=0.4, lam=1.5), + Uniform(DummySampler(1), lower_bound=2.0, upper_bound=5.0), + ] + tm = ProductMeasure(sampler=DigitalNetB2(2, seed=23), marginals=marginals) + + np.testing.assert_allclose(tm.mean, [0.4, 3.5]) + assert "covariance" not in tm.parameters + with pytest.raises( + AttributeError, + match=r"marginal 0 \(ZeroInflatedExpUniform\) does not provide covariance", + ): + _ = tm.covariance + + def test_product_measure_marginals_with_different_dimensions(): n = 32 marginals = [ From 1514d5d6a47e12641b8b6383759b2360abc74f56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:16:49 +0000 Subject: [PATCH 2/4] Fix ProductMeasure.covariance to preserve sparse blocks Co-authored-by: fjhickernell <817530+fjhickernell@users.noreply.github.com> --- qmcpy/true_measure/product_measure.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qmcpy/true_measure/product_measure.py b/qmcpy/true_measure/product_measure.py index dc2317f32..bdce45ac3 100644 --- a/qmcpy/true_measure/product_measure.py +++ b/qmcpy/true_measure/product_measure.py @@ -238,9 +238,8 @@ def covariance(self): block = self._marginal_statistic( marginal, marginal_index, "covariance" ) - if sparse.issparse(block): - block = block.toarray() - block = np.atleast_2d(np.asarray(block)) + if not sparse.issparse(block): + block = np.atleast_2d(np.asarray(block)) expected_shape = (marginal.d, marginal.d) if block.shape != expected_shape: raise DimensionError( @@ -250,11 +249,18 @@ def covariance(self): ) blocks.append(block) + if all(sparse.issparse(b) for b in blocks): + covariance = sparse.block_diag(blocks, format="dia") + covariance.data.setflags(write=False) + return covariance + + dense_blocks = [b.toarray() if sparse.issparse(b) else b for b in blocks] covariance = np.zeros( - (self.d, self.d), dtype=np.result_type(*[block.dtype for block in blocks]) + (self.d, self.d), + dtype=np.result_type(*[b.dtype for b in dense_blocks]), ) start = 0 - for block in blocks: + for block in dense_blocks: stop = start + block.shape[0] covariance[start:stop, start:stop] = block start = stop From 44f1fd6702228e688a3a749902a30f5f1aad7b7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:39:06 +0000 Subject: [PATCH 3/4] Fix test assertions for sparse covariance matrix Co-authored-by: fjhickernell <817530+fjhickernell@users.noreply.github.com> --- test/test_product_measure.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/test_product_measure.py b/test/test_product_measure.py index 9e217fd6a..d4d712feb 100644 --- a/test/test_product_measure.py +++ b/test/test_product_measure.py @@ -1,5 +1,6 @@ import numpy as np import pytest +import scipy.sparse as sp import scipy.stats as stats from qmcpy import ( @@ -60,14 +61,18 @@ def test_product_measure_statistics_for_multiple_1d_marginals(): np.testing.assert_allclose( tm.standard_deviation, [np.sqrt(4.0 / 3.0), np.sqrt(3.0)] ) - np.testing.assert_allclose(tm.covariance, np.diag([4.0 / 3.0, 3.0])) + cov = tm.covariance + cov_dense = cov.toarray() if sp.issparse(cov) else cov + np.testing.assert_allclose(cov_dense, np.diag([4.0 / 3.0, 3.0])) assert tm.mean.shape == (2,) assert tm.variance.shape == (2,) assert tm.standard_deviation.shape == (2,) assert tm.covariance.shape == (2, 2) for statistic in ("mean", "variance", "standard_deviation", "covariance"): - assert not getattr(tm, statistic).flags.writeable + value = getattr(tm, statistic) + flags = value.data.flags if sp.issparse(value) else value.flags + assert not flags.writeable def test_product_measure_normalizes_scalar_1d_statistics(): From 536d3cdd15111e3280ecfddcf60f2bc200e72436 Mon Sep 17 00:00:00 2001 From: Laasya-73 <77721581+Laasya-73@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:17:08 -0500 Subject: [PATCH 4/4] Preserve sparse ProductMeasure covariance --- qmcpy/true_measure/product_measure.py | 14 +++--- test/test_product_measure.py | 71 +++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/qmcpy/true_measure/product_measure.py b/qmcpy/true_measure/product_measure.py index bdce45ac3..c1eeda1e6 100644 --- a/qmcpy/true_measure/product_measure.py +++ b/qmcpy/true_measure/product_measure.py @@ -249,22 +249,24 @@ def covariance(self): ) blocks.append(block) - if all(sparse.issparse(b) for b in blocks): + if any(sparse.issparse(block) for block in blocks): covariance = sparse.block_diag(blocks, format="dia") - covariance.data.setflags(write=False) + data = covariance.data + data.setflags(write=False) + covariance.data = self._read_only_view(data) return covariance - dense_blocks = [b.toarray() if sparse.issparse(b) else b for b in blocks] covariance = np.zeros( (self.d, self.d), - dtype=np.result_type(*[b.dtype for b in dense_blocks]), + dtype=np.result_type(*[block.dtype for block in blocks]), ) start = 0 - for block in dense_blocks: + for block in blocks: stop = start + block.shape[0] covariance[start:stop, start:stop] = block start = stop - return self._read_only_array(covariance) + covariance.setflags(write=False) + return self._read_only_view(covariance) @staticmethod def _expand_bounds(bounds, dimension, name): diff --git a/test/test_product_measure.py b/test/test_product_measure.py index d4d712feb..67475a4bf 100644 --- a/test/test_product_measure.py +++ b/test/test_product_measure.py @@ -92,7 +92,9 @@ def test_product_measure_normalizes_scalar_1d_statistics(): np.testing.assert_allclose( tm.standard_deviation, [np.sqrt(4.0 / 3.0), 3.0] ) - np.testing.assert_allclose(tm.covariance, np.diag([4.0 / 3.0, 9.0])) + np.testing.assert_allclose( + tm.covariance.toarray(), np.diag([4.0 / 3.0, 9.0]) + ) assert tm.mean.shape == (2,) assert tm.variance.shape == (2,) assert tm.standard_deviation.shape == (2,) @@ -123,14 +125,73 @@ def test_product_measure_statistics_preserve_order_and_covariance_blocks(): tm.standard_deviation, [np.sqrt(4.0 / 3.0), np.sqrt(2.0), np.sqrt(3.0)], ) - np.testing.assert_allclose(tm.covariance, expected_covariance) + covariance = tm.covariance.tocsr() + np.testing.assert_allclose(covariance.toarray(), expected_covariance) assert tm.mean.shape == (3,) assert tm.variance.shape == (3,) assert tm.standard_deviation.shape == (3,) - assert tm.covariance.shape == (3, 3) - assert np.array_equal(tm.covariance[:1, 1:], np.zeros((1, 2))) - assert np.array_equal(tm.covariance[1:, :1], np.zeros((2, 1))) + assert covariance.shape == (3, 3) + assert covariance[:1, 1:].nnz == 0 + assert covariance[1:, :1].nnz == 0 + + +def test_product_measure_mixed_covariance_blocks_remain_sparse(): + d = 128 + tm = ProductMeasure( + DummySampler(d + 2), + [ + Uniform(DummySampler(d)), + Gaussian( + DummySampler(2), + covariance=np.array([[1.0, 0.5], [0.5, 1.0]]), + ), + ], + ) + + covariance = tm.covariance + expected = sp.block_diag( + [marginal.covariance for marginal in tm.marginals], format="dia" + ) + + assert sp.issparse(covariance) + assert covariance.format == "dia" + assert covariance.shape == (d + 2, d + 2) + difference = (covariance - expected).tocsr() + difference.eliminate_zeros() + assert difference.nnz == 0 + covariance_csr = covariance.tocsr() + np.testing.assert_allclose( + covariance_csr[-2:, -2:].toarray(), [[1.0, 0.5], [0.5, 1.0]] + ) + assert covariance_csr[:d, d:].nnz == 0 + assert covariance_csr[d:, :d].nnz == 0 + assert not covariance.data.flags.writeable + with pytest.raises(ValueError): + covariance.data.setflags(write=True) + + +def test_product_measure_dense_covariance_cannot_be_made_writeable(): + tm = ProductMeasure( + DummySampler(3), + [ + Gaussian(DummySampler(1), covariance=2.0), + Gaussian( + DummySampler(2), covariance=[[3.0, 0.25], [0.25, 4.0]] + ), + ], + ) + + covariance = tm.covariance + + assert isinstance(covariance, np.ndarray) + np.testing.assert_allclose( + covariance, + [[2.0, 0.0, 0.0], [0.0, 3.0, 0.25], [0.0, 0.25, 4.0]], + ) + assert not covariance.flags.writeable + with pytest.raises(ValueError): + covariance.setflags(write=True) def test_product_measure_missing_marginal_statistic_is_identified():