pythonoptimizationminimization

Correct code to do golden section search over integers


I have an expensive function f which is unimodal and I want to find its minimum. However f is only defined at integer values. I read that golden section search is the right thing to do. My implementation which ignores the integer restriction is:

def golden_section_search(f, a, b, tolerance=1e-6):
    """
    Perform the Golden Section Search for finding the minimum of a unimodal function f.
    
    Parameters:
        f (function): The unimodal function to minimize.
        a (float): Left boundary of the interval.
        b (float): Right boundary of the interval.
        tolerance (float, optional): Tolerance for stopping criterion. Default is 1e-6.
    
    Returns:
        float: Argument that minimizes the function f within the interval [a, b].
    """
    phi = (math.sqrt(5) - 1) / 2  # Golden ratio constant
    
    # Initial points
    x1 = a + (1 - phi) * (b - a)
    x2 = a + phi * (b - a)
    
    # Initial function evaluations
    f_x1 = f(x1)
    f_x2 = f(x2)
    
    while (b - a) > tolerance:
        print(a, b)
        if f_x1 < f_x2:
            b = x2  # Discard the interval [x2, b]
            x2 = x1  # Move x2 to the left
            f_x2 = f_x1  # Reuse the already computed value
            
            # Recompute x1 and its function value
            x1 = a + (1 - phi) * (b - a)
            f_x1 = f(x1)
        else:
            a = x1  # Discard the interval [a, x1]
            x1 = x2  # Move x1 to the right
            f_x1 = f_x2  # Reuse the already computed value
            
            # Recompute x2 and its function value
            x2 = a + phi * (b - a)
            f_x2 = f(x2)
    
    # The minimum is at the midpoint of the final interval
    return (a + b) / 2

As a test function let's use this (this is of course defined everywhere but we pretend it isn't):

def f(x):
    return (x - 3)**2 + 5

To make sure the search only evaluates at integers I tried just doing round(x1) and round(x2) every time x1 and x2 are changed but that made a version that either gives the wrong answer or iterates forever. It gives the wrong answer if I set tolerance = 1 and iterates forever if I set tolerance = 0.9. I am using a = 0 and b = 5 in the function call.

The true minimum is at x = 3.

This is the code which only evaluates f at integer values but does not work:

def golden_section_search_integer(f, a, b, tolerance=1e-6):
    """
    Perform the Golden Section Search for finding the minimum of a unimodal function f,
    but only evaluate f at integer values.
    
    Parameters:
        f (function): The unimodal function to minimize.
        a (int): Left boundary of the interval (integer).
        b (int): Right boundary of the interval (integer).
        tolerance (float, optional): Tolerance for stopping criterion. Default is 1e-6.
    
    Returns:
        int: Integer value that minimizes the function f within the interval [a, b].
    """
    phi = (math.sqrt(5) - 1) / 2  # Golden ratio constant
    
    # Initial points, rounded to integers
    x1 = round(a + (1 - phi) * (b - a))
    x2 = round(a + phi * (b - a))
    
    # Initial function evaluations
    f_x1 = f(x1)
    f_x2 = f(x2)
    
    while (b - a) > tolerance:
        print(a, b, x1, x2)
        
        if f_x1 < f_x2:
            b = x2  # Discard the interval [x2, b]
            x2 = x1  # Move x2 to the left
            f_x2 = f_x1  # Reuse the already computed value
            
            # Recompute x1, ensuring it's an integer, and its function value
            x1 = round(a + (1 - phi) * (b - a))
            if x1 != x2:  # Only evaluate if x1 is different from x2
                f_x1 = f(x1)
        else:
            a = x1  # Discard the interval [a, x1]
            x1 = x2  # Move x1 to the right
            f_x1 = f_x2  # Reuse the already computed value
            
            # Recompute x2, ensuring it's an integer, and its function value
            x2 = round(a + phi * (b - a))
            if x2 != x1:  # Only evaluate if x2 is different from x1
                f_x2 = f(x2)
    
    # The minimum is at the midpoint of the final interval, rounded to the nearest integer
    return round((a + b) / 2)

Solution

  • x1 and x2 can and should be float values otherwise your convergence values will be quantized and do weird things around 1.0. Only the input to f() needs to be integral which can be done without modifying x{1,2}:

    def golden_section_search_integer(f, a, b, tolerance=1e-6):
        # ...
        
        # Initial function evaluations
        f_x1 = f(round(x1))  # <-- round
        f_x2 = f(round(x2))  # <-- round
        
        while (b - a) > tolerance:
            print(a, b)
            if f_x1 < f_x2:
                # ...
                f_x1 = f(round(x1))  # <-- round
            else:
                # ...
                f_x2 = f(round(x2))  # <-- round
        
        # ...
        return (a + b) / 2  # <-- round here if necessary
    
    >>> golden_section_search_integer(f, 0, 5, 0.9)
    0 5
    1.9098300562505255 5
    1.9098300562505255 3.819660112501052
    2.639320225002103 3.819660112501052
    3.454915028125263