Currently, sbi provides multiple ways to associate an observation x_o with a potential function:
- Through the potential's constructor: Potential(prior, x_o=...)
- Through the new bind() method: potential.bind(x_o)
- Through the posterior's set_default_x(): posterior.set_default_x(x_o)
This redundancy can confuse users and creates maintenance burden. The bind() API was introduced specifically to provide a clean, immutable, stateless way to associate observations with potentials. However, the old constructor-based approach still works, and the relationship between set_default_x() (posterior level) and bind() (potential level) is unclear.
The problem at the potential level:
Users can set x_o in three ways:
Way 1: Constructor
potential = PosteriorBasedPotential(estimator, prior, x_o=obs)
Way 2: bind() - the new canonical way
potential = PosteriorBasedPotential(estimator, prior, x_o=None)
bound_potential = potential.bind(obs)
The constructor approach means potentials are created in a "half-configured" state. Many places in the codebase check "if x_o is not None" to decide whether to build flows, set defaults, etc. This conditional logic adds complexity and is a common source of bugs.
The problem at the posterior level:
The set_default_x() method exists on posteriors, but it's not clear how it relates to potential-level binding. Users don't understand when to use set_default_x vs bind(), and the internal implementation has to coordinate between them.
Proposed solution:
Step 1: Deprecate x_o parameter in potential constructors
Deprecate the x_o parameter in all potential class constructors (Note that the code internally sets x_o=None already anyway). This affects:
- BasePotential.init
- PosteriorBasedPotential.init
- LikelihoodBasedPotential.init
- RatioBasedPotential.init
- VectorFieldBasedPotential.init
- EnsemblePotential.init
Step 2: Simplify constructor logic
After deprecation, constructors can assume x_o is None. No conditional flow building - bind() handles this as a second step
Step 3: Document the canonical workflow
The canonical workflow should be:
-
Create potential WITHOUT x_o
potential = PosteriorBasedPotential(estimator, prior)
-
Bind observation when needed
bound_potential = potential.bind(x_o)
-
Use the bound potential
log_prob = bound_potential(theta)
For posteriors:
-
Create posterior
posterior = inference.build_posterior()
-
Set default observation (user-facing convenience)
posterior.set_default_x(x_o)
-
Sample/log_prob use the default
samples = posterior.sample((1000,)) # uses x_o set via set_default_x
Step 4: Clarify set_default_x relationship
Document that set_default_x() is the user-facing API for posteriors, and internally it uses bind() on the underlying potential.
Related PRs:
Currently, sbi provides multiple ways to associate an observation x_o with a potential function:
This redundancy can confuse users and creates maintenance burden. The bind() API was introduced specifically to provide a clean, immutable, stateless way to associate observations with potentials. However, the old constructor-based approach still works, and the relationship between set_default_x() (posterior level) and bind() (potential level) is unclear.
The problem at the potential level:
Users can set x_o in three ways:
Way 1: Constructor
potential = PosteriorBasedPotential(estimator, prior, x_o=obs)
Way 2: bind() - the new canonical way
potential = PosteriorBasedPotential(estimator, prior, x_o=None)
bound_potential = potential.bind(obs)
The constructor approach means potentials are created in a "half-configured" state. Many places in the codebase check "if x_o is not None" to decide whether to build flows, set defaults, etc. This conditional logic adds complexity and is a common source of bugs.
The problem at the posterior level:
The set_default_x() method exists on posteriors, but it's not clear how it relates to potential-level binding. Users don't understand when to use set_default_x vs bind(), and the internal implementation has to coordinate between them.
Proposed solution:
Step 1: Deprecate x_o parameter in potential constructors
Deprecate the x_o parameter in all potential class constructors (Note that the code internally sets x_o=None already anyway). This affects:
Step 2: Simplify constructor logic
After deprecation, constructors can assume x_o is None. No conditional flow building - bind() handles this as a second step
Step 3: Document the canonical workflow
The canonical workflow should be:
Create potential WITHOUT x_o
potential = PosteriorBasedPotential(estimator, prior)
Bind observation when needed
bound_potential = potential.bind(x_o)
Use the bound potential
log_prob = bound_potential(theta)
For posteriors:
Create posterior
posterior = inference.build_posterior()
Set default observation (user-facing convenience)
posterior.set_default_x(x_o)
Sample/log_prob use the default
samples = posterior.sample((1000,)) # uses x_o set via set_default_x
Step 4: Clarify set_default_x relationship
Document that set_default_x() is the user-facing API for posteriors, and internally it uses bind() on the underlying potential.
Related PRs: