Non-linear static analysis with the Newton-Raphson method#
A non-linear static analysis with Analysis requires four callables,
which calculate the external force vector, the internal force vector, the
constitutive stiffness matrix and the geometric stiffness matrix. The tangent
stiffness matrix \([K_T] = [K_C] + [K_G]\) should be the exact derivative of the
internal force vector, which gives quadratic convergence to the Newton-Raphson
method, described in newton_raphson._solver_NR().
The class below, used in the structsolve unit tests, wraps analytic
problems into these callables:
class Problem(object):
"""Wrap analytic callbacks and record every call to ``calc_fint``
Each recorded call stores the number of increments already accepted by
the solver, allowing the iterations of each step to be counted.
"""
def __init__(self, fext, fint, kC, kG):
self.fext = fext
self.fint = fint
self.kC = kC
self.kG = kG
self.an = None
self.fint_calls = []
def calc_fext(self, inc=1., silent=True):
return inc*self.fext
def calc_fint(self, c, silent=True):
self.fint_calls.append((len(self.an.increments), c.copy()))
return self.fint(c)
def calc_kC(self, c=None, NLgeom=False, silent=True):
return csc_matrix(self.kC)
def calc_kG(self, c=None, NLgeom=False, silent=True):
return csc_matrix(self.kG(c))
def analysis(self, **kwargs):
an = Analysis(calc_fext=self.calc_fext, calc_fint=self.calc_fint,
calc_kC=self.calc_kC, calc_kG=self.calc_kG)
for k, v in kwargs.items():
setattr(an, k, v)
self.an = an
return an
def calls_per_step(self):
"""Number of ``calc_fint`` calls spent to reach each accepted step"""
steps = [s for s, _ in self.fint_calls]
return [steps.count(i) for i in range(len(self.an.increments))]
def solve_reference(self, lbd=1., c0=None, tol=1e-15):
"""Plain dense Newton-Raphson to obtain a reference solution"""
c = np.linalg.solve(self.kC, lbd*self.fext) if c0 is None else c0
for _ in range(100):
R = lbd*self.fext - self.fint(c)
if np.linalg.norm(R) <= tol*np.linalg.norm(lbd*self.fext):
return c
c = c + np.linalg.solve(self.kC + self.kG(c), R)
raise RuntimeError('reference solution did not converge')
One of these problems is a chain of springs with a cubic hardening, where \(\{F_{int}\} = [K]\{u\} + a \{u\}^3\):
def hardening_springs(P=50.):
"""Chain of 4 springs with cubic hardening: ``fint = K u + a u**3``"""
n = 4
K = 100.*(2*np.eye(n) - np.eye(n, k=1) - np.eye(n, k=-1))
K[-1, -1] = 100.
a = 50.*np.arange(1, n+1)
fext = np.zeros(n)
fext[-1] = P
return Problem(fext, lambda u: K @ u + a*u**3, K,
lambda u: np.diag(3*a*u**2))
The full load is applied in a single step, starting from the linear solution, and the order of convergence of the iterations is verified:
def test_nr_full_load_quadratic_convergence():
p = hardening_springs()
p.analysis(initialInc=1.)
p.an.static(NLgeom=True, silent=True)
check_increments_and_equilibrium(p)
assert len(p.an.increments) == 1
ref = p.solve_reference()
errors = [np.linalg.norm(c - ref)/np.linalg.norm(ref)
for _, c in p.fint_calls]
orders = convergence_orders(errors)
assert len(orders) >= 1, errors
assert min(orders) > 1.6, (orders, errors)
# plain Newton from the linear predictor needs 9 residual evaluations to
# reach machine precision; relTOL requires at most that
assert len(p.fint_calls) <= 9
The analysis can also be performed in increments, with initialInc defining
the size of the first load increment:
def test_nr_incremental_hardening():
p = hardening_springs()
p.analysis(initialInc=0.1)
p.an.static(NLgeom=True, silent=True)
check_increments_and_equilibrium(p)
# a good predictor with an exact tangent needs few iterations per step
assert max(p.calls_per_step()) <= 5, p.calls_per_step()