Repository: sbi-dev/sbi

Issue:
custom distance function not passed to ABC inference
On trying to pass a custom function to define the distance metric for ABC methods (eg MCABC), the following error is received on running the inference model:

``` ```

Upon inspecting the source code at sbi/inference/abc/abc_base.py, I see that while there's code written to handle the custom function, the code is structured in a way to never reach it (it already assumes that the 'distance' parameter is a string, and only assigns the function to self.distance if distance is a String and not a callable.

![image](https://user-images.githubusercontent.com/98268983/215433044-d57c10c0-8a9a-4c81-84bc-8a3b8e3ec067.png)



Discussion:
Also in the same code, the definition for the L1 distance seems to be off. Should it be torch.sum() instead of torch.mean()? When I tried to use the L1 distance for 2D data, I was able to get points that were twice as far away from the threshold as I expected.

![image](https://user-images.githubusercontent.com/98268983/215433655-72a7e9ef-8499-4090-835e-a73342415811.png)

Thanks for creating the issue. It should be possible to pass a `Callable` custom distance function when creating the `ABC` object. E.g.,
```python
abc = MCABC(simulator, prior, distance=your_custom_distance)
```

the `choose_distance_function` is called only when the passed `distance` is a `str`. 
> Thanks for creating the issue. It should be possible to pass a `Callable` custom distance function when creating the `ABC` object. E.g.,
> 
> ```python
> abc = MCABC(simulator, prior, distance=your_custom_distance)
> ```
> 
> the `choose_distance_function` is called only when the passed `distance` is a `str`.

Yes, that's true, but since `self.distance` attribute is never being set when `distance` is not an `str`, we get an error. So, an else block is needed here.
<img width="1006" alt="image" src="https://user-images.githubusercontent.com/11965439/215443042-539f641d-ec0c-48cf-8b2d-fdda79e55f58.png">

Oh, indeed. This will not work. Thanks for spotting this! 
I will make a PR fixing this. 

Resolving patch:
diff --git a/sbi/inference/abc/abc_base.py b/sbi/inference/abc/abc_base.py
--- a/sbi/inference/abc/abc_base.py
+++ b/sbi/inference/abc/abc_base.py
@@ -33,7 +33,7 @@ def __init__(
                 object with `.log_prob()`and `.sample()` (for example, a PyTorch
                 distribution) can be used.
             distance: Distance function to compare observed and simulated data. Can be
-                a custom function or one of `l1`, `l2`, `mse`.
+                a custom callable function or one of `l1`, `l2`, `mse`.
             num_workers: Number of parallel workers to use for simulations.
             simulation_batch_size: Number of parameter sets that the simulator
                 maps to data x at once. If None, we simulate all parameter sets at the
@@ -48,12 +48,7 @@ def __init__(
         self._show_progress_bars = show_progress_bars
 
         # Select distance function.
-        if type(distance) == str:
-            distances = ["l1", "l2", "mse"]
-            assert (
-                distance in distances
-            ), f"Distance function str must be one of {distances}."
-            self.distance = self.choose_distance_function(distance_type=distance)
+        self.distance = self.get_distance_function(distance)
 
         self._batched_simulator = lambda theta: simulate_in_batches(
             simulator=self._simulator,
@@ -66,8 +61,26 @@ def __init__(
         self.logger = logging.getLogger(__name__)
 
     @staticmethod
-    def choose_distance_function(distance_type: str = "l2") -> Callable:
-        """Return distance function for given distance type."""
+    def get_distance_function(distance_type: Union[str, Callable] = "l2") -> Callable:
+        """Return distance function for given distance type.
+
+        Args:
+            distance_type: string indicating the distance type, e.g., 'l2', 'l1',
+                'mse'. Note that the returned distance function averages over the last
+                dimension, e.g., over the summary statistics.
+
+        Returns:
+            distance_fun: distance functions built from passe string. Returns
+                distance_type is callable.
+        """
+
+        if isinstance(distance_type, Callable):
+            return distance_type
+
+        distances = ["l1", "l2", "mse"]
+        assert (
+            distance_type in distances
+        ), f"{distance_type} must be one of {distances}."
 
         if distance_type == "mse":
             distance = lambda xo, x: torch.mean((xo - x) ** 2, dim=-1)


Test patch:
diff --git a/tests/abc_test.py b/tests/abc_test.py
--- a/tests/abc_test.py
+++ b/tests/abc_test.py
@@ -1,5 +1,5 @@
 import pytest
-from torch import eye, ones, zeros
+from torch import eye, norm, ones, zeros
 from torch.distributions import MultivariateNormal, biject_to
 
 from sbi.inference import MCABC, SMC
@@ -14,8 +14,10 @@
 
 @pytest.mark.slow
 @pytest.mark.parametrize("num_dim", (1, 2))
+@pytest.mark.parametrize("distance", ("l2", lambda x, xo: norm((x - xo), dim=-1)))
 def test_mcabc_inference_on_linear_gaussian(
     num_dim,
+    distance,
     lra=False,
     sass=False,
     sass_expansion_degree=1,
@@ -40,7 +42,7 @@ def test_mcabc_inference_on_linear_gaussian(
     def simulator(theta):
         return linear_gaussian(theta, likelihood_shift, likelihood_cov)
 
-    inferer = MCABC(simulator, prior, simulation_batch_size=10000)
+    inferer = MCABC(simulator, prior, simulation_batch_size=10000, distance=distance)
 
     phat = inferer(
         x_o,
@@ -68,7 +70,11 @@ def simulator(theta):
 def test_mcabc_sass_lra(lra, sass_expansion_degree):
 
     test_mcabc_inference_on_linear_gaussian(
-        num_dim=2, lra=lra, sass=True, sass_expansion_degree=sass_expansion_degree
+        num_dim=2,
+        lra=lra,
+        sass=True,
+        sass_expansion_degree=sass_expansion_degree,
+        distance="l2",
     )
 
 
@@ -161,7 +167,7 @@ def test_smcabc_sass_lra(lra, sass_expansion_degree):
 @pytest.mark.parametrize("kde_bandwidth", ("cv", "silvermann", "scott", 0.1))
 def test_mcabc_kde(kde_bandwidth):
     test_mcabc_inference_on_linear_gaussian(
-        num_dim=2, kde=True, kde_bandwidth=kde_bandwidth
+        num_dim=2, kde=True, kde_bandwidth=kde_bandwidth, distance="l2"
     )