Repository: facebookresearch/nevergrad

Issue:
Bayesian Optimization Ask Tell fails with Choice
## Steps to reproduce

I'm running into an error when I try to use a `Choice` variable with Bayesian Optimization using the ask-tell interface. It claims the value is not registered when I call tell, but I used the suggest-ask-tell pattern, so it should be there.

## Observed Results

Getting an error on the tell call that claims 
```
ValueError: Call does not match registered
```
This appears to be caused by rounding errors when the arrays are tested for equality.

## Expected Results

I would expect that this would run fine.

## Relevant Code

```
import nevergrad as ng


instrumentation = ng.p.Instrumentation(
    x1=ng.p.Scalar(lower=0, upper=1000), 
    x2=ng.p.Scalar(lower=0, upper=1000), 
    x3=ng.p.Choice(list(range(100)))
)

def func(x1, x2, x3):
    return (lambda x1, x2, x3: x1 ** 2 + 1/(x2 + 2) + x1 * x2 + x3)(x1, x2, x3)

starting_ask = 100
optim = ng.optimizers.ParametrizedBO(initialization='Hammersley')(
    parametrization=instrumentation, 
    budget=starting_ask * 2, 
    num_workers=1
)



starting_params = [optim.ask().value for  _ in range(starting_ask)]
starting_tell = [func(*args, **kwargs)  for args, kwargs in starting_params]

preexplained_guesses = [optim.ask() for _ in range(starting_ask)]

for ask, tell in zip(starting_params, starting_tell):
    optim.suggest(*ask[0], **ask[1])
    ask_ = optim.ask()
    optim.tell(candidate=ask_, value=tell)

postexplained_guesses = [optim.ask() for _ in range(starting_ask)]

```

<!--- In any case, don't hesitate to join and ask questions if you need on Nevergrad users Facebook group https://www.facebook.com/groups/nevergradusers/ -->



Discussion:
Hey,
Sorry for the delay, I am currently a bit busy and probably will be until next week :s 
I'll try to come back to you asap
Sounds good - thanks!

Resolving patch:
diff --git a/nevergrad/optimization/optimizerlib.py b/nevergrad/optimization/optimizerlib.py
--- a/nevergrad/optimization/optimizerlib.py
+++ b/nevergrad/optimization/optimizerlib.py
@@ -1322,13 +1322,20 @@ def __init__(self, parametrization: IntOrParameter, budget: Optional[int] = None
 
 
 class _FakeFunction:
-    """Simple function that returns the value which was registerd just before.
+    """Simple function that returns the value which was registered just before.
     This is a hack for BO.
     """
 
-    def __init__(self) -> None:
+    def __init__(self, num_digits: int) -> None:
+        self.num_digits = num_digits
         self._registered: List[Tuple[np.ndarray, float]] = []
 
+    def key(self, num: int) -> str:
+        """Key corresponding to the array sample
+        (uses zero-filling to keep order)
+        """
+        return "x" + str(num).zfill(self.num_digits)
+
     def register(self, x: np.ndarray, value: float) -> None:
         if self._registered:
             raise RuntimeError("Only one call can be registered at a time")
@@ -1337,10 +1344,9 @@ def register(self, x: np.ndarray, value: float) -> None:
     def __call__(self, **kwargs: float) -> float:
         if not self._registered:
             raise RuntimeError("Call must be registered first")
-        x = [kwargs[f"x{i}"] for i in range(len(kwargs))]
+        x = [kwargs[self.key(i)] for i in range(len(kwargs))]
         xr, value = self._registered[0]
-        if not np.array_equal(x, xr):
-            raise ValueError("Call does not match registered")
+        np.testing.assert_array_almost_equal(x, xr, err_msg="Call does not match registered")
         self._registered.clear()
         return value
 
@@ -1364,7 +1370,7 @@ def __init__(
         super().__init__(parametrization, budget=budget, num_workers=num_workers)
         self._transform = transforms.ArctanBound(0, 1)
         self._bo: Optional[BayesianOptimization] = None
-        self._fake_function = _FakeFunction()
+        self._fake_function = _FakeFunction(num_digits=len(str(self.dimension)))
         # configuration
         assert initialization is None or initialization in ["random", "Hammersley", "LHS"], f"Unknown init {initialization}"
         self.initialization = initialization
@@ -1389,7 +1395,7 @@ def __init__(
     @property
     def bo(self) -> BayesianOptimization:
         if self._bo is None:
-            bounds = {f"x{i}": (0.0, 1.0) for i in range(self.dimension)}
+            bounds = {self._fake_function.key(i): (0.0, 1.0) for i in range(self.dimension)}
             self._bo = BayesianOptimization(self._fake_function, bounds, random_state=self._rng)
             if self.gp_parameters is not None:
                 self._bo.set_gp_params(**self.gp_parameters)
@@ -1416,7 +1422,7 @@ def _internal_ask_candidate(self) -> p.Parameter:
             x_probe = next(self.bo._queue)
         except StopIteration:
             x_probe = self.bo.suggest(util)  # this is time consuming
-            x_probe = [x_probe[f"x{i}"] for i in range(len(x_probe))]
+            x_probe = [x_probe[self._fake_function.key(i)] for i in range(len(x_probe))]
         data = self._transform.backward(np.array(x_probe, copy=False))
         candidate = self.parametrization.spawn_child().set_standardized_data(data)
         candidate._meta["x_probe"] = x_probe


Test patch:
diff --git a/nevergrad/optimization/test_optimizerlib.py b/nevergrad/optimization/test_optimizerlib.py
--- a/nevergrad/optimization/test_optimizerlib.py
+++ b/nevergrad/optimization/test_optimizerlib.py
@@ -432,7 +432,7 @@ def test_shiwa_dim1() -> None:
     recom = optimizer.minimize(np.abs)
     assert recom.value < init
 
-    
+
 @pytest.mark.parametrize(  # type: ignore
     "name,param,budget,num_workers,expected",
     [("Shiwa", 1, 10, 1, "Cobyla"),
@@ -456,3 +456,10 @@ def test_shiwa_selection(name: str, param: tp.Any, budget: int, num_workers: int
         assert match.group("name") == expected
 
 
+def test_bo_ordering() -> None:
+    optim = ng.optimizers.ParametrizedBO(initialization='Hammersley')(
+        parametrization=ng.p.Choice(range(12)),
+        budget=10
+    )
+    cand = optim.ask()
+    optim.tell(cand, 12)