使用 openMDAO 处理 SLSQP 优化中的约束

Treatment of constraints in SLSQP optimization with openMDAO

对于 openMDAO,我正在使用 FD 导数并尝试使用 SLSQP 方法解决非线性约束优化问题。每当优化器到达违反其中一个约束的点时,它就会崩溃并显示消息:

Optimization FAILED. Positive directional derivative for linesearch

例如,如果我故意将初始点设置为不可行的设计点,优化器将执行 1 次迭代并退出并出现上述错误(当我从可行点开始时也会发生同样的情况,但随后优化器到达了几次迭代后的不可行点)。

根据 In OpenMDAO, is there a way to ensure that the constraints are respected before proceeding with a computation? 中问题的答案,我假设引发 AnalysisError 异常对我的情况不起作用,对吗?有没有其他方法可以防止优化器进入不可行的区域或至少在 linesearch 上回溯并尝试不同的 direction/distance?还是仅在解析导数可用时才使用 SLSQP 方法?

可重现的测试用例:

import numpy as np
import openmdao.api as om


class d1(om.ExplicitComponent):

        def setup(self):

            # Global design variables
            self.add_input('r', val= [3,3,3])          
            self.add_input('T', val= 20)
            
            # Coupling output
            self.add_output('M', val=0)
            self.add_output('cost', val=0)
            
        def setup_partials(self):
            # Finite difference all partials.
            self.declare_partials('*', '*', method='fd')

        def compute(self, inputs, outputs):
            # define inputs
            r = inputs['r']
            T = inputs['T'][0]
            
            cost =  174.42 * T * (r[0]**2 + 2*r[1]**2 + r[2]**2 + r[0]*r[1] + r[1]*r[2])
            
            M =     456.19 * T * (r[0]**2 + 2*r[1]**2 + r[2]**2 + r[0]*r[1] + r[1]*r[2]) - 599718
                 
            outputs['M'] = M
            outputs['cost'] = cost
            

class MDA(om.Group):

    class ObjCmp(om.ExplicitComponent):

        def setup(self):
            # Global Design Variable
            self.add_input('cost', val=0)

            # Output
            self.add_output('obj', val=0.0)

        def setup_partials(self):
            # Finite difference all partials.
            self.declare_partials('*', '*', method='fd')

        def compute(self, inputs, outputs):
            
            outputs['obj'] = inputs['cost']
           
            
    class ConCmp(om.ExplicitComponent):

        def setup(self):
            # Global Design Variable
            self.add_input('M', val=0)
            
            # Output
            self.add_output('con', val=0.0)

        def setup_partials(self):
            # Finite difference all partials.
            self.declare_partials('*', '*', method='fd')

        def compute(self, inputs, outputs):
            
            # assemble outputs
            outputs['con'] = inputs['M']

    def setup(self):
        
        self.add_subsystem('d1', d1(), promotes_inputs=['r','T'],
                            promotes_outputs=['M','cost'])
                                     
        self.add_subsystem('con_cmp', self.ConCmp(), promotes_inputs=['M'],
                            promotes_outputs=['con'])
                          
        self.add_subsystem('obj_cmp', self.ObjCmp(), promotes_inputs=['cost'],
                           promotes_outputs=['obj'])
                           

# Build the model
prob = om.Problem(model=MDA())
model = prob.model

model.add_design_var('r', lower= [3,3,3], upper= [10,10,10])
model.add_design_var('T', lower= 20, upper= 220)
model.add_objective('obj', scaler=1)
model.add_constraint('con', lower=0)

# Setup the optimization
prob.driver = om.ScipyOptimizeDriver(optimizer='SLSQP', tol=1e-3, disp=True)

prob.setup()
prob.set_solver_print(level=0)
prob.run_driver()

# Printout
print('minimum found at')
print(prob.get_val('T')[0])
print(prob.get_val('r'))

print('constraint')
print(prob.get_val('con')[0])

print('minimum objective')
print(prob.get_val('obj')[0])

您使用的是哪种 SLSQP 方法? pyOptSparse 中有一种实现,ScipyOptimizer 中有一种实现。 pyoptsparse 中的那个比较旧,不遵守边界约束。 Scipy 中的那个较新并且确实如此。 (是的,这很令人困惑,因为它们具有相同的名称并共享一些血统......但不再是同一个优化器)

