We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Clarify `GridSampler` and ask-and-tell interface problem
### What is an issue?

`GridSampler` with ask-and-tell interface does not work the same way as the `optimize` interface due to `RuntimeError` as reported in https://github.com/optuna/optuna/issues/4121 and https://github.com/optuna/optuna/issues/5141. This should be clarified in the GridSampler page, possibly the first note section that sounds related to this problem.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `optuna/samplers/_grid.py`
Content:
```
1 import itertools
2 from numbers import Real
3 from typing import Any
4 from typing import Dict
5 from typing import List
6 from typing import Mapping
7 from typing import Optional
8 from typing import Sequence
9 from typing import Union
10 import warnings
11 
12 import numpy as np
13 
14 from optuna.distributions import BaseDistribution
15 from optuna.logging import get_logger
16 from optuna.samplers import BaseSampler
17 from optuna.samplers._lazy_random_state import LazyRandomState
18 from optuna.study import Study
19 from optuna.trial import FrozenTrial
20 from optuna.trial import TrialState
21 
22 
23 GridValueType = Union[str, float, int, bool, None]
24 
25 
26 _logger = get_logger(__name__)
27 
28 
29 class GridSampler(BaseSampler):
30     """Sampler using grid search.
31 
32     With :class:`~optuna.samplers.GridSampler`, the trials suggest all combinations of parameters
33     in the given search space during the study.
34 
35     Example:
36 
37         .. testcode::
38 
39             import optuna
40 
41 
42             def objective(trial):
43                 x = trial.suggest_float("x", -100, 100)
44                 y = trial.suggest_int("y", -100, 100)
45                 return x**2 + y**2
46 
47 
48             search_space = {"x": [-50, 0, 50], "y": [-99, 0, 99]}
49             study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))
50             study.optimize(objective)
51 
52     Note:
53 
54         :class:`~optuna.samplers.GridSampler` automatically stops the optimization if all
55         combinations in the passed ``search_space`` have already been evaluated, internally
56         invoking the :func:`~optuna.study.Study.stop` method.
57 
58     Note:
59 
60         :class:`~optuna.samplers.GridSampler` does not take care of a parameter's quantization
61         specified by discrete suggest methods but just samples one of values specified in the
62         search space. E.g., in the following code snippet, either of ``-0.5`` or ``0.5`` is
63         sampled as ``x`` instead of an integer point.
64 
65         .. testcode::
66 
67             import optuna
68 
69 
70             def objective(trial):
71                 # The following suggest method specifies integer points between -5 and 5.
72                 x = trial.suggest_float("x", -5, 5, step=1)
73                 return x**2
74 
75 
76             # Non-int points are specified in the grid.
77             search_space = {"x": [-0.5, 0.5]}
78             study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))
79             study.optimize(objective, n_trials=2)
80 
81     Note:
82         A parameter configuration in the grid is not considered finished until its trial is
83         finished. Therefore, during distributed optimization where trials run concurrently,
84         different workers will occasionally suggest the same parameter configuration.
85         The total number of actual trials may therefore exceed the size of the grid.
86 
87     Note:
88         All parameters must be specified when using :class:`~optuna.samplers.GridSampler` with
89         :meth:`~optuna.study.Study.enqueue_trial`.
90 
91     Args:
92         search_space:
93             A dictionary whose key and value are a parameter name and the corresponding candidates
94             of values, respectively.
95         seed:
96             A seed to fix the order of trials as the grid is randomly shuffled. Please note that
97             it is not recommended using this option in distributed optimization settings since
98             this option cannot ensure the order of trials and may increase the number of duplicate
99             suggestions during distributed optimization.
100     """
101 
102     def __init__(
103         self, search_space: Mapping[str, Sequence[GridValueType]], seed: Optional[int] = None
104     ) -> None:
105         for param_name, param_values in search_space.items():
106             for value in param_values:
107                 self._check_value(param_name, value)
108 
109         self._search_space = {}
110         for param_name, param_values in sorted(search_space.items()):
111             self._search_space[param_name] = list(param_values)
112 
113         self._all_grids = list(itertools.product(*self._search_space.values()))
114         self._param_names = sorted(search_space.keys())
115         self._n_min_trials = len(self._all_grids)
116         self._rng = LazyRandomState(seed)
117         self._rng.rng.shuffle(self._all_grids)
118 
119     def reseed_rng(self) -> None:
120         self._rng.rng.seed()
121 
122     def before_trial(self, study: Study, trial: FrozenTrial) -> None:
123         # Instead of returning param values, GridSampler puts the target grid id as a system attr,
124         # and the values are returned from `sample_independent`. This is because the distribution
125         # object is hard to get at the beginning of trial, while we need the access to the object
126         # to validate the sampled value.
127 
128         # When the trial is created by RetryFailedTrialCallback or enqueue_trial, we should not
129         # assign a new grid_id.
130         if "grid_id" in trial.system_attrs or "fixed_params" in trial.system_attrs:
131             return
132 
133         if 0 <= trial.number and trial.number < self._n_min_trials:
134             study._storage.set_trial_system_attr(
135                 trial._trial_id, "search_space", self._search_space
136             )
137             study._storage.set_trial_system_attr(trial._trial_id, "grid_id", trial.number)
138             return
139 
140         target_grids = self._get_unvisited_grid_ids(study)
141 
142         if len(target_grids) == 0:
143             # This case may occur with distributed optimization or trial queue. If there is no
144             # target grid, `GridSampler` evaluates a visited, duplicated point with the current
145             # trial. After that, the optimization stops.
146 
147             _logger.warning(
148                 "`GridSampler` is re-evaluating a configuration because the grid has been "
149                 "exhausted. This may happen due to a timing issue during distributed optimization "
150                 "or when re-running optimizations on already finished studies."
151             )
152 
153             # One of all grids is randomly picked up in this case.
154             target_grids = list(range(len(self._all_grids)))
155 
156         # In distributed optimization, multiple workers may simultaneously pick up the same grid.
157         # To make the conflict less frequent, the grid is chosen randomly.
158         grid_id = int(self._rng.rng.choice(target_grids))
159 
160         study._storage.set_trial_system_attr(trial._trial_id, "search_space", self._search_space)
161         study._storage.set_trial_system_attr(trial._trial_id, "grid_id", grid_id)
162 
163     def infer_relative_search_space(
164         self, study: Study, trial: FrozenTrial
165     ) -> Dict[str, BaseDistribution]:
166         return {}
167 
168     def sample_relative(
169         self, study: Study, trial: FrozenTrial, search_space: Dict[str, BaseDistribution]
170     ) -> Dict[str, Any]:
171         return {}
172 
173     def sample_independent(
174         self,
175         study: Study,
176         trial: FrozenTrial,
177         param_name: str,
178         param_distribution: BaseDistribution,
179     ) -> Any:
180         if "grid_id" not in trial.system_attrs:
181             message = "All parameters must be specified when using GridSampler with enqueue_trial."
182             raise ValueError(message)
183 
184         if param_name not in self._search_space:
185             message = "The parameter name, {}, is not found in the given grid.".format(param_name)
186             raise ValueError(message)
187 
188         # TODO(c-bata): Reduce the number of duplicated evaluations on multiple workers.
189         # Current selection logic may evaluate the same parameters multiple times.
190         # See https://gist.github.com/c-bata/f759f64becb24eea2040f4b2e3afce8f for details.
191         grid_id = trial.system_attrs["grid_id"]
192         param_value = self._all_grids[grid_id][self._param_names.index(param_name)]
193         contains = param_distribution._contains(param_distribution.to_internal_repr(param_value))
194         if not contains:
195             warnings.warn(
196                 f"The value `{param_value}` is out of range of the parameter `{param_name}`. "
197                 f"The value will be used but the actual distribution is: `{param_distribution}`."
198             )
199 
200         return param_value
201 
202     def after_trial(
203         self,
204         study: Study,
205         trial: FrozenTrial,
206         state: TrialState,
207         values: Optional[Sequence[float]],
208     ) -> None:
209         target_grids = self._get_unvisited_grid_ids(study)
210 
211         if len(target_grids) == 0:
212             study.stop()
213         elif len(target_grids) == 1:
214             grid_id = study._storage.get_trial_system_attrs(trial._trial_id)["grid_id"]
215             if grid_id == target_grids[0]:
216                 study.stop()
217 
218     @staticmethod
219     def _check_value(param_name: str, param_value: Any) -> None:
220         if param_value is None or isinstance(param_value, (str, int, float, bool)):
221             return
222 
223         message = (
224             "{} contains a value with the type of {}, which is not supported by "
225             "`GridSampler`. Please make sure a value is `str`, `int`, `float`, `bool`"
226             " or `None` for persistent storage.".format(param_name, type(param_value))
227         )
228         warnings.warn(message)
229 
230     def _get_unvisited_grid_ids(self, study: Study) -> List[int]:
231         # List up unvisited grids based on already finished ones.
232         visited_grids = []
233         running_grids = []
234 
235         # We directly query the storage to get trials here instead of `study.get_trials`,
236         # since some pruners such as `HyperbandPruner` use the study transformed
237         # to filter trials. See https://github.com/optuna/optuna/issues/2327 for details.
238         trials = study._storage.get_all_trials(study._study_id, deepcopy=False)
239 
240         for t in trials:
241             if "grid_id" in t.system_attrs and self._same_search_space(
242                 t.system_attrs["search_space"]
243             ):
244                 if t.state.is_finished():
245                     visited_grids.append(t.system_attrs["grid_id"])
246                 elif t.state == TrialState.RUNNING:
247                     running_grids.append(t.system_attrs["grid_id"])
248 
249         unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids) - set(running_grids)
250 
251         # If evaluations for all grids have been started, return grids that have not yet finished
252         # because all grids should be evaluated before stopping the optimization.
253         if len(unvisited_grids) == 0:
254             unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids)
255 
256         return list(unvisited_grids)
257 
258     @staticmethod
259     def _grid_value_equal(value1: GridValueType, value2: GridValueType) -> bool:
260         value1_is_nan = isinstance(value1, Real) and np.isnan(float(value1))
261         value2_is_nan = isinstance(value2, Real) and np.isnan(float(value2))
262         return (value1 == value2) or (value1_is_nan and value2_is_nan)
263 
264     def _same_search_space(self, search_space: Mapping[str, Sequence[GridValueType]]) -> bool:
265         if set(search_space.keys()) != set(self._search_space.keys()):
266             return False
267 
268         for param_name in search_space.keys():
269             if len(search_space[param_name]) != len(self._search_space[param_name]):
270                 return False
271 
272             for i, param_value in enumerate(search_space[param_name]):
273                 if not self._grid_value_equal(param_value, self._search_space[param_name][i]):
274                     return False
275 
276         return True
277
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/optuna/samplers/_grid.py b/optuna/samplers/_grid.py
--- a/optuna/samplers/_grid.py
+++ b/optuna/samplers/_grid.py
@@ -51,9 +51,12 @@
 
     Note:
 
-        :class:`~optuna.samplers.GridSampler` automatically stops the optimization if all
-        combinations in the passed ``search_space`` have already been evaluated, internally
-        invoking the :func:`~optuna.study.Study.stop` method.
+        This sampler with :ref:`ask_and_tell` raises :exc:`RuntimeError` just after evaluating
+        the final grid. This is because :class:`~optuna.samplers.GridSampler` automatically
+        stops the optimization if all combinations in the passed ``search_space`` have already
+        been evaluated, internally invoking the :func:`~optuna.study.Study.stop` method.
+        As a workaround, we need to handle the error manually as in
+        https://github.com/optuna/optuna/issues/4121#issuecomment-1305289910.
 
     Note:

{"golden_diff": "diff --git a/optuna/samplers/_grid.py b/optuna/samplers/_grid.py\n--- a/optuna/samplers/_grid.py\n+++ b/optuna/samplers/_grid.py\n@@ -51,9 +51,12 @@\n \n     Note:\n \n-        :class:`~optuna.samplers.GridSampler` automatically stops the optimization if all\n-        combinations in the passed ``search_space`` have already been evaluated, internally\n-        invoking the :func:`~optuna.study.Study.stop` method.\n+        This sampler with :ref:`ask_and_tell` raises :exc:`RuntimeError` just after evaluating\n+        the final grid. This is because :class:`~optuna.samplers.GridSampler` automatically\n+        stops the optimization if all combinations in the passed ``search_space`` have already\n+        been evaluated, internally invoking the :func:`~optuna.study.Study.stop` method.\n+        As a workaround, we need to handle the error manually as in\n+        https://github.com/optuna/optuna/issues/4121#issuecomment-1305289910.\n \n     Note:\n", "issue": "Clarify `GridSampler` and ask-and-tell interface problem\n### What is an issue?\n\n`GridSampler` with ask-and-tell interface does not work the same way as the `optimize` interface due to `RuntimeError` as reported in https://github.com/optuna/optuna/issues/4121 and https://github.com/optuna/optuna/issues/5141. This should be clarified in the GridSampler page, possibly the first note section that sounds related to this problem.\n", "before_files": [{"content": "import itertools\nfrom numbers import Real\nfrom typing import Any\nfrom typing import Dict\nfrom typing import List\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\nimport warnings\n\nimport numpy as np\n\nfrom optuna.distributions import BaseDistribution\nfrom optuna.logging import get_logger\nfrom optuna.samplers import BaseSampler\nfrom optuna.samplers._lazy_random_state import LazyRandomState\nfrom optuna.study import Study\nfrom optuna.trial import FrozenTrial\nfrom optuna.trial import TrialState\n\n\nGridValueType = Union[str, float, int, bool, None]\n\n\n_logger = get_logger(__name__)\n\n\nclass GridSampler(BaseSampler):\n    \"\"\"Sampler using grid search.\n\n    With :class:`~optuna.samplers.GridSampler`, the trials suggest all combinations of parameters\n    in the given search space during the study.\n\n    Example:\n\n        .. testcode::\n\n            import optuna\n\n\n            def objective(trial):\n                x = trial.suggest_float(\"x\", -100, 100)\n                y = trial.suggest_int(\"y\", -100, 100)\n                return x**2 + y**2\n\n\n            search_space = {\"x\": [-50, 0, 50], \"y\": [-99, 0, 99]}\n            study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))\n            study.optimize(objective)\n\n    Note:\n\n        :class:`~optuna.samplers.GridSampler` automatically stops the optimization if all\n        combinations in the passed ``search_space`` have already been evaluated, internally\n        invoking the :func:`~optuna.study.Study.stop` method.\n\n    Note:\n\n        :class:`~optuna.samplers.GridSampler` does not take care of a parameter's quantization\n        specified by discrete suggest methods but just samples one of values specified in the\n        search space. E.g., in the following code snippet, either of ``-0.5`` or ``0.5`` is\n        sampled as ``x`` instead of an integer point.\n\n        .. testcode::\n\n            import optuna\n\n\n            def objective(trial):\n                # The following suggest method specifies integer points between -5 and 5.\n                x = trial.suggest_float(\"x\", -5, 5, step=1)\n                return x**2\n\n\n            # Non-int points are specified in the grid.\n            search_space = {\"x\": [-0.5, 0.5]}\n            study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))\n            study.optimize(objective, n_trials=2)\n\n    Note:\n        A parameter configuration in the grid is not considered finished until its trial is\n        finished. Therefore, during distributed optimization where trials run concurrently,\n        different workers will occasionally suggest the same parameter configuration.\n        The total number of actual trials may therefore exceed the size of the grid.\n\n    Note:\n        All parameters must be specified when using :class:`~optuna.samplers.GridSampler` with\n        :meth:`~optuna.study.Study.enqueue_trial`.\n\n    Args:\n        search_space:\n            A dictionary whose key and value are a parameter name and the corresponding candidates\n            of values, respectively.\n        seed:\n            A seed to fix the order of trials as the grid is randomly shuffled. Please note that\n            it is not recommended using this option in distributed optimization settings since\n            this option cannot ensure the order of trials and may increase the number of duplicate\n            suggestions during distributed optimization.\n    \"\"\"\n\n    def __init__(\n        self, search_space: Mapping[str, Sequence[GridValueType]], seed: Optional[int] = None\n    ) -> None:\n        for param_name, param_values in search_space.items():\n            for value in param_values:\n                self._check_value(param_name, value)\n\n        self._search_space = {}\n        for param_name, param_values in sorted(search_space.items()):\n            self._search_space[param_name] = list(param_values)\n\n        self._all_grids = list(itertools.product(*self._search_space.values()))\n        self._param_names = sorted(search_space.keys())\n        self._n_min_trials = len(self._all_grids)\n        self._rng = LazyRandomState(seed)\n        self._rng.rng.shuffle(self._all_grids)\n\n    def reseed_rng(self) -> None:\n        self._rng.rng.seed()\n\n    def before_trial(self, study: Study, trial: FrozenTrial) -> None:\n        # Instead of returning param values, GridSampler puts the target grid id as a system attr,\n        # and the values are returned from `sample_independent`. This is because the distribution\n        # object is hard to get at the beginning of trial, while we need the access to the object\n        # to validate the sampled value.\n\n        # When the trial is created by RetryFailedTrialCallback or enqueue_trial, we should not\n        # assign a new grid_id.\n        if \"grid_id\" in trial.system_attrs or \"fixed_params\" in trial.system_attrs:\n            return\n\n        if 0 <= trial.number and trial.number < self._n_min_trials:\n            study._storage.set_trial_system_attr(\n                trial._trial_id, \"search_space\", self._search_space\n            )\n            study._storage.set_trial_system_attr(trial._trial_id, \"grid_id\", trial.number)\n            return\n\n        target_grids = self._get_unvisited_grid_ids(study)\n\n        if len(target_grids) == 0:\n            # This case may occur with distributed optimization or trial queue. If there is no\n            # target grid, `GridSampler` evaluates a visited, duplicated point with the current\n            # trial. After that, the optimization stops.\n\n            _logger.warning(\n                \"`GridSampler` is re-evaluating a configuration because the grid has been \"\n                \"exhausted. This may happen due to a timing issue during distributed optimization \"\n                \"or when re-running optimizations on already finished studies.\"\n            )\n\n            # One of all grids is randomly picked up in this case.\n            target_grids = list(range(len(self._all_grids)))\n\n        # In distributed optimization, multiple workers may simultaneously pick up the same grid.\n        # To make the conflict less frequent, the grid is chosen randomly.\n        grid_id = int(self._rng.rng.choice(target_grids))\n\n        study._storage.set_trial_system_attr(trial._trial_id, \"search_space\", self._search_space)\n        study._storage.set_trial_system_attr(trial._trial_id, \"grid_id\", grid_id)\n\n    def infer_relative_search_space(\n        self, study: Study, trial: FrozenTrial\n    ) -> Dict[str, BaseDistribution]:\n        return {}\n\n    def sample_relative(\n        self, study: Study, trial: FrozenTrial, search_space: Dict[str, BaseDistribution]\n    ) -> Dict[str, Any]:\n        return {}\n\n    def sample_independent(\n        self,\n        study: Study,\n        trial: FrozenTrial,\n        param_name: str,\n        param_distribution: BaseDistribution,\n    ) -> Any:\n        if \"grid_id\" not in trial.system_attrs:\n            message = \"All parameters must be specified when using GridSampler with enqueue_trial.\"\n            raise ValueError(message)\n\n        if param_name not in self._search_space:\n            message = \"The parameter name, {}, is not found in the given grid.\".format(param_name)\n            raise ValueError(message)\n\n        # TODO(c-bata): Reduce the number of duplicated evaluations on multiple workers.\n        # Current selection logic may evaluate the same parameters multiple times.\n        # See https://gist.github.com/c-bata/f759f64becb24eea2040f4b2e3afce8f for details.\n        grid_id = trial.system_attrs[\"grid_id\"]\n        param_value = self._all_grids[grid_id][self._param_names.index(param_name)]\n        contains = param_distribution._contains(param_distribution.to_internal_repr(param_value))\n        if not contains:\n            warnings.warn(\n                f\"The value `{param_value}` is out of range of the parameter `{param_name}`. \"\n                f\"The value will be used but the actual distribution is: `{param_distribution}`.\"\n            )\n\n        return param_value\n\n    def after_trial(\n        self,\n        study: Study,\n        trial: FrozenTrial,\n        state: TrialState,\n        values: Optional[Sequence[float]],\n    ) -> None:\n        target_grids = self._get_unvisited_grid_ids(study)\n\n        if len(target_grids) == 0:\n            study.stop()\n        elif len(target_grids) == 1:\n            grid_id = study._storage.get_trial_system_attrs(trial._trial_id)[\"grid_id\"]\n            if grid_id == target_grids[0]:\n                study.stop()\n\n    @staticmethod\n    def _check_value(param_name: str, param_value: Any) -> None:\n        if param_value is None or isinstance(param_value, (str, int, float, bool)):\n            return\n\n        message = (\n            \"{} contains a value with the type of {}, which is not supported by \"\n            \"`GridSampler`. Please make sure a value is `str`, `int`, `float`, `bool`\"\n            \" or `None` for persistent storage.\".format(param_name, type(param_value))\n        )\n        warnings.warn(message)\n\n    def _get_unvisited_grid_ids(self, study: Study) -> List[int]:\n        # List up unvisited grids based on already finished ones.\n        visited_grids = []\n        running_grids = []\n\n        # We directly query the storage to get trials here instead of `study.get_trials`,\n        # since some pruners such as `HyperbandPruner` use the study transformed\n        # to filter trials. See https://github.com/optuna/optuna/issues/2327 for details.\n        trials = study._storage.get_all_trials(study._study_id, deepcopy=False)\n\n        for t in trials:\n            if \"grid_id\" in t.system_attrs and self._same_search_space(\n                t.system_attrs[\"search_space\"]\n            ):\n                if t.state.is_finished():\n                    visited_grids.append(t.system_attrs[\"grid_id\"])\n                elif t.state == TrialState.RUNNING:\n                    running_grids.append(t.system_attrs[\"grid_id\"])\n\n        unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids) - set(running_grids)\n\n        # If evaluations for all grids have been started, return grids that have not yet finished\n        # because all grids should be evaluated before stopping the optimization.\n        if len(unvisited_grids) == 0:\n            unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids)\n\n        return list(unvisited_grids)\n\n    @staticmethod\n    def _grid_value_equal(value1: GridValueType, value2: GridValueType) -> bool:\n        value1_is_nan = isinstance(value1, Real) and np.isnan(float(value1))\n        value2_is_nan = isinstance(value2, Real) and np.isnan(float(value2))\n        return (value1 == value2) or (value1_is_nan and value2_is_nan)\n\n    def _same_search_space(self, search_space: Mapping[str, Sequence[GridValueType]]) -> bool:\n        if set(search_space.keys()) != set(self._search_space.keys()):\n            return False\n\n        for param_name in search_space.keys():\n            if len(search_space[param_name]) != len(self._search_space[param_name]):\n                return False\n\n            for i, param_value in enumerate(search_space[param_name]):\n                if not self._grid_value_equal(param_value, self._search_space[param_name][i]):\n                    return False\n\n        return True\n", "path": "optuna/samplers/_grid.py"}], "after_files": [{"content": "import itertools\nfrom numbers import Real\nfrom typing import Any\nfrom typing import Dict\nfrom typing import List\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\nimport warnings\n\nimport numpy as np\n\nfrom optuna.distributions import BaseDistribution\nfrom optuna.logging import get_logger\nfrom optuna.samplers import BaseSampler\nfrom optuna.samplers._lazy_random_state import LazyRandomState\nfrom optuna.study import Study\nfrom optuna.trial import FrozenTrial\nfrom optuna.trial import TrialState\n\n\nGridValueType = Union[str, float, int, bool, None]\n\n\n_logger = get_logger(__name__)\n\n\nclass GridSampler(BaseSampler):\n    \"\"\"Sampler using grid search.\n\n    With :class:`~optuna.samplers.GridSampler`, the trials suggest all combinations of parameters\n    in the given search space during the study.\n\n    Example:\n\n        .. testcode::\n\n            import optuna\n\n\n            def objective(trial):\n                x = trial.suggest_float(\"x\", -100, 100)\n                y = trial.suggest_int(\"y\", -100, 100)\n                return x**2 + y**2\n\n\n            search_space = {\"x\": [-50, 0, 50], \"y\": [-99, 0, 99]}\n            study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))\n            study.optimize(objective)\n\n    Note:\n\n        This sampler with :ref:`ask_and_tell` raises :exc:`RuntimeError` just after evaluating\n        the final grid. This is because :class:`~optuna.samplers.GridSampler` automatically\n        stops the optimization if all combinations in the passed ``search_space`` have already\n        been evaluated, internally invoking the :func:`~optuna.study.Study.stop` method.\n        As a workaround, we need to handle the error manually as in\n        https://github.com/optuna/optuna/issues/4121#issuecomment-1305289910.\n\n    Note:\n\n        :class:`~optuna.samplers.GridSampler` does not take care of a parameter's quantization\n        specified by discrete suggest methods but just samples one of values specified in the\n        search space. E.g., in the following code snippet, either of ``-0.5`` or ``0.5`` is\n        sampled as ``x`` instead of an integer point.\n\n        .. testcode::\n\n            import optuna\n\n\n            def objective(trial):\n                # The following suggest method specifies integer points between -5 and 5.\n                x = trial.suggest_float(\"x\", -5, 5, step=1)\n                return x**2\n\n\n            # Non-int points are specified in the grid.\n            search_space = {\"x\": [-0.5, 0.5]}\n            study = optuna.create_study(sampler=optuna.samplers.GridSampler(search_space))\n            study.optimize(objective, n_trials=2)\n\n    Note:\n        A parameter configuration in the grid is not considered finished until its trial is\n        finished. Therefore, during distributed optimization where trials run concurrently,\n        different workers will occasionally suggest the same parameter configuration.\n        The total number of actual trials may therefore exceed the size of the grid.\n\n    Note:\n        All parameters must be specified when using :class:`~optuna.samplers.GridSampler` with\n        :meth:`~optuna.study.Study.enqueue_trial`.\n\n    Args:\n        search_space:\n            A dictionary whose key and value are a parameter name and the corresponding candidates\n            of values, respectively.\n        seed:\n            A seed to fix the order of trials as the grid is randomly shuffled. Please note that\n            it is not recommended using this option in distributed optimization settings since\n            this option cannot ensure the order of trials and may increase the number of duplicate\n            suggestions during distributed optimization.\n    \"\"\"\n\n    def __init__(\n        self, search_space: Mapping[str, Sequence[GridValueType]], seed: Optional[int] = None\n    ) -> None:\n        for param_name, param_values in search_space.items():\n            for value in param_values:\n                self._check_value(param_name, value)\n\n        self._search_space = {}\n        for param_name, param_values in sorted(search_space.items()):\n            self._search_space[param_name] = list(param_values)\n\n        self._all_grids = list(itertools.product(*self._search_space.values()))\n        self._param_names = sorted(search_space.keys())\n        self._n_min_trials = len(self._all_grids)\n        self._rng = LazyRandomState(seed)\n        self._rng.rng.shuffle(self._all_grids)\n\n    def reseed_rng(self) -> None:\n        self._rng.rng.seed()\n\n    def before_trial(self, study: Study, trial: FrozenTrial) -> None:\n        # Instead of returning param values, GridSampler puts the target grid id as a system attr,\n        # and the values are returned from `sample_independent`. This is because the distribution\n        # object is hard to get at the beginning of trial, while we need the access to the object\n        # to validate the sampled value.\n\n        # When the trial is created by RetryFailedTrialCallback or enqueue_trial, we should not\n        # assign a new grid_id.\n        if \"grid_id\" in trial.system_attrs or \"fixed_params\" in trial.system_attrs:\n            return\n\n        if 0 <= trial.number and trial.number < self._n_min_trials:\n            study._storage.set_trial_system_attr(\n                trial._trial_id, \"search_space\", self._search_space\n            )\n            study._storage.set_trial_system_attr(trial._trial_id, \"grid_id\", trial.number)\n            return\n\n        target_grids = self._get_unvisited_grid_ids(study)\n\n        if len(target_grids) == 0:\n            # This case may occur with distributed optimization or trial queue. If there is no\n            # target grid, `GridSampler` evaluates a visited, duplicated point with the current\n            # trial. After that, the optimization stops.\n\n            _logger.warning(\n                \"`GridSampler` is re-evaluating a configuration because the grid has been \"\n                \"exhausted. This may happen due to a timing issue during distributed optimization \"\n                \"or when re-running optimizations on already finished studies.\"\n            )\n\n            # One of all grids is randomly picked up in this case.\n            target_grids = list(range(len(self._all_grids)))\n\n        # In distributed optimization, multiple workers may simultaneously pick up the same grid.\n        # To make the conflict less frequent, the grid is chosen randomly.\n        grid_id = int(self._rng.rng.choice(target_grids))\n\n        study._storage.set_trial_system_attr(trial._trial_id, \"search_space\", self._search_space)\n        study._storage.set_trial_system_attr(trial._trial_id, \"grid_id\", grid_id)\n\n    def infer_relative_search_space(\n        self, study: Study, trial: FrozenTrial\n    ) -> Dict[str, BaseDistribution]:\n        return {}\n\n    def sample_relative(\n        self, study: Study, trial: FrozenTrial, search_space: Dict[str, BaseDistribution]\n    ) -> Dict[str, Any]:\n        return {}\n\n    def sample_independent(\n        self,\n        study: Study,\n        trial: FrozenTrial,\n        param_name: str,\n        param_distribution: BaseDistribution,\n    ) -> Any:\n        if \"grid_id\" not in trial.system_attrs:\n            message = \"All parameters must be specified when using GridSampler with enqueue_trial.\"\n            raise ValueError(message)\n\n        if param_name not in self._search_space:\n            message = \"The parameter name, {}, is not found in the given grid.\".format(param_name)\n            raise ValueError(message)\n\n        # TODO(c-bata): Reduce the number of duplicated evaluations on multiple workers.\n        # Current selection logic may evaluate the same parameters multiple times.\n        # See https://gist.github.com/c-bata/f759f64becb24eea2040f4b2e3afce8f for details.\n        grid_id = trial.system_attrs[\"grid_id\"]\n        param_value = self._all_grids[grid_id][self._param_names.index(param_name)]\n        contains = param_distribution._contains(param_distribution.to_internal_repr(param_value))\n        if not contains:\n            warnings.warn(\n                f\"The value `{param_value}` is out of range of the parameter `{param_name}`. \"\n                f\"The value will be used but the actual distribution is: `{param_distribution}`.\"\n            )\n\n        return param_value\n\n    def after_trial(\n        self,\n        study: Study,\n        trial: FrozenTrial,\n        state: TrialState,\n        values: Optional[Sequence[float]],\n    ) -> None:\n        target_grids = self._get_unvisited_grid_ids(study)\n\n        if len(target_grids) == 0:\n            study.stop()\n        elif len(target_grids) == 1:\n            grid_id = study._storage.get_trial_system_attrs(trial._trial_id)[\"grid_id\"]\n            if grid_id == target_grids[0]:\n                study.stop()\n\n    @staticmethod\n    def _check_value(param_name: str, param_value: Any) -> None:\n        if param_value is None or isinstance(param_value, (str, int, float, bool)):\n            return\n\n        message = (\n            \"{} contains a value with the type of {}, which is not supported by \"\n            \"`GridSampler`. Please make sure a value is `str`, `int`, `float`, `bool`\"\n            \" or `None` for persistent storage.\".format(param_name, type(param_value))\n        )\n        warnings.warn(message)\n\n    def _get_unvisited_grid_ids(self, study: Study) -> List[int]:\n        # List up unvisited grids based on already finished ones.\n        visited_grids = []\n        running_grids = []\n\n        # We directly query the storage to get trials here instead of `study.get_trials`,\n        # since some pruners such as `HyperbandPruner` use the study transformed\n        # to filter trials. See https://github.com/optuna/optuna/issues/2327 for details.\n        trials = study._storage.get_all_trials(study._study_id, deepcopy=False)\n\n        for t in trials:\n            if \"grid_id\" in t.system_attrs and self._same_search_space(\n                t.system_attrs[\"search_space\"]\n            ):\n                if t.state.is_finished():\n                    visited_grids.append(t.system_attrs[\"grid_id\"])\n                elif t.state == TrialState.RUNNING:\n                    running_grids.append(t.system_attrs[\"grid_id\"])\n\n        unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids) - set(running_grids)\n\n        # If evaluations for all grids have been started, return grids that have not yet finished\n        # because all grids should be evaluated before stopping the optimization.\n        if len(unvisited_grids) == 0:\n            unvisited_grids = set(range(self._n_min_trials)) - set(visited_grids)\n\n        return list(unvisited_grids)\n\n    @staticmethod\n    def _grid_value_equal(value1: GridValueType, value2: GridValueType) -> bool:\n        value1_is_nan = isinstance(value1, Real) and np.isnan(float(value1))\n        value2_is_nan = isinstance(value2, Real) and np.isnan(float(value2))\n        return (value1 == value2) or (value1_is_nan and value2_is_nan)\n\n    def _same_search_space(self, search_space: Mapping[str, Sequence[GridValueType]]) -> bool:\n        if set(search_space.keys()) != set(self._search_space.keys()):\n            return False\n\n        for param_name in search_space.keys():\n            if len(search_space[param_name]) != len(self._search_space[param_name]):\n                return False\n\n            for i, param_value in enumerate(search_space[param_name]):\n                if not self._grid_value_equal(param_value, self._search_space[param_name][i]):\n                    return False\n\n        return True\n", "path": "optuna/samplers/_grid.py"}]}