首页 > 解决方案 > 与显式组件相比,是否可以将“显式输出”添加到隐式组件而无需额外的计算工作?

问题描述

在试图弄清楚代码是否可以简化以避免一些重复时,我想知道是否可以将显式输出添加到隐式组件而不增加额外的计算工作与显式组件相比。显式输出在这里可能不是一个完全正确的术语,因为它取决于另一个隐式确定的输出。从文档中获取节点隐式组件示例:

class Node(om.ImplicitComponent):
    """Computes voltage residual across a node based on incoming and outgoing current."""

    def initialize(self):
        self.options.declare('n_in', default=1, types=int, desc='number of connections with + assumed in')
        self.options.declare('n_out', default=1, types=int, desc='number of current connections + assumed out')

    def setup(self):
        self.add_output('V', val=5., units='V')

        for i in range(self.options['n_in']):
            i_name = 'I_in:{}'.format(i)
            self.add_input(i_name, units='A')

        for i in range(self.options['n_out']):
            i_name = 'I_out:{}'.format(i)
            self.add_input(i_name, units='A')

    def setup_partials(self):
        #note: we don't declare any partials wrt `V` here,
        #      because the residual doesn't directly depend on it
        self.declare_partials('V', 'I*', method='fd')

    def apply_nonlinear(self, inputs, outputs, residuals):
        residuals['V'] = 0.
        for i_conn in range(self.options['n_in']):
            residuals['V'] += inputs['I_in:{}'.format(i_conn)]
        for i_conn in range(self.options['n_out']):
            residuals['V'] -= inputs['I_out:{}'.format(i_conn)]

当我们想计算通过节点的功率时,一种选择是创建一个显式组件,该组件将节点电压和每个节点电流输入和输出作为输入来计算功率,并将其与隐式组件分组. 但是,由于所有参数在隐式组件中都已经可用,并且这种方法在组件之间复制了一些当前的输入/输出循环,我想知道这是否可以直接在隐式组件中完成。由于文档示例提到“ solve_nonlinear 方法提供了一种在隐式组件中显式定义输出的方法”:

def solve_nonlinear(self, inputs, outputs):
    total_abs_current = 0
    for i_conn in range(self.options['n_in']):
        total_abs_current += np.abs(inputs['I_in:{}'.format(i_conn)])
    for i_conn in range(self.options['n_out']):
        total_abs_current += np.abs(inputs['I_out:{}'.format(i_conn)])
    outputs['P_total'] = total_abs_current * outputs['V'] / 2

进一步阅读,文档说仍然需要在apply_nonlinear()方法下添加功率残差。因此,类似:

def apply_nonlinear(self, inputs, outputs, residuals):
    residuals['V'] = 0
    total_abs_current = 0
    for i_conn in range(self.options['n_in']):
        residuals['V'] += inputs['I_in:{}'.format(i_conn)]
        total_abs_current += np.abs(inputs['I_in:{}'.format(i_conn)])
    for i_conn in range(self.options['n_out']):
        residuals['V'] -= inputs['I_out:{}'.format(i_conn)]
        total_abs_current += np.abs(inputs['I_out:{}'.format(i_conn)])
    residuals['P_total'] = outputs['P_total'] - total_abs_current * outputs['V'] / 2

但是,即使solve_linear() 已经明确指定/计算了功率,组件是否真的会使用此函数来“求解”功率?因此,与显式组件方法相比,此实现是否需要更多的计算资源?而当通过linearize()方法指定partials时,它们应该遵循apply_nonlinear()还是solve_nonlinear()计算?

标签: openmdao

解决方案


我通常将这种情况称为伪隐式输出。您有一个分析表达式,因此您实际上并不需要它是隐式的,但您希望将计算与一堆其他隐式的东西结合起来。你有基本的布局权利。您编写了一个solve_nonlinear为您进行计算的方法,并将残差形式添加到apply_nonlinear.

但是,即使solve_linear() 已经明确指定/计算了功率,组件是否真的会使用此函数来“求解”功率?

是的.. 和否:) 简单的答案是(在大多数情况下)该solve_nonlinear方法最终将提供伪隐式输出的值,作为整个全局非线性求解的一部分。对于该特定变量,残差形式将有效地始终返回 0。如果您使用的是块 gauss-seidel 求解器,或者打开了solve_subsystems的牛顿求解器,这将是正确的。

如果您使用纯牛顿方法(没有solve_subsystem),则会发生更微妙的情况。在这种情况下,残差形式实际上驱动了整个计算,并且solve_nonlinear任何隐式组件的方法都不会被调用。这不是运行牛顿求解器的超级常见模式,但它确实经常出现。

我会说伪隐式输出使您可以灵活地以任何一种方式工作,而不会真正损失性能。正如我将在下面讨论的那样,这与将其分解为显式组件之间没有任何实际区别。

因此,与显式组件方法相比,此实现是否需要更多的计算资源?

简短的回答是否定的,至少没有任何有意义的数量。长答案需要深入研究牛顿求解器的数学并了解 OpenMDAO 是如何真正执行 ExplicitComponents 的。有关所有详细信息,您应该查看OpenMDAO 论文的第 5.1 节以及 OpenMDAO 在内部为所有 ExplicitComponents 执行的隐式转换。

总之,OpenMDAO 中的显式组件与您在apply_linearOpenMDAO 内部在需要计算残差时所做的事情完全相同。因此,与 OpenMDAO 在后台所做的相比,您的实现并没有真正添加更多或更少的内容。

    residuals['P_total'] = outputs['P_total'] - total_abs_current * outputs['V'] / 2

不过,这里有一个警告。我会夸大其词以说明情况。假设您在组件中有一个单独的缩放器隐式关系,然后您也向它添加 1e6 伪隐式输出。在这种情况下,您最好将它们分开,因为您正在使牛顿系统变得更大且更昂贵。但一般来说,添加一些额外的伪显式输出根本不会产生太大影响。

当通过linearize()方法指定partials时,它们应该遵循apply_nonlinear()还是solve_nonlinear()计算?

区分apply_nonlinear. 在隐式组件的导数上下文中,完全不用担心您在 solve_nonlinear 中所做的事情!


推荐阅读