class_docstr: Grid search tuner divides search space into evenly spaced grid, and performs brute-force traverse.
  
  Recommended when the search space is small, or if you want to find strictly optimal hyperparameters.
  
  **Implementation**
  
  The original grid search approach performs an exhaustive search through a space consists of ``choice`` and ``randint``.
  
  NNI's implementation extends grid search to support all search spaces types.
  
  When the search space contains continuous parameters like ``normal`` and ``loguniform``,
  grid search tuner works in following steps:
  
  1. Divide the search space into a grid.
  2. Perform an exhaustive searth through the grid.
  3. Subdivide the grid into a finer-grained new grid.
  4. Goto step 2, until experiment end.
  
  As a deterministic algorithm, grid search has no argument.
  
  Examples
  --------
  
  .. code-block::
  
      config.tuner.name = 'GridSearch'
code: def _next_grid(self):
      # update grid information (grid, epoch_bar, divisions) for next epoch
      updated = False
      for i, spec in enumerate(self.space.values()):
          self.epoch_bar[i] = len(self.grid[i])
          if not spec.categorical:
              # further divide intervals
              new_vals = []  # values to append to grid
              new_divs = []  # sub-intervals
              for l, r in self.divisions[i]:
                  mid = (l + r) / 2
                  diff_l = _less(l, mid, spec)
                  diff_r = _less(mid, r, spec)
                  # if l != 0 and r != 1, then they are already in the grid, else they are not
                  # the special case is needed because for normal distribution 0 and 1 will generate infinity
                  if (diff_l or l == 0.0) and (diff_r or r == 1.0):
                      # we can skip these for non-q, but it will complicate the code
                      new_vals.append(mid)
                      updated = True
                  if diff_l:
                      new_divs.append((l, mid))
                      updated = updated or l == 0.0
                  if diff_r:
                      new_divs.append((mid, r))
                      updated = updated or r == 1.0
              self.grid[i] += new_vals
              self.divisions[i] = new_divs
  
      if not updated:  # fully explored
          _logger.info("Search space has been fully explored")
          self.grid = None
      else:
          size = _grid_size_info(self.grid)
          _logger.info(f"Grid subdivided, new size: {size}")
  
imports: import logging
  import math
  import numpy
  from scipy.special import erfinv
  import nni
  from nni.common.hpo_utils import ParameterSpec, deformat_parameters, format_search_space
  from nni.tuner import Tuner
label_desc: Use image node_id 8 for calling the GridSearchTuner obj's underlying member method code with example usage: obj._next_grid() without return types