Repository: facebookresearch/nevergrad

Issue:
Error when calling provide_recommendation() too early
## Steps to reproduce

Create any `scipy.optimize` algorithm from within Nevergrad and ask for a recommendation before the first `ask()` call. With `ng.optimizers.OnePlusOne` on the contrary, `provide_recommendation` returns the initial point as expected.

## Observed Results

Compared to Nevergrad-internal algorithms (like `ng.optimizers.OnePlusOne`), the algorithms obtained from `scipy.optimize` do not allow to get a recommendation before the first `ask()`. I get an `AssertionError: Optimization was not even started` with the current version (Nevergrad 0.3.0, Windows laptop with Anaconda Python 3).

## Expected Results

Similar to the `ng.optimizers.OnePlusOne` above, a recommendation before the first `ask()` should be the initial `x0`.

## Relevant Code

  ```python
In [1]: import nevergrad as ng
In [2]: sqp = ng.optimizers.SQP(instrumentation=2, budget=100)
In [3]: sqp.provide_recommendation()
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-7-b54262d49671> in <module>
----> 1 s.provide_recommendation()

~\Anaconda3\lib\site-packages\nevergrad\optimization\base.py in provide_recommendation(self)
    435             on the function (`objective_function(*candidate.args, **candidate.kwargs)`).
    436         """
--> 437         return self.recommend()  # duplicate method
    438
    439     def recommend(self) -> Candidate:

~\Anaconda3\lib\site-packages\nevergrad\optimization\base.py in recommend(self)
    446             on the function (`objective_function(*candidate.args, **candidate.kwargs)`).
    447         """
--> 448         return self.create_candidate.from_data(self._internal_provide_recommendation(), deterministic=True)
    449
    450     def _internal_tell_not_asked(self, candidate: Candidate, value: float) -> None:

~\Anaconda3\lib\site-packages\nevergrad\optimization\recaster.py in _internal_provide_recommendation(self)
    233         else the best pessimistic point.
    234         """
--> 235         assert self._messaging_thread is not None, 'Optimization was not even started'
    236         if self._messaging_thread.output is not None:
    237             return self._messaging_thread.output  # type: ignore

AssertionError: Optimization was not even started
```

But:
```python
In [4]: one = ng.optimizers.OnePlusOne(instrumentation=2, budget=100)
In [5]: one.provide_recommendation()
Out[5]: Candidate(args=(array([0., 0.]),), kwargs={}, data=[0. 0.])
```

Also after the first `ask()` call, things look right for the `scipy.optimize` algos:
```
In [6]: anewsqp = ng.optimizers.SQP(instrumentation=2, budget=100)
In [7]: offspring = anewsqp.ask()
In [8]: anewsqp.provide_recommendation()
Out[8]: Candidate(args=(array([0., 0.]),), kwargs={}, data=[0. 0.])
```


