OpenMDAO considers component derivatives to be partial derivatives. The framework uses these partial derivatives in order to compute the total derivatives across your whole model. This tutorial is focused on how to define the partial derivatives for components that inherit from ExplicitComponent.

Defining Partial Derivatives on Explicit Components

For any ExplicitComponent you are going to provide derivatives of the outputs with respect to the inputs. Whenever you are going to define derivatives, there are two things you’re required to do:

  1. Declare the partial derivatives via declare_partials.

  2. Specify their values via compute_partials.

Here is an example, based on the Betz Limit Example:

class ActuatorDisc(om.ExplicitComponent):
    """Simple wind turbine model based on actuator disc theory"""

    def setup(self):
        # Inputs
        self.add_input('a', 0.5, desc="Induced Velocity Factor")
        self.add_input('Area', 10.0, units="m**2", desc="Rotor disc area")
        self.add_input('rho', 1.225, units="kg/m**3", desc="Air density")
        self.add_input('Vu', 10.0, units="m/s", desc="Freestream air velocity, upstream of rotor")

        # Outputs
        self.add_output('Vr', 0.0, units="m/s",
                        desc="Air velocity at rotor exit plane")
        self.add_output('Vd', 0.0, units="m/s",
                        desc="Slipstream air velocity, downstream of rotor")
        self.add_output('Ct', 0.0, desc="Thrust Coefficient")
        self.add_output('thrust', 0.0, units="N",
                        desc="Thrust produced by the rotor")
        self.add_output('Cp', 0.0, desc="Power Coefficient")
        self.add_output('power', 0.0, units="W", desc="Power produced by the rotor")

    def setup_partials(self):
        self.declare_partials('Vr', ['a', 'Vu'])
        self.declare_partials('Vd', 'a')
        self.declare_partials('Ct', 'a')
        self.declare_partials('thrust', ['a', 'Area', 'rho', 'Vu'])
        self.declare_partials('Cp', 'a')
        self.declare_partials('power', ['a', 'Area', 'rho', 'Vu'])

    def compute(self, inputs, outputs):
        """ Considering the entire rotor as a single disc that extracts
        velocity uniformly from the incoming flow and converts it to
        power."""

        a = inputs['a']
        Vu = inputs['Vu']

        qA = .5 * inputs['rho'] * inputs['Area'] * Vu**2

        outputs['Vd'] = Vd = Vu * (1 - 2 * a)
        outputs['Vr'] = .5 * (Vu + Vd)

        outputs['Ct'] = Ct = 4 * a * (1 - a)
        outputs['thrust'] = Ct * qA

        outputs['Cp'] = Cp = Ct * (1 - a)
        outputs['power'] = Cp * qA * Vu

    def compute_partials(self, inputs, J):
        """ Jacobian of partial derivatives."""

        a = inputs['a']
        Vu = inputs['Vu']
        Area = inputs['Area']
        rho = inputs['rho']

        # pre-compute commonly needed quantities
        a_times_area = a * Area
        one_minus_a = 1.0 - a
        a_area_rho_vu = a_times_area * rho * Vu

        J['Vr', 'a'] = -Vu
        J['Vr', 'Vu'] = one_minus_a

        J['Vd', 'a'] = -2.0 * Vu

        J['Ct', 'a'] = 4.0 - 8.0 * a

        J['thrust', 'a'] = .5 * rho * Vu**2 * Area * J['Ct', 'a']
        J['thrust', 'Area'] = 2.0 * Vu**2 * a * rho * one_minus_a
        J['thrust', 'rho'] = 2.0 * a_times_area * Vu ** 2 * (one_minus_a)
        J['thrust', 'Vu'] = 4.0 * a_area_rho_vu * (one_minus_a)

        J['Cp', 'a'] = 4.0 * a * (2.0 * a - 2.0) + 4.0 * (one_minus_a)**2

        J['power', 'a'] = 2.0 * Area * Vu**3 * a * rho * (
            2.0 * a - 2.0) + 2.0 * Area * Vu**3 * rho * one_minus_a**2
        J['power', 'Area'] = 2.0 * Vu**3 * a * rho * one_minus_a**2
        J['power', 'rho'] = 2.0 * a_times_area * Vu ** 3 * (one_minus_a)**2
        J['power', 'Vu'] = 6.0 * Area * Vu**2 * a * rho * one_minus_a**2

The calls to declare_partials tell OpenMDAO which partial derivatives to expect. This should be done inside the setup_partials method. It’s not illegal to do it in setup, but there are some cases where it must be called in setup_partials, for example the case where a component has dynamically sized variables. setup_partials iscalled after all shapes, even dynamic ones, are known, so it works on all cases. For the sake of consistency then, it’s best to always call declare_partials in setup_partials. In this example, not all the outputs depend on all the inputs, and you’ll see that if you look at the derivative declarations. Any partial that is not declared is assumed to be zero. You may declare all the partials in just one line as follows (see the documentation on specifying partials for more details):

self.declare_partials('*', '*')

Declaring the partials in this fashion, however, indicates to OpenMDAO that all the partials are nonzero. While you may save yourself a few lines of code using this method, the line savings could come at the expense of performance. Generally, it is better to be more specific, and declare only the nonzero partials.

Important

