Defining Partial Derivatives on Implicit Components

For ImplicitComponent instances, you will provide partial derivatives of residuals with respect to inputs and outputs. Note that this is slightly different than what you do for ExplicitComponent instances, but the general procedure is similar:

  1. Declare the partial derivatives via declare_partials.

  2. Specify their values via linearize.

Residual values are computed in the apply_nonlinear method, so those equations are the ones you will differentiate. For the sake of complete clarity, if your ImplicitComponent does happen to define a solve_nonlinear method, then you will still provide derivatives of the apply_nonlinear method to OpenMDAO.

Here is a simple example to consider:

import openmdao.api as om


class QuadraticComp(om.ImplicitComponent):
    """
    A Simple Implicit Component representing a Quadratic Equation.

    R(a, b, c, x) = ax^2 + bx + c

    Solution via Quadratic Formula:
    x = (-b + sqrt(b^2 - 4ac)) / 2a
    """

    def setup(self):
        self.add_input('a', val=1.)
        self.add_input('b', val=1.)
        self.add_input('c', val=1.)
        self.add_output('x', val=0.)

    def setup_partials(self):
        self.declare_partials(of='x', wrt='*')

    def apply_nonlinear(self, inputs, outputs, residuals):
        a = inputs['a']
        b = inputs['b']
        c = inputs['c']
        x = outputs['x']
        residuals['x'] = a * x ** 2 + b * x + c

    def solve_nonlinear(self, inputs, outputs):
        a = inputs['a']
        b = inputs['b']
        c = inputs['c']
        outputs['x'] = (-b + (b ** 2 - 4 * a * c) ** 0.5) / (2 * a)

    def linearize(self, inputs, outputs, partials):
        a = inputs['a']
        b = inputs['b']
        c = inputs['c']
        x = outputs['x']

        partials['x', 'a'] = x ** 2
        partials['x', 'b'] = x
        partials['x', 'c'] = 1.0
        partials['x', 'x'] = 2 * a * x + b

        self.inv_jac = 1.0 / (2 * a * x + b)

In this component, x is an output, and you take derivatives with respect to it. This might seem a bit strange to you if you’re used to thinking about things from an ExplicitComponent perspective. But for implicit components it is necessary, because the values of those outputs are determined by a solver, like NewtonSolver, which will need to know those derivatives. They are also necessary for the total derivative computations across the whole model. So if your residual is a function of one or more of the component outputs, make sure you provide those partials to OpenMDAO.

Check That Your Derivatives Are Correct!

from openmdao.test_suite.components.quad_implicit import QuadraticComp

p = om.Problem()

p.model.add_subsystem('quad', QuadraticComp())

p.setup()

p.check_partials(compact_print=True);
-------------------------------
Component: QuadraticComp 'quad'
-------------------------------
'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------

'x'        wrt 'a'          | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'x'        wrt 'b'          | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'x'        wrt 'c'          | 1.0000e+00 | 1.0000e+00 | 1.1642e-10 | 1.1642e-10
'x'        wrt 'x'          | 1.0000e+00 | 1.0000e+00 | 1.0000e-06 | 1.0000e-06 >ABS_TOL >REL_TOL

##############################################################
Sub Jacobian with Largest Relative Error: QuadraticComp 'quad'
##############################################################
'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------
'x'        wrt 'x'          | 1.0000e+00 | 1.0000e+00 | 1.0000e-06 | 1.0000e-06