GSoC 2026 - sbi

Development blog for my Google Summer of Code project

GSoC 2026: Final Report

I have published my GSoC 2026 Final Work Submission, covering the project, its results, and the delivered changes. Feedback from mentor:

Jocho successfully analysed the situation in detail, understood the problem and solved it in the most simple way possible. He quickly understood these aspects and their intricacies of the toolbox better than the mentors and suggested simple solutions by his own. The results of the project allows the sbi toolbox now to be more accessible to both users as well as maintainers and to extend for further features.

Many thanks to my mentor Daniel Gedon, Jan Teusen, Abel Abate, and Google Summer of Code for their guidance, support, and the opportunity to contribute to sbi.

Week 9-11: Deprecating set_x() and Finalizing the API Transition

The main focus over the past two weeks was turning the investigation from the previous weeks into a concrete migration path for deprecating set_x(). The original project goal was to make potential functions stateless and provide a more composable API. During implementation, it became clear that completely redesigning the potential function protocol was unnecessary. Instead, the existing set_x() interface can be deprecated by introducing bind() as its replacement and gradually moving the implementation and tests over to it.

The key idea is to make bind() and set_x() independent first, and then reverse their relationship. The first implementation of bind() used set_x() internally. After separating the two interfaces, set_x() can now be implemented in terms of bind(). This makes the eventual removal of set_x() straightforward: once users and internal code have migrated to bind(), deleting set_x() does not require another implementation change.

  1. PR #1943: Introduced bind(), initially using set_x() internally.
  2. PR #1945: Decoupled bind() from set_x(), so the new API no longer depends on the old one.
  3. PR #1946: Reworked set_x() to use bind() internally, effectively flipping their roles and preparing set_x() for deprecation.
  4. PR #1948: Migrated the relevant tests from set_x() to bind().

With these changes, the core migration path for set_x() is now in place. The important part is that bind() is no longer just a wrapper around the old API: it is the underlying interface, while set_x() becomes the compatibility layer that can eventually be removed.

There is also a separate cleanup in progress to remove .to() from potential functions. This is orthogonal to the set_x() deprecation and is tracked independently in PR #1970.

After the core PRs are merged, the remaining work will focus on documentation and cleaning up complexity that is no longer necessary with the new API. For example, BasePotential.return_x_o appears to be unnecessary because the same information can be accessed directly through potential._x_o. There are also a few remaining places where the deprecation warning and imports need to be cleaned up.

One larger API question that came up during this work is whether potentials should allow x_o to be provided during construction at all. The current codebase effectively follows a simpler workflow already: a new potential starts with x_o=None, and the observation is supplied later through bind(). Enforcing this consistently would remove several branches that currently distinguish between potentials initialized with and without an observation. It would also make the user-facing API less ambiguous, since currently both the potential/posterior constructor and the sampling interface can be used to specify the observation.

I am treating this as a potential follow-up project rather than expanding the current scope. The immediate goal is to finish the set_x() migration cleanly, document the new API, and remove complexity that can safely be eliminated as a consequence of the change.

Next step: finish the remaining core PRs, update the documentation around bind(), and identify the cleanup changes that can be safely made once the new API is established.

Week 6-8: Investigating set_x() and Narrowing the Scope

Over the past weeks, implementation work shifted toward a deeper investigation of the existing set_x() interface. What initially appeared to be a localized refactoring turned out to involve substantially more interactions between potential functions and posterior implementations than expected.

To better understand these interactions, I prepared an internal analysis document tracing how observations propagate through the codebase. Discussing these findings with my mentors revealed several additional edge cases and special implementations that need to be considered before attempting larger API changes.

Based on these discussions, the current plan is to first pursue smaller, self-contained improvements that simplify the existing implementation before returning to the broader goal of making set_x() stateless. This should provide useful improvements to the codebase while reducing the risk of making sweeping changes before fully understanding all interactions.

Next step: identify and implement the first small cleanup PR while continuing to refine the analysis of the remaining set_x() interactions. Also from 20.07 to 31.07, I'll be working on it full time.

Week 3-5: First Implementation & API Pivot

The first working prototype of the protocol-based potential function API is up in PR #1876, including a comparison notebook (sbi_old_vs_new_api.ipynb) demonstrating the new API for NLE side by side with the current one.

While building the prototype I made two observations that suggest a simpler path to the original goal of stateless potential functions:

  1. Stateless via copy instead of protocol redesign. Making set_x() return a new instance rather than mutating the existing one is enough to achieve statelesness for most workflows. The main open question is how this interacts with batched sampling, where the observation is swapped repeatedly.
  2. Remove x_o from the setup step entirely. Across all trainers (NLE, NPE, NRE), x_o is already always set to None when build_posterior is called internally. This means the only meaningful place to provide it is at sample() time, which is already how most user-facing code reads. Enforcing this would reduce surface area rather than add to it.

Next step: implement and test the simplified approach in a separate branch to see whether it passes existing tests without adding new abstractions.

Week 1&2: Bonding Period

During the bonding period, I focused on reducing uncertainty before implementation starts.

I finished a UML diagram comparing the current class hierarchy against a proposed protocol-based redesign for the potential function API. For now, the diagrams and design notes are only shared internally with mentors while the design direction is still under discussion.

I also prepared several design choices for the API redesign and identified a minimal subset of the existing tests relevant to potential functions. This makes development iterations significantly faster since I do not have to run unrelated tests continuously.

Besides the API work, I explored tooling for more efficient development workflows and token usage, including Claude Code 101, Claude Code in Action, Serena, and RTK.

Next step: moving from design preparation into the first implementation phase.

Preparing for GSoC 2026

"Set up a blog" it says in the GSoC Guides, so here we go:

Welcome to the start of my GSoC 2026 journey with sbi. I'll use this blog to document progress, implementation details, lessons learned, and technical insights throughout the summer.

The proposal phase is complete, and now the focus shifts toward execution. My accepted proposal can be found here: GSoC 2026 Proposal.

During the bonding period, my main focus will be understanding the potential functions inside the sbi.inference.potentials module and how the rest of the inference pipeline depends on them. Its API is exposed in sbi/inference/potentials/__init__.py:

__all__ = ["likelihood_estimator_based_potential",
           "mixed_likelihood_estimator_based_potential",
           "posterior_estimator_based_potential",
           "ratio_estimator_based_potential",
           "vector_field_estimator_based_potential"]

My first step toward understanding them is writing docstring examples, see PR #1820.