Repository: facebookresearch/nevergrad

Issue:
Optimizer creates candidate from out of bounds args
## Steps to reproduce

**Note:** I don't know if this is properly handled (doesn't cause the optimizer to mis-behave) but here is my observation:
  1. Define a bounded instrumentation variable (ex. instru.var.Array(1).asfloat().bounded(0,5))
  2. Create a candidate from out of space args (ex. 10)
  3. Optimizer.tell(candidate, arbitrary value)

## Observed Results

the candidate is created normally and the optimizer accepts it.

## Expected Results

throwing an exception due to output of bounds args.






Discussion:
Good point, this should be easy to solve, by updating the transforms:
https://github.com/facebookresearch/nevergrad/blob/b8fdfa72ce6674ff10c9d668d252c9257025c742/nevergrad/instrumentation/transforms.py#L107-L173
You're welcome to do it if you want to, otherwise I should have time to do it by wednesday I think

Resolving patch:
diff --git a/nevergrad/instrumentation/transforms.py b/nevergrad/instrumentation/transforms.py
--- a/nevergrad/instrumentation/transforms.py
+++ b/nevergrad/instrumentation/transforms.py
@@ -125,6 +125,9 @@ def forward(self, x: np.ndarray) -> np.ndarray:
         return self._b + self._a * np.tanh(x)  # type: ignore
 
     def backward(self, y: np.ndarray) -> np.ndarray:
+        if np.max(y) > self.max_val or np.min(y) < self.min_val:
+            raise ValueError(f"Only data between {self.min_val} and {self.max_val} "
+                             "can be transformed back (bounds lead to infinity).")
         return np.arctanh((y - self._b) / self._a)  # type: ignore
 
     def _short_repr(self) -> str:
@@ -152,6 +155,8 @@ def forward(self, x: np.ndarray) -> np.ndarray:
         return self._b + self._a * np.arctan(x)  # type: ignore
 
     def backward(self, y: np.ndarray) -> np.ndarray:
+        if np.max(y) > self.max_val or np.min(y) < self.min_val:
+            raise ValueError(f"Only data between {self.min_val} and {self.max_val} can be transformed back.")
         return np.tan((y - self._b) / self._a)  # type: ignore
 
     def _short_repr(self) -> str:
@@ -167,6 +172,8 @@ def forward(self, x: np.ndarray) -> np.ndarray:
         return stats.norm.cdf(x)  # type: ignore
 
     def backward(self, y: np.ndarray) -> np.ndarray:
+        if np.max(y) > 1 or np.min(y) < 0:
+            raise ValueError("Only data between 0 and 1 can be transformed back (bounds lead to infinity).")
         return stats.norm.ppf(y)  # type: ignore
 
     def _short_repr(self) -> str:


Test patch:
diff --git a/nevergrad/instrumentation/test_transforms.py b/nevergrad/instrumentation/test_transforms.py
--- a/nevergrad/instrumentation/test_transforms.py
+++ b/nevergrad/instrumentation/test_transforms.py
@@ -3,7 +3,8 @@
 # This source code is licensed under the MIT license found in the
 # LICENSE file in the root directory of this source tree.
 
-from typing import List
+from typing import List, Optional, Type
+import pytest
 import numpy as np
 from ..common import testing
 from . import transforms
@@ -37,3 +38,19 @@ def test_back_and_forth(transform: transforms.Transform, string: str) -> None:
 def test_vals(transform: transforms.Transform, x: List[float], expected: List[float]) -> None:
     y = transform.forward(np.array(x))
     np.testing.assert_almost_equal(y, expected, decimal=5)
+
+
+@testing.parametrized(
+    tanh=(transforms.TanhBound(0, 5), [2, 4], None),
+    tanh_err=(transforms.TanhBound(0, 5), [2, 4, 6], ValueError),
+    arctan=(transforms.ArctanBound(0, 5), [2, 4, 5], None),
+    arctan_err=(transforms.ArctanBound(0, 5), [-1, 4, 5], ValueError),
+    cumdensity=(transforms.CumulativeDensity(), [0, .5], None),
+    cumdensity_err=(transforms.CumulativeDensity(), [-0.1, .5], ValueError),
+)
+def test_out_of_bound(transform: transforms.Transform, x: List[float], expected: Optional[Type[Exception]]) -> None:
+    if expected is None:
+        transform.backward(np.array(x))
+    else:
+        with pytest.raises(expected):
+            transform.backward(np.array(x))
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
@@ -238,7 +238,7 @@ def test_optimization_discrete_with_one_sample() -> None:
 
 
 @pytest.mark.parametrize("name", ["TBPSA", "PSO", "TwoPointsDE"])  # type: ignore
-def test_population_pickle(name: str):  # this test is added because some generic class (like Population) can fail to be pickled
+def test_population_pickle(name: str) -> None:  # this test is added because some generic class (like Population) can fail to be pickled
     # example of work around:
     # "self.population = base.utils.Population[DEParticle]([])"
     # becomes: