# NonlinearBlockGS

## Contents

# NonlinearBlockGS¶

NonlinearBlockGS applies Block Gauss-Seidel (also known as fixed-point iteration) to the components and subsystems in the system. This is mainly used to solve cyclic connections. You should try this solver for systems that satisfy the following conditions:

System (or subsystem) contains a cycle, though subsystems may.

System does not contain any implicit states, though subsystems may.

NonlinearBlockGS is a block solver, so you can specify different nonlinear solvers in the subsystems and they will be utilized to solve the subsystem nonlinear problem.

Note that you may not know if you satisfy the second condition, so choosing a solver can be a trial-and-error proposition. If NonlinearBlockGS doesn’t work, then you will need to use NewtonSolver.

Here, we choose NonlinearBlockGS to solve the Sellar problem, which has two components with a cyclic dependency, has no implicit states, and works very well with Gauss-Seidel.

`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]])
```

`SellarDerivatives`

class definition

```
class SellarDerivatives(om.Group):
"""
Group containing the Sellar MDA. This version uses the disciplines with derivatives.
"""
def setup(self):
self.add_subsystem('d1', SellarDis1withDerivatives(), promotes=['x', 'z', 'y1', 'y2'])
self.add_subsystem('d2', SellarDis2withDerivatives(), promotes=['z', 'y1', 'y2'])
self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)', obj=0.0,
x=0.0, z=np.array([0.0, 0.0]), y1=0.0, y2=0.0),
promotes=['obj', 'x', 'z', 'y1', 'y2'])
self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1', con1=0.0, y1=0.0),
promotes=['con1', 'y1'])
self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0', con2=0.0, y2=0.0),
promotes=['con2', 'y2'])
self.set_input_defaults('x', 1.0)
self.set_input_defaults('z', np.array([5.0, 2.0]))
```

```
import numpy as np
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.setup()
prob.model.nonlinear_solver = om.NonlinearBlockGS()
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: NLBGS Converged in 8 iterations
[25.58830237]
[12.05848815]
```

This solver runs all of the subsystems each iteration, passing data along all connections including the cyclic ones. 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:

# NonlinearBlockGS Options¶

Option | Default | Acceptable Values | Acceptable Types | Description |
---|---|---|---|---|

aitken_initial_factor | 1.0 | N/A | N/A | initial value for Aitken relaxation factor |

aitken_max_factor | 1.5 | N/A | N/A | upper limit for Aitken relaxation factor |

aitken_min_factor | 0.1 | N/A | N/A | lower limit for Aitken relaxation factor |

atol | 1e-10 | N/A | N/A | absolute error tolerance |

cs_reconverge | True | [True, False] | ['bool'] | When True, when this driver solves under a complex step, nudge the Solution vector by a small amount so that it reconverges. |

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 |

reraise_child_analysiserror | False | [True, False] | ['bool'] | When the option is true, a solver will reraise any AnalysisError that arises during subsolve; when false, it will continue solving. |

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. |

use_aitken | False | [True, False] | ['bool'] | set to True to use Aitken relaxation |

use_apply_nonlinear | False | [True, False] | ['bool'] | Set to True to always call apply_nonlinear on the solver's system after solve_nonlinear has been called. |

## NonlinearBlockGS Constructor¶

The call signature for the `NonlinearBlockGS`

constructor is:

- NonlinearBlockGS.__init__(
**kwargs)[source]Initialize all attributes.

## Aitken relaxation¶

This solver implements Aitken relaxation, as described in Algorithm 1 of this paper on aerostructual design optimization. The relaxation is turned off by default, but it may help convergence for more tightly coupled models.

## Residual Calculation¶

The `Unified Derivatives Equations`

are formulated so that explicit equations (via `ExplicitComponent`

) are also expressed
as implicit relationships, and their residual is also calculated in “apply_nonlinear”, which runs the component a second time and
saves the difference in the output vector as the residual. However, this would require an extra call to `compute`

, which is
inefficient for slower components. To eliminate the inefficiency of running the model twice every iteration the NonlinearBlockGS
driver saves a copy of the output vector and uses that to calculate the residual without rerunning the model. This does require
a little more memory, so if you are solving a model where memory is more of a concern than execution time, you can set the
“use_apply_nonlinear” option to True to use the original formulation that calls “apply_nonlinear” on the subsystem.

## NonlinearBlockGS Option Examples¶

**maxiter**

`maxiter`

lets you specify the maximum number of Gauss-Seidel iterations to apply. In this example, we
cut it back from the default, ten, down to two, so that it terminates a few iterations earlier and doesn’t
reach the specified absolute or relative tolerance.

```
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.setup()
nlbgs = prob.model.nonlinear_solver = om.NonlinearBlockGS()
# basic test of number of iterations
nlbgs.options['maxiter'] = 1
prob.run_model()
print(prob.model.nonlinear_solver._iter_count)
```

```
NL: NLBGSSolver 'NL: NLBGS' on system '' failed to converge in 1 iterations.
1
```

```
nlbgs.options['maxiter'] = 5
prob.run_model()
print(prob.model.nonlinear_solver._iter_count)
```

```
NL: NLBGSSolver 'NL: NLBGS' on system '' failed to converge in 5 iterations.
5
```

```
# test of number of iterations AND solution after exit at maxiter
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
nlbgs.options['maxiter'] = 3
prob.set_solver_print()
prob.run_model()
print(prob.get_val('y1'))
print(prob.get_val('y2'))
print(prob.model.nonlinear_solver._iter_count)
```

```
NL: NLBGS Converged in 3 iterations
[25.58830237]
[12.05848815]
3
```

**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 one of two ways. If the “use_apply_nonlinear” option
is set to False (its default), then the norm is calculated by subtracting a cached previous value of the
outputs from the current value. If “use_apply_nonlinear” is True, then the norm is calculated by calling
apply_nonlinear on all of the subsystems. In this case, `ExplicitComponents`

are executed a second time.
If this norm value is lower than the absolute tolerance `atol`

, the iteration will terminate.

```
import numpy as np
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.setup()
nlbgs = prob.model.nonlinear_solver = om.NonlinearBlockGS()
nlbgs.options['atol'] = 1e-4
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: NLBGS Converged in 5 iterations
[25.5883027]
[12.05848818]
```

**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 one of two ways. If the “use_apply_nonlinear” option
is set to False (its default), then the norm is calculated by subtracting a cached previous value of the
outputs from the current value. If “use_apply_nonlinear” is True, then the norm is calculated by calling
apply_nonlinear on all of the subsystems. In this case, `ExplicitComponents`

are executed a second time.
If the ratio of the currently calculated norm to the initial residual norm is lower than the relative tolerance
`rtol`

, the iteration will terminate.

```
import numpy as np
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarDerivatives
prob = om.Problem(model=SellarDerivatives())
prob.setup()
nlbgs = prob.model.nonlinear_solver = om.NonlinearBlockGS()
nlbgs.options['rtol'] = 1e-3
prob.set_val('x', 1.)
prob.set_val('z', np.array([5.0, 2.0]))
prob.run_model()
print(prob.get_val('y1'), 25.5883027, .00001)
print(prob.get_val('y2'), 12.05848819, .00001)
```

```
NL: NLBGS Converged in 4 iterations
[25.58828563] 25.5883027 1e-05
[12.0584865] 12.05848819 1e-05
```