Explicit vs implicit systems

Main takeaway

Explicit systems have outputs defined only by the inputs, whereas implicit systems can have outputs that are defined as a function of both inputs and outputs.

from IPython.display import YouTubeVideo; YouTubeVideo('006Zc7s-CkY', width=1024, height=576)

Implicit vs explicit functions

First, let’s introduce the basic ideas of implicit and explicit functions. The words “implicit” and “explicit” have all sorts of meanings in the English language that vary contextually, but here we adopt a specific subset of meanings. Specifically,

  • outputs of explicit functions depend only on the inputs

  • outputs of implicit functions can depend on the inputs and outputs

\(y=5x-2\) is an example of an explicit equation; \(x\) is the input and \(y\) is the output. \(y\) only depends on \(x\); not on \(y\).

\(y^4+xy=0\) is an implicit equation. \(y\) depends on itself and \(x\).

Sometimes you can rearrange implicit functions into explicit ones, but you can always rearrange explicit functions to be implicit. An implicit function is the most general and inclusive form of a function. All systems can be written as a combination of implicit systems.

Section 3.3 in Engineering Design Optimization goes into more theoretical detail and background about how explicit and implicit systems differ, and how we can write all systems in general as implicit.

When to make an implicit model

Usually if there’s a real loop in the physical system this is a good hint for an implicit system. An example would be the coupling between aerodynamic and structures for aerostructural wing design. Even within a single discipline, though, you can easily have implicit systems.

A common example is solving a linear system. By posing it as an implicit system, you can solve for \(Ax-b=0\). OpenMDAO has a built-in LinearSystemComp that is a linear system.

In general, whenever there’s some dependence on surrounding values or residuals you will have an implicit system. That being said, there are times that your system may appear implicit but it can be rewritten as an explicit system. Because implicit systems require a solver, it might benefit you to write your systems as explicit if you can to reduce solver overhead. But, solving an implicit system might be your best bet and may well be necessary. It’s quite problem dependent.

An implicit component and equivalent explicit system

All right, let’s get to some code. I like looking at code to better understand math sometimes. Hopefully this helps you!

My colleague Jennifer Gratz had a great idea, like she often does. She suggested showing an implicit component and an equivalent implicit system comprised of explicit components. This is a neat idea because it shows that even if you only have explicit components in your model, you can still have implicit coupling due to the data passing between those models. Additionally, if you have an implicit system that can be broken down into constituent explicit components, you might benefit from that different setup.

The examples I show below are the same equations that are shown in the video lesson. Specifically, our two equations are \(y=x^2\) and \(x=3y\).

Let’s start with the implicit system. I’ll first define a singular implicit component and add it to an OpenMDAO problem, along with a solver, then run the model. This solves for the correct \(x\) and \(y\) values.

import openmdao.api as om


class NonlinearEquations(om.ImplicitComponent):
    def setup(self):
        self.add_output('x', val=1.0)
        self.add_output('y', val=1.0)
        self.declare_partials('*', '*')

    def apply_nonlinear(self, inputs, outputs, residuals):
        x = outputs['x']
        y = outputs['y']
        residuals['x'] = 3.0*x**2 - x
        residuals['y'] = 9.0*y**2 - y

    def linearize(self, inputs, outputs, jacobian):
        x = outputs['x']
        y = outputs['y']
        jacobian['x', 'x'] = 6.0*x - 1.0
        jacobian['y', 'y'] = 18.0*y - 1.0


p = om.Problem()
model = p.model

model.add_subsystem('implicit_component', NonlinearEquations())
model.nonlinear_solver = om.NewtonSolver(solve_subsystems=False)
model.linear_solver = om.DirectSolver()

p.setup()
p.run_model()
p.model.list_outputs()
NL: Newton Converged in 8 iterations
0 Explicit Output(s) in 'model'


2 Implicit Output(s) in 'model'

varname             val         
------------------  ------------
implicit_component
  x                 [0.33333333]
  y                 [0.11111111]
[('implicit_component.x', {'val': array([0.33333333])}),
 ('implicit_component.y', {'val': array([0.11111111])})]

Next, let’s break this one implicit component down into two separate explicit equations. Once we have these two components, we’ll need to add them to an OpenMDAO model and connect the \(x\) and \(y\) variables so the components correctly talk to each other. Again we’ll need a solver.

class SquareX(om.ExplicitComponent):
    def setup(self):
        self.add_input('x', val=1.0)
        self.add_output('y', val=1.0)
        self.declare_partials('y', 'x')

    def compute(self, inputs, outputs):
        x = inputs['x']
        outputs['y'] = x**2

    def compute_partials(self, inputs, partials):
        partials['y', 'x'] = 2 * inputs['x']

class TimesThreeY(om.ExplicitComponent):
    def setup(self):
        self.add_input('y', val=1.0)
        self.add_output('x', val=1.0)
        self.declare_partials('x', 'y', val=3.)

    def compute(self, inputs, outputs):
        y = inputs['y']
        outputs['x'] = 3.0*y


p = om.Problem()
model = p.model

model.add_subsystem('square_x', SquareX())
model.add_subsystem('times_three_y', TimesThreeY())

model.connect('square_x.y', 'times_three_y.y')
model.connect('times_three_y.x', 'square_x.x')

model.nonlinear_solver = om.NewtonSolver(solve_subsystems=False)
model.linear_solver = om.DirectSolver()

p.setup()
p.run_model()
p.model.list_outputs()
NL: Newton Converged in 6 iterations
2 Explicit Output(s) in 'model'

varname        val         
-------------  ------------
square_x
  y            [0.11111111]
times_three_y
  x            [0.33333333]


0 Implicit Output(s) in 'model'
[('square_x.y', {'val': array([0.11111111])}),
 ('times_three_y.x', {'val': array([0.33333333])})]

In both cases the solver converges to the correct values of \(x=1/3\) and \(y=1/9\). Although \(x\) and \(y\) are represented by same mathematical relationships, how you set up your model can differ. There are times when you must use implicit components to correctly model your system.

How to use implicit models correctly

Any time you have an ImplicitComponent from OpenMDAO, you have to resolve the implicitness by using nonlinear and linear solvers. Typically, you have to use a Newton solver to converge a system with an ImplicitComponent (unless you converge the residuals some other way). You cannot use a Nonlinear Block Gauss-Seidel solver alone in a system with an ImplicitComponent. This is because NLBGS is a cyclic solver that cannot resolve implicit states, whereas Newton can.

When you have an implicit model, you generally want to put the solver at the lowest level possible. What I mean by this is that you only want the solver wrapped around the subsystems of the model that are coupled or have implicit states. This minimizes computational overhead by not unnecessarily rerunning portions of the code that are not needed by the solver to converge the state values.

When working with complicated implicit models, you may need to use a nested system of solvers. At each level, you must ensure that your implicit states or coupling variables are converged properly. Otherwise, your results are not physically meaningful. Other lessons, such as Solving coupled systems go into more detail about how best to solve implicit systems.