There are a few more options to declare_partials that are worth taking a look at. There is support for when your derivatives are constant, and there is support for specifying derivatives in a sparse AIJ format. The full details can be found in the documentation on specifying partials.

After you declare the nonzero partial derivatives, you need to implement the compute_partials method to perform the actual derivative computations. OpenMDAO will call this method whenever it needs to work with the partial derivatives. The values are stored in the Jacobian object, J, and get used in the linear solutions that are necessary to compute model-level total derivatives. This API results in the assembly of a Jacobian matrix in memory. The compute_partials API is the most appropriate way to declare derivatives in the vast majority of circumstances, and you should use it unless you have a good reason not to.

Providing Derivatives Using the Matrix-Free API

Sometimes you don’t want to assemble the full partial-derivative Jacobian of your component in memory. The reasons why you might not want this are beyond the scope of this tutorial. For now, let’s assume that if matrix assembly won’t work for your application, that you are likely already well aware of this issue. So if you can’t imagine why you would want to use a matrix-free API, you may disregard the following link. If you do need to work matrix-free, there is a compute_jacvec_product API, examples of which can be found in the feature document for ExplicitComponent.

How Do I Know If My Derivatives Are Correct?

It is really important, if you are going to provide analytic derivatives, that you make sure they are correct. It is hard to overstate the importance of accurate derivatives in the convergence of analysis and optimization problems. OpenMDAO provides a helper function to make it easier to verify your partial derivatives. Any time you implement analytic derivatives, or change the nonlinear equations of your analysis, you should check your partial derivatives this way.

from openmdao.test_suite.test_examples.test_betz_limit import ActuatorDisc

prob = om.Problem()

prob.model.add_subsystem('a_disk', ActuatorDisc())

prob.setup()
prob.check_partials(compact_print=True);
--------------------------------
Component: ActuatorDisc 'a_disk'
--------------------------------
'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------

'Cp'       wrt 'Area'       | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Cp'       wrt 'Vu'         | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Cp'       wrt 'a'          | 1.0000e+00 | 1.0000e+00 | 2.0000e-06 | 2.0000e-06 >ABS_TOL >REL_TOL
'Cp'       wrt 'rho'        | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Ct'       wrt 'Area'       | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Ct'       wrt 'Vu'         | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Ct'       wrt 'a'          | 0.0000e+00 | 4.0000e-06 | 4.0000e-06 | 1.0000e+00 >ABS_TOL >REL_TOL
'Ct'       wrt 'rho'        | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Vd'       wrt 'Area'       | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Vd'       wrt 'Vu'         | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Vd'       wrt 'a'          | 2.0000e+01 | 2.0000e+01 | 5.7511e-10 | 2.8756e-11
'Vd'       wrt 'rho'        | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Vr'       wrt 'Area'       | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'Vr'       wrt 'Vu'         | 5.0000e-01 | 5.0000e-01 | 0.0000e+00 | 0.0000e+00
'Vr'       wrt 'a'          | 1.0000e+01 | 1.0000e+01 | 9.3132e-10 | 9.3132e-11
'Vr'       wrt 'rho'        | 0.0000e+00 | 0.0000e+00 | 0.0000e+00 | nan
'power'    wrt 'Area'       | 3.0625e+02 | 3.0625e+02 | 0.0000e+00 | 0.0000e+00
'power'    wrt 'Vu'         | 9.1875e+02 | 9.1875e+02 | 9.1076e-05 | 9.9130e-08 >ABS_TOL
'power'    wrt 'a'          | 6.1250e+03 | 6.1250e+03 | 1.2250e-02 | 2.0001e-06 >ABS_TOL >REL_TOL
'power'    wrt 'rho'        | 2.5000e+03 | 2.5000e+03 | 0.0000e+00 | 0.0000e+00
'thrust'   wrt 'Area'       | 6.1250e+01 | 6.1250e+01 | 7.1054e-15 | 1.1601e-16
'thrust'   wrt 'Vu'         | 1.2250e+02 | 1.2250e+02 | 5.9605e-06 | 4.8657e-08 >ABS_TOL
'thrust'   wrt 'a'          | 0.0000e+00 | 2.4501e-03 | 2.4501e-03 | 1.0000e+00 >ABS_TOL >REL_TOL
'thrust'   wrt 'rho'        | 5.0000e+02 | 5.0000e+02 | 0.0000e+00 | 0.0000e+00

###############################################################
Sub Jacobian with Largest Relative Error: ActuatorDisc 'a_disk'
###############################################################
'<output>' wrt '<variable>' | calc mag.  | check mag. | a(cal-chk) | r(cal-chk)
-------------------------------------------------------------------------------
'Ct'       wrt 'a'          | 0.0000e+00 | 4.0000e-06 | 4.0000e-06 | 1.0000e+00

Important

check_partials is really important when you’re coding derivatives. It has some options to give you more detailed outputs for debugging and to let you limit which components get tested. You should look over the complete documentation on check_partials before you start doing heavy development with derivatives.

There is a lot of information there, but for now, just take a look at the r(fwd-chk) column, which shows the norm of the relative difference between the analytic derivatives Jacobian and one that was approximated using finite differences. Here, all the numbers are really small, and that’s what you want to see. It’s rare, except for linear functions, that the finite difference and analytic derivatives will match exactly, but they should be pretty close.