你不应该在超出范围时引发分析错误。如果您需要严格遵守边界,我建议在 pyoptsparse 中使用 IPopt(如果您可以编译)或切换到 ScipyOptimizerDriver 及其 SLSQP 实现。

根据你的问题,我不太清楚你是在谈论边界约束还是 inequality/equality 约束。如果是后者,那么就没有任何优化器可以保证您始终保持在可行区域中。像 IPopt 这样的内部点方法会更好地留在区域内,但不是 100% 的时间。

一般来说,使用基于梯度的优化,即使在约束区域之外,也能使问题平滑和连续,这一点非常重要。如果space中有部分是你绝对不能进去的,那你就需要把那些变量做成设计变量,使用bound constraints。这有时需要稍微重新制定您的问题公式,可能通过添加一种兼容性约束来表示“设计变量 = computed_value”。然后您可以确保将设计变量传递给任何需要值严格在界限内的东西,并且(希望)收敛的答案也将满足您的兼容性约束。

如果您提供某种测试用例或示例,我可以用更具体的建议修改我的答案。

根据您提供的测试用例,这里的问题是您的缩放 objective 和约束真的很差(您还有一些非常奇怪的编码选择......我修改了)。

运行 OpenMDAO scaling report 显示您的 objective 和约束值的大小都在 1e6 左右:

这非常大,是您问题的根源。一个(非常粗略的)经验法则是你的 objectives 和约束应该在顺序 1 附近。这不是硬性规定,但通常是一个很好的起点。有时其他缩放会更好,如果你有非常大或小的导数......但是 SQP 方法的某些部分对 objective 的缩放和约束值直接敏感。所以尽量让它们大致保持在 1 的范围内是个好主意。

向 objective 和约束添加 ref=1e6 为数值方法收敛问题提供了足够的解决方案:

            Current function value: [0.229372]
            Iterations: 8
            Function evaluations: 8
            Gradient evaluations: 8
Optimization Complete
-----------------------------------
minimum found at
20.00006826587515
[3.61138704 3.         3.61138704]
constraint
197.20821903413162
minimum objective
229371.99547899762

这是我修改的代码(包括删除您组中似乎没有做任何事情的额外 class 定义):

import numpy as np
import openmdao.api as om


class d1(om.ExplicitComponent):

        def setup(self):

            # Global design variables
            self.add_input('r', val= [3,3,3])          
            self.add_input('T', val= 20)
            
            # Coupling output
            self.add_output('M', val=0)
            self.add_output('cost', val=0)
            
        def setup_partials(self):
            # Finite difference all partials.
            self.declare_partials('*', '*', method='cs')

        def compute(self, inputs, outputs):
            # define inputs
            r = inputs['r']
            T = inputs['T'][0]
            
            cost =  174.42 * T * (r[0]**2 + 2*r[1]**2 + r[2]**2 + r[0]*r[1] + r[1]*r[2])
            
            M =     456.19 * T * (r[0]**2 + 2*r[1]**2 + r[2]**2 + r[0]*r[1] + r[1]*r[2]) - 599718
                 
            outputs['M'] = M
            outputs['cost'] = cost
            

class MDA(om.Group):

    def setup(self):
        
        self.add_subsystem('d1', d1(), promotes_inputs=['r','T'],
                            promotes_outputs=['M','cost'])
                                     
        # self.add_subsystem('con_cmp', self.ConCmp(), promotes_inputs=['M'],
        #                     promotes_outputs=['con'])
                          
        # self.add_subsystem('obj_cmp', self.ObjCmp(), promotes_inputs=['cost'],
        #                    promotes_outputs=['obj'])
                           

# Build the model
prob = om.Problem(model=MDA())
model = prob.model

model.add_design_var('r', lower= [3,3,3], upper= [10,10,10])
model.add_design_var('T', lower= 20, upper= 220)
model.add_objective('cost', ref=1e6)
model.add_constraint('M', lower=0, ref=1e6)

# Setup the optimization
prob.driver = om.ScipyOptimizeDriver(optimizer='SLSQP', tol=1e-3, disp=True)

prob.setup()
prob.set_solver_print(level=0)

prob.set_val('r', 7.65)
prob.run_driver()

# Printout
print('minimum found at')
print(prob.get_val('T')[0])
print(prob.get_val('r'))

print('constraint')
print(prob.get_val('M')[0])

print('minimum objective')
print(prob.get_val('cost')[0])