Discussion:
Hi @brockho 
Thanks for pointing out! Do you want to try to solve this yourself? ;) Doing this means adding a test in nevergrad/optimization/test_recaster.py (basically exactly what you've just written), and updating the code in recaster.py to send zero instead of raising the error.
If you don't, I can try to do it tomorrow ;)

Resolving patch:
diff --git a/nevergrad/optimization/oneshot.py b/nevergrad/optimization/oneshot.py
--- a/nevergrad/optimization/oneshot.py
+++ b/nevergrad/optimization/oneshot.py
@@ -17,7 +17,7 @@
 
 
 def convex_limit(points: np.ndarray) -> int:
-    """Given points in order from best to worst, 
+    """Given points in order from best to worst,
     Returns the length of the maximum initial segment of points such that quasiconvexity is verified."""
     d = len(points[0])
     hull = ConvexHull(points[:d+1], incremental=True)
@@ -32,7 +32,7 @@ def convex_limit(points: np.ndarray) -> int:
 
 def avg_of_k_best(archive: utils.Archive[utils.Value], method: str = "dimfourth") -> ArrayLike:
     """Operators inspired by the work of Yann Chevaleyre, Laurent Meunier, Clement Royer, Olivier Teytaud, Fabien Teytaud.
-    
+
     Parameters
     ----------
     archive: utils.Archive[utils.Value]
@@ -128,12 +128,13 @@ def _internal_ask(self) -> ArrayLike:
     def _internal_provide_recommendation(self) -> ArrayLike:
         if self.stupid:
             return self._internal_ask()
-        if self.recommendation_rule == "average_of_best":
-            return avg_of_k_best(self.archive, "dimfourth")
-        if self.recommendation_rule == "average_of_exp_best":
-            return avg_of_k_best(self.archive, "exp")
-        if self.recommendation_rule == "average_of_hull_best":
-            return avg_of_k_best(self.archive, "hull")
+        elif self.archive:
+            if self.recommendation_rule == "average_of_best":
+                return avg_of_k_best(self.archive, "dimfourth")
+            if self.recommendation_rule == "average_of_exp_best":
+                return avg_of_k_best(self.archive, "exp")
+            if self.recommendation_rule == "average_of_hull_best":
+                return avg_of_k_best(self.archive, "hull")
         return super()._internal_provide_recommendation()
 
 
@@ -157,7 +158,7 @@ class RandomSearchMaker(base.ConfiguredOptimizer):
          - "random": uses a randomized pattern for the scale.
          - "auto": scales in function of dimension and budget (see XXX)
     recommendation_rule: str
-        "average_of_best" or "pessimistic" or "average_of_exp_best"; "pessimistic" is 
+        "average_of_best" or "pessimistic" or "average_of_exp_best"; "pessimistic" is
         the default and implies selecting the pessimistic best.
     """
 
@@ -253,7 +254,7 @@ def _internal_ask(self) -> ArrayLike:
         return self._opposable_data
 
     def _internal_provide_recommendation(self) -> ArrayLike:
-        if self.recommendation_rule == "average_of_best":
+        if self.archive and self.recommendation_rule == "average_of_best":
             return avg_of_k_best(self.archive)
         return super()._internal_provide_recommendation()
 
diff --git a/nevergrad/optimization/optimizerlib.py b/nevergrad/optimization/optimizerlib.py
--- a/nevergrad/optimization/optimizerlib.py
+++ b/nevergrad/optimization/optimizerlib.py
@@ -242,8 +242,8 @@ def _internal_tell(self, x: ArrayLike, value: float) -> None:
 
     def _internal_provide_recommendation(self) -> ArrayLike:
         if self._es is None:
-            raise RuntimeError("Either ask or tell method should have been called before")
-        cma_best = self.es.best_x if self._fcmaes else self.es.result.xbest
+            return self.current_bests["pessimistic"].x
+        cma_best: tp.Optional[ArrayLike] = self.es.best_x if self._fcmaes else self.es.result.xbest
         if cma_best is None:
             return self.current_bests["pessimistic"].x
         return cma_best
@@ -709,7 +709,7 @@ class SplitOptimizer(base.Optimizer):
     num_optims: number of optimizers
     num_vars: number of variable per optimizer.
     progressive: True if we want to progressively add optimizers during the optimization run.
-    
+
     If progressive = True, the optimizer is forced at OptimisticNoisyOnePlusOne.
 
     E.g. for 5 optimizers, each of them working on 2 variables, we can use:
@@ -1220,7 +1220,10 @@ def _internal_tell_candidate(self, candidate: p.Parameter, value: float) -> None
         self._fake_function._registered.clear()
 
     def _internal_provide_recommendation(self) -> ArrayLike:
-        return self._transform.backward(np.array([self.bo.max["params"][f"x{i}"] for i in range(self.dimension)]))
+        if self.archive:
+            return self._transform.backward(np.array([self.bo.max["params"][f"x{i}"] for i in range(self.dimension)]))
+        else:
+            return super()._internal_provide_recommendation()
 
 
 class ParametrizedBO(base.ConfiguredOptimizer):
diff --git a/nevergrad/optimization/recaster.py b/nevergrad/optimization/recaster.py
--- a/nevergrad/optimization/recaster.py
+++ b/nevergrad/optimization/recaster.py
@@ -237,10 +237,11 @@ def _internal_provide_recommendation(self) -> base.ArrayLike:
         """Returns the underlying optimizer output if provided (ie if the optimizer did finish)
         else the best pessimistic point.
         """
-        assert self._messaging_thread is not None, 'Optimization was not even started'
-        if self._messaging_thread.output is not None:
+        if (self._messaging_thread is not None and
+                self._messaging_thread.output is not None):
             return self._messaging_thread.output  # type: ignore
-        return self.current_bests["pessimistic"].x
+        else:
+            return self.current_bests["pessimistic"].x
 
     def __del__(self) -> None:
         # explicitly ask the thread to stop (better be safe :))


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
@@ -63,6 +63,7 @@ def check_optimizer(
     for k in range(1, num_attempts + 1):
         fitness = Fitness(optimum)
         optimizer = optimizer_cls(parametrization=len(optimum), budget=budget, num_workers=num_workers)
+        assert isinstance(optimizer.provide_recommendation(), ng.p.Parameter), "Recommendation should be available from start"
         with warnings.catch_warnings():
             # tests do not need to be efficient
             warnings.filterwarnings("ignore", category=base.InefficientSettingsWarning)
diff --git a/nevergrad/optimization/test_recaster.py b/nevergrad/optimization/test_recaster.py
--- a/nevergrad/optimization/test_recaster.py
+++ b/nevergrad/optimization/test_recaster.py
@@ -4,10 +4,11 @@
 # LICENSE file in the root directory of this source tree.
 
 import time
-from typing import Callable, Optional, Any
+import typing as tp
 import numpy as np
-from ..common.typetools import ArrayLike
-from ..common import testing
+import nevergrad as ng
+from nevergrad.common.typetools import ArrayLike
+from nevergrad.common import testing
 from . import recaster
 from . import optimizerlib
 
@@ -22,7 +23,7 @@ def test_message() -> None:
     np.testing.assert_equal(message.result, 3)
 
 
-def fake_caller(func: Callable[[int], int]) -> int:
+def fake_caller(func: tp.Callable[[int], int]) -> int:
     output = 0
     for k in range(10):
         output += func(k)
@@ -33,7 +34,7 @@ def fake_caller(func: Callable[[int], int]) -> int:
     finished=(10, 30),
     unfinished=(2, None),  # should not hang at deletion!
 )
-def test_messaging_thread(num_iter: int, output: Optional[int]) -> None:
+def test_messaging_thread(num_iter: int, output: tp.Optional[int]) -> None:
     thread = recaster.MessagingThread(fake_caller)
     num_answers = 0
     while num_answers < num_iter:
@@ -56,11 +57,11 @@ def fake_cost_function(x: ArrayLike) -> float:
 
 class FakeOptimizer(recaster.SequentialRecastOptimizer):
 
-    def get_optimization_function(self) -> Callable[[Callable[..., Any]], ArrayLike]:
+    def get_optimization_function(self) -> tp.Callable[[tp.Callable[..., tp.Any]], ArrayLike]:
         # create a new instance to avoid deadlock
         return self.__class__(self.parametrization, self.budget, self.num_workers)._optim_function
 
-    def _optim_function(self, func: Callable[..., Any]) -> ArrayLike:
+    def _optim_function(self, func: tp.Callable[..., tp.Any]) -> ArrayLike:
         suboptim = optimizerlib.OnePlusOne(parametrization=2, budget=self.budget)
         recom = suboptim.minimize(func)
         return recom.get_standardized_data(reference=self.parametrization)
@@ -82,3 +83,15 @@ def test_recast_optimizer_and_stop() -> None:
     optimizer = FakeOptimizer(parametrization=2, budget=100)
     optimizer.ask()
     # thread is not finished... but should not hang!
+
+
+def test_provide_recommendation() -> None:
+    opt = optimizerlib.SQP(parametrization=2, budget=100)
+    assert isinstance(opt.provide_recommendation(), ng.p.Parameter), "Recommendation should be available from start"
+    # the recommended solution should be the better one among the told points
+    x1 = opt.ask()
+    opt.tell(x1, 10)
+    x2 = opt.ask()
+    opt.tell(x2, 5)
+    recommendation = opt.provide_recommendation()
+    np.testing.assert_array_almost_equal(recommendation.value, x2.value)