Prescribed displacements imposed exactly#
A prescribed displacement can be imposed exactly as a linear constraint on the degrees of freedom, \(\{c\} = [T]\{c_r\} + \{c_{p0}\} + \lambda \{c_{p1}\}\), where \([T]\) is a basis of the null space of the constraints and \(\{c_{p0}\}\), \(\{c_{p1}\}\) are particular solutions for the constant and the incremented prescribed values, as done by the null-space method of panels. The reduced residual is
and its derivative with respect to the load factor,
depends on the configuration and on the load factor. calc_fext receives
both when it declares the keyword arguments c and lbd, see
callbacks. Under a prescribed displacement alone, \(F_{ext} = 0\), the
reference vector calc_fext(inc=1.) and the reduced internal force vector
both vanish at equilibrium, and the solvers use \(\partial R/\partial
\lambda\) for the displacement scale of the arc-length methods, the predictor
of the first Newton-Raphson increment and the relative convergence
criterion.
Three hardening springs in series, with the displacement of the last node prescribed:
class SpringsProblem(object):
def __init__(self, k=(100., 150., 200.), a=(20., 40., 60.), d0=0.,
d1=1., F=0., with_lbd=True):
self.k = np.asarray(k)
self.a = np.asarray(a)
self.cp0 = np.array([0., 0., d0])
self.cp1 = np.array([0., 0., d1])
self.Fext = np.array([F, 0., 0.])
self.with_lbd = with_lbd
self.an = None
# (lbd, c_r) of each call to calc_fext with lbd, and (inc, ||R_r||,
# number of converged increments) of each call to calc_fint
self.fext_calls = []
self.fint_calls = []
def full(self, c_r, lbd):
return T @ c_r + self.cp0 + lbd*self.cp1
def elongations(self, c):
return np.array([c[0], c[1] - c[0], c[2] - c[1]])
def fint_full(self, c):
k, a = self.k, self.a
f = k*self.elongations(c) + a*self.elongations(c)**3
return np.array([f[0] - f[1], f[1] - f[2], f[2]])
def kT_full(self, c):
ke = self.k + 3*self.a*self.elongations(c)**2
return np.array([[ke[0] + ke[1], -ke[1], 0.],
[-ke[1], ke[1] + ke[2], -ke[2]],
[0., -ke[2], ke[2]]])
def residual(self, c_r, lbd):
return T.T @ (lbd*self.Fext - self.fint_full(self.full(c_r, lbd)))
def dR_dlbd(self, c_r, lbd):
c = self.full(c_r, lbd)
return T.T @ (self.Fext - self.kT_full(c) @ self.cp1)
def reaction(self, c_r, lbd):
r"""Force of the support on node 3, `c_{p1}^T r / d_1`, with `r =
F_{int}(c) - \lambda F_{ext}`"""
c = self.full(c_r, lbd)
r = self.fint_full(c) - lbd*self.Fext
return self.cp1 @ r/self.cp1[2]
# callables of structsolve.Analysis
def calc_fext(self, inc=1., silent=True, c=None, lbd=None):
if c is None:
return inc*(T.T @ self.Fext)
self.fext_calls.append((lbd, c.copy()))
return inc*self.dR_dlbd(c, lbd)
def calc_fint(self, c, inc=1., silent=True):
step = len(self.an.increments) if self.an is not None else 0
self.fint_calls.append((inc, np.linalg.norm(self.residual(c, inc)),
step))
return T.T @ self.fint_full(self.full(c, inc))
def calc_kC(self, c=None, NLgeom=False, inc=1., silent=True):
if c is None or not NLgeom:
return csr_matrix(T.T @ self.kT_full(np.zeros(3)) @ T)
return csr_matrix(T.T @ self.kT_full(self.full(c, inc)) @ T)
def calc_kG(self, c=None, NLgeom=False, silent=True):
return csr_matrix((2, 2))
def analysis(self, **kwargs):
if self.with_lbd:
calc_fext = self.calc_fext
else:
calc_fext = lambda inc=1., silent=True: self.calc_fext(inc)
an = Analysis(calc_fext, self.calc_fint, self.calc_kC, self.calc_kG)
for key, value in kwargs.items():
setattr(an, key, value)
self.an = an
return an
def exact(self, lbd):
r"""Solution without the load on node 1: the three springs carry the
same force `P`, found from `\sum_i e_i(P) = x_3`"""
assert self.Fext[0] == 0.
x3 = self.cp0[2] + lbd*self.cp1[2]
def e(P, i):
return brentq(lambda ei: self.k[i]*ei + self.a[i]*ei**3 - P,
-1e4, 1e4, xtol=1e-15, rtol=1e-15)
def gap(P):
return sum(e(P, i) for i in range(3)) - x3
P = 0. if x3 == 0 else brentq(gap, -1e4, 1e4, xtol=1e-14,
rtol=1e-15)
e1, e2 = e(P, 0), e(P, 1)
return np.array([e1, e1 + e2]), P
def solve_reference(self, lbd, tol=1e-14):
"""Dense Newton-Raphson with continuation on the load factor"""
c_r = np.zeros(2)
for l in np.linspace(0, lbd, 21)[1:]:
for _ in range(100):
R = self.residual(c_r, l)
if np.linalg.norm(R) <= tol*max(1., np.linalg.norm(
self.dR_dlbd(c_r, l))):
break
kT = T.T @ self.kT_full(self.full(c_r, l)) @ T
c_r = c_r + np.linalg.solve(kT, R)
return c_r
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
'arc_length_crisfield'])
def test_displacement_control_relative_criterion(method):
r"""The prescribed displacement alone, with the relative convergence
criterion only: every converged increment is the exact solution, and
``calc_fext`` receives the load factor of the configuration"""
pb = SpringsProblem(d1=0.8)
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-10,
absTOL=None)
an.static(NLgeom=True, silent=True)
assert np.isclose(an.increments[-1], 1.)
assert len(an.increments) > 2
for lbd, c_r in zip(an.increments, an.cs):
ref, P = pb.exact(lbd)
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
assert np.isclose(pb.reaction(c_r, lbd), P, rtol=1e-8)
assert (np.linalg.norm(pb.residual(c_r, lbd))
<= 1e-9*np.linalg.norm(lbd*pb.dR_dlbd(c_r, lbd)))
# the arc-length solvers evaluate dR/dlbd at the configurations and load
# factors of their iterations
if method != 'NR':
assert len(pb.fext_calls) > len(an.increments)
lbds = [lbd for lbd, _ in pb.fext_calls]
assert lbds[0] == 0.
assert all(lbd is not None for lbd in lbds)
assert len(set(lbds)) > 2
A constant prescribed displacement together with an incremented one and a load:
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
'arc_length_crisfield'])
def test_constant_and_incremented_prescriptions(method):
r"""A constant prescribed displacement `d_0`, held during the analysis,
with a load `\lambda F` on node 1 and an incremented displacement
`\lambda d_1`"""
pb = SpringsProblem(d0=-0.3, d1=0.6, F=40.)
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-11)
an.static(NLgeom=True, silent=True)
assert np.isclose(an.increments[-1], 1.)
for lbd, c_r in zip(an.increments, an.cs):
ref = pb.solve_reference(lbd)
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
# at lbd = 0 the constant displacement alone is not in equilibrium with
# c_r = 0: the first increment starts from a non-null residual
assert np.linalg.norm(pb.residual(np.zeros(2), 0.)) > 1.