Nonlinear static analysis with structsolve#

The callables of structsolve.Analysis, of structsolve, for the cylinder of Nonlinear static analysis, see Solving with structsolve. They loop over the elements with update_fint(), update_KCNL() and update_KG(), assemble, and work on the unknown degrees of freedom bu, the partition of the free degrees of freedom:

class Callables(object):
    """The callables of structsolve.Analysis for the ``elements`` of a model
    under the external forces ``fext``.

    They loop over the elements with the update functions and assemble, and
    work on the unknown degrees of freedom ``bu`` only, the partition of the
    free dofs of structsolve: the solution vector of the solvers is ``c =
    u[bu]``, and the force vectors and matrices are restricted to ``bu``.
    The prescribed degrees of freedom are zero.
    """
    def __init__(self, elements, points, weights, KC0, bu, fext):
        self.elements = elements
        self.points = points
        self.weights = weights
        self.N = KC0.shape[0]
        self.KC0 = KC0
        self.KC0uu = KC0[bu, :][:, bu]
        self.bu = bu
        self.fext = fext
        num_elements = len(elements)
        self.KCNLr = np.zeros(KCNL_SPARSE_SIZE*num_elements, dtype=INT)
        self.KCNLc = np.zeros(KCNL_SPARSE_SIZE*num_elements, dtype=INT)
        self.KCNLv = np.zeros(KCNL_SPARSE_SIZE*num_elements, dtype=DOUBLE)
        self.KGr = np.zeros(KG_SPARSE_SIZE*num_elements, dtype=INT)
        self.KGc = np.zeros(KG_SPARSE_SIZE*num_elements, dtype=INT)
        self.KGv = np.zeros(KG_SPARSE_SIZE*num_elements, dtype=DOUBLE)

    def expand(self, c):
        """The displacements of the model from those of the unknown dofs."""
        u = np.zeros(self.N, dtype=DOUBLE)
        u[self.bu] = c
        return u

    def calc_fext(self, inc=1., silent=False):
        return inc*self.fext[self.bu]

    def calc_fint(self, c, silent=False):
        fint = np.zeros(self.N, dtype=DOUBLE)
        u = self.expand(c)
        for shell in self.elements:
            update_fint(u, shell, self.points, self.weights, fint)
        return fint[self.bu]

    def calc_kC(self, c=None, NLgeom=False, silent=False):
        if not NLgeom:
            return self.KC0uu
        # the generated code accumulates with "+=" over the integration
        # points, so the value arrays must be zeroed on every call. The row
        # and column arrays are rewritten identically each time, and could
        # be filled once for a fixed mesh
        self.KCNLv *= 0
        u = self.expand(c)
        for shell in self.elements:
            update_KCNL(u, shell, self.points, self.weights, self.KCNLr,
                        self.KCNLc, self.KCNLv)
        KCNL = coo_matrix((self.KCNLv, (self.KCNLr, self.KCNLc)),
                          shape=(self.N, self.N)).tocsc()
        # KCNL also carries the geometric term of the nonlinear membrane
        # stress, so that KG stays homogeneous of degree one in u and keeps
        # its linear buckling meaning. KC0 + KCNL + KG is d(fint)/du
        return (self.KC0 + KCNL)[self.bu, :][:, self.bu]

    def calc_kG(self, c=None, NLgeom=False, silent=False):
        self.KGv *= 0
        u = self.expand(c)
        for shell in self.elements:
            update_KG(u, shell, self.points, self.weights, self.KGr,
                      self.KGc, self.KGv)
        KG = coo_matrix((self.KGv, (self.KGr, self.KGc)),
                        shape=(self.N, self.N)).tocsc()
        return KG[self.bu, :][:, self.bu]

The analysis, at the end of the test of Nonlinear static analysis, applies the load in one increment, Analysis.initialInc = 1., with at most seven corrections, Analysis.maxNumIter = 7, and checks that the increment was not cut:

callables = Callables(elements, points, weights, KC0, bu, fext)
an = Analysis(calc_fext=callables.calc_fext,
              calc_fint=callables.calc_fint,
              calc_kC=callables.calc_kC,
              calc_kG=callables.calc_kG)
an.initialInc = 1.
# at most 7 corrections, otherwise structsolve cuts the load step
an.maxNumIter = 7
increments, cs = an.static(NLgeom=True)
if increments != [1.]:
    raise RuntimeError('Not converged!')

