structsolve 0.6.1__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.6.1/structsolve.egg-info → structsolve-0.8.0}/PKG-INFO +8 -2
- {structsolve-0.6.1 → structsolve-0.8.0}/README.md +7 -1
- {structsolve-0.6.1 → structsolve-0.8.0}/pyproject.toml +1 -1
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/analysis.py +21 -3
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length.py +48 -38
- structsolve-0.8.0/structsolve/callbacks.py +160 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/linear_buckling.py +68 -29
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/newton_raphson.py +86 -26
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/static.py +32 -1
- {structsolve-0.6.1 → structsolve-0.8.0/structsolve.egg-info}/PKG-INFO +8 -2
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/SOURCES.txt +2 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_linear_buckling.py +1 -1
- structsolve-0.8.0/tests/test_load_factor_dependent_load.py +298 -0
- structsolve-0.8.0/tests/test_reuse.py +174 -0
- structsolve-0.6.1/structsolve/callbacks.py +0 -68
- {structsolve-0.6.1 → structsolve-0.8.0}/LICENSE +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/setup.cfg +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/__init__.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arpackutils.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/freq.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/logger.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/sparseutils.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_analysis.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_arc_length.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_arpackutils.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_follower_freq_static.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_follower_loads.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_freq.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_linear_buckling_unsymmetric.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_sparse_first.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_sparseutils.py +0 -0
- {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_static.py +0 -0
- {structsolve-0.6.1 → 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
|
|
@@ -54,6 +54,8 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
54
54
|
- Nonlinear statics using Newton-Raphson
|
|
55
55
|
- Nonlinear statics using the Arc-Length method
|
|
56
56
|
- Support for unsymmetric problems, such as those involving follower forces
|
|
57
|
+
- Load vectors that depend on the load factor, e.g. prescribed displacements
|
|
58
|
+
imposed exactly as linear constraints
|
|
57
59
|
|
|
58
60
|
Currently these solvers are pretty much compatible with my other repositories
|
|
59
61
|
[panels](https://github.com/saullocastro/panels),
|
|
@@ -63,7 +65,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
63
65
|
Citing this library
|
|
64
66
|
===================
|
|
65
67
|
|
|
66
|
-
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.
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
Documentation
|
|
@@ -78,6 +80,10 @@ History
|
|
|
78
80
|
|
|
79
81
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
80
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
|
|
85
|
+
* version 0.7.0 (2026-10-13)
|
|
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
|
|
81
87
|
* version 0.6.1 (2026-10-08)
|
|
82
88
|
- Pyodide support: `await micropip.install("structsolve")` in the browser, and the whole test suite runs in Pyodide in GitHub Actions
|
|
83
89
|
- `lb` and `freq` always run the sparse solvers first, whatever the size of the problem, with verified results; the dense solvers are only a last resort within `max_dense_size`, with a `DenseFallbackWarning`
|
|
@@ -18,6 +18,8 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
18
18
|
- Nonlinear statics using Newton-Raphson
|
|
19
19
|
- Nonlinear statics using the Arc-Length method
|
|
20
20
|
- Support for unsymmetric problems, such as those involving follower forces
|
|
21
|
+
- Load vectors that depend on the load factor, e.g. prescribed displacements
|
|
22
|
+
imposed exactly as linear constraints
|
|
21
23
|
|
|
22
24
|
Currently these solvers are pretty much compatible with my other repositories
|
|
23
25
|
[panels](https://github.com/saullocastro/panels),
|
|
@@ -27,7 +29,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
27
29
|
Citing this library
|
|
28
30
|
===================
|
|
29
31
|
|
|
30
|
-
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.
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
Documentation
|
|
@@ -42,6 +44,10 @@ History
|
|
|
42
44
|
|
|
43
45
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
44
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
|
|
49
|
+
* version 0.7.0 (2026-10-13)
|
|
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
|
|
45
51
|
* version 0.6.1 (2026-10-08)
|
|
46
52
|
- Pyodide support: `await micropip.install("structsolve")` in the browser, and the whole test suite runs in Pyodide in GitHub Actions
|
|
47
53
|
- `lb` and `freq` always run the sparse solvers first, whatever the size of the problem, with verified results; the dense solvers are only a last resort within `max_dense_size`, with a `DenseFallbackWarning`
|
|
@@ -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
|
|
@@ -120,7 +120,8 @@ class Analysis(object):
|
|
|
120
120
|
``calc_fext(inc=1., silent=False)``, must return a 1-D array with the
|
|
121
121
|
external force vector. Required for linear and non-linear static
|
|
122
122
|
analyses. The non-linear solvers call it with ``inc=1.`` and scale the
|
|
123
|
-
returned vector by the load factor.
|
|
123
|
+
returned vector by the load factor. It may declare the optional
|
|
124
|
+
keyword arguments ``c`` and ``lbd``, see the notes below.
|
|
124
125
|
calc_fint : callable, optional
|
|
125
126
|
``calc_fint(c, silent=False)``, must return a 1-D array with the
|
|
126
127
|
internal force vector for the solution vector ``c``. Required for
|
|
@@ -150,6 +151,16 @@ class Analysis(object):
|
|
|
150
151
|
configuration ``c`` for a unit load factor, the derivative of the
|
|
151
152
|
residual with respect to the load factor, used by the arc-length
|
|
152
153
|
solvers.
|
|
154
|
+
- ``calc_fext(inc=1., c=c, lbd=lbd)`` also receives the current load
|
|
155
|
+
factor when it declares ``lbd``, for a derivative `\partial
|
|
156
|
+
R/\partial \lambda` that depends on the load factor, e.g. prescribed
|
|
157
|
+
displacements imposed exactly as linear constraints, where the
|
|
158
|
+
reference vector ``calc_fext(inc=1.)`` may be null. The solvers then
|
|
159
|
+
use `\partial R/\partial \lambda` for the displacement scale of the
|
|
160
|
+
arc-length methods, for the predictor of the first Newton-Raphson
|
|
161
|
+
increment and as a reference of the relative convergence criterion,
|
|
162
|
+
see :mod:`.callbacks`. The linear static analysis never passes
|
|
163
|
+
``lbd``.
|
|
153
164
|
|
|
154
165
|
The tangent stiffness matrix may then be unsymmetric, which the solvers
|
|
155
166
|
support, since they use LU factorizations.
|
|
@@ -291,6 +302,8 @@ class Analysis(object):
|
|
|
291
302
|
configuration-dependent load, see :meth:`.static`"""
|
|
292
303
|
msg('Configuration-dependent load: load correction iterations',
|
|
293
304
|
level=2, silent=silent)
|
|
305
|
+
# k0 is factorized once for all the corrections
|
|
306
|
+
solve_k0 = None
|
|
294
307
|
for iteration in range(1, self.maxNumIter + 1):
|
|
295
308
|
fext = self.calc_fext(inc=1., c=c, silent=True)
|
|
296
309
|
fint = k0 @ c
|
|
@@ -302,7 +315,12 @@ class Analysis(object):
|
|
|
302
315
|
return c
|
|
303
316
|
if not np.isfinite(Rnorm):
|
|
304
317
|
break
|
|
305
|
-
|
|
318
|
+
if solve_k0 is None:
|
|
319
|
+
try:
|
|
320
|
+
solve_k0 = _factorize(k0)
|
|
321
|
+
except RuntimeError:
|
|
322
|
+
break
|
|
323
|
+
c = c + solve_k0(R)
|
|
306
324
|
warn('The load correction iterations did not converge, the load may '
|
|
307
325
|
'exceed the critical load of the linearized problem', level=1,
|
|
308
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
|
|
|
@@ -110,6 +79,21 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
110
79
|
solution of the reference vector. The callables that accept ``inc``
|
|
111
80
|
receive the current load factor, see :mod:`.callbacks`.
|
|
112
81
|
|
|
82
|
+
When ``calc_fext`` also accepts the keyword argument ``lbd``, e.g. for
|
|
83
|
+
prescribed displacements imposed exactly, it receives the load factor
|
|
84
|
+
together with the configuration, `\{F_{ext}\}` above being then
|
|
85
|
+
`\partial R/\partial \lambda` at the current `c` and `\lambda`;
|
|
86
|
+
`\{u_{ref}\}` is the linear solution of `\partial R/\partial \lambda`
|
|
87
|
+
at `c = 0`, `\lambda = 0`, since the reference vector may be null, e.g.
|
|
88
|
+
under a prescribed displacement alone; and `\lambda \, \partial
|
|
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.
|
|
96
|
+
|
|
113
97
|
The initial arc length corresponds to a load factor increment of
|
|
114
98
|
``an.initialInc`` along the initial tangent. The convergence and
|
|
115
99
|
divergence checks are the same as in the Newton-Raphson solver, see
|
|
@@ -136,7 +120,13 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
136
120
|
|
|
137
121
|
fext = an.calc_fext(inc=1., silent=True)
|
|
138
122
|
kC0 = an.calc_kC(silent=silent)
|
|
139
|
-
|
|
123
|
+
lbd_dependent = callbacks.is_load_factor_dependent(an)
|
|
124
|
+
if lbd_dependent:
|
|
125
|
+
# dR/dlbd at c = 0 and lbd = 0
|
|
126
|
+
q0 = callbacks.calc_load_vector(an, np.zeros_like(fext), fext, 0.)
|
|
127
|
+
u_ref = solve(kC0, q0, silent=True)
|
|
128
|
+
else:
|
|
129
|
+
u_ref = solve(kC0, fext, silent=True)
|
|
140
130
|
scale2 = u_ref.dot(u_ref)
|
|
141
131
|
if not scale2 > 0:
|
|
142
132
|
scale2 = 1.
|
|
@@ -174,8 +164,13 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
174
164
|
warn('Singular tangent stiffness matrix at the last converged state! Analysis stopped',
|
|
175
165
|
level=1, silent=silent)
|
|
176
166
|
break
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
|
173
|
+
dc_q_conv = solve_kT_conv(q_conv)
|
|
179
174
|
norm_q = np.sqrt(inner(dc_q_conv, 1., dc_q_conv, 1.))
|
|
180
175
|
if arc_length is None:
|
|
181
176
|
arc_length = an.initialInc*norm_q
|
|
@@ -197,6 +192,8 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
197
192
|
Rnorms = []
|
|
198
193
|
solve_kT = solve_kT_conv
|
|
199
194
|
dc_q = dc_q_conv
|
|
195
|
+
# the last dR/dlbd evaluated, with the tangent stiffness matrix
|
|
196
|
+
q_iter = q_conv
|
|
200
197
|
iter_kT = 0
|
|
201
198
|
iteration = 0
|
|
202
199
|
while True:
|
|
@@ -204,8 +201,12 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
204
201
|
fint = callbacks.calc_fint(an, c + dc, lbd + dlbd)
|
|
205
202
|
fext_total = (lbd + dlbd)*fext
|
|
206
203
|
R = fext_total - fint
|
|
204
|
+
q_ref = None
|
|
205
|
+
if lbd_dependent:
|
|
206
|
+
q_ref = (lbd + dlbd)*q_iter
|
|
207
207
|
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
|
|
208
|
-
fext_ref=dlbd*fext
|
|
208
|
+
fext_ref=dlbd*fext,
|
|
209
|
+
q_ref=q_ref)
|
|
209
210
|
msg('Iteration: %d, lbd %1.5f, max(|R|) = %1.3e, relative ||R|| = %1.3e'
|
|
210
211
|
% (iteration, lbd + dlbd, np.abs(R).max(), Rrel), level=2, silent=silent)
|
|
211
212
|
if conv:
|
|
@@ -225,7 +226,14 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
225
226
|
except RuntimeError:
|
|
226
227
|
warn('Singular tangent stiffness matrix', level=2, silent=silent)
|
|
227
228
|
break
|
|
228
|
-
|
|
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)
|
|
233
|
+
dc_q = solve_kT(q_iter)
|
|
234
|
+
else:
|
|
235
|
+
dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc,
|
|
236
|
+
fext))
|
|
229
237
|
iter_kT = 1
|
|
230
238
|
dc_R = solve_kT(R)
|
|
231
239
|
|
|
@@ -261,7 +269,9 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
261
269
|
if converged and lbd + dlbd > 1. and lbd < 1.:
|
|
262
270
|
msg('Load factor 1.0 exceeded, correcting to load factor 1.0...', level=1, silent=silent)
|
|
263
271
|
t = (1. - lbd)/dlbd
|
|
264
|
-
converged, dc1 = _NR_iterations(an, c, t*dc, 1., fext,
|
|
272
|
+
converged, dc1, _ = _NR_iterations(an, c, t*dc, 1., fext,
|
|
273
|
+
kT0=None, silent=silent, lbd_dependent=lbd_dependent,
|
|
274
|
+
q=q_iter)
|
|
265
275
|
if converged:
|
|
266
276
|
msg('Converged at load factor 1.00000', level=2, silent=silent)
|
|
267
277
|
an.cs.append((c + dc1).copy())
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
r"""Calls to the callables of :class:`.Analysis` used by the non-linear solvers
|
|
2
|
+
|
|
3
|
+
The callables may depend on the load factor `\lambda` and on the
|
|
4
|
+
configuration through their optional keyword arguments, which are passed only
|
|
5
|
+
when the callable declares them explicitly in its signature (a ``**kwargs``
|
|
6
|
+
does not count), such that the callables written for the previous versions
|
|
7
|
+
are called exactly as before:
|
|
8
|
+
|
|
9
|
+
- ``inc``: the current load factor `\lambda`, passed to ``calc_fint``,
|
|
10
|
+
``calc_kC`` and ``calc_kG``. It allows configuration-dependent loads, e.g.
|
|
11
|
+
follower pressures, whose configuration-dependent part enters
|
|
12
|
+
``calc_fint`` and whose load stiffness enters ``calc_kC`` or ``calc_kG``,
|
|
13
|
+
both scaled by `\lambda`. The residual remains
|
|
14
|
+
|
|
15
|
+
.. math::
|
|
16
|
+
|
|
17
|
+
\{R(c, \lambda)\} = \lambda \{F_{ext}\} - \{F_{int}(c, \lambda)\}
|
|
18
|
+
|
|
19
|
+
with ``calc_kC + calc_kG`` its exact Jacobian `-\partial R/\partial c`.
|
|
20
|
+
|
|
21
|
+
- ``c``: the current configuration, passed to ``calc_fext``, which must then
|
|
22
|
+
return the load vector of that configuration for a unit load factor, i.e.
|
|
23
|
+
`\partial R/\partial \lambda` at `c`. The arc-length solvers use it in the
|
|
24
|
+
predictor and in the constraint instead of the constant reference vector
|
|
25
|
+
``calc_fext(inc=1.)``.
|
|
26
|
+
|
|
27
|
+
- ``lbd``: the current load factor `\lambda`, passed to ``calc_fext``
|
|
28
|
+
together with ``c``, for a load vector `\partial R/\partial \lambda` that
|
|
29
|
+
depends on the load factor as well, e.g. prescribed displacements imposed
|
|
30
|
+
exactly as linear constraints `c = [T] c_r + \{c_{p0}\} + \lambda
|
|
31
|
+
\{c_{p1}\}`, whose reduced residual
|
|
32
|
+
|
|
33
|
+
.. math::
|
|
34
|
+
|
|
35
|
+
\{R_r(c_r, \lambda)\} = [T]^T \left( \lambda \{F_{ext}\} -
|
|
36
|
+
\{F_{int}([T] c_r + \{c_{p0}\} + \lambda \{c_{p1}\})\} \right)
|
|
37
|
+
|
|
38
|
+
has `\partial R_r/\partial \lambda = [T]^T(\{F_{ext}\} - [K_T]
|
|
39
|
+
\{c_{p1}\})`, with `[K_T]` evaluated at the current configuration and
|
|
40
|
+
load factor. The keyword ``inc`` keeps its meaning, the factor that
|
|
41
|
+
multiplies the returned vector, and the solvers keep calling
|
|
42
|
+
``calc_fext(inc=1., ...)``. When ``calc_fext`` declares ``lbd``:
|
|
43
|
+
|
|
44
|
+
- the arc-length solvers pass ``lbd`` with ``c`` in the predictor and in
|
|
45
|
+
the constraint;
|
|
46
|
+
- the arc-length solvers scale the displacements with the linear solution
|
|
47
|
+
of `\partial R/\partial \lambda` at `c = 0, \lambda = 0`, instead of
|
|
48
|
+
the linear solution of the reference vector ``calc_fext(inc=1.)``, and
|
|
49
|
+
the Newton-Raphson solver uses this linear solution as the predictor of
|
|
50
|
+
its first step;
|
|
51
|
+
- the relative convergence criterion also considers the norm of
|
|
52
|
+
`\lambda \, \partial R/\partial \lambda`, see
|
|
53
|
+
:func:`.newton_raphson._check_convergence`, since under a prescribed
|
|
54
|
+
displacement alone the reference vector and the reduced internal force
|
|
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.
|
|
62
|
+
|
|
63
|
+
``lbd`` is never passed by the linear static analysis,
|
|
64
|
+
``Analysis.static(NLgeom=False)``.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
import inspect
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def accepts_kwarg(func, name):
|
|
71
|
+
"""Tell whether ``func`` declares the keyword argument ``name``"""
|
|
72
|
+
try:
|
|
73
|
+
sig = inspect.signature(func)
|
|
74
|
+
except (TypeError, ValueError):
|
|
75
|
+
return False
|
|
76
|
+
par = sig.parameters.get(name)
|
|
77
|
+
return par is not None and par.kind in (par.POSITIONAL_OR_KEYWORD,
|
|
78
|
+
par.KEYWORD_ONLY)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def calc_fint(an, c, lbd):
|
|
82
|
+
r"""``an.calc_fint`` at ``c``, with ``inc=lbd`` when accepted"""
|
|
83
|
+
kwargs = dict(inc=lbd) if accepts_kwarg(an.calc_fint, 'inc') else {}
|
|
84
|
+
return an.calc_fint(c=c, silent=True, **kwargs)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def calc_kT(an, c, lbd):
|
|
88
|
+
r"""Tangent stiffness matrix ``calc_kC + calc_kG`` at ``c``, with
|
|
89
|
+
``inc=lbd`` passed to the callables that accept it"""
|
|
90
|
+
kwC = dict(inc=lbd) if accepts_kwarg(an.calc_kC, 'inc') else {}
|
|
91
|
+
kwG = dict(inc=lbd) if accepts_kwarg(an.calc_kG, 'inc') else {}
|
|
92
|
+
kC = an.calc_kC(c=c, NLgeom=True, silent=True, **kwC)
|
|
93
|
+
kG = an.calc_kG(c=c, NLgeom=True, silent=True, **kwG)
|
|
94
|
+
return kC + kG
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_load_configuration_dependent(an):
|
|
98
|
+
"""Tell whether ``an.calc_fext`` accepts the configuration ``c``"""
|
|
99
|
+
return accepts_kwarg(an.calc_fext, 'c')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def is_load_factor_dependent(an):
|
|
103
|
+
"""Tell whether ``an.calc_fext`` accepts the load factor ``lbd``"""
|
|
104
|
+
return accepts_kwarg(an.calc_fext, 'lbd')
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def calc_load_vector(an, c, fext, lbd=None):
|
|
108
|
+
r"""Derivative of the residual with respect to the load factor
|
|
109
|
+
|
|
110
|
+
Returns `\{q\} = \partial R/\partial \lambda` at the configuration ``c``
|
|
111
|
+
and the load factor ``lbd``, used by the solvers in the predictors, in
|
|
112
|
+
the arc-length constraint and in the convergence criterion. It is the
|
|
113
|
+
external load vector only for loads that do not depend on the
|
|
114
|
+
configuration; with prescribed displacements imposed exactly it also
|
|
115
|
+
contains `-[T]^T [K_T] \{c_{p1}\}`, see the module docstring.
|
|
116
|
+
|
|
117
|
+
With prescribed displacements, `\{q\}` is not their reaction force: it
|
|
118
|
+
has the size of the reduced problem and acts on the free degrees of
|
|
119
|
+
freedom. It is the out-of-balance force produced by a unit increment of
|
|
120
|
+
the load factor at the current configuration, with the free degrees of
|
|
121
|
+
freedom held fixed. The reaction is a quantity of the full problem,
|
|
122
|
+
e.g. the generalized reaction work-conjugate to `\lambda`, `P =
|
|
123
|
+
\{c_{p1}\}^T (\{F_{int}(c)\} - \lambda \{F_{ext}\})`. ``structsolve``
|
|
124
|
+
does not compute it; see for instance ``MultiDomain.reaction_pd`` of
|
|
125
|
+
`panels`.
|
|
126
|
+
|
|
127
|
+
``an.calc_fext`` is called with ``inc=1.`` and only with the keyword
|
|
128
|
+
arguments that it declares:
|
|
129
|
+
|
|
130
|
+
- ``c=c``, when it declares ``c``;
|
|
131
|
+
- ``lbd=lbd``, when it declares ``lbd`` and ``lbd`` is not ``None``.
|
|
132
|
+
|
|
133
|
+
When none of them is passed, `\{q\}` is the constant reference vector,
|
|
134
|
+
and ``fext`` is returned without calling ``an.calc_fext``.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
an : :class:`.Analysis`
|
|
139
|
+
Analysis object.
|
|
140
|
+
c : array-like
|
|
141
|
+
Configuration.
|
|
142
|
+
fext : array-like
|
|
143
|
+
Reference vector ``an.calc_fext(inc=1.)``.
|
|
144
|
+
lbd : float or None, optional
|
|
145
|
+
Load factor.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
q : array-like
|
|
150
|
+
`\partial R/\partial \lambda`.
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
kwargs = {}
|
|
154
|
+
if is_load_configuration_dependent(an):
|
|
155
|
+
kwargs['c'] = c
|
|
156
|
+
if lbd is not None and is_load_factor_dependent(an):
|
|
157
|
+
kwargs['lbd'] = lbd
|
|
158
|
+
if not kwargs:
|
|
159
|
+
return fext
|
|
160
|
+
return an.calc_fext(inc=1., silent=True, **kwargs)
|
|
@@ -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))
|