Skip to content

Simplify x_o workflow: enforce bind() as the canonical way to associate observations with potentials #1994

Description

@Jocho-Smith

Currently, sbi provides multiple ways to associate an observation x_o with a potential function:

  1. Through the potential's constructor: Potential(prior, x_o=...)
  2. Through the new bind() method: potential.bind(x_o)
  3. 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:

  1. Create potential WITHOUT x_o
    potential = PosteriorBasedPotential(estimator, prior)

  2. Bind observation when needed
    bound_potential = potential.bind(x_o)

  3. Use the bound potential
    log_prob = bound_potential(theta)

For posteriors:

  1. Create posterior
    posterior = inference.build_posterior()

  2. Set default observation (user-facing convenience)
    posterior.set_default_x(x_o)

  3. 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:

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions