We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Feature to determine whether `GridSampler` has exhausted the grid
### Motivation

Currently, there is no good way in which we can determine whether the grid of `GridSampler` is exhausted when resuming a study.
While the method `_get_unvisited_grid_ids()` exists, it is a protected method and should not be used outside of the library.

### Description

I suggest adding a method called something like `is_exhausted()` that is public and simply returns `len(self._get_unvisited_grid_ids()) == 0`

### Alternatives (optional)

Alternatively, it may be useful to add keyword arguments to be passed to `GridSampler` that give more control over what happens if the grid is already exhausted. For example, one could add a keyword like `reevaluate_when_exhausted: bool`.

### Additional context (optional)

I encountered this suggestion when receiving the following warning: `GridSampler` is re-evaluating a configuration because the grid has been exhausted. This may happen due to a timing issue during distributed optimization or when re-running optimizations on already finished studies."

This issue further builds upon the discussion of https://github.com/optuna/optuna/issues/3256

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

{"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@@ -277,3 +277,9 @@\n                     return False\n \n         return True\n+\n+    def is_exhausted(self, study: Study) -> bool:\n+        \"\"\"\n+        Return True if all the possible params are evaluated, otherwise return False.\n+        \"\"\"\n+        return len(self._get_unvisited_grid_ids(study)) == 0\n", "issue": "Feature to determine whether `GridSampler` has exhausted the grid\n### Motivation\r\n\r\nCurrently, there is no good way in which we can determine whether the grid of `GridSampler` is exhausted when resuming a study.\r\nWhile the method `_get_unvisited_grid_ids()` exists, it is a protected method and should not be used outside of the library.\r\n\r\n### Description\r\n\r\nI suggest adding a method called something like `is_exhausted()` that is public and simply returns `len(self._get_unvisited_grid_ids()) == 0`\r\n\r\n### Alternatives (optional)\r\n\r\nAlternatively, it may be useful to add keyword arguments to be passed to `GridSampler` that give more control over what happens if the grid is already exhausted. For example, one could add a keyword like `reevaluate_when_exhausted: bool`.\r\n\r\n### Additional context (optional)\r\n\r\nI encountered this suggestion when receiving the following warning: `GridSampler` is re-evaluating a configuration because the grid has been exhausted. This may happen due to a timing issue during distributed optimization or when re-running optimizations on already finished studies.\"\r\n\r\nThis issue further builds upon the discussion of https://github.com/optuna/optuna/issues/3256\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        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"}], "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\n    def is_exhausted(self, study: Study) -> bool:\n        \"\"\"\n        Return True if all the possible params are evaluated, otherwise return False.\n        \"\"\"\n        return len(self._get_unvisited_grid_ids(study)) == 0\n", "path": "optuna/samplers/_grid.py"}]}