The solution is u = callables.expand(cs[-1]). With the default convergence criterion of Crisfield, \(\zeta \le 10^{-9}\), the iterations give \(\zeta\) = 2.5e-2, 2.2e-4, 2.0e-9 and then the round-off, about 1e-15, the quadratic convergence of a consistent tangent stiffness matrix.

Follower pressure#

The callables of a follower pressure, FollowerCallables, which receive the load factor inc, are given in Solving with structsolve. A cylinder clamped at one end and free at the other, with an unsymmetric load stiffness matrix, solved with these callables for both kinematics, checking the equilibrium with the whole follower pressure and the quadratic convergence of the Newton-Raphson iterations:

@pytest.mark.parametrize('kinematics', sorted(KINEMATICS))
def test_clamped_free_cylinder(kinematics):
    r"""Cylinder of radius 1 m, length 1 m and thickness 10 mm, clamped at
    `x = 0` and free at `x = L`, under the external follower pressure `p =
    -2 (1 + \cos(3 y/R)/2)` bar, in two equal load increments"""
    R, L, h = 1., 1., 0.01
    prop = isotropic_plate(thickness=h, E=E, nu=NU)
    mod, elements, points, weights, x, y, N = cylinder_model(kinematics, L,
            R, prop, nx=7, ny=24)
    DOF = mod.DOF
    bk = np.zeros(N, dtype=bool)
    for i in range(DOF):
        bk[i::DOF] |= isclose(x, 0)
    bu = ~bk
    nint = len(points)
    # p[i, j] at xi = points[i] and eta = points[j], from the circumferential
    # coordinate of node 1 of each element
    pressures = []
    for shell in elements:
        yj = y[shell.n1] + shell.ley*(points + 1)/2
        p = -2.e5*(1 + 0.5*np.cos(3*yj/R))
        pressures.append(np.ascontiguousarray(np.ones((nint, 1))*p[None, :]))

    callables = FollowerCallables(mod, elements, points, weights, N, bu,
                                  pressures)
    an = Analysis(calc_fext=callables.calc_fext,
                  calc_fint=callables.calc_fint,
                  calc_kC=callables.calc_kC,
                  calc_kG=callables.calc_kG)
    an.initialInc = 0.5
    an.maxInc = 0.5
    increments, cs = an.static(NLgeom=True)
    # every increment converged, none was cut
    assert np.allclose(increments, [0.5, 1.]), increments
    u = callables.expand(cs[-1])
    print('max |w|/h', np.abs(u[6::DOF]).max()/h)

    # equilibrium with the whole follower pressure, F_p(u) of the pressure p
    fint = np.zeros(N)
    fp = np.zeros(N)
    for shell, p in zip(elements, pressures):
        mod.update_fint(u, shell, points, weights, fint)
        mod.update_fext_follower(u, shell, points, weights, p, fp)
    residual = np.linalg.norm((fint - fp)[bu])/np.linalg.norm(fp[bu])
    print('relative residual', residual)
    assert residual < 1.e-8
    # the follower part of the load is not negligible
    assert np.linalg.norm((fp - callables.fp0)[bu]) > 1.e-3*np.linalg.norm(
            callables.fp0[bu])

    # the free end makes the load stiffness matrix unsymmetric
    KC = callables.calc_kC(cs[-1], NLgeom=True, inc=1.)
    KCnl = callables.calc_kC(cs[-1], NLgeom=True, inc=0.)
    KCf = (KC - KCnl).toarray()
    asym = np.abs(KCf - KCf.T).max()/np.abs(KCf).max()
    print('asymmetry of the load stiffness matrix', asym)
    assert asym > 1.e-3

    # quadratic convergence in each increment: Crisfield's zeta, the
    # criterion of structsolve, is squared by the last iteration, down to
    # the round-off
    w = crisfield_weights(callables.KC0[bu, :][:, bu])
    f = callables.fp0[bu]
    for lbd in increments:
        history = [crisfield_zeta(inc*f - fi, inc*f, fi, w)
                   for inc, fi in callables.iterations if inc == lbd]
        print('load factor', lbd, 'zeta', history)
        assert history[-1] < 1.e-9
        assert len(history) <= 6
        prev, curr = history[-2], history[-1]
        assert curr < max(10.*prev**2, 1.e-13), history