Computing analytic derivatives

Level: Beginner-Intermediate

Topics: Model differentiation

Here you will compute and provide the analytic derivatives for an OpenMDAO component, then check the results using check_partials.

Definition of the first system

Here is the simple component. It has two inputs (x is an array, y is a scalar) and one output (z is an array). Your goal is to add the compute_partials() method to the component so that your analytic derivatives match the finite difference checks. I’d suggest trying to differentiate these expressions by hand, but feel free to use a symbolic solver (like Wolfram Alpha or Mathematica) if you prefer.

Because we are using finite difference to check the partial derivatives, you should expect your analytic derivatives to match to about 1.e-6. This is due to the approximation error introduced by FD.

import numpy as np

import openmdao.api as om


class SimpleComponent(om.ExplicitComponent):
    def setup(self):
        self.add_input('x', shape=3)
        self.add_input('y')
        self.add_output('z', shape=2)

        self.declare_partials('z', 'x')
        self.declare_partials('z', 'y')

    def compute(self, inputs, outputs):
        x = inputs['x']
        y = inputs['y']
        
        outputs['z'][0] = np.sum(x) * y
        outputs['z'][1] = x[2] - y**2 * x[0]        

prob = om.Problem()
model = prob.model

model.add_subsystem('comp', SimpleComponent(), promotes=['*'])

prob.setup()

prob.set_val('x', [2.2, 5.3, 4.1])
prob.set_val('y', [1.5])

prob.run_model()

data = prob.check_partials()
---------------------------------
Component: SimpleComponent 'comp'
---------------------------------

  comp: 'z' wrt 'x'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 3.579455e+00 (fd:forward)
    Absolute Error (Jan - Jfd) : 3.579455e+00 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[ 1.5   1.5   1.5 ]
 [-2.25  0.    1.  ]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  comp: 'z' wrt 'y'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.334616e+01 (fd:forward)
    Absolute Error (Jan - Jfd) : 1.334616e+01 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0.]
 [0.]]

    Raw FD Derivative (Jfd)
