Ejemplo n.º 1
0
 def test_round_integer_vars(self):
     """Verify that some fractional points are properly rounded."""
     point = np.array([0.1, 2.3, -3.5, 4.6])
     ru.round_integer_vars(point, np.array([0, 2]))
     self.assertListEqual(point.tolist(), [0.0, 2.3, -4.0, 4.6],
                          msg='Failed when integer_vars is subset')
     point = np.array([0.1, 2.3, -3.5, 4.6])
     ru.round_integer_vars(point, np.array([]))
     self.assertListEqual(point.tolist(), [0.1, 2.3, -3.5, 4.6],
                          msg='Failed when integer_vars is empty')
     point = np.array([0.1, 2.3, -3.5, 4.6])
     ru.round_integer_vars(point, np.array([0, 1, 2, 3]))
     self.assertListEqual(point.tolist(), [0.0, 2.0, -4.0, 5.0],
                          msg='Failed when integer_vars is everything')
Ejemplo n.º 2
0
def minimize_rbf(settings, n, k, var_lower, var_upper, node_pos,
                 rbf_lambda, rbf_h, integer_vars):
    """Compute the minimum of the RBF interpolant.

    Compute the minimum of the RBF interpolant with a PyOmo model.

    Parameters
    ----------

    settings : rbfopt_settings.RbfSettings
        Global and algorithmic settings.

    n : int
        The dimension of the problem, i.e. size of the space.

    k : int
        Number of nodes, i.e. interpolation points.

    var_lower : List[float]
        Vector of variable lower bounds.

    var_upper : List[float]
        Vector of variable upper bounds.

    node_pos : List[List[float]]
        List of coordinates of the nodes.

    rbf_lambda : List[float]
        The lambda coefficients of the RBF interpolant, corresponding
        to the radial basis functions. List of dimension k.

    rbf_h : List[float]
        The h coefficients of the RBF interpolant, corresponding to
        the polynomial. List of dimension n+1.

    integer_vars: List[int] or None
        A list containing the indices of the integrality constrained
        variables. If None or empty list, all variables are assumed to
        be continuous.

    Returns
    -------
    float
        A minimizer. It is difficult to do global optimization so
        typically this method returns a local minimum.

    Raises
    ------
    ValueError
        If the type of radial basis function is not supported.
    RuntimeError
        If the solver cannot be found.
    """    

    assert(len(var_lower)==n)
    assert(len(var_upper)==n)
    assert(len(rbf_lambda)==k)
    assert(len(node_pos)==k)
    assert(isinstance(settings, RbfSettings))

    # Determine the size of the P matrix
    p = ru.get_size_P_matrix(settings, n)
    assert(len(rbf_h)==(p))

    # Instantiate model
    if (ru.get_degree_polynomial(settings) == 1):
        model = rbfopt_degree1_models
    elif (ru.get_degree_polynomial(settings) == 0):
        model = rbfopt_degree0_models
    else:
        raise ValueError('RBF type ' + settings.rbf + ' not supported')

    instance = model.create_min_rbf_model(settings, n, k, var_lower, 
                                          var_upper, node_pos, rbf_lambda,
                                          rbf_h, integer_vars)

    # Initialize variables for local search
    initialize_instance_variables(settings, instance)

    # Instantiate optimizer
    opt = pyomo.opt.SolverFactory(config.MINLP_SOLVER_EXEC, solver_io='nl')
    if opt is None:
        raise RuntimeError('Solver ' + config.MINLP_SOLVER_EXEC + ' not found')
    set_minlp_solver_options(opt)

    # Solve and load results
    try:
        results = opt.solve(instance, keepfiles = False,
                            tee = settings.print_solver_output)
        if ((results.solver.status == pyomo.opt.SolverStatus.ok) and 
            (results.solver.termination_condition == 
             TerminationCondition.optimal)):
            # this is feasible and optimal
            instance.solutions.load_from(results)
            point = [instance.x[i].value for i in instance.N]
            ru.round_integer_vars(point, integer_vars)
        else:
            point = None
    except:
        point = None

    return point
Ejemplo n.º 3
0
def maximize_one_over_mu(settings, n, k, var_lower, var_upper, node_pos,
                         mat, integer_vars):
    """Compute the maximum of :math: `1/\mu`.

    Construct a PyOmo model to maximize :math: `1/\mu`

    See paper by Costa and Nannicini, equation (7) pag 4, and the
    references therein.

    Parameters
    ----------
 
    settings : rbfopt_settings.RbfSettings
        Global and algorithmic settings.

    n : int
        The dimension of the problem, i.e. size of the space.

    k : int
        Number of nodes, i.e. interpolation points.

    var_lower : List[float]
        Vector of variable lower bounds.
    
    var_upper : List[float]
        Vector of variable upper bounds.

    node_pos : List[List[float]]
        List of coordinates of the nodes

    mat : numpy.matrix
        The matrix necessary for the computation. This is the inverse
        of the matrix [Phi P; P^T 0], see paper as cited above. Must
        be a square numpy.matrix of appropriate dimension.

    integer_vars : List[int] or None
        A list containing the indices of the integrality constrained
        variables. If None or empty list, all variables are assumed to
        be continuous.

    Returns
    -------
    float
        A maximizer. It is difficult to do global optimization so
        typically this method returns a local maximum.

    Raises
    ------
    ValueError
        If the type of radial basis function is not supported.
    RuntimeError
        If the solver cannot be found.
    """
    assert(len(var_lower)==n)
    assert(len(var_upper)==n)
    assert(len(node_pos)==k)
    assert(isinstance(mat, np.matrix))
    assert(isinstance(settings, RbfSettings))

    # Determine the size of the P matrix
    p = ru.get_size_P_matrix(settings, n)
    assert(mat.shape==(k + p, k + p))

    # Instantiate model
    if (ru.get_degree_polynomial(settings) == 1):
        model = rbfopt_degree1_models
    elif (ru.get_degree_polynomial(settings) == 0):
        model = rbfopt_degree0_models
    else:
        raise ValueError('RBF type ' + settings.rbf + ' not supported')

    instance = model.create_max_one_over_mu_model(settings, n, k, var_lower, 
                                                  var_upper, node_pos, mat,
                                                  integer_vars)

    # Initialize variables for local search
    initialize_instance_variables(settings, instance)

    # Instantiate optimizer
    opt = pyomo.opt.SolverFactory(config.MINLP_SOLVER_EXEC, solver_io='nl')
    if opt is None:
        raise RuntimeError('Solver ' + config.MINLP_SOLVER_EXEC + ' not found')
    set_minlp_solver_options(opt)

    # Solve and load results
    try:
        results = opt.solve(instance, keepfiles = False, 
                            tee = settings.print_solver_output)
        if ((results.solver.status == pyomo.opt.SolverStatus.ok) and 
            (results.solver.termination_condition == 
             TerminationCondition.optimal)):
            # this is feasible and optimal
            instance.solutions.load_from(results)
            point = [instance.x[i].value for i in instance.N]
            ru.round_integer_vars(point, integer_vars)
        else:
            point = None
    except:
        point = None

    return point
Ejemplo n.º 4
0
def rbf_optimize(settings, dimension, var_lower, var_upper, objfun,
                 objfun_fast = None, integer_vars = None, 
                 init_node_pos = None, init_node_val = None,
                 output_stream = sys.stdout):
    """Optimize a black-box function.

    Optimize an unknown function over a box using the enhanced RBF
    method.

    Parameters
    ----------

    settings : rbfopt_settings.RbfSettings
        Global and algorithmic settings.

    dimension : int
        The dimension of the problem, i.e. size of the space.

    var_lower : List[float]
        Vector of variable lower bounds.

    var_upper : List[float]
        Vector of variable upper bounds.

    objfun : Callable[List[float]]
        The unknown function we want to optimize.

    objfun_fast : Callable[List[float]]
        A faster, lower quality version of the unknown function we
        want to optimize. If None, it is assumed that such a version
        of the function is not available.

    integer_vars : List[int] or None
        A list containing the indices of the integrality constrained
        variables. If None or empty list, all variables are assumed to
        be continuous.

    init_node_pos : List[List[float]] or None
        Coordinates of points at which the function value is known. If
        None, the initial points will be generated by the
        algorithm. This must be of length at least dimension + 1, if
        provided.

    init_node_val : List[float] or None
        Function values corresponding to the points given in
        init_node_pos. Should be None if the previous argument is
        None.

    output_stream : file or None 
        A stream object that will be used to print output. By default,
        this will be the standard output stream.

    Returns
    ---
    (float, List[float], int, int, int)
        A quintuple (value, point, itercount, evalcount,
        fast_evalcount) containing the objective function value of the
        best solution found, the corresponding value of the decision
        variables, the number of iterations of the algorithm, the
        total number of function evaluations, and the number of these
        evaluations that were performed in 'fast' mode.
    """

    assert(len(var_lower) == dimension)
    assert(len(var_upper) == dimension)
    assert((integer_vars is None) or (len(integer_vars) == 0) or
           (max(integer_vars) < dimension))
    assert(init_node_pos is None or 
           (len(init_node_pos) == len(init_node_val) and
            len(init_node_pos) >= dimension + 1))
    assert(isinstance(settings, RbfSettings))

    # Start timing
    start_time = time.time()

    # Set the value of 'auto' parameters if necessary
    l_settings = settings.set_auto_parameters(dimension, var_lower,
                                              var_upper, integer_vars)

    # Local and global RBF models are usually the same
    best_local_rbf, best_global_rbf = l_settings.rbf, l_settings.rbf
    
    # We use n to denote the dimension of the problem, same notation
    # of the paper. This is redundant but it simplifies our life.
    n = dimension

    # Set random seed. Some of the (external) libraries use numpy's
    # random generator, we use python's internal generator, so we have
    # to seed both for consistency.
    random.seed(l_settings.rand_seed)
    np.random.seed(l_settings.rand_seed)

    # Iteration number
    itercount = 0

    # Total number of function evaluations in accurate mode
    evalcount = 0
    # Total number of fast function evaluation
    fast_evalcount = 0

    # Identifier of the current step within the cyclic optimization
    # strategy counter. This typically increases at every iteration,
    # but sometimes we may decide to repeat a step.
    current_step = 0

    # Current number of consecutive local searches
    num_cons_ls = 0

    # Number of consecutive cycles without improvement
    num_stalled_cycles = 0

    # Number of consecutive discarded points
    num_cons_discarded = 0

    # Number of restarts in fast mode
    num_fast_restarts = 0
    
    # Initialize identifiers of the search steps
    inf_step = 0
    local_search_step = (l_settings.num_global_searches + 1)
    cycle_length = (l_settings.num_global_searches + 2)
    restoration_step = (l_settings.num_global_searches + 3)
    # Determine which step is the first of each loop
    first_step = (inf_step if l_settings.do_infstep else inf_step + 1)

    # Initialize settings for two-phase optimization.
    # two_phase_optimization indicates if the fast buy noisy objective
    # function is available.
    # is_best_fast indicates if the best known objective function
    # value was evaluated in fast mode or in accurate mode.
    # current_mode indicates the evaluation mode for the objective
    # function at a given stage.
    if (objfun_fast is not None):
        two_phase_optimization = True
        is_best_fast = True
        current_mode = 'fast'
    else:
        two_phase_optimization = False
        is_best_fast = False
        current_mode = 'accurate'

    # Round variable bounds to integer if necessary
    ru.round_integer_bounds(var_lower, var_upper, integer_vars)

    # List of node coordinates is node_pos, list of node values is in
    # node_val. We keep a current list and a global list; they can be
    # different in case of restarts.

    # We must choose the initial interpolation points. If they are not
    # given, generate them using the chosen strategy.
    if (init_node_pos is None):
        node_pos = ru.initialize_nodes(l_settings, var_lower, var_upper, 
                                       integer_vars)
        if (current_mode == 'accurate'):
            node_val = [objfun(point) for point in node_pos]
            evalcount += len(node_val)
        else:
            node_val = [objfun_fast(point) for point in node_pos]
            fast_evalcount += len(node_val)
        node_is_fast = [current_mode == 'fast' for val in node_val]
    else:
        node_pos = init_node_pos
        node_val = init_node_val
        # We assume that initial points provided by the user are
        # 'accurate'.
        node_is_fast = [False for val in node_val]

    # Make a copy, in the original space
    all_node_pos = [point for point in node_pos]
    all_node_val = [val for val in node_val]
    # Store if each function evaluation is fast or accurate
    all_node_is_fast = [val for val in node_is_fast]
    # We need to remember the index of the first node in all_node_pos
    # after every restart
    all_node_pos_size_at_restart = 0

    # Rescale the domain of the function
    node_pos = [ru.transform_domain(l_settings, var_lower, var_upper, point)
                for point in node_pos]

    (l_lower,
     l_upper) = ru.transform_domain_bounds(l_settings, var_lower, var_upper)

    # Current minimum value among the nodes, and its index
    fmin_index = node_val.index(min(node_val))
    fmin = node_val[fmin_index]
    # Current maximum value among the nodes
    fmax = max(all_node_val)
    # Denominator of errormin
    gap_den = (abs(l_settings.target_objval) 
               if (abs(l_settings.target_objval) >= l_settings.eps_zero)
               else 1.0)
    # Shift due to fast function evaluation
    gap_shift = (ru.get_fast_error_bounds(l_settings, fmin)[1]
                 if is_best_fast else 0.0)
    # Current minimum distance from the optimum
    gap = ((fmin + gap_shift - l_settings.target_objval)/gap_den)

    # Best value function at the beginning of an optimization cycle
    fmin_cycle_start = fmin

    # Print the initialization points
    for (i, val) in enumerate(node_val):
        min_dist = ru.get_min_distance(node_pos[i], node_pos[:i] + 
                                       node_pos[(i+1):])
        print('Iteration {:3d}'.format(itercount) + 
              ' {:16s}'.format('Initialization') +
              ': objval{:s}'.format('~' if node_is_fast[i] else ' ') +
              ' {:16.6f}'.format(val) +
              ' min_dist {:9.4f}'.format(min_dist) +
              ' gap {:8.2f}'.format(gap*100),
              file = output_stream)

    # Main loop
    while (itercount < l_settings.max_iterations and
           evalcount < l_settings.max_evaluations and
           time.time() - start_time < l_settings.max_clock_time and
           gap > l_settings.eps_opt):
        # If the user wants to skip inf_step as in the original paper
        # of Gutmann (2001), we proceed to the next iteration.
        if (current_step == inf_step and not l_settings.do_infstep):
            current_step = (current_step+1) % cycle_length
            continue

        # Check if we should restart. We only restart if the initial
        # sampling strategy is random, otherwise it makes little sense.
        if (num_cons_discarded >= l_settings.max_consecutive_discarded or 
            (num_stalled_cycles >= l_settings.max_stalled_cycles and
             evalcount + n + 1 < l_settings.max_evaluations and
             l_settings.init_strategy != 'all_corners' and
             l_settings.init_strategy != 'lower_corners')):
            print('Restart at iteration {:3d}'.format(itercount),
                  file = output_stream)
            output_stream.flush()
            # We update the number of fast restarts here, so that if
            # we hit the limit on fast restarts, we can evaluate
            # points in accurate mode after restarting (even if
            # current_mode is updated in a subsequent block of code)
            num_fast_restarts += (1 if current_mode == 'fast' else 0.0)
            # Store the current number of nodes
            all_node_pos_size_at_restart = len(all_node_pos)
            # Compute a new set of starting points
            node_pos = ru.initialize_nodes(l_settings, var_lower, var_upper, 
                                           integer_vars)
            if (current_mode == 'accurate' or
                num_fast_restarts > l_settings.max_fast_restarts or
                fast_evalcount + n + 1 >= l_settings.max_fast_evaluations):
                node_val = [objfun(point) for point in node_pos]
                evalcount += len(node_val)
            else:
                node_val = [objfun_fast(point) for point in node_pos]
                fast_evalcount += len(node_val)
            node_is_fast = [current_mode == 'fast' for val in node_val]
            # Print the initialization points
            for (i, val) in enumerate(node_val):
                min_dist = ru.get_min_distance(node_pos[i], node_pos[:i] + 
                                               node_pos[(i+1):])
                print('Iteration {:3d}'.format(itercount) + 
                      ' {:16s}'.format('Initialization') +
                      ': objval{:s}'.format('~' if node_is_fast[i] else ' ') +
                      ' {:16.6f}'.format(val) +
                      ' min_dist {:9.4f}'.format(min_dist) +
                      ' gap {:8.2f}'.format(gap*100),
                      file = output_stream)
            all_node_pos.extend(node_pos)
            all_node_val.extend(node_val)
            all_node_is_fast.extend(node_is_fast)
            # Rescale the domain of the function
            node_pos = [ru.transform_domain(l_settings, var_lower,
                                            var_upper, point)
                        for point in node_pos]            
            (l_lower, l_upper) = ru.transform_domain_bounds(l_settings,
                                                            var_lower,
                                                            var_upper)
            # Update all counters and values to restart properly
            fmin_index = node_val.index(min(node_val))
            fmin = node_val[fmin_index]
            fmax = max(node_val)
            fmin_cycle_start = fmin
            num_stalled_cycles = 0
            num_cons_discarded = 0
            is_best_fast = node_is_fast[fmin_index]

        # Number of nodes at current iteration
        k = len(node_pos)

        # Compute indices of fast node evaluations (sparse format)
        fast_node_index = ([i for (i, val) in enumerate(node_is_fast) if val]
                           if two_phase_optimization else list())

        # If function scaling is automatic, determine which one to use
        if (settings.function_scaling == 'auto' and 
            current_step <= first_step):
            sorted_node_val = sorted(node_val)
            if (sorted_node_val[len(sorted_node_val)//2] - 
                sorted_node_val[0] > l_settings.log_scaling_threshold):
                l_settings.function_scaling = 'log'
            else:
                l_settings.function_scaling = 'off'
        
        # Rescale nodes if necessary
        (scaled_node_val, scaled_fmin, scaled_fmax,
         node_err_bounds) = ru.transform_function_values(l_settings, node_val,
                                                         fmin, fmax,
                                                         fast_node_index)

        # If RBF selection is automatic, at the beginning of each
        # cycle check if a different RBF yields a better model
        if (settings.rbf == 'auto' and k > n+1 and current_step <= first_step):
            best_local_rbf = ms.get_best_rbf_model(l_settings, n, k, node_pos,
                                                   scaled_node_val,
                                                   int(math.ceil(k*0.1)))
            best_global_rbf = ms.get_best_rbf_model(l_settings, n, k, node_pos,
                                                    scaled_node_val,
                                                    int(math.ceil(k*0.7)))
        # If we are in local search or just before local search, use a
        # local model.
        if (current_step >= (local_search_step - 1)):
            l_settings.rbf = best_local_rbf
        # Otherwise, global.
        else:
            l_settings.rbf = best_global_rbf

        try:
            # Compute the matrices necessary for the algorithm
            Amat = ru.get_rbf_matrix(l_settings, n, k, node_pos)
            Amatinv = ru.get_matrix_inverse(l_settings, Amat)

            # Compute RBF interpolant at current stage
            if (fast_node_index):
                # Get coefficients for the exact RBF
                rc = ru.get_rbf_coefficients(l_settings, n, k, 
                                             Amat, scaled_node_val)
                # RBF with some fast function evaluations
                rc = aux.get_noisy_rbf_coefficients(l_settings, n, k, 
                                                    Amat[:k, :k],
                                                    Amat[:k, k:],
                                                    scaled_node_val,
                                                    fast_node_index,
                                                    node_err_bounds,
                                                    rc[0], rc[1])
                (rbf_l, rbf_h) = rc
            else:
                # Fully accurate RBF
                rc = ru.get_rbf_coefficients(l_settings, n, k, 
                                             Amat, scaled_node_val)
                (rbf_l, rbf_h) = rc
                
        except np.linalg.LinAlgError:
            # Error in the solution of the linear system. We must
            # switch to a restoration phase.
            current_step = restoration_step
            node_val.pop()
            node_pos.pop()
            if (node_is_fast.pop()):
                fast_node_index.pop()
            (scaled_node_val, scaled_fmin, scaled_fmax,
             node_err_bounds) = ru.transform_function_values(l_settings,
                                                             node_val,
                                                             fmin, fmax,
                                                             fast_node_index)
            k = len(node_pos)


        # For displaying purposes, record what type of iteration we
        # are performing
        iteration_id = ''
        
        # Initialize the new point to None
        next_p = None

        if (current_step == inf_step):
            # Infstep: explore the parameter space
            next_p = aux.maximize_one_over_mu(l_settings, n, k, l_lower,
                                              l_upper, node_pos, Amatinv,
                                              integer_vars)
            iteration_id = 'InfStep'

        elif (current_step == restoration_step):
            # Restoration: pick a random point, far enough from
            # current points
            restoration_done = False
            cons_restoration = 0
            while (not restoration_done and
                   cons_restoration < l_settings.max_consecutive_restoration):
                next_p = [random.uniform(var_lower[i], var_upper[i])
                          for i in range(n)]
                ru.round_integer_vars(next_p, integer_vars)
                if (ru.get_min_distance(next_p, node_pos) >
                    l_settings.min_dist):
                    # Try inverting the RBF matrix to see if
                    # nonsingularity is restored
                    try:
                        Amat = ru.get_rbf_matrix(l_settings, n, k + 1,
                                                 node_pos + [next_p])
                        Amatinv = ru.get_matrix_inverse(l_settings, Amat)
                        restoration_done = True
                    except np.linalg.LinAlgError:
                        cons_restoration += 1
                else:
                    cons_restoration += 1
            if (not restoration_done):
                print('Restoration phase keeps failing. Abort.',
                      file = output_stream)
                # This will force the optimization process to return
                break
            iteration_id = 'Restoration'
            
        elif (current_step == local_search_step):
            # Local search: compute the minimum of the RBF.
            min_rbf = aux.minimize_rbf(l_settings, n, k, l_lower,
                                       l_upper, node_pos,
                                       rbf_l, rbf_h, integer_vars)
            if (min_rbf is not None):
                min_rbf_val = ru.evaluate_rbf(l_settings, min_rbf, n, k, 
                                              node_pos, rbf_l, rbf_h)
            # If the RBF cannot me minimized, or if the minimum is
            # larger than the node with smallest value, just take the
            # node with the smallest value.
            if (min_rbf is None or 
                (min_rbf_val >= scaled_fmin + l_settings.eps_zero)):
                min_rbf = node_pos[fmin_index]
                min_rbf_val = scaled_fmin
            # Check if point can be accepted: is there an improvement?
            if (min_rbf_val <= (scaled_fmin - l_settings.eps_impr * 
                                max(1.0, abs(scaled_fmin)))):
                target_val = min_rbf_val
                next_p = min_rbf
                iteration_id = 'LocalStep'                    
            else:
                # If the point is not improving, we solve a global
                # search problem, rescaling the search box to enforce
                # some sort of local search
                target_val = scaled_fmin - 0.01*max(1.0, abs(scaled_fmin))
                local_varl = [max(l_lower[i], min_rbf[i] -
                                  l_settings.local_search_box_scaling * 
                                  0.33 * (l_upper[i] - l_lower[i]))
                              for i in range(n)]
                local_varu = [min(l_upper[i], min_rbf[i] +
                                  l_settings.local_search_box_scaling * 
                                  0.33 * (l_upper[i] - l_lower[i]))
                              for i in range(n)]
                ru.round_integer_bounds(local_varl, local_varu,
                                        integer_vars)
                next_p  = aux.maximize_h_k(l_settings, n, k, local_varl,
                                           local_varu, node_pos, rbf_l, 
                                           rbf_h, Amatinv, target_val,
                                           integer_vars)
                iteration_id = 'AdjLocalStep'

        else:
            # Global search: compromise between finding a good value
            # of the objective function, and improving the model.
            # Choose target value for the objective function. To do
            # so, we need the minimum of the RBF interpolant.
            min_rbf = aux.minimize_rbf(l_settings, n, k, l_lower,
                                       l_upper, node_pos,
                                       rbf_l, rbf_h, integer_vars)
            if (min_rbf is not None):
                min_rbf_val = ru.evaluate_rbf(l_settings, min_rbf, n, k, 
                                              node_pos, rbf_l, rbf_h)
            # If the RBF cannot me minimized, or if the minimum is
            # larger than the node with smallest value, just take the
            # node with the smallest value.
            if (min_rbf is None or 
                min_rbf_val >= scaled_fmin + l_settings.eps_zero):
                min_rbf = node_pos[fmin_index]
                min_rbf_val = scaled_fmin
            # The scaling factor is 1 - h/kappa, where h goes from
            # 0 to kappa-1 over the course of one global search
            # cycle, and kappa is the number of global searches.
            scaling = (1 - ((current_step - 1) /
                            l_settings.num_global_searches))**2
            # Compute the function value used to determine the target
            # value. This is given by the sorted value in position
            # sigma_n, where sigma_n is a function described in the
            # paper by Gutmann (2001). If clipping is disabled, we
            # simply take the largest function value.
            if (l_settings.targetval_clipping):
                local_fmax = ru.get_fmax_current_iter(l_settings, n, k, 
                                                      current_step, 
                                                      scaled_node_val)
            else:
                local_fmax = fmax

            target_val = (min_rbf_val - 
                          scaling * (local_fmax - min_rbf_val))

            # If the global search is almost a local search, we
            # restrict the search to a box following Regis and
            # Shoemaker (2007)
            if (scaling <= config.LOCAL_SEARCH_THRESHOLD):
                local_varl = [max(l_lower[i], min_rbf[i] -
                                  l_settings.local_search_box_scaling * 
                                  math.sqrt(scaling) * 
                                  (l_upper[i] - l_lower[i]))
                              for i in range(n)]
                local_varu = [min(l_upper[i], min_rbf[i] +
                                  l_settings.local_search_box_scaling * 
                                  math.sqrt(scaling) * 
                                  (l_upper[i] - l_lower[i]))
                              for i in range(n)]
                ru.round_integer_bounds(local_varl, local_varu,
                                        integer_vars)
            # Otherwise, use original bounds
            else:
                local_varl = l_lower
                local_varu = l_upper

            next_p  = aux.maximize_h_k(l_settings, n, k, local_varl,
                                       local_varu, node_pos, rbf_l,
                                       rbf_h, Amatinv, target_val,
                                       integer_vars)
            iteration_id = 'GlobalStep'

        # -- end if

        # If previous points were evaluated in low quality and we are
        # now in high-quality local search mode, then we should verify
        # if it is better to evaluate a brand new point or re-evaluate
        # a previously known point.
        if ((two_phase_optimization == True) and
            (current_step == local_search_step) and
            (current_mode == 'accurate')):
            (ind, bump) = ru.get_min_bump_node(l_settings, n, k, Amat, 
                                               scaled_node_val,
                                               fast_node_index, 
                                               node_err_bounds, target_val)
            
            if (ind is not None and next_p is not None):
                # Check if the newly proposed point is very close to
                # an existing one.
                if (ru.get_min_distance(next_p, node_pos) >
                    l_settings.min_dist):
                    # If not, compute bumpiness of the newly proposed point.
                    n_bump = ru.get_bump_new_node(l_settings, n, k, node_pos, 
                                                  scaled_node_val, next_p,
                                                  fast_node_index,
                                                  node_err_bounds, target_val)
                else:
                    # If yes, we will simply reevaluate the existing
                    # point (if it can be reevaluated).
                    ind = ru.get_min_distance_index(next_p, node_pos)
                    n_bump = (float('inf') if node_is_fast[ind] 
                              else float('-inf'))
                if (n_bump > bump):
                    # In this case we want to put the new point at the
                    # same location as one of the old points.  Remove
                    # the noisy function evaluation from the data
                    # structures, so that the point can be evaluated
                    # in accurate mode.
                    next_p = node_pos.pop(ind)
                    node_val.pop(ind)
                    node_is_fast.pop(ind)
                    all_node_pos.pop(all_node_pos_size_at_restart + ind)
                    all_node_val.pop(all_node_pos_size_at_restart + ind)
                    all_node_is_fast.pop(all_node_pos_size_at_restart + ind)
                    # We must update k here to make sure it is consistent
                    # until the start of the next iteration.
                    k = len(node_pos)
                                                     
        # If the optimization failed or the point is too close to
        # current nodes, discard it. Otherwise, add it to the list.
        if ((next_p is None) or 
            (ru.get_min_distance(next_p, node_pos) <= l_settings.min_dist)):
            current_step = (current_step+1) % cycle_length
            num_cons_ls = 0
            num_cons_discarded += 1
            print('Iteration {:3d}'.format(itercount) + ' Discarded',
                  file = output_stream)
            output_stream.flush()
        else:
            min_dist = ru.get_min_distance(next_p, node_pos)
            # Transform back to original space if necessary
            next_p_orig = ru.transform_domain(l_settings, var_lower,
                                              var_upper, next_p, True)
            # Evaluate the new point, in accurate mode or fast mode
            if (current_mode == 'accurate'):
                next_val = objfun(next_p_orig)
                evalcount += 1
                node_is_fast.append(False)
            else: 
                next_val = objfun_fast(next_p_orig)
                fast_evalcount += 1
                # Check if the point improves over existing points, or
                # if it could be optimal according to tolerances. In
                # this case, perform a double evaluation.
                best_possible = fmin + (ru.get_fast_error_bounds(l_settings,
                                                                 fmin)[0]
                                        if is_best_fast else 0.0)
                if ((next_val <= best_possible -
                     l_settings.eps_impr*max(1.0, abs(best_possible))) or
                    (next_val <= l_settings.target_objval +
                     l_settings.eps_opt*abs(l_settings.target_objval) -
                     ru.get_fast_error_bounds(l_settings, next_val)[0])):
                    print('Iteration {:3d}'.format(itercount) + 
                          ' {:16s}'.format(iteration_id) + ': objval~' +
                          ' {:16.6f}'.format(next_val) +
                          ' min_dist {:9.4f}'.format(min_dist) +
                          ' gap {:8.2f}'.format(gap*100),
                          file = output_stream)
                    output_stream.flush()
                    next_val = objfun(next_p_orig)
                    evalcount += 1
                    node_is_fast.append(False)
                else:
                    node_is_fast.append(True)

            # Add to the lists
            node_pos.append(next_p)
            node_val.append(next_val)
            all_node_pos.append(next_p_orig)
            all_node_val.append(next_val)
            all_node_is_fast.append(node_is_fast[-1])

            if ((current_step == local_search_step) and
                (next_val <= fmin - l_settings.eps_impr*max(1.0,abs(fmin))) and
                (num_cons_ls < l_settings.max_consecutive_local_searches - 1)):
                # Keep doing local search
                num_cons_ls += 1
            else:
                current_step = (current_step+1) % cycle_length
                num_cons_ls = 0
            num_cons_discarded = 0
                        
            # Update fmin
            if (next_val < fmin):
                fmin_index = k
                fmin = next_val
                is_best_fast = node_is_fast[-1]
            fmax = max(fmax, next_val)
            # Shift due to fast function evaluation
            gap_shift = (ru.get_fast_error_bounds(l_settings, next_val)[1]
                         if is_best_fast else 0.0)
            gap = min(gap, (next_val + gap_shift - l_settings.target_objval) /
                      gap_den)

            print('Iteration {:3d}'.format(itercount) + 
                  ' {:16s}'.format(iteration_id) +
                  ': objval{:s}'.format('~' if node_is_fast[-1] else ' ') +
                  ' {:16.6f}'.format(next_val) +
                  ' min_dist {:9.4f}'.format(min_dist) +
                  ' gap {:8.2f}'.format(gap*100),
                  file = output_stream)
            output_stream.flush()

        # Update iteration number
        itercount += 1

        # At the beginning of each loop of the cyclic optimization
        # strategy, check if the main loop is stalling
        if (current_step <= first_step): 
            if (fmin <= (fmin_cycle_start - l_settings.max_stalled_objfun_impr
                         * max(1.0, abs(fmin_cycle_start)))):
                num_stalled_cycles = 0
                fmin_cycle_start = fmin
            else:
                num_stalled_cycles += 1

        # Check if we should switch to the second phase of two-phase
        # optimization. The conditions for switching are:
        # 1) Optimization in fast mode restarted too many times.
        # 2) We reached the limit of fast mode iterations.
        if ((two_phase_optimization == True) and (current_mode == 'fast') and
            ((num_fast_restarts > l_settings.max_fast_restarts) or
             (itercount >= l_settings.max_fast_iterations) or
             (fast_evalcount >= l_settings.max_fast_evaluations))):
            print('Switching to accurate mode ' +
                  'at iteration {:3d}'.format(itercount),
                  file = output_stream)
            output_stream.flush()
            current_mode = 'accurate'            
            
    # -- end while

    # Find best point and return it
    i = all_node_val.index(min(all_node_val))
    fmin = all_node_val[i]
    gap_shift = (ru.get_fast_error_bounds(l_settings, fmin)[1]
                   if all_node_is_fast[i] else 0.0)
    gap = ((fmin + gap_shift - l_settings.target_objval) / gap_den)

    print('Summary: iterations {:3d}'.format(itercount) + 
          ' evaluations {:3d}'.format(evalcount) + 
          ' fast_evals {:3d}'.format(fast_evalcount) + 
          ' clock time {:7.2f}'.format(time.time() - start_time) + 
          ' objval{:s}'.format('~' if (all_node_is_fast[i]) else ' ') +
          ' {:15.6f}'.format(fmin) + 
          ' gap {:6.2f}'.format(100*gap),
          file = output_stream)
    output_stream.flush()

    return (all_node_val[i], all_node_pos[i],
            itercount, evalcount, fast_evalcount)