PR #1-2, Weeks 3-5: implement core protocol + bind_observation + minimal notebook example#1876
PR #1-2, Weeks 3-5: implement core protocol + bind_observation + minimal notebook example#1876Jocho-Smith wants to merge 10 commits into
Conversation
Implements the first GSoC 2026 deliverable for the PotentialFunction API redesign. Layer 1 - Core Protocol: - Add PotentialFunction protocol (stateless callable: theta, x -> log_prob) - Add PotentialFunctionWithDevice protocol with device property - Add validate_potential() helper for runtime protocol validation Layer 2 - Binding Utilities: - Add bind_observation() utility for creating sampler-ready functions - Add BoundPotential class as alternative class-based binding - Implement IID handling via sum_iid parameter - Implement device validation with clear error messages Testing: - Add comprehensive unit tests (18 tests) covering: - Protocol validation and compliance - bind_observation functionality - IID handling (sum_iid) - Device validation and mismatch errors - Statelessness (property-based: repeated calls yield identical outputs) - Gradient tracking - Backward compatibility with existing LikelihoodBasedPotential This enables: - Stateless potential functions without hidden state - Easy integration with PyMC, and custom PyTorch samplers - Clear device handling with explicit error messages - Backward compatibility: existing potentials continue to work
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## gsoc-2026 #1876 +/- ##
=============================================
+ Coverage 87.89% 87.98% +0.09%
=============================================
Files 143 146 +3
Lines 13353 13684 +331
=============================================
+ Hits 11736 12040 +304
- Misses 1617 1644 +27
Flags with carried forward coverage won't be shown. Click here to find out more.
|
I was thinking the same thing. For NN Builder API Refactor also there would be 8 PRs. Also my upcoming PR 2 requires my PR 1 to be merged, so I would be creating PR 2 as draft PR. Maybe a GSoC specific branch would be a good thing. Am okay with whatever maintainers prefer. |
|
Thanks, @Jocho-Smith and @satwiksps! just briefly here to unblock you. Yes, it's actually a good idea, if your PRs are introducing breaking changes. We are planning to do a release soon, and we don't want to introduce breaking changes in there. That said, the PRs that go into that branch should be treated as if they go into So, yes, let's merge your PRs, into an intermediate Hope this helps! |
…#1880) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.1 to 7.0.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](codecov/codecov-action@e79a696...fb8b358) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…4.0 (sbi-dev#1881) Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases) - [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md) - [Commits](sigstore/gh-action-sigstore-python@04cffa1...5b79a39) --- updated-dependencies: - dependency-name: sigstore/gh-action-sigstore-python dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…115: AssertionError
|
In this comment I do two things:
Both points are related in a somewhat tricky way, which I try to explain here. Contribution of this PRSo with some delay, here the minimal working example of the proposed protocol change: Some notes and quetsions1: I have been as explicit about the 'build posterior' step as possible while still being within suggestions of the how_to_guides. For the old API I demonstrate this by calling potential_fn, parameter_transform = likelihood_estimator_based_potential(
likelihood_estimator, prior, x_o
)
posterior_old = MCMCPosterior(
potential_fn, proposal=prior, theta_transform=parameter_transform, warmup_steps=10
)which is actually equivalent to posterior = trainer.build_posterior(prior=prior, sample_with='mcmc')(except for setting This way of calling the potential function before the sampler in two separate expressions is motivated here: https://github.com/sbi-dev/sbi/blob/main/docs/how_to_guide/09_sampler_interface.ipynb However, I don't see any situation where users would have an incentive to manually call these potential functions. Even the most finely tuned MCMCPosterior setup I found in the how_to_guides just call
2: The
3: I discovered an unexpected dependence on the This motivated the introduction of PotentialFunctionWithDevice in this PR, however I'm not using it explicitly in this test notebook, only implicitly in the "Lower-Level: Using the Protocol Directly" section for the custom potential I would there.
Pivot idea 1: A simple alternative solutionWhen playing with some examples I made the following observation: Currently the BasePotential handles class BasePotential:
def __init__(self, prior, x_o=None, device="cpu"):
self.device = device
self.prior = prior
self.set_x(x_o)
def set_x(self, x_o, x_is_iid=True):
if x_o is not None:
x_o = process_x(x_o).to(self.device)
self._x_o = x_o
self._x_is_iid = x_is_iidMore precisely:
Usage looks like this then: potential = BasePotential(prior)
potential.set_x(x_o)
# same object was modifiedThis Assuming that the goal is only to make the potentials stateless, this simple modification would do the trick class BasePotential:
def __init__(
self,
prior,
x_o=None,
device="cpu",
x_is_iid=True,
):
self.device = device
self.prior = prior
self._x_o = x_o
self._x_is_iid = x_is_iid
def set_x(self, x_o, x_is_iid=True):
if x_o is not None:
x_o = process_x(x_o).to(self.device)
return self.__class__(
prior=self.prior,
x_o=x_o,
device=self.device,
x_is_iid=x_is_iid,
)So the usage becomes The obvious consequence is that we cannot 'reuse' the same potential_fn object anymore the way it was possible before, without copying it (which would increase memory consumption and operations needed). The question is, is that happening? From what I see, we have 3 cases here:
Pivot idea 2: remove set_x from
|
|
Overall, I think the Main ideaPotentials should carry no How x_o reaches the potential: three pathsThere are three ways Path 1: explicit at call time: Path 2: pre-configured default: posterior1 = DirectPosterior(...).set_default_x(x_o)
accept_reject_fn = get_density_thresholder(posterior1, quantile=1e-4)
proposal = RestrictedPrior(prior, accept_reject_fn, posterior=posterior1, sample_with="sir")
Path 3: what we implement for an expert user: Undocumented case: Specific Samplers in TSNPEWhen Under the new design this should be: def log_prob(self, theta, x=None, ...):
x = self._x_else_default_x(x)
bound = bind_observation(self._raw_potential, x)
potential_values = bound(theta)
normalization = self.estimate_normalization_constant(x, bound)
...The same refactor applies to What we should keep based on my investigation and our discussion today
Issues in this PR (DG: IMPORTANTLY this is mostly claude generated, take it as an inspiration for now since we are discussing the design so far)1. When 2. IID prior double-counting (correctness bug) In the 3. sig = inspect.signature(self.potential_fn)
num_params = len(sig.parameters)
if num_params == 1:
return self.potential_fn(theta)
else:
return self.potential_fn(theta, self.x_o)This breaks for 4.
5.
|
Implements the first GSoC 2026 deliverable for the PotentialFunction API redesign.
Layer 1 - Core Protocol:
Layer 2 - Binding Utilities:
Testing:
Open questions:
Note: