structsolve 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structsolve-0.7.0/structsolve.egg-info → structsolve-0.8.0}/PKG-INFO +4 -2
- {structsolve-0.7.0 → structsolve-0.8.0}/README.md +3 -1
- {structsolve-0.7.0 → structsolve-0.8.0}/pyproject.toml +1 -1
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/analysis.py +9 -2
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/arc_length.py +22 -45
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/callbacks.py +7 -1
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/linear_buckling.py +68 -29
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/newton_raphson.py +53 -29
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/static.py +32 -1
- {structsolve-0.7.0 → structsolve-0.8.0/structsolve.egg-info}/PKG-INFO +4 -2
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve.egg-info/SOURCES.txt +1 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_linear_buckling.py +1 -1
- structsolve-0.8.0/tests/test_reuse.py +174 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/LICENSE +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/setup.cfg +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/__init__.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/arpackutils.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/freq.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/logger.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve/sparseutils.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_analysis.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_arc_length.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_arpackutils.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_follower_freq_static.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_follower_loads.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_freq.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_linear_buckling_unsymmetric.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_load_factor_dependent_load.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_sparse_first.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_sparseutils.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_static.py +0 -0
- {structsolve-0.7.0 → structsolve-0.8.0}/tests/test_static_deflection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -65,7 +65,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
65
65
|
Citing this library
|
|
66
66
|
===================
|
|
67
67
|
|
|
68
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.
|
|
68
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.8.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
Documentation
|
|
@@ -80,6 +80,8 @@ History
|
|
|
80
80
|
|
|
81
81
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
82
82
|
|
|
83
|
+
* version 0.8.0 (2026-10-14)
|
|
84
|
+
- Fewer evaluations and factorizations, same solutions: `dR/dlbd` is evaluated together with the tangent stiffness matrix only, Newton-Raphson reuses the factorization of the tangent while it is not updated, the follower linear static analysis factorizes `K0` once, and `lb` estimates its shift and the inertia of `K` once for all its attempts
|
|
83
85
|
* version 0.7.0 (2026-10-13)
|
|
84
86
|
- `calc_fext` may declare the keyword argument `lbd` and then receives the current load factor with the configuration, for prescribed displacements imposed exactly as linear constraints; the solvers use `dR/dlbd` for the arc-length scale, the first Newton-Raphson predictor and the relative convergence criterion. Callables without `lbd` behave as before
|
|
85
87
|
* version 0.6.1 (2026-10-08)
|
|
@@ -29,7 +29,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
29
29
|
Citing this library
|
|
30
30
|
===================
|
|
31
31
|
|
|
32
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.
|
|
32
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.8.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
Documentation
|
|
@@ -44,6 +44,8 @@ History
|
|
|
44
44
|
|
|
45
45
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
46
46
|
|
|
47
|
+
* version 0.8.0 (2026-10-14)
|
|
48
|
+
- Fewer evaluations and factorizations, same solutions: `dR/dlbd` is evaluated together with the tangent stiffness matrix only, Newton-Raphson reuses the factorization of the tangent while it is not updated, the follower linear static analysis factorizes `K0` once, and `lb` estimates its shift and the inertia of `K` once for all its attempts
|
|
47
49
|
* version 0.7.0 (2026-10-13)
|
|
48
50
|
- `calc_fext` may declare the keyword argument `lbd` and then receives the current load factor with the configuration, for prescribed displacements imposed exactly as linear constraints; the solvers use `dR/dlbd` for the arc-length scale, the first Newton-Raphson predictor and the relative convergence criterion. Callables without `lbd` behave as before
|
|
49
51
|
* version 0.6.1 (2026-10-08)
|
|
@@ -2,7 +2,7 @@ from __future__ import absolute_import
|
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
|
-
from .static import solve
|
|
5
|
+
from .static import solve, _factorize
|
|
6
6
|
from .logger import msg, warn
|
|
7
7
|
from . import callbacks
|
|
8
8
|
from .newton_raphson import _check_convergence
|
|
@@ -302,6 +302,8 @@ class Analysis(object):
|
|
|
302
302
|
configuration-dependent load, see :meth:`.static`"""
|
|
303
303
|
msg('Configuration-dependent load: load correction iterations',
|
|
304
304
|
level=2, silent=silent)
|
|
305
|
+
# k0 is factorized once for all the corrections
|
|
306
|
+
solve_k0 = None
|
|
305
307
|
for iteration in range(1, self.maxNumIter + 1):
|
|
306
308
|
fext = self.calc_fext(inc=1., c=c, silent=True)
|
|
307
309
|
fint = k0 @ c
|
|
@@ -313,7 +315,12 @@ class Analysis(object):
|
|
|
313
315
|
return c
|
|
314
316
|
if not np.isfinite(Rnorm):
|
|
315
317
|
break
|
|
316
|
-
|
|
318
|
+
if solve_k0 is None:
|
|
319
|
+
try:
|
|
320
|
+
solve_k0 = _factorize(k0)
|
|
321
|
+
except RuntimeError:
|
|
322
|
+
break
|
|
323
|
+
c = c + solve_k0(R)
|
|
317
324
|
warn('The load correction iterations did not converge, the load may '
|
|
318
325
|
'exceed the critical load of the linearized problem', level=1,
|
|
319
326
|
silent=silent)
|
|
@@ -8,12 +8,9 @@ control the arc-length scaling and adaptation.
|
|
|
8
8
|
|
|
9
9
|
"""
|
|
10
10
|
import numpy as np
|
|
11
|
-
from scipy.sparse import csc_matrix
|
|
12
|
-
from scipy.sparse.linalg import splu
|
|
13
11
|
|
|
14
12
|
from .logger import msg, warn
|
|
15
|
-
from .static import solve
|
|
16
|
-
from .sparseutils import remove_null_cols
|
|
13
|
+
from .static import solve, _factorize
|
|
17
14
|
from . import callbacks
|
|
18
15
|
from .newton_raphson import (_check_convergence, _check_divergence,
|
|
19
16
|
_NR_iterations, INC_CUT_FACTOR)
|
|
@@ -31,34 +28,6 @@ MIN_GROWTH_FACTOR = 0.5
|
|
|
31
28
|
MAX_NUM_STEPS = 1000
|
|
32
29
|
|
|
33
30
|
|
|
34
|
-
def _factorize(kT):
|
|
35
|
-
"""LU factorization of the tangent stiffness matrix
|
|
36
|
-
|
|
37
|
-
Null rows and columns are removed, the corresponding values of the
|
|
38
|
-
solution are zero.
|
|
39
|
-
|
|
40
|
-
Parameters
|
|
41
|
-
----------
|
|
42
|
-
kT : sparse matrix
|
|
43
|
-
Tangent stiffness matrix.
|
|
44
|
-
|
|
45
|
-
Returns
|
|
46
|
-
-------
|
|
47
|
-
solve_kT : callable
|
|
48
|
-
Function that returns the solution ``x`` of ``kT x = b``.
|
|
49
|
-
|
|
50
|
-
"""
|
|
51
|
-
kT_red, used_cols = remove_null_cols(kT, silent=True)
|
|
52
|
-
lu = splu(csc_matrix(kT_red))
|
|
53
|
-
|
|
54
|
-
def solve_kT(b):
|
|
55
|
-
x = np.zeros(b.shape[0], dtype=np.result_type(b, lu.U.dtype))
|
|
56
|
-
x[used_cols] = lu.solve(b[used_cols])
|
|
57
|
-
return x
|
|
58
|
-
|
|
59
|
-
return solve_kT
|
|
60
|
-
|
|
61
|
-
|
|
62
31
|
def _solver_arc_length(an, method, silent=False):
|
|
63
32
|
r"""Arc-length solver
|
|
64
33
|
|
|
@@ -117,12 +86,13 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
117
86
|
`\{u_{ref}\}` is the linear solution of `\partial R/\partial \lambda`
|
|
118
87
|
at `c = 0`, `\lambda = 0`, since the reference vector may be null, e.g.
|
|
119
88
|
under a prescribed displacement alone; and `\lambda \, \partial
|
|
120
|
-
R/\partial \lambda
|
|
121
|
-
convergence
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
89
|
+
R/\partial \lambda` is an additional reference of the relative
|
|
90
|
+
convergence criterion. `\partial R/\partial \lambda` is evaluated
|
|
91
|
+
together with the tangent stiffness matrix, at the same iterate, i.e.
|
|
92
|
+
once per update of the tangent stiffness matrix, and used in the
|
|
93
|
+
bordered solution; the last one evaluated is the reference of the next
|
|
94
|
+
convergence checks. At the converged state it is kept with the tangent
|
|
95
|
+
stiffness matrix when the step is repeated with a shorter arc length.
|
|
126
96
|
|
|
127
97
|
The initial arc length corresponds to a load factor increment of
|
|
128
98
|
``an.initialInc`` along the initial tangent. The convergence and
|
|
@@ -194,8 +164,12 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
194
164
|
warn('Singular tangent stiffness matrix at the last converged state! Analysis stopped',
|
|
195
165
|
level=1, silent=silent)
|
|
196
166
|
break
|
|
197
|
-
|
|
198
|
-
|
|
167
|
+
# load vector of the converged configuration, dR/dlbd, kept with
|
|
168
|
+
# the tangent stiffness matrix when the step is repeated
|
|
169
|
+
if step_num == 1 and lbd_dependent:
|
|
170
|
+
q_conv = q0
|
|
171
|
+
else:
|
|
172
|
+
q_conv = callbacks.calc_load_vector(an, c, fext, lbd)
|
|
199
173
|
dc_q_conv = solve_kT_conv(q_conv)
|
|
200
174
|
norm_q = np.sqrt(inner(dc_q_conv, 1., dc_q_conv, 1.))
|
|
201
175
|
if arc_length is None:
|
|
@@ -218,6 +192,8 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
218
192
|
Rnorms = []
|
|
219
193
|
solve_kT = solve_kT_conv
|
|
220
194
|
dc_q = dc_q_conv
|
|
195
|
+
# the last dR/dlbd evaluated, with the tangent stiffness matrix
|
|
196
|
+
q_iter = q_conv
|
|
221
197
|
iter_kT = 0
|
|
222
198
|
iteration = 0
|
|
223
199
|
while True:
|
|
@@ -227,9 +203,6 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
227
203
|
R = fext_total - fint
|
|
228
204
|
q_ref = None
|
|
229
205
|
if lbd_dependent:
|
|
230
|
-
# dR/dlbd at the current iterate
|
|
231
|
-
q_iter = callbacks.calc_load_vector(an, c + dc, fext,
|
|
232
|
-
lbd + dlbd)
|
|
233
206
|
q_ref = (lbd + dlbd)*q_iter
|
|
234
207
|
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
|
|
235
208
|
fext_ref=dlbd*fext,
|
|
@@ -254,6 +227,9 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
254
227
|
warn('Singular tangent stiffness matrix', level=2, silent=silent)
|
|
255
228
|
break
|
|
256
229
|
if lbd_dependent:
|
|
230
|
+
# dR/dlbd at the iterate of the tangent stiffness matrix
|
|
231
|
+
q_iter = callbacks.calc_load_vector(an, c + dc, fext,
|
|
232
|
+
lbd + dlbd)
|
|
257
233
|
dc_q = solve_kT(q_iter)
|
|
258
234
|
else:
|
|
259
235
|
dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc,
|
|
@@ -293,8 +269,9 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
293
269
|
if converged and lbd + dlbd > 1. and lbd < 1.:
|
|
294
270
|
msg('Load factor 1.0 exceeded, correcting to load factor 1.0...', level=1, silent=silent)
|
|
295
271
|
t = (1. - lbd)/dlbd
|
|
296
|
-
converged, dc1 = _NR_iterations(an, c, t*dc, 1., fext,
|
|
297
|
-
silent=silent, lbd_dependent=lbd_dependent
|
|
272
|
+
converged, dc1, _ = _NR_iterations(an, c, t*dc, 1., fext,
|
|
273
|
+
kT0=None, silent=silent, lbd_dependent=lbd_dependent,
|
|
274
|
+
q=q_iter)
|
|
298
275
|
if converged:
|
|
299
276
|
msg('Converged at load factor 1.00000', level=2, silent=silent)
|
|
300
277
|
an.cs.append((c + dc1).copy())
|
|
@@ -52,7 +52,13 @@ are called exactly as before:
|
|
|
52
52
|
`\lambda \, \partial R/\partial \lambda`, see
|
|
53
53
|
:func:`.newton_raphson._check_convergence`, since under a prescribed
|
|
54
54
|
displacement alone the reference vector and the reduced internal force
|
|
55
|
-
vector both vanish at equilibrium
|
|
55
|
+
vector both vanish at equilibrium;
|
|
56
|
+
- ``calc_fext(c=c, lbd=lbd)`` is called at the configurations where the
|
|
57
|
+
tangent stiffness matrix is updated, right after ``calc_kC`` and
|
|
58
|
+
``calc_kG`` at the same ``c`` and load factor, such that the callables
|
|
59
|
+
may share their computations, and the last `\partial R/\partial
|
|
60
|
+
\lambda` evaluated is the reference of the convergence checks until the
|
|
61
|
+
next update.
|
|
56
62
|
|
|
57
63
|
``lbd`` is never passed by the linear static analysis,
|
|
58
64
|
``Analysis.static(NLgeom=False)``.
|
|
@@ -27,23 +27,11 @@ SHIFT_SEARCH_MAX_GROWTH = 1e10
|
|
|
27
27
|
PARITY_REL_GAP = 1e-4
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def
|
|
31
|
-
r"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
smallest `|(\mu + \sigma)/(\mu - \sigma)|`. This ordering selects the most
|
|
36
|
-
negative `\mu`, i.e. the lowest positive load multipliers `-1/\mu`, only
|
|
37
|
-
when `\sigma` is larger than `|\mu|` of these critical eigenvalues.
|
|
38
|
-
Otherwise the eigenvalues closest to `-\sigma` are returned.
|
|
39
|
-
|
|
40
|
-
The largest `|\mu|` is estimated with power iterations on
|
|
41
|
-
`[K]^{-1}[K_G]`, whose norm ratio in the `[K]`-norm increases towards the
|
|
42
|
-
largest `|\mu|`, and multiplied by ``safety``. The default shift ``1.`` is
|
|
43
|
-
returned when ``K`` is singular, not positive definite, or the linear
|
|
44
|
-
solution is inaccurate.
|
|
45
|
-
|
|
46
|
-
"""
|
|
30
|
+
def _estimate_mu_max(K, KG, max_iter=50, rel_tol=1e-3):
|
|
31
|
+
r"""Largest `|\mu|` of ``KG u = mu K u``, estimated by power
|
|
32
|
+
iterations, see :func:`_estimate_sigma`, or ``None`` when ``K`` is
|
|
33
|
+
singular, not positive definite, or the linear solution is
|
|
34
|
+
inaccurate"""
|
|
47
35
|
try:
|
|
48
36
|
with warnings.catch_warnings():
|
|
49
37
|
warnings.simplefilter("ignore")
|
|
@@ -54,7 +42,7 @@ def _estimate_sigma(K, KG, safety=10., max_iter=50, rel_tol=1e-3):
|
|
|
54
42
|
for i in range(max_iter):
|
|
55
43
|
xKx = x @ Kx
|
|
56
44
|
if not np.isfinite(xKx) or xKx <= 0:
|
|
57
|
-
return
|
|
45
|
+
return None
|
|
58
46
|
x = x / np.sqrt(xKx)
|
|
59
47
|
rhs = KG @ x
|
|
60
48
|
y = lu.solve(rhs)
|
|
@@ -62,19 +50,45 @@ def _estimate_sigma(K, KG, safety=10., max_iter=50, rel_tol=1e-3):
|
|
|
62
50
|
if i == 0:
|
|
63
51
|
residual = np.linalg.norm(Ky - rhs)
|
|
64
52
|
if not residual <= 1e-6 * np.linalg.norm(rhs):
|
|
65
|
-
return
|
|
53
|
+
return None
|
|
66
54
|
# ||y||_K / ||x||_K, with ||x||_K = 1
|
|
67
55
|
ratio = np.sqrt(abs(y @ Ky))
|
|
68
56
|
if not np.isfinite(ratio) or ratio == 0:
|
|
69
|
-
return
|
|
57
|
+
return None
|
|
70
58
|
converged = i > 0 and ratio - mu_max <= rel_tol * ratio
|
|
71
59
|
mu_max = max(mu_max, ratio)
|
|
72
60
|
if converged:
|
|
73
61
|
break
|
|
74
62
|
x, Kx = y, Ky
|
|
75
|
-
return
|
|
63
|
+
return mu_max
|
|
76
64
|
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _estimate_sigma(K, KG, safety=10., max_iter=50, rel_tol=1e-3,
|
|
69
|
+
mu_max=False):
|
|
70
|
+
r"""Shift of the Cayley mode used by :func:`lb`
|
|
71
|
+
|
|
72
|
+
With ``sigma > 0``, ``eigsh(A=KG, M=K, sigma=sigma, mode='cayley',
|
|
73
|
+
which='SM')`` returns the eigenvalues `\mu` of ``KG u = mu K u`` with the
|
|
74
|
+
smallest `|(\mu + \sigma)/(\mu - \sigma)|`. This ordering selects the most
|
|
75
|
+
negative `\mu`, i.e. the lowest positive load multipliers `-1/\mu`, only
|
|
76
|
+
when `\sigma` is larger than `|\mu|` of these critical eigenvalues.
|
|
77
|
+
Otherwise the eigenvalues closest to `-\sigma` are returned.
|
|
78
|
+
|
|
79
|
+
The largest `|\mu|` is estimated with power iterations on
|
|
80
|
+
`[K]^{-1}[K_G]`, whose norm ratio in the `[K]`-norm increases towards the
|
|
81
|
+
largest `|\mu|`, and multiplied by ``safety``. The default shift ``1.`` is
|
|
82
|
+
returned when ``K`` is singular, not positive definite, or the linear
|
|
83
|
+
solution is inaccurate. An estimate of :func:`_estimate_mu_max` may be
|
|
84
|
+
given as ``mu_max``, e.g. to try another ``safety`` factor.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
if mu_max is False:
|
|
88
|
+
mu_max = _estimate_mu_max(K, KG, max_iter=max_iter, rel_tol=rel_tol)
|
|
89
|
+
if mu_max is None:
|
|
77
90
|
return 1.
|
|
91
|
+
return safety * mu_max
|
|
78
92
|
|
|
79
93
|
|
|
80
94
|
def _negative_pivots(A):
|
|
@@ -135,7 +149,7 @@ def _lu_solve(lu, b):
|
|
|
135
149
|
|
|
136
150
|
|
|
137
151
|
def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
138
|
-
check_inertia=True):
|
|
152
|
+
check_inertia=True, K_positive_definite=None):
|
|
139
153
|
r"""Verify the eigenpairs of `([K] + \lambda [K_G])\{u\} = \{0\}`
|
|
140
154
|
|
|
141
155
|
The eigenpairs may be real or complex.
|
|
@@ -155,6 +169,10 @@ def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
|
155
169
|
symmetric matrices, ``check_inertia=True``, the complex `\lambda` are
|
|
156
170
|
ignored. Missing negative load multipliers are not detected.
|
|
157
171
|
|
|
172
|
+
Whether ``K`` is positive definite may be given as
|
|
173
|
+
``K_positive_definite``, a boolean or a function returning it, e.g. to
|
|
174
|
+
verify several solutions with one factorization of ``K``.
|
|
175
|
+
|
|
158
176
|
Returns
|
|
159
177
|
-------
|
|
160
178
|
error : str or None
|
|
@@ -181,7 +199,11 @@ def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
|
181
199
|
return None
|
|
182
200
|
lam = lam[np.isreal(lam)].real
|
|
183
201
|
positive = lam[lam > 0]
|
|
184
|
-
if
|
|
202
|
+
if K_positive_definite is None:
|
|
203
|
+
K_positive_definite = _is_positive_definite
|
|
204
|
+
if positive.size and (K_positive_definite(K) if
|
|
205
|
+
callable(K_positive_definite) else
|
|
206
|
+
K_positive_definite):
|
|
185
207
|
s = positive.max()*(1 - min_rel_gap)
|
|
186
208
|
expected = int(np.count_nonzero(positive < s))
|
|
187
209
|
count = _negative_pivots(K + s*KG) if expected else None
|
|
@@ -599,15 +621,16 @@ def _eigs_shift_search(K, KG, k, tol, check_rtol, silent):
|
|
|
599
621
|
return mu, eigvecs
|
|
600
622
|
|
|
601
623
|
|
|
602
|
-
def _eigsh_cayley(K, KG, k, tol, silent, safety=10
|
|
624
|
+
def _eigsh_cayley(K, KG, k, tol, silent, safety=10., mu_max=False):
|
|
603
625
|
r"""Solution with :func:`scipy.sparse.linalg.eigsh` in Cayley mode
|
|
604
626
|
|
|
605
627
|
The shift is estimated with :func:`_estimate_sigma` and the given
|
|
606
|
-
``safety`` factor
|
|
628
|
+
``safety`` factor, from the estimate ``mu_max`` of
|
|
629
|
+
:func:`_estimate_mu_max` when given. ARPACK's ``ncv`` is chosen by
|
|
607
630
|
:func:`.arpackutils.capped_eigsh`, which avoids the Intel MKL bug.
|
|
608
631
|
|
|
609
632
|
"""
|
|
610
|
-
sigma = _estimate_sigma(K, KG, safety=safety)
|
|
633
|
+
sigma = _estimate_sigma(K, KG, safety=safety, mu_max=mu_max)
|
|
611
634
|
msg('sigma={0}'.format(sigma), level=4, silent=silent)
|
|
612
635
|
k = min(k, K.shape[0] - 2)
|
|
613
636
|
mu, eigvecs = capped_eigsh(A=KG, k=k, M=K, sigma=sigma, which='SM',
|
|
@@ -901,16 +924,32 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
901
924
|
msg('{0} of {1} rows of KG are non-null'.format(num_active,
|
|
902
925
|
K.shape[0]), level=3, silent=silent)
|
|
903
926
|
|
|
927
|
+
# the estimate of the shift and the inertia of K are computed once
|
|
928
|
+
# for all the attempts
|
|
929
|
+
once = {}
|
|
930
|
+
|
|
931
|
+
def mu_max():
|
|
932
|
+
if 'mu_max' not in once:
|
|
933
|
+
once['mu_max'] = _estimate_mu_max(K, KG)
|
|
934
|
+
return once['mu_max']
|
|
935
|
+
|
|
936
|
+
def K_positive_definite(K):
|
|
937
|
+
if 'pd' not in once:
|
|
938
|
+
once['pd'] = _is_positive_definite(K)
|
|
939
|
+
return once['pd']
|
|
940
|
+
|
|
904
941
|
def verify(mu, eigvecs):
|
|
905
942
|
with np.errstate(divide='ignore', invalid='ignore'):
|
|
906
|
-
return _check_eigenpairs(K, KG, -1./mu, eigvecs, check_rtol
|
|
943
|
+
return _check_eigenpairs(K, KG, -1./mu, eigvecs, check_rtol,
|
|
944
|
+
K_positive_definite=K_positive_definite)
|
|
907
945
|
|
|
908
946
|
strategies = [
|
|
909
947
|
('eigsh() in Cayley mode',
|
|
910
|
-
lambda: _eigsh_cayley(K, KG, num_eigvalues, tol, silent
|
|
948
|
+
lambda: _eigsh_cayley(K, KG, num_eigvalues, tol, silent,
|
|
949
|
+
mu_max=mu_max())),
|
|
911
950
|
('eigsh() in Cayley mode with a 10 times larger shift',
|
|
912
951
|
lambda: _eigsh_cayley(K, KG, num_eigvalues, tol, silent,
|
|
913
|
-
safety=100
|
|
952
|
+
safety=100., mu_max=mu_max())),
|
|
914
953
|
]
|
|
915
954
|
dense = ('eigh() with condensed dofs',
|
|
916
955
|
lambda: _eigh_condensed(K, KG, num_eigvalues))
|
|
@@ -8,7 +8,7 @@ and they are shared with the arc-length solvers of
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
10
|
from .logger import msg, warn
|
|
11
|
-
from .static import solve
|
|
11
|
+
from .static import solve, _factorize
|
|
12
12
|
from . import callbacks
|
|
13
13
|
|
|
14
14
|
|
|
@@ -139,7 +139,7 @@ def _check_divergence(an, Rnorm, Rnorms, iteration, silent=False):
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False,
|
|
142
|
-
lbd_dependent=False):
|
|
142
|
+
lbd_dependent=False, q=None):
|
|
143
143
|
r"""Newton-Raphson iterations at a fixed load factor
|
|
144
144
|
|
|
145
145
|
Parameters
|
|
@@ -161,9 +161,15 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False,
|
|
|
161
161
|
A boolean to tell whether the log messages should be printed.
|
|
162
162
|
lbd_dependent : bool, optional
|
|
163
163
|
Whether ``calc_fext`` accepts ``lbd``, see :mod:`.callbacks`;
|
|
164
|
-
``total*q``, with `\{q\} = \partial R/\partial \lambda
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
``total*q``, with `\{q\} = \partial R/\partial \lambda`, is then an
|
|
165
|
+
additional reference of the relative convergence criterion, see
|
|
166
|
+
:func:`._check_convergence`. `\{q\}` is evaluated together with the
|
|
167
|
+
tangent stiffness matrix, at the same iterate, and the last one
|
|
168
|
+
evaluated is the reference until the next update of the tangent.
|
|
169
|
+
q : array-like or None, optional
|
|
170
|
+
The last `\{q\}` evaluated before these iterations, the reference
|
|
171
|
+
until the first update of the tangent stiffness matrix, used only
|
|
172
|
+
when ``lbd_dependent=True``.
|
|
167
173
|
|
|
168
174
|
Returns
|
|
169
175
|
-------
|
|
@@ -171,14 +177,18 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False,
|
|
|
171
177
|
Whether the iterations converged.
|
|
172
178
|
dc : array-like
|
|
173
179
|
Solution increment, such that ``c + dc`` is the solution.
|
|
180
|
+
q : array-like or None
|
|
181
|
+
The last `\{q\}` evaluated, ``None`` when ``lbd_dependent=False``.
|
|
174
182
|
|
|
175
183
|
"""
|
|
176
184
|
modified_NR = an.modified_NR
|
|
177
185
|
fext_total = total*fext
|
|
186
|
+
if not lbd_dependent:
|
|
187
|
+
q = None
|
|
178
188
|
q_ref = None
|
|
179
189
|
Rnorms = []
|
|
180
190
|
iter_kT = 0
|
|
181
|
-
|
|
191
|
+
solve_kT = None
|
|
182
192
|
fint = None
|
|
183
193
|
|
|
184
194
|
iteration = 0
|
|
@@ -187,28 +197,37 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False,
|
|
|
187
197
|
if fint is None:
|
|
188
198
|
fint = callbacks.calc_fint(an, c + dc, total)
|
|
189
199
|
R = fext_total - fint
|
|
190
|
-
if
|
|
191
|
-
q_ref = total*
|
|
200
|
+
if q is not None:
|
|
201
|
+
q_ref = total*q
|
|
192
202
|
|
|
193
203
|
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
|
|
194
204
|
q_ref=q_ref)
|
|
195
205
|
msg('Iteration: %d, max(|R|) = %1.3e, relative ||R|| = %1.3e'
|
|
196
206
|
% (iteration, np.abs(R).max(), Rrel), level=2, silent=silent)
|
|
197
207
|
if conv:
|
|
198
|
-
return True, dc
|
|
208
|
+
return True, dc, q
|
|
199
209
|
if _check_divergence(an, Rnorm, Rnorms, iteration, silent=silent):
|
|
200
|
-
return False, dc
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
return False, dc, q
|
|
211
|
+
|
|
212
|
+
# the factorization of the tangent stiffness matrix is reused while
|
|
213
|
+
# the matrix is not updated
|
|
214
|
+
try:
|
|
215
|
+
if modified_NR and iteration > 1 and iter_kT < an.compute_every_n:
|
|
216
|
+
iter_kT += 1
|
|
217
|
+
elif modified_NR and iteration == 1 and kT0 is not None:
|
|
218
|
+
solve_kT = _factorize(kT0)
|
|
219
|
+
iter_kT = 1
|
|
220
|
+
else:
|
|
221
|
+
solve_kT = _factorize(callbacks.calc_kT(an, c + dc, total))
|
|
222
|
+
iter_kT = 1
|
|
223
|
+
if lbd_dependent:
|
|
224
|
+
# dR/dlbd at the iterate of the tangent stiffness matrix
|
|
225
|
+
q = callbacks.calc_load_vector(an, c + dc, fext, total)
|
|
226
|
+
except RuntimeError:
|
|
227
|
+
warn('Singular tangent stiffness matrix', level=2, silent=silent)
|
|
228
|
+
return False, dc, q
|
|
229
|
+
|
|
230
|
+
varc = solve_kT(R)
|
|
212
231
|
|
|
213
232
|
eta = 1.
|
|
214
233
|
fint = None
|
|
@@ -261,16 +280,19 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
261
280
|
`\lambda = 0` gives the predictor of the first increment, its linear
|
|
262
281
|
solution `[K_0]^{-1}\{q\}`, instead of the linear solution of the
|
|
263
282
|
reference vector, which is null under a prescribed displacement alone,
|
|
264
|
-
and `\lambda \|q
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
283
|
+
and `\lambda \|q\|` is an additional reference of the relative
|
|
284
|
+
convergence criterion. `\{q\}` is evaluated together with the tangent
|
|
285
|
+
stiffness matrix, at the same iterate, i.e. ``calc_fext`` is called once
|
|
286
|
+
per update of the tangent stiffness matrix, and the last `\{q\}`
|
|
287
|
+
evaluated is the reference of the next convergence checks, also in the
|
|
288
|
+
next step.
|
|
268
289
|
|
|
269
290
|
By default full Newton-Raphson is used, i.e. the tangent stiffness matrix
|
|
270
291
|
``kT = kC + kG`` is rebuilt at every iteration, which gives quadratic
|
|
271
292
|
convergence when ``kT`` is the exact Jacobian of ``fint``. With
|
|
272
293
|
``an.modified_NR = True`` the tangent stiffness matrix is rebuilt only at
|
|
273
294
|
the first iteration of each step and every ``an.compute_every_n``
|
|
295
|
+
iterations, and its LU factorization is reused in the other
|
|
274
296
|
iterations.
|
|
275
297
|
|
|
276
298
|
With ``an.line_search = True``, the full Newton step (``eta = 1``) is tried
|
|
@@ -315,7 +337,8 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
315
337
|
fext = an.calc_fext(inc=1., silent=True)
|
|
316
338
|
kC0 = an.calc_kC(silent=silent)
|
|
317
339
|
q = None
|
|
318
|
-
|
|
340
|
+
lbd_dependent = callbacks.is_load_factor_dependent(an)
|
|
341
|
+
if lbd_dependent:
|
|
319
342
|
# dR/dlbd at c = 0 and lbd = 0
|
|
320
343
|
q = callbacks.calc_load_vector(an, np.zeros_like(fext), fext, 0.)
|
|
321
344
|
# predictor of the first step: linear solution scaled by the load factor
|
|
@@ -339,12 +362,13 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
339
362
|
kT0 = None
|
|
340
363
|
if an.modified_NR and step_num == 1 and not an.kT_initial_state:
|
|
341
364
|
kT0 = kC0
|
|
342
|
-
converged, dc = _NR_iterations(an, c, dc, total, fext,
|
|
343
|
-
|
|
344
|
-
lbd_dependent=q is not None)
|
|
365
|
+
converged, dc, q_new = _NR_iterations(an, c, dc, total, fext,
|
|
366
|
+
kT0=kT0, silent=silent, lbd_dependent=lbd_dependent, q=q)
|
|
345
367
|
|
|
346
368
|
if converged:
|
|
347
369
|
msg('Converged at load factor %1.5f' % total, level=2, silent=silent)
|
|
370
|
+
# the dR/dlbd of a step that failed is not kept
|
|
371
|
+
q = q_new
|
|
348
372
|
c = c + dc
|
|
349
373
|
total_last = total
|
|
350
374
|
an.cs.append(c.copy()) #NOTE copy required
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from scipy.sparse
|
|
2
|
+
from scipy.sparse import csc_matrix
|
|
3
|
+
from scipy.sparse.linalg import spsolve, splu
|
|
3
4
|
|
|
4
5
|
from .logger import msg
|
|
5
6
|
from .sparseutils import remove_null_cols
|
|
@@ -43,6 +44,36 @@ def solve(a, b, silent=False, **kwargs):
|
|
|
43
44
|
return x
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _factorize(kT):
|
|
48
|
+
"""LU factorization of the tangent stiffness matrix
|
|
49
|
+
|
|
50
|
+
Null rows and columns are removed, the corresponding values of the
|
|
51
|
+
solution are zero, as in :func:`.solve`. The factorization is reused by
|
|
52
|
+
the non-linear solvers while the tangent stiffness matrix is not
|
|
53
|
+
updated.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
kT : sparse matrix
|
|
58
|
+
Tangent stiffness matrix.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
solve_kT : callable
|
|
63
|
+
Function that returns the solution ``x`` of ``kT x = b``.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
kT_red, used_cols = remove_null_cols(kT, silent=True)
|
|
67
|
+
lu = splu(csc_matrix(kT_red))
|
|
68
|
+
|
|
69
|
+
def solve_kT(b):
|
|
70
|
+
x = np.zeros(b.shape[0], dtype=np.result_type(b, lu.U.dtype))
|
|
71
|
+
x[used_cols] = lu.solve(b[used_cols])
|
|
72
|
+
return x
|
|
73
|
+
|
|
74
|
+
return solve_kT
|
|
75
|
+
|
|
76
|
+
|
|
46
77
|
def static(K, fext, silent=False):
|
|
47
78
|
r"""Linear static analysis
|
|
48
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -65,7 +65,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
65
65
|
Citing this library
|
|
66
66
|
===================
|
|
67
67
|
|
|
68
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.
|
|
68
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.8.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
Documentation
|
|
@@ -80,6 +80,8 @@ History
|
|
|
80
80
|
|
|
81
81
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
82
82
|
|
|
83
|
+
* version 0.8.0 (2026-10-14)
|
|
84
|
+
- Fewer evaluations and factorizations, same solutions: `dR/dlbd` is evaluated together with the tangent stiffness matrix only, Newton-Raphson reuses the factorization of the tangent while it is not updated, the follower linear static analysis factorizes `K0` once, and `lb` estimates its shift and the inertia of `K` once for all its attempts
|
|
83
85
|
* version 0.7.0 (2026-10-13)
|
|
84
86
|
- `calc_fext` may declare the keyword argument `lbd` and then receives the current load factor with the configuration, for prescribed displacements imposed exactly as linear constraints; the solvers use `dR/dlbd` for the arc-length scale, the first Newton-Raphson predictor and the relative convergence criterion. Callables without `lbd` behave as before
|
|
85
87
|
* version 0.6.1 (2026-10-08)
|
|
@@ -169,7 +169,7 @@ def test_lb_sparse_raises_when_verification_fails(monkeypatch):
|
|
|
169
169
|
|
|
170
170
|
K, KG = load_saved_matrices('plate_ssss_Nxx')
|
|
171
171
|
|
|
172
|
-
def wrong_eigsh(K, KG, k, tol, silent, safety=10
|
|
172
|
+
def wrong_eigsh(K, KG, k, tol, silent, safety=10., mu_max=False):
|
|
173
173
|
rng = np.random.RandomState(0)
|
|
174
174
|
return -rng.rand(k), rng.randn(K.shape[0], k)
|
|
175
175
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
r"""Reuse of evaluations and factorizations by the solvers
|
|
2
|
+
|
|
3
|
+
- With a ``calc_fext`` that declares ``lbd``, `\partial R/\partial \lambda`
|
|
4
|
+
is evaluated only together with the tangent stiffness matrix, right after
|
|
5
|
+
``calc_kC`` and ``calc_kG`` at the same configuration, see
|
|
6
|
+
:mod:`structsolve.callbacks`; the solutions are those of the tests of
|
|
7
|
+
``test_load_factor_dependent_load.py``.
|
|
8
|
+
- Newton-Raphson factorizes the tangent stiffness matrix once per update.
|
|
9
|
+
- The linear static analysis with a configuration-dependent load factorizes
|
|
10
|
+
``K0`` once.
|
|
11
|
+
- :func:`.lb` estimates its shift and the positive definiteness of ``K``
|
|
12
|
+
once for all its attempts.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pytest
|
|
17
|
+
from scipy.sparse import csr_matrix
|
|
18
|
+
|
|
19
|
+
from structsolve import Analysis, lb
|
|
20
|
+
from structsolve import linear_buckling, newton_raphson, analysis
|
|
21
|
+
from test_load_factor_dependent_load import SpringsProblem
|
|
22
|
+
from test_linear_buckling import load_saved_matrices
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Recorder(SpringsProblem):
|
|
26
|
+
r"""The springs, recording the sequence of the calls with ``c``"""
|
|
27
|
+
def __init__(self, **kwargs):
|
|
28
|
+
super().__init__(**kwargs)
|
|
29
|
+
self.events = []
|
|
30
|
+
|
|
31
|
+
def calc_fext(self, inc=1., silent=True, c=None, lbd=None):
|
|
32
|
+
if c is not None:
|
|
33
|
+
self.events.append(('fext', c.copy(), lbd))
|
|
34
|
+
return super().calc_fext(inc=inc, silent=silent, c=c, lbd=lbd)
|
|
35
|
+
|
|
36
|
+
def calc_kC(self, c=None, NLgeom=False, inc=1., silent=True):
|
|
37
|
+
if c is not None and NLgeom:
|
|
38
|
+
self.events.append(('kC', c.copy(), inc))
|
|
39
|
+
return super().calc_kC(c=c, NLgeom=NLgeom, inc=inc, silent=silent)
|
|
40
|
+
|
|
41
|
+
def calc_kG(self, c=None, NLgeom=False, silent=True):
|
|
42
|
+
if c is not None and NLgeom:
|
|
43
|
+
self.events.append(('kG', c.copy(), None))
|
|
44
|
+
return super().calc_kG(c=c, NLgeom=NLgeom, silent=silent)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_events(pb, origin_calls=1):
|
|
48
|
+
r"""Every `\partial R/\partial \lambda` but those at the origin follows
|
|
49
|
+
the tangent stiffness matrix at the same configuration and load factor,
|
|
50
|
+
and there is at most one per tangent"""
|
|
51
|
+
ev = pb.events
|
|
52
|
+
fext = [i for i, e in enumerate(ev) if e[0] == 'fext']
|
|
53
|
+
tangents = [i for i, e in enumerate(ev) if e[0] == 'kC']
|
|
54
|
+
assert len(fext) <= len(tangents) + origin_calls
|
|
55
|
+
for i in fext:
|
|
56
|
+
_, c, lbd = ev[i]
|
|
57
|
+
if i < 2 or ev[i - 1][0] != 'kG':
|
|
58
|
+
assert not np.any(c) and lbd == 0., ev[i]
|
|
59
|
+
continue
|
|
60
|
+
assert np.array_equal(ev[i - 1][1], c)
|
|
61
|
+
assert ev[i - 2][0] == 'kC' and np.array_equal(ev[i - 2][1], c)
|
|
62
|
+
assert ev[i - 2][2] == lbd
|
|
63
|
+
return len(fext), len(tangents)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def count(monkeypatch, module, name):
|
|
67
|
+
calls = []
|
|
68
|
+
func = getattr(module, name)
|
|
69
|
+
|
|
70
|
+
def wrapper(*args, **kwargs):
|
|
71
|
+
calls.append(1)
|
|
72
|
+
return func(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
monkeypatch.setattr(module, name, wrapper)
|
|
75
|
+
return calls
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
|
|
79
|
+
'arc_length_crisfield'])
|
|
80
|
+
@pytest.mark.parametrize('modified_NR', [False, True])
|
|
81
|
+
def test_load_vector_evaluated_with_the_tangent(method, modified_NR):
|
|
82
|
+
pb = Recorder(d0=-0.3, d1=0.6, F=40.)
|
|
83
|
+
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-11,
|
|
84
|
+
modified_NR=modified_NR)
|
|
85
|
+
an.static(NLgeom=True, silent=True)
|
|
86
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
87
|
+
for lbd, c_r in zip(an.increments, an.cs):
|
|
88
|
+
ref = pb.solve_reference(lbd)
|
|
89
|
+
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
|
|
90
|
+
n_fext, n_tangents = check_events(pb)
|
|
91
|
+
# dR/dlbd at the origin, then at most one per tangent
|
|
92
|
+
assert n_fext >= 2
|
|
93
|
+
if method == 'NR':
|
|
94
|
+
# one per tangent, none at the converged iterates
|
|
95
|
+
assert n_fext == n_tangents + 1
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_newton_raphson_one_factorization_per_tangent(monkeypatch):
|
|
99
|
+
factorizations = count(monkeypatch, newton_raphson, '_factorize')
|
|
100
|
+
for modified_NR in (False, True):
|
|
101
|
+
del factorizations[:]
|
|
102
|
+
pb = Recorder(k=(10., 15., 20.), a=(200., 400., 600.), d1=1.)
|
|
103
|
+
an = pb.analysis(NL_method='NR', initialInc=0.5, relTOL=1e-12,
|
|
104
|
+
modified_NR=modified_NR, compute_every_n=3,
|
|
105
|
+
kT_initial_state=True)
|
|
106
|
+
an.static(NLgeom=True, silent=True)
|
|
107
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
108
|
+
n_tangents = sum(1 for e in pb.events if e[0] == 'kC')
|
|
109
|
+
assert len(factorizations) == n_tangents
|
|
110
|
+
corrections = sum(1 for e in pb.fint_calls) - len(an.increments)
|
|
111
|
+
if modified_NR:
|
|
112
|
+
# the factorization is reused between the updates
|
|
113
|
+
assert len(factorizations) < corrections
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_newton_raphson_singular_tangent_cuts_the_step():
|
|
117
|
+
"""A singular tangent stops the iterations of the step with a warning,
|
|
118
|
+
the step being repeated with a smaller increment"""
|
|
119
|
+
pb = SpringsProblem(F=10., d1=0.)
|
|
120
|
+
calls = []
|
|
121
|
+
|
|
122
|
+
def calc_kC(c=None, NLgeom=False, inc=1., silent=True):
|
|
123
|
+
if c is not None and NLgeom and not calls:
|
|
124
|
+
calls.append(1)
|
|
125
|
+
return csr_matrix(np.array([[1., 1.], [1., 1.]]))
|
|
126
|
+
return pb.calc_kC(c=c, NLgeom=NLgeom, inc=inc, silent=silent)
|
|
127
|
+
|
|
128
|
+
an = pb.analysis(initialInc=0.5, relTOL=1e-10)
|
|
129
|
+
an.calc_kC = calc_kC
|
|
130
|
+
an.static(NLgeom=True, silent=True)
|
|
131
|
+
assert calls and np.isclose(an.increments[-1], 1.)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_linear_static_follower_factorizes_K0_once(monkeypatch):
|
|
135
|
+
factorizations = count(monkeypatch, analysis, '_factorize')
|
|
136
|
+
K0 = csr_matrix(np.array([[4., -1., 0.], [-1., 4., -1.], [0., -1., 3.]]))
|
|
137
|
+
F0 = np.array([1., 0., 2.])
|
|
138
|
+
B = 0.3*np.array([[0., 1., 0.], [0., 0., 1.], [1., 0., 0.]])
|
|
139
|
+
|
|
140
|
+
def calc_fext(inc=1., silent=True, c=None):
|
|
141
|
+
return inc*(F0 if c is None else F0 + B @ c)
|
|
142
|
+
|
|
143
|
+
an = Analysis(calc_fext, None, lambda c=None, NLgeom=False, silent=True:
|
|
144
|
+
K0, None)
|
|
145
|
+
an.relTOL = 1e-12
|
|
146
|
+
_, cs = an.static(NLgeom=False, silent=True)
|
|
147
|
+
exact = np.linalg.solve(K0.toarray() - B, F0)
|
|
148
|
+
assert np.allclose(cs[-1], exact, rtol=1e-10)
|
|
149
|
+
assert len(factorizations) == 1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_lb_estimates_once_for_all_attempts(monkeypatch):
|
|
153
|
+
K, KG = load_saved_matrices('plate_ssss_Nxx')
|
|
154
|
+
ref, _ = lb(K, KG, silent=True)
|
|
155
|
+
estimates = count(monkeypatch, linear_buckling, '_estimate_mu_max')
|
|
156
|
+
inertia = count(monkeypatch, linear_buckling, '_is_positive_definite')
|
|
157
|
+
eigsh_cayley = linear_buckling._eigsh_cayley
|
|
158
|
+
attempts = []
|
|
159
|
+
|
|
160
|
+
def first_fails(K, KG, k, tol, silent, safety=10., mu_max=False):
|
|
161
|
+
attempts.append(safety)
|
|
162
|
+
mu, v = eigsh_cayley(K, KG, k, tol, silent, safety=safety,
|
|
163
|
+
mu_max=mu_max)
|
|
164
|
+
if len(attempts) == 1:
|
|
165
|
+
# a wrong eigenvector
|
|
166
|
+
v = v[:, ::-1]
|
|
167
|
+
return mu, v
|
|
168
|
+
|
|
169
|
+
monkeypatch.setattr(linear_buckling, '_eigsh_cayley', first_fails)
|
|
170
|
+
eigvals, _ = lb(K, KG, silent=True)
|
|
171
|
+
assert attempts == [10., 100.]
|
|
172
|
+
assert np.allclose(eigvals[:5], ref[:5], rtol=1e-8)
|
|
173
|
+
assert len(estimates) == 1
|
|
174
|
+
assert len(inertia) == 1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|