NonlinearBlockJac#
NonlinearBlockJac is a nonlinear solver that uses the block Jacobi method to solve the system. When to choose this solver over NonlinearBlockGS is an advanced topic, but it is valid for systems that satisfy the same conditions:
System (or subsystem) contains a cycle, though subsystems may.
System does not contain any implicit states, though subsystems may.
Note that you may not know if you satisfy the second condition, so choosing a solver can be “trial and error.” If NonlinearBlockJac doesn’t work, then you will need to use NewtonSolver
The main difference over NonlinearBlockGS
is that data passing is delayed until after all subsystems have been
executed.
Here, we choose NonlinearBlockJac to solve the Sellar problem, which has two components with a cyclic dependency, has no implicit states, and works with Jacobi, although it fails to converge before hitting the default iteration limit of 10. In the next section we’ll show how to increase the number of allowed iterations so that it will converge.
SellarDis1withDerivatives
class definition
class SellarDis1withDerivatives(SellarDis1):
"""
Component containing Discipline 1 -- derivatives version.
"""
def setup_partials(self):
# Analytic Derivs
self.declare_partials(of='*', wrt='*')
def compute_partials(self, inputs, partials):
"""
Jacobian for Sellar discipline 1.
"""
partials['y1', 'y2'] = -0.2
partials['y1', 'z'] = np.array([[2.0 * inputs['z'][0], 1.0]])
partials['y1', 'x'] = 1.0
SellarDis2withDerivatives
class definition
class SellarDis2withDerivatives(SellarDis2):
"""
Component containing Discipline 2 -- derivatives version.
"""
def setup_partials(self):
# Analytic Derivs
self.declare_partials(of='*', wrt='*')
def compute_partials(self, inputs, J):
"""
Jacobian for Sellar discipline 2.
"""
y1 = inputs['y1']
if y1.real < 0.0:
y1 *= -1
if y1.real < 1e-8:
y1 = 1e-8
J['y2', 'y1'] = .5*y1**-.5
J['y2', 'z'] = np.array([[1.0, 1.0]])
import numpy as np
import openmdao.api as om
from openmdao.test_suite.components.sellar import SellarDis1withDerivatives, SellarDis2withDerivatives
prob = om.Problem()
model = prob.model
model.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
model.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
model.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
z=np.array([0.0, 0.0]), x=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
model.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes=['con1', 'y1'])
model.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes=['con2', 'y2'])
model.linear_solver = om.LinearBlockGS()
model.nonlinear_solver = om.NonlinearBlockJac()
prob.setup()
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
prob.run_model()
print(prob['y1'])
print(prob['y2'])
NL: NLBJSolver 'NL: NLBJ' on system '' failed to converge in 10 iterations.
[25.58830249]
[12.05848818]
This solver runs all of the subsystems each iteration, but just passes the data along all connections simultaneously once per iteration. After each iteration, the iteration count and the residual norm are checked to see if termination has been satisfied.
You can control the termination criteria for the solver using the following options:
NonlinearBlockJac Options#
Option | Default | Acceptable Values | Acceptable Types | Description |
---|---|---|---|---|
atol | 1e-10 | N/A | N/A | absolute error tolerance |
debug_print | False | [True, False] | ['bool'] | If true, the values of input and output variables at the start of iteration are printed and written to a file after a failure to converge. |
err_on_non_converge | False | [True, False] | ['bool'] | When True, AnalysisError will be raised if we don't converge. |
iprint | 1 | N/A | ['int'] | whether to print output |
maxiter | 10 | N/A | ['int'] | maximum number of iterations |
restart_from_successful | False | [True, False] | ['bool'] | If True, the states are cached after a successful solve and used to restart the solver in the case of a failed solve. |
rtol | 1e-10 | N/A | N/A | relative error tolerance |
stall_limit | 0 | N/A | N/A | Number of iterations after which, if the residual norms are identical within the stall_tol, then terminate as if max iterations were reached. Default is 0, which disables this feature. |
stall_tol | 1e-12 | N/A | N/A | When stall checking is enabled, the threshold below which the residual norm is considered unchanged. |
stall_tol_type | rel | ['abs', 'rel'] | N/A | Specifies whether the absolute or relative norm of the residual is used for stall detection. |
NonlinearBlockJac Constructor#
The call signature for the NonlinearBlockJac
constructor is:
- NonlinearBlockJac.__init__(**kwargs)
Initialize all attributes.
NonlinearBlockJac Option Examples#
maxiter
This lets you specify the maximum number of allowed Jacobi iterations during a solve. In this example, we increase it from the default, 10, up to 20, so that it successfully converges.
from openmdao.test_suite.components.sellar import SellarDis1withDerivatives, SellarDis2withDerivatives
prob = om.Problem()
model = prob.model
model.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
model.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
model.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
z=np.array([0.0, 0.0]), x=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
model.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes=['con1', 'y1'])
model.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes=['con2', 'y2'])
model.linear_solver = om.LinearBlockGS()
nlbgs = model.nonlinear_solver = om.NonlinearBlockJac()
nlbgs.options['maxiter'] = 20
prob.setup()
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
prob.run_model()
print(prob['y1'])
print(prob['y2'])
NL: NLBJ Converged in 13 iterations
[25.58830237]
[12.05848815]
atol
Here, we set the absolute tolerance to a looser value that will trigger an earlier termination. After
each iteration, the norm of the residuals is calculated by calling apply_nonlinear
on all of the components.
If this norm value is lower than the absolute
tolerance atol
, the iteration will terminate.
from openmdao.test_suite.components.sellar import SellarDis1withDerivatives, SellarDis2withDerivatives
prob = om.Problem()
model = prob.model
model.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
model.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
model.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
z=np.array([0.0, 0.0]), x=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
model.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes=['con1', 'y1'])
model.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes=['con2', 'y2'])
model.linear_solver = om.LinearBlockGS()
nlbgs = model.nonlinear_solver = om.NonlinearBlockJac()
nlbgs.options['atol'] = 1e-2
prob.setup()
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
prob.run_model()
print(prob['y1'])
print(prob['y2'])
NL: NLBJ Converged in 6 iterations
[25.58861716]
[12.05857185]
rtol
Here, we set the relative tolerance to a looser value that will trigger an earlier termination. After
each iteration, the norm of the residuals is calculated by calling apply_nonlinear
on all of the components.
If the ratio of the currently calculated norm to the
initial residual norm is lower than the relative tolerance rtol
, the iteration will terminate.
from openmdao.test_suite.components.sellar import SellarDis1withDerivatives, SellarDis2withDerivatives
prob = om.Problem()
model = prob.model
model.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
model.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
model.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
z=np.array([0.0, 0.0]), x=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
model.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes=['con1', 'y1'])
model.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes=['con2', 'y2'])
model.linear_solver = om.LinearBlockGS()
nlbgs = model.nonlinear_solver = om.NonlinearBlockJac()
nlbgs.options['rtol'] = 1e-3
prob.setup()
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
prob.run_model()
print(prob.get_val('y1'))
print(prob.get_val('y2'))
NL: NLBJ Converged in 5 iterations
[25.58914915]
[12.05691422]