[[11.6      ]
 [-6.6000022]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Another simple case – derivatives of a fluid pump model

This example component is a real engineering model of a simple pump, taken from OpenConcept. The component looks a bit more complicated than the previous one, but that’s because there are multiple inputs and outputs, each corresponding to a meaningful engineering value. Additionally, the variable naming and descriptions are much more explicit here.

Again, please write a compute_partials() method that correctly computes the derivatives. Here we are checking against a complex-step approximation so we should be able to match it to machine precision.

class SimplePump(om.ExplicitComponent):
    def initialize(self):
        self.options.declare("num_nodes", default=1, desc="Number of flight/control conditions")
        self.options.declare("efficiency", default=0.35, desc="Efficiency (dimensionless)")
        self.options.declare("weight_base", default=0.0, desc="Pump base weight")
        self.options.declare("weight_inc", default=1 / 450, desc="Incremental pump weight (kg/W)")

    def setup(self):
        nn = self.options["num_nodes"]
        weight_inc = self.options["weight_inc"]

        self.add_input("power_rating", units="W", desc="Pump electrical power rating")
        self.add_input("mdot_coolant", units="kg/s", desc="Coolant mass flow rate", val=np.ones((nn,)))
        self.add_input("delta_p", units="Pa", desc="Pump pressure rise", val=np.ones((nn,)))
        self.add_input("rho_coolant", units="kg/m**3", desc="Coolant density", val=np.ones((nn,)))

        self.add_output("elec_load", units="W", desc="Pump electrical load", val=np.ones((nn,)))
        self.add_output("component_weight", units="kg", desc="Pump weight")
        self.add_output("component_sizing_margin", units=None, val=np.ones((nn,)), desc="Comp sizing margin")

        self.declare_partials(
            ["elec_load", "component_sizing_margin"],
            ["rho_coolant", "delta_p", "mdot_coolant"],
            rows=np.arange(nn),
            cols=np.arange(nn),
        )
        self.declare_partials(["component_sizing_margin"], ["power_rating"], rows=np.arange(nn), cols=np.zeros(nn))
        self.declare_partials(["component_weight"], ["power_rating"], val=weight_inc)

    def compute(self, inputs, outputs):
        eta = self.options["efficiency"]
        weight_inc = self.options["weight_inc"]
        weight_base = self.options["weight_base"]

        outputs["component_weight"] = weight_base + weight_inc * inputs["power_rating"]

        # compute the fluid power
        vol_flow_rate = inputs["mdot_coolant"] / inputs["rho_coolant"]  # m3/s
        fluid_power = vol_flow_rate * inputs["delta_p"]
        outputs["elec_load"] = fluid_power / eta
        outputs["component_sizing_margin"] = outputs["elec_load"] / inputs["power_rating"]
        

prob = om.Problem()
nn = 3

rho_coolant = 1020 * np.ones(nn)
mdot_coolant = np.linspace(0.6, 1.2, nn)
delta_p = np.linspace(2e4, 4e4, nn)
power_rating = 1000

prob.model.add_subsystem("pump", SimplePump(num_nodes=nn), promotes_inputs=["*"])
prob.setup(force_alloc_complex=True)

prob.set_val("power_rating", power_rating, units="W")
prob.set_val("delta_p", delta_p, units="Pa")
prob.set_val("mdot_coolant", mdot_coolant, units="kg/s")
prob.set_val("rho_coolant", rho_coolant, units="kg/m**3")

prob.run_model()

data = prob.check_partials(method='cs')
----------------------------
Component: SimplePump 'pump'
----------------------------

  pump: 'component_sizing_margin' wrt 'delta_p'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 4.525349e-06 (cs:None)
    Absolute Error (Jan - Jfd) : 4.525349e-06 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[1.68067227e-06 0.00000000e+00 0.00000000e+00]
 [0.00000000e+00 2.52100840e-06 0.00000000e+00]
 [0.00000000e+00 0.00000000e+00 3.36134454e-06]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'component_sizing_margin' wrt 'mdot_coolant'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.508450e-01 (cs:None)
    Absolute Error (Jan - Jfd) : 1.508450e-01 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[0.05602241 0.         0.        ]
 [0.         0.08403361 0.        ]
 [0.         0.         0.11204482]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'component_sizing_margin' wrt 'power_rating'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.578848e-04 (cs:None)
    Absolute Error (Jan - Jfd) : 1.578848e-04 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0.]
 [0.]
 [0.]]

    Raw FD Derivative (Jfd)
[[-3.36134454e-05]
 [-7.56302521e-05]
 [-1.34453782e-04]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'component_sizing_margin' wrt 'rho_coolant'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.547890e-04 (cs:None)
    Absolute Error (Jan - Jfd) : 1.547890e-04 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[-3.29543582e-05  0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00 -7.41473060e-05  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00 -1.31817433e-04]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'component_weight' wrt 'power_rating'
    Analytic Magnitude: 2.222222e-03
          Fd Magnitude: 2.222222e-03 (cs:None)
    Absolute Error (Jan - Jfd) : 0.000000e+00

    Relative Error (Jan - Jfd) / Jfd : 0.000000e+00

    Raw Analytic Derivative (Jfor)
[[0.00222222]]

    Raw FD Derivative (Jfd)
[[0.00222222]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'elec_load' wrt 'delta_p'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 4.525349e-03 (cs:None)
    Absolute Error (Jan - Jfd) : 4.525349e-03 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[0.00168067 0.         0.        ]
 [0.         0.00252101 0.        ]
 [0.         0.         0.00336134]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'elec_load' wrt 'mdot_coolant'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.508450e+02 (cs:None)
    Absolute Error (Jan - Jfd) : 1.508450e+02 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[ 56.02240896   0.           0.        ]
 [  0.          84.03361345   0.        ]
 [  0.           0.         112.04481793]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  pump: 'elec_load' wrt 'rho_coolant'
    Analytic Magnitude: 0.000000e+00
          Fd Magnitude: 1.547890e-01 (cs:None)
    Absolute Error (Jan - Jfd) : 1.547890e-01 *

    Relative Error (Jan - Jfd) / Jfd : 1.000000e+00 *

    Raw Analytic Derivative (Jfor)
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]

    Raw FD Derivative (Jfd)
[[-0.03295436  0.          0.        ]
 [ 0.         -0.07414731  0.        ]
 [ 0.          0.         -0.13181743]]
 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -