我正在尝试创建一个脚本,以最小化三个不同方向的两条线末端之间的最大距离。第一条线 A 具有固定的原点和长度,而线 B 的原点和长度是我试图优化的参数,以最小化端点之间的最大距离。我有两个约束,原点之间的欧几里德距离必须大于 0.1,并且每次旋转的线 B 的端点必须比该旋转的线 A 的端点距离线 A 的原点至少远 0.1 个单位。当我运行脚本时,约束将被忽略。
这是我创建优化问题的代码。约束 1 和 2 都被忽略。
# Define the length of line A
length_A = 0.5
# Define the endpoint positions of line A after each rotation
A_origin = np.array([0, 0])
A_ends = np.array([
A_origin + [0, -length_A], # Initial position
A_origin + [-length_A, 0], # After 90-degree rotation
A_origin + [0, length_A]
])
# Display results
print("Origin of line A:", A_origin)
print("Ends of line A:", A_ends)
# Objective function: Minimize the maximum distance between endpoints of A and B
def max_distance_to_A(params, A_ends):
B_origin = np.array(params[:2])
length_B = params[2]
# Define rotations for line B endpoint based on origin and length
B_ends = np.array([
B_origin + [0, -length_B], # Initial position
B_origin + [-length_B, 0], # After 90-degree rotation
B_origin + [0, length_B] # After 180-degree rotation
])
# Calculate distances between each corresponding end of lines A and B
distances = [np.linalg.norm(B_end - A_end) for B_end, A_end in zip(B_ends, A_ends)]
# Objective: Minimize the maximum distance across all rotations
return max(distances)
# Constraint function: Ensures all constraints are met
def distance_constraints(params, A_ends):
B_origin = np.array(params[:2])
length_B = params[2]
# Constraint 1: Origin of line B must be at least 0.1 away from origin of A
constraint1 = (np.linalg.norm(B_origin) - 0.1)
# Constraint 2: End of line B should not be closer to origin [0,0] than the end of line A
B_ends = np.array([
B_origin + [0, -length_B],
B_origin + [-length_B, 0],
B_origin + [0, length_B]
])
constraint2 = []
for B_end, A_end in zip(B_ends, A_ends):
dist_B_end_to_origin = np.linalg.norm(B_end)
dist_A_end_to_origin = np.linalg.norm(A_end)
constraint2.append(dist_B_end_to_origin - dist_A_end_to_origin - 0.1)
all_constraints = [constraint1] + constraint2
# Combine all constraints
return all_constraints
# Wrapper for constraints in SciPy format
def constraint_func(params, A_ends):
constraints = distance_constraints(params, A_ends)
return [{'type': 'ineq', 'fun': lambda params, c=c:c} for c in constraints]
然后这是我运行 Scipy 优化功能的代码
def debug_callback(params):
print(f"Current parameters: {params}")
print(f"Constraint values: {distance_constraints(params, A_ends)}")
print(f"Objective value: {max_distance_to_A(params, A_ends)}")
initial_guess = [0.5, 0.5, 0.6] # [x, y, length_B]
# Optimize
result = minimize(
fun=max_distance_to_A,
x0=initial_guess,
args=(A_ends,),
constraints=constraint_func(initial_guess, A_ends),
method='SLSQP',
callback=debug_callback,
options={'disp': True, 'ftol': 1e-5}
)
# Extract optimal origin and length of line B
B_origin_opt = result.x[:2]
length_B_opt = result.x[2]
# Display results
print("Optimal origin of line B:", B_origin_opt)
print("Optimal length of line B:", length_B_opt)
print("Minimum of maximum distance:", result.fun)
print(f"Constraint values: {distance_constraints(result.x, A_ends)}")
当我运行代码时,scipy 最小化目标函数,同时忽略约束,然后运行直到达到迭代限制,因为不遵守约束,但不会尝试更改控制变量来解决此问题。
Objective value: 1.2382318026714528e-08
Iteration limit reached (Exit mode 9)
Current function value: 1.2382318026714528e-08
Iterations: 100
Function evaluations: 1088
Gradient evaluations: 100
Optimal origin of line B: [-1.22005976e-09 -5.21614408e-09]
Optimal length of line B: 0.49999999289408037
Minimum of maximum distance: 1.2382318026714528e-08
Constraint values: [-0.09999999464306945, -0.10000000188977556, -0.10000000588585986, -0.10000001232206371]
这里最简单的解决方法是更改约束,以便它们直接引用
distance_constraints
。
constraints=[{'type': 'ineq', 'fun': distance_constraints, 'args': (A_ends,)}],
(注意:虽然
distance_constraints()
返回多个值,但可以将其放入单个约束中,因为 SciPy 支持向量值约束。)
import numpy as np
from scipy.optimize import minimize
import matplotlib.pyplot as plt
# Define the length of line A
length_A = 0.5
# Define the endpoint positions of line A after each rotation
A_origin = np.array([0, 0])
A_ends = np.array([
A_origin + [0, -length_A], # Initial position
A_origin + [-length_A, 0], # After 90-degree rotation
A_origin + [0, length_A]
])
# Display results
print("Origin of line A:", A_origin)
print("Ends of line A:", A_ends)
# Objective function: Minimize the maximum distance between endpoints of A and B
def max_distance_to_A(params, A_ends):
B_origin = np.array(params[:2])
length_B = params[2]
# Define rotations for line B endpoint based on origin and length
B_ends = np.array([
B_origin + [0, -length_B], # Initial position
B_origin + [-length_B, 0], # After 90-degree rotation
B_origin + [0, length_B] # After 180-degree rotation
])
# Calculate distances between each corresponding end of lines A and B
distances = [np.linalg.norm(B_end - A_end) for B_end, A_end in zip(B_ends, A_ends)]
# Objective: Minimize the maximum distance across all rotations
return max(distances)
# Constraint function: Ensures all constraints are met
def distance_constraints(params, A_ends):
B_origin = np.array(params[:2])
length_B = params[2]
# Constraint 1: Origin of line B must be at least 0.1 away from origin of A
constraint1 = (np.linalg.norm(B_origin) - 0.1)
# Constraint 2: End of line B should not be closer to origin [0,0] than the end of line A
B_ends = np.array([
B_origin + [0, -length_B],
B_origin + [-length_B, 0],
B_origin + [0, length_B]
])
constraint2 = []
for B_end, A_end in zip(B_ends, A_ends):
dist_B_end_to_origin = np.linalg.norm(B_end)
dist_A_end_to_origin = np.linalg.norm(A_end)
constraint2.append(dist_B_end_to_origin - dist_A_end_to_origin - 0.1)
all_constraints = [constraint1] + constraint2
# Combine all constraints
return all_constraints
def debug_callback(params):
print(f"Current parameters: {params}")
print(f"Constraint values: {distance_constraints(params, A_ends)}")
print(f"Objective value: {max_distance_to_A(params, A_ends)}")
initial_guess = [0.5, 0.5, 0.6] # [x, y, length_B]
# Optimize
result = minimize(
fun=max_distance_to_A,
x0=initial_guess,
args=(A_ends,),
constraints=[{'type': 'ineq', 'fun': distance_constraints, 'args': (A_ends,)}],
method='SLSQP',
callback=debug_callback,
options={'disp': True, 'ftol': 1e-5}
)
# Extract optimal origin and length of line B
B_origin_opt = result.x[:2]
length_B_opt = result.x[2]
# Display results
print("Optimal origin of line B:", B_origin_opt)
print("Optimal length of line B:", length_B_opt)
print("Minimum of maximum distance:", result.fun)
print(f"Constraint values: {distance_constraints(result.x, A_ends)}")
从输出中,我们可以看出约束都是非负的。
但是约束/目标与我们要解决的问题相符吗?对于这种事情,我喜欢编写一些绘图代码,以直观地表示手头的问题。
def get_B_ends(params):
B_origin = np.array(params[:2])
length_B = params[2]
# Constraint 1: Origin of line B must be at least 0.1 away from origin of A
constraint1 = (np.linalg.norm(B_origin) - 0.1)
# Constraint 2: End of line B should not be closer to origin [0,0] than the end of line A
B_ends = np.array([
B_origin + [0, -length_B],
B_origin + [-length_B, 0],
B_origin + [0, length_B]
])
return B_origin, B_ends
def plot_optimization(result, initial_guess):
for pt in A_ends:
plt.plot([A_origin[0], pt[0]], [A_origin[1], pt[1]], 'ro-', label='A')
B_origin, B_ends = get_B_ends(initial_guess)
for pt in B_ends:
plt.plot([B_origin[0], pt[0]], [B_origin[1], pt[1]], 'bo-', label='initial guess')
B_origin, B_ends = get_B_ends(result.x)
# print(A_origin, A_ends)
# print(B_origin, B_ends)
for pt in B_ends:
plt.plot([B_origin[0], pt[0]], [B_origin[1], pt[1]], 'go-', label='final optimized')
plt.axis('equal')
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys())
plt.show()
plot_optimization(result, initial_guess)
输出:
比较您的目标、约束条件和优化过程的输出,我发现有三件事需要评论。
问题 1 和 2 不是我能解决的问题 - 你可能已经想到了我指出的事情。不过,问题 3 是可以解决的。
对我来说,这看起来像一个局部最小值问题:优化问题包含多个局部最小值,并且局部优化器无法区分局部最小值和全局最小值之间的区别。
要检查这一点,一个简单的方法是改变初始值,看看是否可以找到更好的结果。
initial_guess = [-0.01, 0.0, 0.6]
通过这个初步猜测,它找到了更好的结果。这说明需要全局优化。
我通常使用盆地跳跃来解决这样的问题。 Basinhopping 是一种两阶段方法,在随机跳跃和局部最小化之间交替。
但是,为了尊重您的约束,它需要您定义一个接受函数。此函数检测随机跳跃步骤是否以您的约束所禁止的方式更改了参数。
示例:
from scipy.optimize import basinhopping
initial_guess = [0.5, 0.5, 0.6] # [x, y, length_B]
ftol = 1e-5
def accept_test(f_new=None, x_new=None, f_old=None, x_old=None):
cons = distance_constraints(x_new, A_ends)
# SLSQP allows constraint violations of up to ftol
# Allow that here too
accept = (np.array(cons) >= -ftol).all()
return accept
result = basinhopping(
max_distance_to_A,
x0=initial_guess,
minimizer_kwargs=dict(
args=(A_ends,),
constraints=[{'type': 'ineq', 'fun': distance_constraints, 'args': (A_ends,)}],
method='SLSQP',
callback=debug_callback,
options={'disp': True, 'ftol': ftol}
),
accept_test=accept_test,
niter=100,
)
# Extract optimal origin and length of line B
B_origin_opt = result.x[:2]
length_B_opt = result.x[2]
# Display results
print("Optimal origin of line B:", B_origin_opt)
print("Optimal length of line B:", length_B_opt)
print("Minimum of maximum distance:", result.fun)
print(f"Constraint values: {distance_constraints(result.x, A_ends)}")
plot_optimization(result, initial_guess)
print(result)
输出: