Repository: optuna/optuna

Issue:
Support discrete parameters by `Trial.suggest_float()`.
This is a followup of #1021 , and it focuses on  https://github.com/optuna/optuna/pull/1021#pullrequestreview-376794576. The full discussion of `suggest_float` can be found the original issue  https://github.com/optuna/optuna/issues/510.

## Description

Please add `step` parameter to `Trial.suggest_float` in order to suggest discrete parameter like [`Trial.suggest_discrete_uniform()`](https://optuna.readthedocs.io/en/latest/reference/trial.html#optuna.trial.Trial.suggest_discrete_uniform).

I think we have two  points to be discussed as follows:
- parameter name
- `log=True` and ~~`step!=1`~~ `step is not None`

### Parameter name

I think `step` is the first choice to be consistent with [`Trial.suggest_int`](https://optuna.readthedocs.io/en/latest/reference/trial.html#optuna.trial.Trial.suggest_int). The `Trial.suggest_int` employed `step` in https://github.com/optuna/optuna/pull/910.

### `log=True` and ~~`step!=1`~~ `step is not None`

In https://github.com/optuna/optuna/issues/510, I think the design discussion about this case has not been finished yet. So, I think it would be reasonable to raise `NotImplemented` for now.


Discussion:

Resolving patch:
diff --git a/optuna/integration/chainermn.py b/optuna/integration/chainermn.py
--- a/optuna/integration/chainermn.py
+++ b/optuna/integration/chainermn.py
@@ -189,14 +189,14 @@ def __init__(self, trial, comm):
         self.delegate = trial
         self.comm = comm
 
-    def suggest_float(self, name, low, high, *, log=False):
-        # type: (str, float, float, bool) -> float
+    def suggest_float(self, name, low, high, *, log=False, step=None):
+        # type: (str, float, float, bool, Optional[float]) -> float
 
         def func():
             # type: () -> float
 
             assert self.delegate is not None
-            return self.delegate.suggest_float(name, low, high, log=log)
+            return self.delegate.suggest_float(name, low, high, log=log, step=step)
 
         return self._call_with_mpi(func)
 
diff --git a/optuna/trial.py b/optuna/trial.py
--- a/optuna/trial.py
+++ b/optuna/trial.py
@@ -30,8 +30,8 @@ class BaseTrial(object, metaclass=abc.ABCMeta):
     """
 
     @abc.abstractmethod
-    def suggest_float(self, name, low, high, *, log=False):
-        # type: (str, float, float, bool) -> float
+    def suggest_float(self, name, low, high, *, log=False, step=None):
+        # type: (str, float, float, bool, Optional[float])-> float
 
         raise NotImplementedError
 
@@ -178,22 +178,25 @@ def _init_relative_params(self):
             self.study, trial, self.relative_search_space
         )
 
-    def suggest_float(self, name, low, high, *, log=False):
-        # type: (str, float, float, bool) -> float
+    def suggest_float(self, name, low, high, *, log=False, step=None):
+        # type: (str, float, float, bool, Optional[float]) -> float
         """Suggest a value for the floating point parameter.
 
-        Note that this is a wrapper method for :func:`~optuna.trial.Trial.suggest_uniform`
-        and :func:`~optuna.trial.Trial.suggest_loguniform`.
+        Note that this is a wrapper method for :func:`~optuna.trial.Trial.suggest_uniform`,
+        :func:`~optuna.trial.Trial.suggest_loguniform` and
+        :func:`~optuna.trial.Trial.suggest_discrete_uniform`.
 
         .. versionadded:: 1.3.0
 
         .. seealso::
-            Please see also :func:`~optuna.trial.Trial.suggest_uniform` and
-            :func:`~optuna.trial.Trial.suggest_loguniform`.
+            Please see also :func:`~optuna.trial.Trial.suggest_uniform`,
+            :func:`~optuna.trial.Trial.suggest_loguniform` and
+            :func:`~optuna.trial.Trial.suggest_discrete_uniform`.
 
         Example:
 
-            Suggest a momentum and learning rate for neural network training.
+            Suggest a momentum, learning rate and scaling factor of learning rate
+            for neural network training.
 
             .. testsetup::
 
@@ -214,9 +217,10 @@ def objective(trial):
                     momentum = trial.suggest_float('momentum', 0.0, 1.0)
                     learning_rate_init = trial.suggest_float('learning_rate_init',
                                                              1e-5, 1e-3, log=True)
+                    power_t = trial.suggest_float('power_t', 0.2, 0.8, step=0.1)
                     clf = MLPClassifier(hidden_layer_sizes=(100, 50), momentum=momentum,
                                         learning_rate_init=learning_rate_init,
-                                        solver='sgd', random_state=0)
+                                        solver='sgd', random_state=0, power_t=power_t)
                     clf.fit(X_train, y_train)
 
                     return clf.score(X_test, y_test)
@@ -237,15 +241,25 @@ def objective(trial):
                 If ``log`` is true, the value is sampled from the range in the log domain.
                 Otherwise, the value is sampled from the range in the linear domain.
                 See also :func:`suggest_uniform` and :func:`suggest_loguniform`.
+            step:
+                A step of discretization.
 
         Returns:
             A suggested float value.
         """
 
-        if log:
-            return self.suggest_loguniform(name, low, high)
+        if step is not None:
+            if log:
+                raise NotImplementedError(
+                    "The parameter `step` is not supported when `log` is True."
+                )
+            else:
+                return self.suggest_discrete_uniform(name, low, high, step)
         else:
-            return self.suggest_uniform(name, low, high)
+            if log:
+                return self.suggest_loguniform(name, low, high)
+            else:
+                return self.suggest_uniform(name, low, high)
 
     def suggest_uniform(self, name, low, high):
         # type: (str, float, float) -> float
@@ -960,13 +974,25 @@ def __init__(self, params, number=0):
         self._datetime_start = datetime.now()
         self._number = number
 
-    def suggest_float(self, name, low, high, *, log=False):
-        # type: (str, float, float, bool) -> float
+    def suggest_float(self, name, low, high, *, log=False, step=None):
+        # type: (str, float, float, bool, Optional[float]) -> float
 
-        if log:
-            return self._suggest(name, distributions.LogUniformDistribution(low=low, high=high))
+        if step is not None:
+            if log:
+                raise NotImplementedError(
+                    "The parameter `step` is not supported when `log` is True."
+                )
+            else:
+                return self._suggest(
+                    name, distributions.DiscreteUniformDistribution(low=low, high=high, q=step)
+                )
         else:
-            return self._suggest(name, distributions.UniformDistribution(low=low, high=high))
+            if log:
+                return self._suggest(
+                    name, distributions.LogUniformDistribution(low=low, high=high)
+                )
+            else:
+                return self._suggest(name, distributions.UniformDistribution(low=low, high=high))
 
     def suggest_uniform(self, name, low, high):
         # type: (str, float, float) -> float


Test patch:
diff --git a/tests/test_trial.py b/tests/test_trial.py
--- a/tests/test_trial.py
+++ b/tests/test_trial.py
@@ -44,6 +44,13 @@ def test_check_distribution_suggest_float(storage_init_func):
 
     assert x3 == x4
 
+    x5 = trial.suggest_float("x3", 10, 20, step=1.0)
+    x6 = trial.suggest_discrete_uniform("x3", 10, 20, 1.0)
+
+    assert x5 == x6
+    with pytest.raises(NotImplementedError):
+        trial.suggest_float("x4", 1e-5, 1e-2, log=True, step=1e-5)
+
 
 @parametrize_storage
 def test_check_distribution_suggest_uniform(storage_init_func):
@@ -185,6 +192,9 @@ def test_suggest_low_equals_high(storage_init_func):
         assert trial.suggest_float("g", 0.5, 0.5, log=False) == 0.5  # Suggesting a param.
         assert trial.suggest_float("g", 0.5, 0.5, log=False) == 0.5  # Suggesting the same param.
         assert mock_object.call_count == 0
+        assert trial.suggest_float("h", 0.5, 0.5, step=1.0) == 0.5  # Suggesting a param.
+        assert trial.suggest_float("h", 0.5, 0.5, step=1.0) == 0.5  # Suggesting the same param.
+        assert mock_object.call_count == 0
 
 
 @parametrize_storage