Skip to content

Support domain inclusion in chained TrueMeasure transformations - #609

Open
Laasya-73 wants to merge 5 commits into
developfrom
feature/true-measure-domain-inclusion
Open

Support domain inclusion in chained TrueMeasure transformations#609
Laasya-73 wants to merge 5 commits into
developfrom
feature/true-measure-domain-inclusion

Conversation

@Laasya-73

Copy link
Copy Markdown
Collaborator

Summary

This PR generalizes compatibility checking for chained TrueMeasure transformations.

Previously, consecutive transformations were considered compatible only when the range of the preceding transformation exactly matched the domain of the next:

$$ [ R_{j-1} = D_j ] $$

This is more restrictive than necessary. The updated rule accepts the chain whenever the preceding range is contained within the next domain:

$$ [ R_{j-1} \subseteq D_j ] $$

This allows valid chained transformations with strict domain inclusion while continuing to reject genuinely incompatible ranges.

Changes

  • Add _range_in_domain() to AbstractTrueMeasure.
  • Replace exact domain/range equality checking with coordinate-wise containment.
  • Preserve NumPy broadcasting between (1, 2) and (d, 2) domain/range representations.
  • Support finite and unbounded interval bounds.
  • Update compatibility error messages to reflect containment semantics.
  • Add tests covering:
    • exact equality;
    • strict inclusion;
    • unbounded domains;
    • multidimensional inclusion;
    • broadcasting;
    • malformed/non-broadcastable bounds;
    • actual out-of-domain failures.
  • Add demos/true_measure_domain_inclusion.ipynb demonstrating:
    • the previous and updated compatibility behavior;
    • valid 1D and multidimensional TrueMeasure chains;
    • rejection of incompatible chains;
    • the distinction between domain compatibility, deterministic transport, and importance sampling;
    • the Gaussian/Logistic QMC example and the relationship between the importance-sampling likelihood ratio and the corresponding transport Jacobian.

Scope

This change is intentionally limited to domains/ranges represented as one-dimensional intervals or multidimensional axis-aligned boxes.

It does not introduce arbitrary measure-transport semantics, disconnected support representations, or changes to the existing importance-sampling implementation.

Notes

This PR replaces #602 after moving the feature branch into the QMCSoftware repository.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.71%. Comparing base (dc3d923) to head (c25697b).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #609      +/-   ##
===========================================
+ Coverage    88.65%   88.71%   +0.05%     
===========================================
  Files          104      104              
  Lines         8508     8530      +22     
===========================================
+ Hits          7543     7567      +24     
+ Misses         965      963       -2     
Flag Coverage Δ
alltests 88.71% <100.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
qmcpy/true_measure/abstract_true_measure.py 94.65% <100.00%> (+2.91%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Generalizes chained TrueMeasure compatibility from exact range/domain equality to interval containment.

Changes:

  • Adds broadcast-aware range containment checks and revised errors.
  • Adds unit tests for valid and invalid chains.
  • Adds a detailed demonstration notebook.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
qmcpy/true_measure/abstract_true_measure.py Implements containment-based compatibility.
test/test_true_measures.py Tests containment and chaining behavior.
demos/true_measure_domain_inclusion.ipynb Demonstrates the new semantics and mathematical context.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +52 to +54
lower_bounds_valid = np.all(domain[:, 0] <= transform_range[:, 0])
upper_bounds_valid = np.all(transform_range[:, 1] <= domain[:, 1])
return bool(lower_bounds_valid and upper_bounds_valid)
Comment on lines +106 to +114
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import pandas as pd\n",
"from matplotlib.patches import Rectangle\n",
"from scipy.stats import logistic, norm\n",
"\n",
"from qmcpy import DigitalNetB2, Kumaraswamy, Uniform\n",
"from qmcpy.true_measure.abstract_true_measure import AbstractTrueMeasure\n",
"from qmcpy.util import ParameterError\n",
@fjhickernell

Copy link
Copy Markdown
Member

@Laasya-73 you should add some examples in the demo notebook of integration with the chained measures.

Co-authored-by: fjhickernell <817530+fjhickernell@users.noreply.github.com>
@Laasya-73

Copy link
Copy Markdown
Collaborator Author

@Laasya-73 you should add some examples in the demo notebook of integration with the chained measures.

Sure, I will add and update the PR.

@sou-cheng-choi sou-cheng-choi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Laasya-73

  1. Could you add the notebook to mkdocs.yml? Thank you.
  2. Validate input to _range_in_domain:
>>> AbstractTrueMeasure._range_in_domain(np.empty((0, 2)),[[0, 1]]) 
True
>>> AbstractTrueMeasure._range_in_domain([["a", "z"]], [["a", "z"]])
True
  1. In the notebook, from qmcpy import DigitalNetB2, Kumaraswamy, Uniform from qmcpy.true_measure.abstract_true_measure import AbstractTrueMeasure can be simplified to from qmcpy import AbstractTrueMeasure, DigitalNetB2, Kumaraswamy, Uniform

  2. In a command line window, you could run "make format" to beautify the files. Give it a try and see if it helps.

  3. For the notebook sub-section title, "### One compatibility visual", do you want to make it "D."? Sections 3 and 5 have sub-section labels. Would you like to make them consistent?

@sou-cheng-choi

Copy link
Copy Markdown
Member

@Laasya-73

Please request for re-review frojm us whenever the changes are ready.

@Laasya-73

Copy link
Copy Markdown
Collaborator Author

@sou-cheng-choi

Thanks! The changes are now addressed and pushed. I’ve re-requested your review.

@sou-cheng-choi sou-cheng-choi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Laasya-73

Thank you for the continuous enhancements.

Could you please review the following examples for me? I want to ensure that I have not missed anything. Please add fast unit tests whenever applicable.

  1. Issues with range, mean, and variance, as shown. in the following examples:

(a)

>>> from qmcpy import *
>>> inner = Uniform(DigitalNetB2(1), 0.25, 0.75)
>>> outer = Uniform(inner, 0.25, 0.75)
>>> samples = outer.gen_samples(2**14)
>>> declared_range = outer.range.ravel()
>>> composite_range = 0.25 + 0.5 * inner.range.ravel()
>>> declared_variance = outer.variance
>>> composite_variance = 0.25**2 / 12
>>> sample_variance = samples.var()
>>> print(f"  {declared_range=} vs. {composite_range=}  Cf.  observed min/max: [{samples.min():.5f}, {samples.max():.5f}]")
  declared_range=array([0.25, 0.75]) vs. composite_range=array([0.375, 0.625])  Cf.  observed min/max: [0.37501, 0.62500]
>>> print(f"  {declared_variance=:.6f}  vs.   actual/sample variance:  {composite_variance:.6f} / {sample_variance:.6f}")
  declared_variance=0.020833  vs.   actual/sample variance:  0.005208 / 0.005208

(b)

    inner = Uniform(DigitalNetB2(1, seed=7), 0.25, 0.75)
    outer = Kumaraswamy(inner)
    samples = outer.gen_samples(2**15)
    declared_mean = float(np.ravel(outer.mean)[0])  # 0.53333
    sample_mean = float(samples.mean())   # .53971

(c)

>>> import numpy as np
>>> for outer in [Kumaraswamy(Uniform(DigitalNetB2(1, seed=7), 0.25, 0.75)),
...               Gaussian(Uniform(DigitalNetB2(1, seed=7), 0.25, 0.75))]:
...     s = outer.gen_samples(2**15)
...     dv, sv = float(np.ravel(outer.variance)[0]), s.var()
...     print(f"{type(outer).__name__:12s} declared var {dv:.6f} vs sample {sv:.6f}  ({abs(dv-sv)/dv:.0%} off)")
Kumaraswamy  declared var 0.048889 vs sample 0.009355  (81% off)
Gaussian     declared var 1.000000 vs sample 0.142652  (86% off)

(d)

inner = BernoulliCont(DigitalNetB2(1, seed=7), lam=0.9)
outer = Kumaraswamy(inner, a=2.0, b=2.0)
samples = outer.gen_samples(2**16)
declared_variance = float(np.ravel(outer.variance)[0])  # 0.0489
sample_variance = samples.var()  # 0.0384 
  1. -inf is returned in the following example. Please look into it:
>>> Gaussian(Uniform(DigitalNetB2(1, randomize="FALSE"), 0.0, 0.75)).gen_samples(2)
array([[       -inf],
       [-0.31863936]])
  1. _weight() returns non-zero weights outside domain:
    dd = DigitalNetB2(1, seed=7)
    uniform_weights = Uniform(dd, 0.25, 0.75)._weight(np.array([[0.1], [0.3], [0.5], [0.9]])).ravel()  # [2., 2., 2., 2.]
    kumaraswamy_weights = Kumaraswamy(dd, a=2, b=2)._weight(np.array([[-0.5], [0.5], [1.5]])).ravel()  # [-1.5,  1.5, -7.5]

@Laasya-73

Copy link
Copy Markdown
Collaborator Author

@sou-cheng-choi

Thank you for these examples. I reviewed all four composition cases and added fast unit tests covering Uniform -> Uniform, Uniform -> Kumaraswamy, Uniform -> Gaussian, and BernoulliCont -> Kumaraswamy.

For 1, I found that the discrepancy comes from the existing recursive composition behavior: samples apply each transform in sequence, while the outer range/moment attributes describe the standalone outer distribution. This also occurs in the BernoulliCont -> Kumaraswamy case where the ranges match exactly, so I kept the public metadata semantics unchanged in this PR.

I also fixed the Gaussian endpoint issue in 2 and the out-of-support _weight() behavior in 3, with fast tests for both. The focused TrueMeasure suite now passes all tests.

@sou-cheng-choi sou-cheng-choi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Laasya-73

Thank you for the quick turn around.

  1. I have pulled your code and rerun examples in comment #1 above but the issues seem to still exist. Could you check them again?

  2. We still have -inf with the following three instances:

JohnsonsSU(DigitalNetB2(1, randomize="FALSE")).gen_samples(2)

BrownianMotion(DigitalNetB2(2, randomize="FALSE"),
            decomp_type="BROWNIANBRIDGE").gen_samples(2)

SciPyWrapper(DigitalNetB2(1, randomize="FALSE"), _st.norm()).gen_samples(2)
  1. An additional exception that I have found today is as follows:
m = Kumaraswamy(Uniform(DigitalNetB2(1), 0.25, 0.75))
CustomFun(m, g=lambda t: t[..., 0] ** 2)

returning

qmcpy.util.exceptions_warnings.ParameterError: The range of the composed transform is not compatible with this true measure
  1. One question I have is whether changing the clipping bound in _clip_unit_interval() in qmcpy/true_measure/abstract_true_measure.py from np.finfo(float).eps to np.finfo(float).tiny will be better.

  2. After all the bugs or issues are resolved, please consider updating the documentation in code and/or demo, for example, inconsistent labels 3A vs 5.1 in the demo. It will also be good to request re-review from @fjhickernell and @alegresor .

@Laasya-73

Copy link
Copy Markdown
Collaborator Author

@sou-cheng-choi

Thank you. I rechecked these cases more deeply.

For # 1, I found that the discrepancy comes from an existing difference in how chained TrueMeasures are interpreted: gen_samples() recursively applies all transforms, while AbstractIntegrand currently treats the inner transform as an importance-sampling proposal for the outer measure. This is also the cause of the CustomFun exception in # 3.

Before changing that behavior, could you confirm whether ordinary TrueMeasure chaining should consistently represent the full recursive transformation, including when used with CustomFun? If so, I will keep importance sampling as a separate explicit target/proposal behavior rather than inferring it from chaining.

For # 2, I found the remaining unprotected quantile paths in JohnsonsSU, BrownianBridge, and SciPyWrapper and will update them consistently. For # 4, I checked tiny; since 1 - np.finfo(float).tiny == 1.0, it does not protect the upper endpoint, so I would retain the current eps bound.

I will also clean up the demo labels once the behavior is settled.

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.

6 participants