Repository: sbi-dev/sbi

Issue:
SNPE and SNLE build_posterior handle sample_with=rejection arg differently. 
Description:

We've identified an inconsistency in the behavior of the `build_posterior` method across different inference methods within the SBI library, specifically between SNPE (Sequential Neural Posterior Estimation) and SNLE/SNRE (Sequential Neural Likelihood/Ratio Estimation). This inconsistency pertains to the use of rejection sampling to build a posterior.

**Current Behavior:**
- In **SNPE**, using `snpe.build_posterior` with `sample_with="rejection"` returns a `DirectPosterior`. To obtain a `RejectionPosterior`, one must additionally pass a proposal distribution in `rejection_sampling_parameters`.
- In **SNLE and SNRE**, using `snle.build_posterior` (or `snre.build_posterior`) with `sample_with="rejection"` always results in a `RejectionPosterior`, with no option to obtain what would analogously be a `DirectPosterior`.
That's inconsistent.

## Checklist
- [ ] Add `sample_with="direct`
- [ ] Update documentation?




Discussion:
I am against ditching the `DirectPosterior`. I am opting for option 1.

Resolving patch:
diff --git a/sbi/inference/snpe/snpe_base.py b/sbi/inference/snpe/snpe_base.py
--- a/sbi/inference/snpe/snpe_base.py
+++ b/sbi/inference/snpe/snpe_base.py
@@ -433,9 +433,10 @@ def build_posterior(
         self,
         density_estimator: Optional[nn.Module] = None,
         prior: Optional[Distribution] = None,
-        sample_with: str = "rejection",
+        sample_with: str = "direct",
         mcmc_method: str = "slice_np",
         vi_method: str = "rKL",
+        direct_sampling_parameters: Dict[str, Any] = {},
         mcmc_parameters: Dict[str, Any] = {},
         vi_parameters: Dict[str, Any] = {},
         rejection_sampling_parameters: Dict[str, Any] = {},
@@ -455,7 +456,7 @@ def build_posterior(
                 If `None`, use the latest neural density estimator that was trained.
             prior: Prior distribution.
             sample_with: Method to use for sampling from the posterior. Must be one of
-                [`mcmc` | `rejection` | `vi`].
+                [`direct` | `mcmc` | `rejection` | `vi`].
             mcmc_method: Method used for MCMC sampling, one of `slice_np`, `slice`,
                 `hmc`, `nuts`. Currently defaults to `slice_np` for a custom numpy
                 implementation of slice sampling; select `hmc`, `nuts` or `slice` for
@@ -463,12 +464,11 @@ def build_posterior(
             vi_method: Method used for VI, one of [`rKL`, `fKL`, `IW`, `alpha`]. Note
                 some of the methods admit a `mode seeking` property (e.g. rKL) whereas
                 some admit a `mass covering` one (e.g fKL).
+            direct_sampling_parameters: Additional kwargs passed to `DirectPosterior`.
             mcmc_parameters: Additional kwargs passed to `MCMCPosterior`.
             vi_parameters: Additional kwargs passed to `VIPosterior`.
             rejection_sampling_parameters: Additional kwargs passed to
-                `RejectionPosterior` or `DirectPosterior`. By default,
-                `DirectPosterior` is used. Only if `rejection_sampling_parameters`
-                contains `proposal`, a `RejectionPosterior` is instantiated.
+                `RejectionPosterior`.
 
         Returns:
             Posterior $p(\theta|x)$  with `.sample()` and `.log_prob()` methods
@@ -499,21 +499,29 @@ def build_posterior(
             x_o=None,
         )
 
-        if sample_with == "rejection":
-            if "proposal" in rejection_sampling_parameters.keys():
-                self._posterior = RejectionPosterior(
-                    potential_fn=potential_fn,
-                    device=device,
-                    x_shape=self._x_shape,
-                    **rejection_sampling_parameters,
-                )
-            else:
-                self._posterior = DirectPosterior(
-                    posterior_estimator=posterior_estimator,  # type: ignore
-                    prior=prior,
-                    x_shape=self._x_shape,
-                    device=device,
+        if sample_with == "direct":
+            self._posterior = DirectPosterior(
+                posterior_estimator=posterior_estimator,  # type: ignore
+                prior=prior,
+                x_shape=self._x_shape,
+                device=device,
+                **direct_sampling_parameters,
+            )
+        elif sample_with == "rejection":
+            if "proposal" not in rejection_sampling_parameters.keys():
+                raise ValueError(
+                    "You passed `sample_with='rejection' but you did not specify a "
+                    "`proposal` in `rejection_sampling_parameters`. Until sbi "
+                    "v0.22.0, this was interpreted as directly sampling from the "
+                    "posterior. As of sbi v0.23.0, you instead have to use "
+                    "`sample_with='direct'` to do so."
                 )
+            self._posterior = RejectionPosterior(
+                potential_fn=potential_fn,
+                device=device,
+                x_shape=self._x_shape,
+                **rejection_sampling_parameters,
+            )
         elif sample_with == "mcmc":
             self._posterior = MCMCPosterior(
                 potential_fn=potential_fn,


Test patch:
diff --git a/tests/inference_on_device_test.py b/tests/inference_on_device_test.py
--- a/tests/inference_on_device_test.py
+++ b/tests/inference_on_device_test.py
@@ -153,9 +153,7 @@ def simulator(theta):
         elif sampling_method in ["rejection", "direct"]:
             # all other cases: rejection, direct
             posterior = inferer.build_posterior(
-                sample_with=(
-                    "rejection" if sampling_method == "direct" else sampling_method
-                ),
+                sample_with=sampling_method,
                 rejection_sampling_parameters=(
                     {"proposal": prior}
                     if sampling_method == "rejection" and method == SNPE_C
diff --git a/tests/save_and_load_test.py b/tests/save_and_load_test.py
--- a/tests/save_and_load_test.py
+++ b/tests/save_and_load_test.py
@@ -10,7 +10,7 @@
 @pytest.mark.parametrize(
     "inference_method, sampling_method",
     (
-        (SNPE, "rejection"),
+        (SNPE, "direct"),
         (SNLE, "mcmc"),
         (SNRE, "mcmc"),
         pytest.param(SNRE, "vi", marks=pytest.mark.xfail),  # bug: see #684