2023-03-17 20:29:20 +01:00
# -*- coding: utf-8 -*-
import numpy as np
def cubic_poly ( x , a0 , a1 , a2 , a3 ) :
return a0 + a1 * x + a2 * x * * 2 + a3 * x * * 3
def polyfit4 ( x1 , x2 , x3 , x4 , y1 , y2 , y3 , y4 ) :
2023-03-18 13:20:35 +01:00
''' Function to compute the coefficients of the cubic Lagrange polynomial that interpolates the four points (x1, y1), (x2, y2), (x3, y3), (x4, y4).
The coefficients are computed using a linear least - squares fit to the four points .
The condition number of the matrix ( A ^ T * A ) is critical to the numerical stability of the solution .
It happens that the minimum condition number is achieved when the four points are equally spaced and span the interval [ - pi / 3 , pi / 3 ] .
'''
2023-03-17 20:29:20 +01:00
A = np . array ( [ [ 1 , x1 , x1 * * 2 , x1 * * 3 ] , [ 1 , x2 , x2 * * 2 , x2 * * 3 ] , [ 1 , x3 , x3 * * 2 , x3 * * 3 ] , [ 1 , x4 , x4 * * 2 , x4 * * 3 ] ] )
y = np . array ( [ y1 , y2 , y3 , y4 ] )
a = np . linalg . solve ( A . transpose ( ) @A , A . transpose ( ) @y )
return a
2023-03-18 14:46:11 +01:00
def polyfit4_mpi3_pi3 ( y1 , y2 , y3 , y4 ) :
''' Function to compute the coefficients of the cubic Lagrange polynomial that interpolates the four points (-pi/3, y1), (-pi/9, y2), (pi/9, y3), (pi/3, y4).
The coefficients are computed using a linear least - squares fit to the four points .
The matrix ( A ^ T * A ) is precomputed for efficiency
'''
y = np . array ( [ y1 , y2 , y3 , y4 ] )
AT = np . array ( [ [ 1 , 1 , 1 , 1 ] ,
[ - np . pi / 3 , - np . pi / 9 , np . pi / 9 , np . pi / 3 ] ,
[ np . pi * * 2 / 9 , np . pi * * 2 / 81 , np . pi * * 2 / 81 , np . pi * * 2 / 9 ] ,
[ - np . pi * * 3 / 27 , - np . pi * * 3 / 729 , np . pi * * 3 / 729 , np . pi * * 3 / 27 ] ] ) # A^T
ATAinv = np . array ( [ [ 41 / 64 , 0 , - 405 / ( 64 * np . pi * * 2 ) , 0 ] ,
[ 0 , 3285 / ( 64 * np . pi * * 2 ) , 0 , - 29889 / ( 64 * np . pi * * 4 ) ] ,
[ - 405 / ( 64 * np . pi * * 2 ) , 0 , 6561 / ( 64 * np . pi * * 4 ) , 0 ] ,
[ 0 , - 29889 / ( 64 * np . pi * * 4 ) , 0 , 295245 / ( 64 * np . pi * * 6 ) ] ] ) # (A^T * A)^-1
return ATAinv @ ( AT @y )
2023-03-18 13:20:35 +01:00
def map_interval ( x , a1 , b1 , a2 , b2 ) :
''' Maps a value or array " x " from the interval [a1, b1] to the interval [a2, b2]. '''
return ( x - a1 ) * ( b2 - a2 ) / ( b1 - a1 ) + a2
def cubic_lagrange_minimize ( f , a , b , tol = 1e-6 , callback = None ) :
2023-03-17 20:29:20 +01:00
''' Function to find minimum of f over interval [a, b] using cubic Lagrange polynomial interpolation.
2023-03-18 13:20:35 +01:00
If the function is monotonic , then the minimum is one of the bounds of the interval , and the minimum is found in two iterations .
2023-03-17 20:29:20 +01:00
The best estimate of the minimum is returned .
The number of function evaluations is 2 + 2 * Niter .
2023-03-18 13:20:35 +01:00
callback is a function that is called at each iteration with the current estimate of the minimum and other internal parameters of the algorithm .
The callback function should have the following signature : callback ( x_sol , y_sol , i , quadratic_solution , x , a ) , where quadratic_solution = ( delta , x_sol_1 , x_sol_2 , y_sol_1 , y_sol_2 ) is the solution of the quadratic equation of the first derivative of the Lagrange polynomial , x = ( x0 , x1 , x2 , x3 ) are the endpoints of the interval , and a = ( a0 , a1 , a2 , a3 ) are the coefficients of the Lagrange polynomial .
2023-03-17 20:29:20 +01:00
'''
# initialize interval endpoints and function values
x0 , x1 , x2 , x3 = a , a * 2. / 3. + b * 1. / 3. , a * 1. / 3. + b * 2. / 3. , b
f0 , f3 = f ( x0 ) , f ( x3 )
2023-03-18 13:20:35 +01:00
x_prev = ( x0 + x3 ) / 2.
2023-03-18 10:06:08 +01:00
small_coefficient = 1e-9
2023-03-18 13:20:35 +01:00
Niter = int ( np . ceil ( np . log ( np . abs ( b - a ) / tol ) / np . log ( 3 ) ) ) # reduction factor of interval size at each iteration is 3 because we sample two points in the interval each iteration
2023-03-17 20:29:20 +01:00
for i in range ( Niter ) :
# Compute function values at two points in the interval
x1 , x2 = x0 * 2. / 3. + x3 * 1. / 3. , x0 * 1. / 3. + x3 * 2. / 3.
f1 , f2 = f ( x1 ) , f ( x2 )
2023-03-18 13:20:35 +01:00
# Remap the x values to the interval [-pi/3, pi/3] to minimize the condition number of the matrix (A^T * A) in the least-squares fit
x0m , x1m , x2m , x3m = map_interval ( x0 , x0 , x3 , - np . pi / 3 , np . pi / 3 ) , map_interval ( x1 , x0 , x3 , - np . pi / 3 , np . pi / 3 ) , map_interval ( x2 , x0 , x3 , - np . pi / 3 , np . pi / 3 ) , map_interval ( x3 , x0 , x3 , - np . pi / 3 , np . pi / 3 )
2023-03-17 20:29:20 +01:00
# compute Lagrange polynomial using least-squares fit to 4 points, which is equivalent to the cubic Lagrange polynomial
2023-03-18 14:46:11 +01:00
a0 , a1 , a2 , a3 = polyfit4_mpi3_pi3 ( f0 , f1 , f2 , f3 ) # equivalent to a0, a1, a2, a3 = polyfit4(x0m, x1m, x2m, x3m, f0, f1, f2, f3)
2023-03-18 13:20:35 +01:00
x_sol_1 , x_sol_2 , y_sol_1 , y_sol_2 , delta = None , None , None , None , None
2023-03-17 20:29:20 +01:00
# Solve the first derivative of the Lagrange polynomial for a zero
2023-03-18 10:06:08 +01:00
if np . abs ( a3 ) > small_coefficient :
2023-03-17 20:29:20 +01:00
delta = - 3 * a1 * a3 + a2 * * 2
if delta < 0 :
2023-03-18 13:20:35 +01:00
x_sol = x0m if f0 < f3 else x3m # just choose the interval tha contains the minimum of the linear polynomial
2023-03-17 20:29:20 +01:00
y_sol = cubic_poly ( x_sol , a0 , a1 , a2 , a3 )
else : # solve for the two solutions of the quadratic equation of the first derivative of the Lagrange polynomial
x_sol_1 = ( - a2 + np . sqrt ( delta ) ) / ( 3 * a3 )
x_sol_2 = ( - a2 - np . sqrt ( delta ) ) / ( 3 * a3 )
y_sol_1 = cubic_poly ( x_sol_1 , a0 , a1 , a2 , a3 )
y_sol_2 = cubic_poly ( x_sol_2 , a0 , a1 , a2 , a3 )
x_sol = x_sol_1 if y_sol_1 < y_sol_2 else x_sol_2
y_sol = y_sol_1 if y_sol_1 < y_sol_2 else y_sol_2
2023-03-18 10:06:08 +01:00
elif np . abs ( a2 ) > small_coefficient : # if a3 is zero, then the Lagrange polynomial is a quadratic polynomial
2023-03-17 20:29:20 +01:00
x_sol = - a1 / ( 2 * a2 )
y_sol = cubic_poly ( x_sol , a0 , a1 , a2 , a3 )
else : # if a3 and a2 are zero, then the Lagrange polynomial is a linear polynomial
2023-03-18 13:20:35 +01:00
x_sol = x0m if f0 < f3 else x3m # just choose the interval tha contains the minimum of the linear polynomial
2023-03-17 20:29:20 +01:00
y_sol = cubic_poly ( x_sol , a0 , a1 , a2 , a3 )
2023-03-18 13:20:35 +01:00
# transform the solution back to the original interval
x_sol = map_interval ( x_sol , - np . pi / 3 , np . pi / 3 , x0 , x3 )
if x_sol_1 is not None :
x_sol_1 = map_interval ( x_sol_1 , - np . pi / 3 , np . pi / 3 , x0 , x3 )
if x_sol_2 is not None :
x_sol_2 = map_interval ( x_sol_2 , - np . pi / 3 , np . pi / 3 , x0 , x3 )
if callback is not None :
callback ( x_sol , y_sol , i , ( delta , x_sol_1 , x_sol_2 , y_sol_1 , y_sol_2 ) , ( x0 , x1 , x2 , x3 ) , ( a0 , a1 , a2 , a3 ) )
2023-03-17 20:29:20 +01:00
if np . abs ( x_sol - x_prev ) < tol :
break
# Determine which interval contains the minimum of the cubic polynomial
if x_sol < x1 :
x3 , f3 = x1 , f1
elif x_sol < x2 :
x0 , f0 = x1 , f1
x3 , f3 = x2 , f2
else :
x0 , f0 = x2 , f2
x_prev = x_sol
# return best estimate of minimum
if y_sol < f0 and y_sol < f3 :
return x_sol
elif f0 < f3 :
return x0
else :
return x3
2023-03-18 13:20:35 +01:00
def cubic_lagrange_minimize_callback_simple ( x_sol , y_sol , i , quadratic_solution , x , a ) :
print ( f ' Iteration { i } : f( { x_sol } ) = { y_sol } ' )
def cubic_lagrange_minimize_callback_detailed ( x_sol , y_sol , i , quadratic_solution , x , a ) :
print ( ' -------------------------------------------------------------------- ' )
print ( f ' Iteration { i } ' )
print ( ' X values : ' , x )
print ( ' Cubic poly coeffs : ' , a )
print ( f ' Quadratic solution : delta = { quadratic_solution [ 0 ] } , x_sol_1 = { quadratic_solution [ 1 ] } , x_sol_2 = { quadratic_solution [ 2 ] } , y_sol_1 = { quadratic_solution [ 3 ] } , y_sol_2 = { quadratic_solution [ 4 ] } ' )
print ( f ' Current solution : f( { x_sol } ) = { y_sol } ' )
2023-03-17 20:29:20 +01:00
if __name__ == ' __main__ ' :
2023-03-18 14:46:11 +01:00
import colorsys
import matplotlib . pyplot as plt
functions_list = [
lambda x : x * * 2 + np . sin ( 5 * x ) ,
lambda x : ( x - 1 ) * * 2 ,
lambda x : - x ,
lambda x : np . exp ( x ) ,
lambda x : np . exp ( - x ) ,
lambda x : - np . exp ( - x * * 2 ) ,
lambda x : np . exp ( - x * * 2 ) ,
lambda x : np . ones_like ( x )
]
def random_color ( min_saturation = 0.5 , lightness_range = ( 0.5 , 1.0 ) ) :
''' Returns a random color where the maximum lightness is 0.5 and the saturation is always greater than 0.5. '''
h = np . random . uniform ( 0 , 360 ) # Hue is a value between 0 and 360
s = np . random . uniform ( min_saturation , 1 ) # Saturation is a value between min_saturation and 1
l = np . random . uniform ( lightness_range [ 0 ] , lightness_range [ 1 ] ) # Lightness is a value between 0 and max_lightness
return [ x for x in colorsys . hls_to_rgb ( h / 360 , l , s ) ] # Convert HSL to RGB
2023-03-18 13:20:35 +01:00
2023-03-17 20:29:20 +01:00
def test_cubic_lagrange_polynomial ( ) :
x = np . linspace ( 0 , 16 , 100 )
x1 , x2 , x3 , x4 = 1 , 3 , 7 , 15
y1 , y2 , y3 , y4 = 1 , - 2 , 3.5 , 5
a0 , a1 , a2 , a3 = polyfit4 ( x1 , x2 , x3 , x4 , y1 , y2 , y3 , y4 )
print ( a0 , a1 , a2 , a3 )
y = cubic_poly ( x , a0 , a1 , a2 , a3 )
# plt.plot(x, cubic_lagrange(x, x1, x2, x3, x4, y1, y2, y3, y4), 'b', label='cubic Lagrange ChatGPT one shot')
2023-03-18 13:20:35 +01:00
plt . plot ( x , y , ' g ' , label = ' Cubic Lagrange polynomial through linear least-squares ' )
2023-03-17 20:29:20 +01:00
plt . plot ( [ x1 , x2 , x3 , x4 ] , [ y1 , y2 , y3 , y4 ] , ' ro ' )
plt . grid ( )
plt . show ( )
def test_cubic_lagrange_minimize ( ) :
2023-03-18 14:46:11 +01:00
for f in functions_list :
a , b = - 1.2 , 1.5
x = np . linspace ( a , b , 100 )
x_min = cubic_lagrange_minimize ( f , a , b , tol = 1e-6 , callback = cubic_lagrange_minimize_callback_detailed )
print ( ' Solution : ' , x_min , f ( x_min ) )
c = random_color ( .5 , ( 0.5 , 0.7 ) )
plt . plot ( x , f ( x ) , color = c )
plt . plot ( x_min , f ( x_min ) , ' o ' , color = c )
2023-03-17 20:29:20 +01:00
plt . grid ( )
plt . show ( )
2023-03-18 13:20:35 +01:00
# test_cubic_lagrange_polynomial()
2023-03-17 20:29:20 +01:00
test_cubic_lagrange_minimize ( )