Source code for openmdao.solvers.linear.linear_block_gs
"""Define the LinearBlockGS class."""
import numpy as np
from openmdao.solvers.solver import BlockLinearSolver
[docs]
class LinearBlockGS(BlockLinearSolver):
"""
Linear block Gauss-Seidel solver.
Parameters
----------
**kwargs : dict
Options dictionary.
Attributes
----------
_delta_d_n_1 : ndarray
Cached change in the d_output vectors for the previous iteration. Only used if the
aitken acceleration option is turned on.
_theta_n_1 : float
Cached relaxation factor from previous iteration. Only used if the aitken acceleration
option is turned on.
"""
SOLVER = 'LN: LNBGS'
[docs]
def __init__(self, **kwargs):
"""
Initialize all attributes.
"""
super().__init__(**kwargs)
self._theta_n_1 = None
self._delta_d_n_1 = None
def _declare_options(self):
"""
Declare options before kwargs are processed in the init method.
"""
super()._declare_options()
self.options.declare('use_aitken', types=bool, default=False,
desc='set to True to use Aitken relaxation')
self.options.declare('aitken_min_factor', default=0.1,
desc='lower limit for Aitken relaxation factor')
self.options.declare('aitken_max_factor', default=1.5,
desc='upper limit for Aitken relaxation factor')
self.options.declare('aitken_initial_factor', default=1.0,
desc='initial value for Aitken relaxation factor')
def _iter_initialize(self):
"""
Perform any necessary pre-processing operations.
Returns
-------
float
initial error.
float
error at the first iteration.
"""
if self.options['use_aitken']:
if self._mode == 'fwd':
self._delta_d_n_1 = self._system()._doutputs.asarray(copy=True)
else:
self._delta_d_n_1 = self._system()._dresiduals.asarray(copy=True)
self._theta_n_1 = 1.0
return super()._iter_initialize()
def _single_iteration(self):
"""
Perform the operations in the iteration loop.
"""
system = self._system()
mode = self._mode
use_aitken = self.options['use_aitken']
if use_aitken:
aitken_min_factor = self.options['aitken_min_factor']
aitken_max_factor = self.options['aitken_max_factor']
# some variables that are used for Aitken's relaxation
delta_d_n_1 = self._delta_d_n_1
theta_n_1 = self._theta_n_1
# store a copy of the outputs, used to compute the change in outputs later
if self._mode == 'fwd':
d_out_vec = system._doutputs
else:
d_out_vec = system._dresiduals
d_n = d_out_vec.asarray(copy=True)
delta_d_n = d_out_vec.asarray(copy=True)
relevance = system._relevance
if mode == 'fwd':
parent_offset = system._dresiduals._root_offset
for subsys in relevance.filter(system._all_subsystem_iter()):
# must always do the transfer on all procs even if subsys not local
system._transfer('linear', mode, subsys.name)
if not subsys._is_local:
continue
b_vec = subsys._dresiduals
scope_out, scope_in = system._get_matvec_scope(subsys)
# we use _vars_union to combine relevant variables from the current solve
# with those of the subsystem solve, because for recursive block linear solves
# we'll be skipping a direct call to _apply_linear and instead counting on
# _apply_linear to be called once at the bottom of the recursive block linear
# solve on the component, using the full set of relevant variables from the
# top group in the block linear solve and all intervening groups (assuming all
# of those groups are doing block linear solves).
scope_out = self._vars_union(self._scope_out, scope_out)
scope_in = self._vars_union(self._scope_in, scope_in)
off = b_vec._root_offset - parent_offset
if subsys._iter_call_apply_linear():
subsys._apply_linear(None, mode, scope_out, scope_in)
b_vec *= -1.0
b_vec += self._rhs_vec[off:off + len(b_vec)]
else:
b_vec.set_val(self._rhs_vec[off:off + len(b_vec)])
subsys._solve_linear(mode, scope_out, scope_in)
else: # rev
subsystems = list(relevance.filter(system._all_subsystem_iter()))
subsystems.reverse()
parent_offset = system._doutputs._root_offset
for subsys in subsystems:
if subsys._is_local:
b_vec = subsys._doutputs
b_vec.set_val(0.0)
system._transfer('linear', mode, subsys.name)
b_vec *= -1.0
off = b_vec._root_offset - parent_offset
b_vec += self._rhs_vec[off:off + len(b_vec)]
scope_out, scope_in = system._get_matvec_scope(subsys)
scope_out = self._vars_union(self._scope_out, scope_out)
scope_in = self._vars_union(self._scope_in, scope_in)
subsys._solve_linear(mode, scope_out, scope_in)
if subsys._iter_call_apply_linear():
subsys._apply_linear(None, mode, scope_out, scope_in)
else:
b_vec.set_val(0.0)
else: # subsys not local
system._transfer('linear', mode, subsys.name)
if use_aitken:
if self._mode == 'fwd':
d_resid_vec = system._dresiduals
d_out_vec = system._doutputs
else:
d_resid_vec = system._doutputs
d_out_vec = system._dresiduals
theta_n = self.options['aitken_initial_factor']
# compute the change in the outputs after the NLBGS iteration
delta_d_n -= d_out_vec.asarray()
delta_d_n *= -1
if self._iter_count >= 2:
# Compute relaxation factor. This method is used by Kenway et al. in
# "Scalable Parallel Approach for High-Fidelity Steady-State Aero-
# elastic Analysis and Adjoint Derivative Computations" (ln 22 of Algo 1)
temp = delta_d_n.copy()
temp -= delta_d_n_1
# If MPI, piggyback on the residual vector to perform a distributed norm.
if system.comm.size > 1:
backup_r = d_resid_vec.asarray(copy=True)
d_resid_vec.set_val(temp)
temp_norm = d_resid_vec.get_norm()
else:
temp_norm = np.linalg.norm(temp)
if temp_norm == 0.:
temp_norm = 1e-12 # prevent division by 0 below
# If MPI, piggyback on the output and residual vectors to perform a distributed
# dot product.
if system.comm.size > 1:
backup_o = d_out_vec.asarray(copy=True)
d_out_vec.set_val(delta_d_n)
tddo = d_resid_vec.dot(d_out_vec)
d_resid_vec.set_val(backup_r)
d_out_vec.set_val(backup_o)
else:
tddo = temp.dot(delta_d_n)
theta_n = theta_n_1 * (1 - tddo / temp_norm ** 2)
else:
# keep the initial the relaxation factor
pass
# limit relaxation factor to the specified range
theta_n = max(aitken_min_factor, min(aitken_max_factor, theta_n))
# save relaxation factor for the next iteration
self._theta_n_1 = theta_n
d_out_vec.set_val(d_n)
# compute relaxed outputs
d_out_vec += theta_n * delta_d_n
# save update to use in next iteration
delta_d_n_1[:] = delta_d_n