Exact connections with the null-space method#
The domains of a MultiDomain assembly were originally connected
with penalty stiffnesses, see MultiDomain.get_kC_conn() and
[castro2017Multidomain], still available with conn_method='penalty'.
The penalty method adds, for each quantity \(q_k\)
that must be continuous across a connection, the energy:
where \(k_k\) is the translation or rotation penalty constant, 'kt' or
'kr', and the integral is along the connected edges, or over the area of
the connection 'SB'. The continuity is then only approximate. Low penalty
constants make the assembly too flexible, and high ones make the stiffness
matrix ill-conditioned. The default constants of calc_kt_kr() are a
compromise, and they can still lead to errors of a few percent, see
Comparison with the penalty method.
With conn_method='null-space', the default, the connections are imposed
exactly, as with penalty constants going to infinity, without any penalty
constant:
md = MultiDomain(panels, conn) # conn_method='null-space'
and the penalty method is selected with:
md = MultiDomain(panels, conn, conn_method='penalty')
The connections that can be imposed with the null-space method are given in
nullspace.NULL_SPACE_FUNCS: 'SSxcte', 'SSycte',
'BFxcte', 'BFycte' and 'SB', for all the models, see
Quantities imposed. The damaged connection 'SB_TSL' represents
the physical stiffness of a cohesive interface, see Cohesive zone model of the multi-domain approach,
and it keeps its penalty stiffness also with conn_method='null-space'.
Theory#
The quantities \(q_k\) of Eq. (1) are linear combinations of the Ritz constants \(\{c\}\) of the two domains. Since Bardell’s functions are polynomials, see Bardell’s Hierarchical Functions, the difference \(q_k^{(1)} - q_k^{(2)}\) is a polynomial along the connection, of the natural coordinate \(\xi\) or \(\eta\) shared by the two domains. It vanishes everywhere when it vanishes at as many points as the number of its coefficients, and the connections become the homogeneous linear constraints:
where each row of \([B]\) is \(q_k^{(1)} - q_k^{(2)}\) at one Gauss-Legendre
point along the connection, or over the area of 'SB', see
nullspace.constraint_matrix(). The first four functions of Bardell
are cubic and the function \(i \ge 4\) has the order \(i\), so that
\(\max(m_1, m_2, 4)\) points are used along \(x\) and \(\max(n_1, n_2, 4)\) along
\(y\). The zero energy of Eq. (1) and the constraints of
Eq. (2) are equivalent: the vectors with zero penalty energy
are exactly those that satisfy the constraints, which is verified for every
connection and model in tests/multidomain/test_null_space.py.
Why not Lagrange multipliers#
The constraints could be imposed with Lagrange multipliers \(\{\lambda\}\), which add one unknown per constraint and give the system:
This matrix is indefinite. For the linear buckling and frequency analyses
the geometric stiffness matrix and the mass matrix have zero rows and
columns for the multipliers, giving infinite eigenvalues that the
shift-and-invert eigenvalue solvers of structsolve do not handle. Instead
of adding unknowns, the null-space method removes them, giving the same
solution.
Elimination of the constraints#
All the vectors that satisfy Eq. (2) are written with a basis \([T]\) of the null space of \([B]\), \([B] [T] = [0]\):
where \(\{c_r\}\) are the independent Ritz constants. Substituting Eq. (3) into the total potential energy gives the reduced system:
where \([K]\), \([K_G]\), \([M]\) and \(\{f\}\) are the matrices and vectors of the
domains, which do not contain the connections imposed exactly. The reduced
stiffness matrix is symmetric and positive definite whenever the connected
assembly has no rigid-body motion, like the stiffness matrix of the penalty
method, and the static, linear buckling, frequency and non-linear solvers of
structsolve are used unchanged. The constraints are linear in \(\{c\}\),
therefore \([T]\) does not change during a non-linear analysis:
\([T]\) is built by nullspace.null_space_basis(), one connection at a
time:
the rows of \([B]\) of the connection are normalized to a unit norm and written in the independent Ritz constants of the connections already processed, \([B] [T]\);
the rank of the constraints is revealed with a QR decomposition with column pivoting, \([B] [T] [P] = [Q] [R]\), and each pivot larger than the tolerance
tolgives one independent constraint;the Ritz constants of the pivot columns are eliminated, \(\{c_e\} = -[R_{11}]^{-1} [R_{12}] \{c_k\}\), where \(\{c_k\}\) are the remaining constants, and \([T]\) is updated.
Redundant constraints are found in step 2 and dropped. They happen for
instance at a corner shared by four domains, around a closed cylinder, or
where a boundary condition flag already removes the term that a constraint
would remove. Bardell’s functions of order 5 and higher vanish together with
their slope at the ends of the domain, \(\xi = \pm 1\), so the constraints of
an edge involve only the first four functions across it, and \([T]\) has very
few non-zero terms: the elimination is similar to the assembly of the
hierarchical elements of the p-version of the finite element method. A
connection along an interior line, e.g. 'BFycte' at the middle of the
base, involves all the functions across the line.
The terms removed by the boundary condition flags are zero rows and columns of the matrices of the domains, as with the penalty method, and remain in \(\{c_r\}\) with zero rows and columns in the reduced matrices.
\([T]\) is stored in MultiDomain.T and reused while the connections, the
number of terms and the boundary condition flags do not change, see
MultiDomain.get_T().
Quantities imposed#
The quantities imposed by each connection are those of the corresponding
penalty kernels, with p1 and p2 the first and second panels of the
connection. For 'SB', the two panels share the axes \(x, y, z\), \(z\) being
normal to their mid-surfaces, p1 is the panel on the positive side of the
interface along \(z\) and p2 the one on the negative side, e.g. a stiffener
base on the side of positive \(z\) of a skin is p1 and the skin p2,
see MultiDomain.get_kC_conn():
Connection |
Classical laminated plate theory (3 DOFs) |
Shear deformation theories (5 DOFs) |
|---|---|---|
|
\(u, v, w, w_{,x}\), see |
\(u, v, w, \phi_x, \Phi_y\), and \(w_{,x}\) for the TSDT, see
|
|
\(u, v, w, w_{,y}\), see |
\(u, v, w, \phi_x, \Phi_y\), and \(w_{,y}\) for the TSDT, see
|
|
\(u_1 = u_2\), \(v_1 = w_2\), \(w_1 = -v_2\), \(w_{1,y} - v_1/r_1 =
w_{2,y} - v_2/r_2\), see |
\(u_1 = u_2\), \(v_1 = w_2\), \(w_1 = -v_2\), \(\Phi_{y1} = \Phi_{y2}\) |
|
\(u_1 = w_2\), \(v_1 = v_2\), \(w_1 = -u_2\), \(w_{1,x} = w_{2,x}\), see
|
\(u_1 = w_2\), \(v_1 = v_2\), \(w_1 = -u_2\), \(\phi_{x1} = \phi_{x2}\) |
|
\(u_1 + d_{sb} w_{1,x} = u_2\), \(v_1 + d_{sb} w_{1,y} = v_2\), \(w_1 =
w_2\), with \(d_{sb} = h_1/2 + h_2/2\), see |
\(u, v, w\) at the interface, \(z = -h_1/2\) of |
where \(\Phi_y = \phi_y + v/r\) with the Sanders-Koiter kinematics and \(\Phi_y
= \phi_y\) otherwise, and the terms \(v/r\) of the classical laminated plate
theory are present only with the Sanders-Koiter kinematics. The keys
'kt' and 'kr' of the connections are not used, apart from 'kr'
of 'SB' with the shear deformation theories, which tells whether the
rotations are connected.
Usage#
The matrices and vectors computed by MultiDomain have the size of
all the Ritz constants and do not contain the connections imposed exactly:
solving them directly would leave the domains disconnected. They are reduced
with MultiDomain.reduce() before solving, and the solutions are expanded to
all the Ritz constants with MultiDomain.expand(), which are used by
the post-processing methods, e.g. MultiDomain.uvw(),
MultiDomain.stress() and MultiDomain.plot(). With
conn_method='penalty', \([T]\) is the identity matrix, so that the same
code runs with both methods.
Linear static analysis:
from structsolve import solve
md = MultiDomain(panels, conn)
kC = md.calc_kC()
fext = md.calc_fext()
c = md.expand(solve(md.reduce(kC), md.reduce(fext)))
Linear buckling analysis with the pre-buckling state of a static analysis:
from structsolve import lb, static
kC = md.reduce(md.calc_kC())
incs, cs = static(kC, md.reduce(md.calc_fext()))
c = md.expand(cs[0])
kG = md.reduce(md.calc_kG(c=c))
eigvals, eigvecs = lb(kC, kG)
eigvecs = md.expand(eigvecs)
Frequency analysis:
from structsolve import freq
eigvals, eigvecs = freq(md.reduce(md.calc_kC()), md.reduce(md.calc_kM()))
eigvecs = md.expand(eigvecs)
Geometrically non-linear analysis, with the functions of the reduced problem
given by MultiDomain.get_reduced_functions():
from structsolve import Analysis
an = Analysis(*md.get_reduced_functions())
an.static(NLgeom=True)
cs = [md.expand(c_r) for c_r in an.cs]
The functions create_cylinder(),
create_cylinder_blade_stiffened(), the analyses of
panels.multidomain.cylinder and
panels.multidomain.cylinder_blade_stiffened, and
tstiff2d_1stiff_freq(), tstiff2d_1stiff_compression() and
tstiff2d_1stiff_flutter() take the argument conn_method.
Comparison with the penalty method#
The results below are verified in tests/multidomain/test_null_space.py.
The penalty method is more flexible than the exact connections, and its
error decreases with the inverse of the penalty constants. For a thin plate
divided in 2x2 domains, with the classical laminated plate theory, the error
of the first frequencies is 1.3% with the default constants of
calc_kt_kr(), and it becomes 0.13%, 0.013% and 0.0013% with 10, 100 and
1000 times higher constants. The condition number of the stiffness matrix
grows with the penalty constants: for the 4-domain plate of
tests/multidomain/test_conn_4panels_kt_kr.py, the reduced stiffness matrix
of the null-space method has a condition number about 600 times lower than
the stiffness matrix of the penalty method with the default constants, and
\(6 \times 10^4\) times lower than with 100 times higher constants.
Case |
Penalty, default constants |
Penalty, higher constants |
Null-space |
|---|---|---|---|
Frequencies of a thick plate divided in 2x2 domains, FSDT, error with respect to the single domain (Noor 1973) |
\(7.8 \times 10^{-2}\) |
\(9.5 \times 10^{-5}\) (1000 times) |
\(3 \times 10^{-12}\) |
Laminates \([0/90]\) over \([0/90]\) with |
\(8 \times 10^{-6}\) (\(10^5\) times) |
\(6 \times 10^{-13}\) |
|
Same, large deflection with the von Karman kinematics |
\(10^{-15}\) |
||
Blade-stiffened cylinder, buckling load with the pre-buckling state of a static analysis, \(N_{xx}\) (N/m) |
-39993.8 |
-41694.9 (1000 times) |
-41696.7 |
Closed cylinder, buckling load \(N_{xx}\) (N/m) |
-47056.0 |
-47113.0 |
|
T-stiffened panel, first frequency (rad/s) |
48.2733 |
48.3858 (100 times) |
48.3875 |
The default penalty constants underestimate the buckling load of the blade-stiffened cylinder by 4%, whereas the higher constants converge to the result of the null-space method.
Prescribed displacements imposed exactly#
The prescribed displacements of Shell.add_point_pd(),
Shell.add_distr_pd_fixed_x() and Shell.add_distr_pd_fixed_y()
are imposed by default with the penalty method: their forces \(k u_p\) enter
the external force vector and the penalty stiffnesses, e.g. of
fkCpd(), fkCld_xcte() and fkCld_ycte(), must be added to
the stiffness matrix by the user. With pd_method='null-space' they are
imposed exactly, as inhomogeneous linear constraints eliminated together
with the connections, without any penalty constant:
md = MultiDomain(panels, conn, pd_method='null-space')
A single panel is wrapped in a MultiDomain without connections,
MultiDomain([shell], conn=[], pd_method='null-space'). Both settings are
independent: the connections may be imposed with either conn_method.
The default pd_method='penalty' keeps the previous behaviour.
The same methods of Shell define the prescribed displacements.
With pd_method='null-space' the values of the penalty constants are not
used: a component is prescribed when its penalty constant is neither
None nor zero and its value is given, the same rule as for the penalty
method, where a null penalty constant prescribes nothing. The rotations
\(\phi_x\) and \(\phi_y\) of the approximation functions of the model, \(-w_{,x}\)
and \(-w_{,y}\) for the classical laminated plate theory, the fields \(\phi_x\)
and \(\phi_y\) for the shear deformation theories, plus \(v/r\) in \(\phi_y\) with
the Sanders-Koiter kinematics, can also be prescribed, only with
pd_method='null-space', through the keyword arguments kphix,
phixp, kphiy and phiyp of Shell.add_point_pd(), or
kphix, funcphix, kphiy and funcphiy of the distributed ones.
For example, a uniform end-shortening \(\delta\) of a plate, incremented with
the load factor, with the lateral contraction free:
p.add_distr_pd_fixed_x(0., ku=1., funcu=lambda y: 0.)
p.add_distr_pd_fixed_x(p.a, ku=1., funcu=lambda y: -delta, cte=False)
p.add_point_pd(0., 0., 0, None, 1., 0., 0, None) # v = 0 at a corner
Theory#
The connections give the homogeneous constraints \([B_h] \{c\} = \{0\}\) of Eq. (2), and the prescribed displacements the inhomogeneous constraints:
where \(\{d_0\}\) collects the constant prescribed values, cte=True, and
\(\{d_1\}\) those scaled by the load factor \(\lambda\), cte=False. Each row
of \([B_d]\) is the prescribed component at a point, or at a Gauss-Legendre
point along a prescribed line, with max(n, 4) points along \(y\) for the
lines x = cte and max(m, 4) along \(x\) for the lines y = cte, see
nullspace.pd_constraint_matrix(). As for the connections, the
component along the line is a polynomial with that many coefficients: the
constraint holds along the whole line when the prescribed function is a
polynomial of degree up to max(n, 4) - 1, e.g. a uniform or a linear
end-shortening; otherwise it holds at the Gauss-Legendre points, the
prescribed function being interpolated by the approximation functions.
With \([B] = [B_h; B_d]\), every admissible configuration is
where \([T]\) spans the null space of \([B]\) and \(\{c_{p0}\}\), \(\{c_{p1}\}\)
are particular solutions, \([B] \{c_{p0}\} = \{0; d_0\}\) and \([B]
\{c_{p1}\} = \{0; d_1\}\), see MultiDomain.get_cp(). They are
obtained in the same elimination as \([T]\), see
nullspace.Elimination: the constraints are eliminated one block
at a time, the connections first, each block written in the independent
Ritz constants left by the previous ones, \([B] ([T] \{c_r\} + \{c_p\}) =
\{d\}\), and decomposed with a QR decomposition with column pivoting, which
gives both the new \([T]\) and the correction of \(\{c_p\}\) for the
right-hand side \(\{d\} - [B] \{c_p\}\). The data of the decompositions are
kept, such that \(\{c_{p0}\}\) and \(\{c_{p1}\}\) are computed again without
any decomposition when only the prescribed values change, e.g. in a loop
over the end-shortening; \([T]\) changes only with the positions and the
components prescribed.
Substituting Eq. (6) into the residual of the unconstrained
system, \(\{R(c, \lambda)\} = \lambda \{F_{ext}\} - \{F_{int}(c, \lambda)\}\),
and projecting it with \([T]^T\) gives the reduced problem solved by
structsolve:
\([K_{T, r}]\) is the exact Jacobian, and \(\partial R_r/\partial \lambda\)
depends on the configuration and on the load factor. structsolve 0.7.0
passes the load factor to calc_fext when it declares the keyword
argument lbd, and uses \(\partial R_r/\partial \lambda\) in the predictors
and in the constraint of the arc-length methods, and as a reference of the
relative convergence criterion, since under a prescribed displacement alone
both \([T]^T \{F_{ext}\}\) and \([T]^T \{F_{int}\}\) vanish at equilibrium, see
MultiDomain.get_reduced_functions().
The linear static analysis solves
see MultiDomain.linear_static(), the loads of calc_fext(inc=1.)
being scaled by \(\lambda\) as in the analyses of structsolve. The linear
buckling analysis about the pre-buckling state \(\{c(\lambda)\} = \{c_0\} +
\lambda \{c_1\}\), with \(\{c_0\}\) due to the constant prescribed
displacements and \(\{c_1\}\) due to the load and the incremented ones,
solves \(([K_r] + [K_{G, r}(c_0)] + \lambda [K_{G, r}(c_1)]) \{\phi_r\} =
\{0\}\), see MultiDomain.linear_buckling(). The modes satisfy the
homogeneous constraints, \(\{\phi\} = [T] \{\phi_r\}\): under displacement
control the prescribed components of the modes are zero, whereas under the
equivalent load they are free. For a flat plate the modes of the linear
buckling analysis are out of plane and both critical loads are equal, for a
cylinder, whose modes couple the membrane and bending displacements, the
critical end-shortening of a cylinder of 5 panels is 3.8% higher than the
end-shortening of the critical axial load with x2u=1.
Reactions#
With \(\{r(c, \lambda)\} = \{F_{int}(c)\} - \lambda \{F_{ext}\}\) the residual of the unconstrained system, the force applied by the supports on the structure, the generalized reaction work-conjugate to the set \(j\) of prescribed displacements, the constant or the incremented ones, is
see MultiDomain.reaction_pd(). It does not depend on the choice of
the particular solution, since \([T]^T \{r\} = \{0\}\) at equilibrium.
\(P_j \delta\) is the work of the reactions for the prescribed values
multiplied by \(\delta\): for a uniform end-shortening \(\delta\) of an edge
with the total axial reaction \(N\), \(P_1 = N \delta\), and \(P_1 = \partial
U/\partial \lambda\) is the force conjugate to the load factor of a
displacement-controlled analysis.
The resultant of the reactions along selected prescribed lines or points,
in the direction \(u\), \(v\) or \(w\) of the panels, is \(F_u = \{c_u\}^T \{r\}\),
with \(\{c_u\}\) a solution of the constraints for a unit translation \(u = 1\)
of the selected prescriptions that prescribe \(u\), all the other prescribed
values being zero, see MultiDomain.reaction_resultant(). A line whose
ends are shared with a prescription that is not selected, directly or
through a connection, has no resultant of its own, the force at the shared
corner being undetermined, and an error is raised: for instance, the total
axial force at the end of a cylinder made of several panels is the
resultant of all the lines of that end. The sign is that of the force
applied by the support on the structure, \(F_u = -N\) at the shortened end.
Redundant and inconsistent prescriptions#
As for the connections, a constraint that is a linear combination of the
previous ones is redundant and dropped, e.g. at the corner where a
prescribed edge meets a connection or another prescribed edge, or on a term
already removed by a boundary condition flag. A redundant constraint is
dropped only when its right-hand side is consistent, i.e. satisfied by the
particular solution within rhs_tol of MultiDomain.get_T(),
\(10^{-8}\) by default, relative to the largest prescribed value. Otherwise
MultiDomain.get_cp() raises a ValueError that names the
prescription, the panel, the boundary condition flags that restrain it, and
the connections and prescriptions imposed before it on the same or
connected panels:
a prescribed edge meeting another one with a different value at a corner, or across a connection;
a non-zero value on a term removed by a boundary condition flag, e.g. \(u = \delta\) along
x = awithx2u = 0: the message tells to set the flag to 1, whereas \(u = 0\) is consistent and dropped;the constant and the incremented sets must each be consistent by themselves, since the constraints hold for every \(\lambda\): the same line cannot have a constant and an incremented prescription of the same component;
a function that is not a polynomial, prescribed along a line whose end is restrained by a flag, is consistent only within the interpolation error, which may exceed
rhs_tolwith few terms.
API#
MultiDomain(..., pd_method='null-space')MultiDomain.get_T()andMultiDomain.get_cp(): \([T]\), \(\{c_{p0}\}\) and \(\{c_{p1}\}\), also in the attributesT,c_p0andc_p1MultiDomain.expand():expand(c_r, lbd)returns the configuration of Eq. (6),expand(c_r)returns \([T] \{c_r\}\), e.g. for the modesMultiDomain.get_reduced_functions(): the callables ofstructsolve.Analysisfor the non-linear analyses, Newton-Raphson and the arc-length methods, requiringstructsolve>=0.7.0. The linear static analysis ofAnalysis.static(NLgeom=False)cannot contain the constant prescribed displacements and raises an error, seeMultiDomain.linear_static()MultiDomain.linear_static()andMultiDomain.linear_buckling()MultiDomain.reaction_pd()andMultiDomain.reaction_resultant()MultiDomain.calc_fext()excludes the forces of the prescribed displacements withpd_method='null-space'
Example#
A simply supported plate under a uniform end-shortening, with the lateral edges free in-plane: linear buckling, then the post-buckling path under displacement control, with a small lateral load as imperfection:
import numpy as np
from structsolve import Analysis
from panels.shell import Shell
from panels.multidomain import MultiDomain
a, b, h = 0.3, 0.2, 1e-3
p = Shell(a=a, b=b, m=8, n=8, model='plate_clpt_donnell', stack=[0],
plyt=h, laminaprop=(70e9, 0.3))
for edge in ('x1', 'x2', 'y1', 'y2'):
setattr(p, edge + 'u', 1.) # u and v free at the edges, w = 0
setattr(p, edge + 'v', 1.)
md = MultiDomain([p], conn=[], pd_method='null-space')
def prescribe(delta):
p.clear_disps()
p.add_distr_pd_fixed_x(0., ku=1., funcu=lambda y: 0.)
p.add_distr_pd_fixed_x(a, ku=1., funcu=lambda y: -delta, cte=False)
p.add_point_pd(0., 0., 0, None, 1., 0., 0, None)
# linear buckling: the eigenvalue is the critical end-shortening
prescribe(1.)
eigvals, modes = md.linear_buckling(num_eigvalues=3)
delta_cr = eigvals[0]
# post-buckling up to three times the critical end-shortening
prescribe(3*delta_cr)
p.add_point_load(a/2, b/2, 0., 0., 2.)
an = Analysis(*md.get_reduced_functions())
an.NL_method = 'arc_length_riks'
an.maxArcLength = 100.
an.static(NLgeom=True)
cs = [md.expand(c_r, lbd) for c_r, lbd in zip(an.cs, an.increments)]
N = [-md.reaction_resultant(c, [(p, 'x', a)], lbd)[0]
for c, lbd in zip(cs, an.increments)]
The arc length of structsolve is scaled by the linear solution, which
has no out-of-plane displacement under an end-shortening, therefore the
post-buckling path is long in these units, and maxArcLength may need to
be increased. The tests of tests/multidomain/test_null_space_pd.py give
more examples, among them:
@pytest.mark.parametrize('layout', ['single', '2x2'])
@pytest.mark.parametrize('model', ['plate_clpt_donnell', 'plate_fsdt_donnell'])
def test_membrane_patch_test_isotropic(layout, model):
r"""Uniform end-shortening `\delta`, with the lateral contraction free:
exact uniform strain, `u = -\delta x/a`, `v = \nu \delta y/a`, and the
reactions `P = N \delta` and `F_u = -N`, `N = E h b \delta/a`"""
a, b, h, delta = 1., 0.6, 2e-3, 1e-3
if layout == 'single':
ps = [plate(a, b, h, model=model)]
conn = []
loaded, fixed = ps, ps
else:
ps, conn = two_by_two(a, b, h=h, model=model)
loaded, fixed = (ps[1], ps[3]), (ps[0], ps[2])
for p in fixed:
end_shortening(p, delta, x2=False)
for p in loaded:
end_shortening(p, delta, x1=False)
ps[0].add_point_pd(0., 0., 0, None, 1., 0., 0, None)
md = MultiDomain(ps, conn, pd_method='null-space')
c = md.linear_static()
for p in ps:
X, Y, f = fields(p, c)
assert np.allclose(f['u'], -delta*X/a, rtol=0, atol=1e-12*delta)
assert np.allclose(f['v'], NU*delta*Y/a, rtol=0, atol=1e-12*delta)
# w is decoupled, but its round-off is the membrane round-off
# amplified by the stiffness ratio, of order (a/h)**2
assert np.allclose(f['w'], 0, rtol=0, atol=1e-8*delta)
N = E*h*b*delta/a
P = md.reaction_pd(c, NLgeom=False)
assert np.isclose(P['inc'], N*delta, rtol=1e-10)
assert P['cte'] == 0.
F = md.reaction_resultant(c, [(p, 'x', p.a) for p in loaded],
NLgeom=False)
assert np.isclose(F[0], -N, rtol=1e-10)
assert np.all(np.isnan(F[1:]))
F = md.reaction_resultant(c, [(p, 'x', 0.) for p in fixed], NLgeom=False)
assert np.isclose(F[0], N, rtol=1e-10)
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
'arc_length_crisfield'])
def test_nonlinear_displacement_control(method):
r"""Post-buckling under displacement control: every converged increment
satisfies `\|[T]^T r\| \leq` ``relTOL`` `\|\lambda \partial R_r/\partial
\lambda\|`, the methods agree, and the reaction is below the linear one"""
md, p, delta = plate_postbuckling()
an = Analysis(*md.get_reduced_functions())
an.NL_method = method
an.initialInc = 0.2
#NOTE the arc length is scaled by the linear solution, without the
# out-of-plane deflection of the post-buckling path
an.maxArcLength = 100.
an.static(NLgeom=True, silent=True)
assert np.isclose(an.increments[-1], 1.)
fext, fint, kC, kG = md.get_reduced_functions()
T = md.get_T()
for c_r, lbd in zip(an.cs, an.increments):
c = md.expand(c_r, lbd)
r = md._reaction_residual(c, lbd, True)
q = fext(inc=1., c=c_r, lbd=lbd)
assert np.linalg.norm(T.T @ r) <= 1.01e-6*np.linalg.norm(lbd*q)
P = md.reaction_pd(c, 1.)['inc']/delta
N_lin = E*p.plyt*p.b*delta/p.a
assert 0.5 < P/N_lin < 0.7
assert np.isclose(P, 2420., rtol=2e-3)
Penalty vs null-space displacement control#
The penalty method imposes a prescribed displacement \(u_p\) with the energy
\(\frac{1}{2} k \int (u - u_p)^2\), i.e. a spring of stiffness \(k\) between the
structure and the support: the structure is more flexible, the prescribed
value is not reached, and the error decreases with \(1/k\), while the
condition number of the stiffness matrix grows with \(k\). The null-space
method has no penalty constant. The cases below were run with the models of
tests/multidomain/test_null_space_pd.py, the penalty stiffnesses being
those of fkCld_xcte() and fkCpd(); \(E h\) is the membrane
stiffness of the isotropic plate and \(A_{11}\) that of the laminate of the
cylinder.
Accuracy and conditioning, linear static. Plate under a uniform
end-shortening with the lateral contraction restrained along \(y = 0, b\),
m = n = 10, 300 Ritz constants, 280 independent ones with the null-space
method. Errors of the Ritz constants and of the axial reaction, and condition
number of the stiffness matrix; the reduced stiffness matrix of the
null-space method has the condition number \(1.4 \times 10^{11}\):
\(k / (E h)\) |
\(10\) |
\(10^2\) |
\(10^4\) |
\(10^6\) |
\(10^8\) |
\(10^{10}\) |
|---|---|---|---|---|---|---|
error of \(\{c\}\) |
\(1.6 \times 10^{-1}\) |
\(2.0 \times 10^{-2}\) |
\(2.0 \times 10^{-4}\) |
\(2.0 \times 10^{-6}\) |
\(2.0 \times 10^{-8}\) |
\(3.1 \times 10^{-9}\) |
error of the reaction |
\(-1.8 \times 10^{-1}\) |
\(-2.2 \times 10^{-2}\) |
\(-2.2 \times 10^{-4}\) |
\(-2.2 \times 10^{-6}\) |
\(-2.2 \times 10^{-8}\) |
\(-1.2 \times 10^{-6}\) |
condition number |
\(2.5 \times 10^{12}\) |
\(2.0 \times 10^{13}\) |
\(2.0 \times 10^{15}\) |
\(2.0 \times 10^{17}\) |
\(1.8 \times 10^{19}\) |
\(1.8 \times 10^{21}\) |
The reaction of the penalty method, \(k \int (u_p - u)\), is the product of a large constant and a small difference, and loses accuracy by cancellation for the highest constants.
Linear buckling. Cylinder of 5 panels under a uniform end-shortening,
see test_cylinder_end_shortening. The error of the critical
end-shortening of the penalty method is \(1.4 \times 10^{-1}\), \(1.3 \times
10^{-2}\), \(1.3 \times 10^{-3}\), \(1.3 \times 10^{-4}\) and \(1.3 \times
10^{-6}\) for \(k/A_{11} = 10, 10^2, 10^3, 10^4, 10^6\), from above, since the
penalty springs shorten the cylinder less than the prescribed value. Both
methods take about 3.8 s. With \(k/A_{11} = 10^6\) the sparse eigenvalue
solver of structsolve fails its verification of the residuals of the
eigenpairs, because of the conditioning, and structsolve.lb falls back to
the dense solver with a DenseFallbackWarning.
Non-linear analysis. Post-buckling of the plate at twice the critical
end-shortening, m = n = 8, Newton-Raphson with 8 increments; error of
the axial reaction of the penalty method with the default convergence
tolerance relTOL = 1e-6 and with relTOL = 1e-10:
\(k / (E h)\) |
\(10^2\) |
\(10^4\) |
\(10^6\) |
|---|---|---|---|
|
\(-3.6 \times 10^{-2}\) |
\(-5.5 \times 10^{-4}\) |
\(-2.1 \times 10^{-1}\) |
|
\(-3.6 \times 10^{-2}\) |
\(-3.8 \times 10^{-4}\) |
\(-1.9 \times 10^{-5}\) |
The relative convergence criterion of the penalty method is relative to the penalty forces \(k u_p\), which grow with \(k\): with the default tolerance, the highest penalty constant gives the largest error, 21%, after converging in fewer iterations. The null-space method has no such trade-off, its reference being \(\lambda \partial R_r/\partial \lambda\), see Prescribed displacements imposed exactly.
Run time. For the linear analyses both methods have the same cost: the
elimination of the constraints is a small fraction of the assembly. For the
non-linear analysis above the run time is dominated by the assembly of the
tangent stiffness matrix: 0.96 s and 1.29 s with the null-space method for
relTOL = 1e-6 and 1e-10, against 0.44 s and 0.90 s with the penalty
method and \(k/(E h) = 10^4\). The null-space method converges to a much
smaller residual in absolute terms. Its \(\partial R_r/\partial \lambda\)
requires the product \([K_T] \{c_{p1}\}\), computed without assembling
\([K_T]\), see Tangent stiffness matrix and its product by a vector, at the cost of one evaluation of the
internal force vector, and structsolve 0.8.0 evaluates it only together
with the tangent stiffness matrix. With panels 0.12.0 before these two
improvements and the tabulation of the approximation functions, see
Approximation functions tabulated at the integration points, the same analyses took 12.6 s and 15.8 s against 4.4 s
and 9.2 s.
When to use each method. The null-space method is exact, needs no
penalty constant, keeps the conditioning of the model, and gives reactions
that do not depend on any constant; it is recommended for displacement
control, e.g. the end-shortening of panels and shells, and for linear
buckling under prescribed displacements. The penalty method remains the
default for backward compatibility, and it is needed when the prescribed
displacement represents a physical stiffness, e.g. a load cell or a loading
fixture, or for models that rely on the penalty forms, e.g. the cohesive zone
examples of Cohesive zone model of the multi-domain approach, whose reaction is given by
MultiDomain.reaction_line_pd_xcte().
Limitations#
A connection imposed exactly constrains the domains exactly as the penalty method does with infinite penalty constants, which may constrain the approximation more than the penalty method with the default constants does:
When the two domains have a different number of terms along the connection, the terms of the trace of the richer domain that the other domain cannot represent are forced to vanish.
The boundary condition flags at the ends of an edge of one domain are imposed on the other domain.
With
'SB'and the classical laminated plate theory, \(u_1 = u_2 - d_{sb} w_{,x}\). Restraining the normal in-plane displacement of an edge in both domains, e.g. \(u = 0\) at \(x = 0\), also restrains the rotation \(w_{,x}\) at that edge, which is clamped. The in-plane displacements of the bonded laminates must be restrained consistently with the single laminate that they represent, e.g. restraining only the tangential displacement of the edges, as intest_skin_base_nonlinear_equals_single_laminate.
The prescribed displacements of Shell.add_point_pd(),
Shell.add_distr_pd_fixed_x() and Shell.add_distr_pd_fixed_y()
remain penalty stiffnesses by default, and are imposed exactly with
pd_method='null-space', see Prescribed displacements imposed exactly. The
traction-separation law of 'SB_TSL' remains a penalty stiffness.
The rank of the constraints is found with the tolerance tol of
MultiDomain.get_T(), 1e-9 by default, which applies to the
constraints normalized to a unit norm. The independent constraints have
pivots many orders of magnitude above the round-off errors of the redundant
ones, and a warning is issued when this gap is smaller than \(10^4\).
API#
The methods of MultiDomain for the null-space method are
MultiDomain.get_T(), MultiDomain.reduce(),
MultiDomain.expand() and MultiDomain.get_reduced_functions(),
and for the prescribed displacements MultiDomain.get_cp(),
MultiDomain.linear_static(), MultiDomain.linear_buckling(),
MultiDomain.reaction_pd() and MultiDomain.reaction_resultant(),
see Prescribed displacements imposed exactly.