text: # Copyright (c) Microsoft Corporation
  # All rights reserved.
  #
  # MIT License
  #
  # Permission is hereby granted, free of charge,
  # to any person obtaining a copy of this software and associated
  # documentation files (the "Software"), to deal in the Software without restriction,
  # including without limitation the rights to use, copy, modify, merge, publish,
  # distribute, sublicense, and/or sell copies of the Software, and
  # to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  # The above copyright notice and this permission notice shall be included
  # in all copies or substantial portions of the Software.
  #
  # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  # BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  '''
  target_space.py
  '''
  
  import numpy as np
  import nni.parameter_expressions as parameter_expressions
  
  
  def _hashable(params):
      """ ensure that an point is hashable by a python dict """
      return tuple(map(float, params))
  
  
  class TargetSpace():
      """
      Holds the param-space coordinates (X) and target values (Y)
      """
  
      def __init__(self, pbounds, random_state=None):
          """
          Parameters
          ----------
          pbounds : dict
              Dictionary with parameters names as keys and a tuple with minimum
              and maximum values.
  
          random_state : int, RandomState, or None
              optionally specify a seed for a random number generator
          """
          self.random_state = random_state
  
          # Get the name of the parameters
          self._keys = sorted(pbounds)
          # Create an array with parameters bounds
          self._bounds = np.array(
              [item[1] for item in sorted(pbounds.items(), key=lambda x: x[0])]
          )
  
          # check values type
          for _bound in self._bounds:
              if _bound['_type'] == 'choice':
                  try:
                      [float(val) for val in _bound['_value']]
                  except ValueError:
                      raise ValueError("GP Tuner supports only numerical values")
  
          # preallocated memory for X and Y points
          self._params = np.empty(shape=(0, self.dim))
          self._target = np.empty(shape=(0))
  
          # keep track of unique points we have seen so far
          self._cache = {}
  
      def __contains__(self, params):
          '''
          check if a parameter is already registered
          '''
          return _hashable(params) in self._cache
  
      def len(self):
          '''
          length of registered params and targets
          '''
          assert len(self._params) == len(self._target)
          return len(self._target)
  
      @property
      def params(self):
          '''
          params: numpy array
          '''
          return self._params
  
      @property
      def target(self):
          '''
          target: numpy array
          '''
          return self._target
  
      @property
      def dim(self):
          '''
          dim: int
              length of keys
          '''
          return len(self._keys)
  
      @property
      def keys(self):
          '''
          keys: numpy array
          '''
          return self._keys
  
      @property
      def bounds(self):
          '''bounds'''
          return self._bounds
  
      def params_to_array(self, params):
          ''' dict to array '''
          try:
              assert set(params) == set(self.keys)
          except AssertionError:
              raise ValueError(
                  "Parameters' keys ({}) do ".format(sorted(params)) +
                  "not match the expected set of keys ({}).".format(self.keys)
              )
          return np.asarray([params[key] for key in self.keys])
  
      def array_to_params(self, x):
          '''
          array to dict
  
          maintain int type if the paramters is defined as int in search_space.json
          '''
          try:
              assert len(x) == len(self.keys)
          except AssertionError:
              raise ValueError(
                  "Size of array ({}) is different than the ".format(len(x)) +
                  "expected number of parameters ({}).".format(self.dim())
              )
  
          params = {}
          for i, _bound in enumerate(self._bounds):
              if _bound['_type'] == 'choice' and all(isinstance(val, int) for val in _bound['_value']):
                  params.update({self.keys[i]: int(x[i])})
              elif _bound['_type'] in ['randint']:
                  params.update({self.keys[i]: int(x[i])})
              else:
                  params.update({self.keys[i]:  x[i]})
  
          return params
  
      def register(self, params, target):
          """
          Append a point and its target value to the known data.
  
          Parameters
          ----------
          x : dict
  
          y : float
              target function value
          """
  
          x = self.params_to_array(params)
          if x in self:
              #raise KeyError('Data point {} is not unique'.format(x))
              print('Data point {} is not unique'.format(x))
  
          # Insert data into unique dictionary
          self._cache[_hashable(x.ravel())] = target
  
          self._params = np.concatenate([self._params, x.reshape(1, -1)])
          self._target = np.concatenate([self._target, [target]])
  
      def random_sample(self):
          """
          Creates a random point within the bounds of the space.
  
          """
          params = np.empty(self.dim)
          for col, _bound in enumerate(self._bounds):
              if _bound['_type'] == 'choice':
                  params[col] = parameter_expressions.choice(
                      _bound['_value'], self.random_state)
              elif _bound['_type'] == 'randint':
                  params[col] = self.random_state.randint(
                      _bound['_value'][0], _bound['_value'][1], size=1)
              elif _bound['_type'] == 'uniform':
                  params[col] = parameter_expressions.uniform(
                      _bound['_value'][0], _bound['_value'][1], self.random_state)
              elif _bound['_type'] == 'quniform':
                  params[col] = parameter_expressions.quniform(
                      _bound['_value'][0], _bound['_value'][1], _bound['_value'][2], self.random_state)
              elif _bound['_type'] == 'loguniform':
                  params[col] = parameter_expressions.loguniform(
                      _bound['_value'][0], _bound['_value'][1], self.random_state)
              elif _bound['_type'] == 'qloguniform':
                  params[col] = parameter_expressions.qloguniform(
                      _bound['_value'][0], _bound['_value'][1], _bound['_value'][2], self.random_state)
              
          return params
  
      def max(self):
          """Get maximum target value found and corresponding parametes."""
          try:
              res = {
                  'target': self.target.max(),
                  'params': dict(
                      zip(self.keys, self.params[self.target.argmax()])
                  )
              }
          except ValueError:
              res = {}
          return res
  
      def res(self):
          """Get all target values found and corresponding parametes."""
          params = [dict(zip(self.keys, p)) for p in self.params]
  
          return [
              {"target": target, "params": param}
              for target, param in zip(self.target, params)
          ]
  