structsolve 0.6.0__tar.gz → 0.7.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.0/structsolve.egg-info → structsolve-0.7.0}/PKG-INFO +10 -4
- {structsolve-0.6.0 → structsolve-0.7.0}/README.md +8 -3
- {structsolve-0.6.0 → structsolve-0.7.0}/pyproject.toml +2 -1
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/analysis.py +12 -1
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/arc_length.py +38 -5
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/arpackutils.py +36 -4
- structsolve-0.7.0/structsolve/callbacks.py +154 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/linear_buckling.py +4 -3
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/newton_raphson.py +46 -10
- {structsolve-0.6.0 → structsolve-0.7.0/structsolve.egg-info}/PKG-INFO +10 -4
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve.egg-info/SOURCES.txt +1 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_linear_buckling_unsymmetric.py +3 -0
- structsolve-0.7.0/tests/test_load_factor_dependent_load.py +298 -0
- structsolve-0.6.0/structsolve/callbacks.py +0 -68
- {structsolve-0.6.0 → structsolve-0.7.0}/LICENSE +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/setup.cfg +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/__init__.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/freq.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/logger.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/sparseutils.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve/static.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_analysis.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_arc_length.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_arpackutils.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_follower_freq_static.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_follower_loads.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_freq.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_linear_buckling.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_sparse_first.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_sparseutils.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.0}/tests/test_static.py +0 -0
- {structsolve-0.6.0 → structsolve-0.7.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.7.0
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Operating System :: Microsoft :: Windows
|
|
23
23
|
Classifier: Operating System :: Unix
|
|
24
|
+
Classifier: Environment :: WebAssembly :: Emscripten
|
|
24
25
|
Requires-Python: >=3.8
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
License-File: LICENSE
|
|
@@ -53,6 +54,8 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
53
54
|
- Nonlinear statics using Newton-Raphson
|
|
54
55
|
- Nonlinear statics using the Arc-Length method
|
|
55
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
|
|
56
59
|
|
|
57
60
|
Currently these solvers are pretty much compatible with my other repositories
|
|
58
61
|
[panels](https://github.com/saullocastro/panels),
|
|
@@ -62,7 +65,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
62
65
|
Citing this library
|
|
63
66
|
===================
|
|
64
67
|
|
|
65
|
-
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.7.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
Documentation
|
|
@@ -77,9 +80,12 @@ History
|
|
|
77
80
|
|
|
78
81
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
79
82
|
|
|
80
|
-
* version 0.
|
|
83
|
+
* version 0.7.0 (2026-10-13)
|
|
84
|
+
- `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
|
+
* version 0.6.1 (2026-10-08)
|
|
86
|
+
- Pyodide support: `await micropip.install("structsolve")` in the browser, and the whole test suite runs in Pyodide in GitHub Actions
|
|
81
87
|
- `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`
|
|
82
|
-
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`)
|
|
88
|
+
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`), computing more than 15 eigenpairs in passes whose eigenvectors are made orthonormal with a Rayleigh-Ritz projection
|
|
83
89
|
- Unsymmetric `lb`: a shift search finds the lowest positive real load multiplier, instead of silently returning negative or complex ones
|
|
84
90
|
- The test extra no longer requires `buckling` and `composites`
|
|
85
91
|
* version 0.5.3 (2026-10-06)
|
|
@@ -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.7.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
Documentation
|
|
@@ -42,9 +44,12 @@ History
|
|
|
42
44
|
|
|
43
45
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
44
46
|
|
|
45
|
-
* version 0.
|
|
47
|
+
* version 0.7.0 (2026-10-13)
|
|
48
|
+
- `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
|
+
* version 0.6.1 (2026-10-08)
|
|
50
|
+
- Pyodide support: `await micropip.install("structsolve")` in the browser, and the whole test suite runs in Pyodide in GitHub Actions
|
|
46
51
|
- `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`
|
|
47
|
-
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`)
|
|
52
|
+
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`), computing more than 15 eigenpairs in passes whose eigenvectors are made orthonormal with a Rayleigh-Ritz projection
|
|
48
53
|
- Unsymmetric `lb`: a shift search finds the lowest positive real load multiplier, instead of silently returning negative or complex ones
|
|
49
54
|
- The test extra no longer requires `buckling` and `composites`
|
|
50
55
|
* version 0.5.3 (2026-10-06)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structsolve"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
description = "Structural analysis solvers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "BSD-2-Clause"}
|
|
@@ -31,6 +31,7 @@ classifiers = [
|
|
|
31
31
|
"Programming Language :: Python :: 3.14",
|
|
32
32
|
"Operating System :: Microsoft :: Windows",
|
|
33
33
|
"Operating System :: Unix",
|
|
34
|
+
"Environment :: WebAssembly :: Emscripten",
|
|
34
35
|
]
|
|
35
36
|
dependencies = [
|
|
36
37
|
"numpy",
|
|
@@ -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.
|
|
@@ -110,6 +110,20 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
110
110
|
solution of the reference vector. The callables that accept ``inc``
|
|
111
111
|
receive the current load factor, see :mod:`.callbacks`.
|
|
112
112
|
|
|
113
|
+
When ``calc_fext`` also accepts the keyword argument ``lbd``, e.g. for
|
|
114
|
+
prescribed displacements imposed exactly, it receives the load factor
|
|
115
|
+
together with the configuration, `\{F_{ext}\}` above being then
|
|
116
|
+
`\partial R/\partial \lambda` at the current `c` and `\lambda`;
|
|
117
|
+
`\{u_{ref}\}` is the linear solution of `\partial R/\partial \lambda`
|
|
118
|
+
at `c = 0`, `\lambda = 0`, since the reference vector may be null, e.g.
|
|
119
|
+
under a prescribed displacement alone; and `\lambda \, \partial
|
|
120
|
+
R/\partial \lambda`, evaluated at the current iterate before each
|
|
121
|
+
convergence check, is an additional reference of the relative
|
|
122
|
+
convergence criterion. The same `\partial R/\partial \lambda` is used
|
|
123
|
+
in the bordered solution when the tangent stiffness matrix is updated at
|
|
124
|
+
that iterate; with ``an.modified_NR = True`` it is one call to
|
|
125
|
+
``calc_fext`` per iteration.
|
|
126
|
+
|
|
113
127
|
The initial arc length corresponds to a load factor increment of
|
|
114
128
|
``an.initialInc`` along the initial tangent. The convergence and
|
|
115
129
|
divergence checks are the same as in the Newton-Raphson solver, see
|
|
@@ -136,7 +150,13 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
136
150
|
|
|
137
151
|
fext = an.calc_fext(inc=1., silent=True)
|
|
138
152
|
kC0 = an.calc_kC(silent=silent)
|
|
139
|
-
|
|
153
|
+
lbd_dependent = callbacks.is_load_factor_dependent(an)
|
|
154
|
+
if lbd_dependent:
|
|
155
|
+
# dR/dlbd at c = 0 and lbd = 0
|
|
156
|
+
q0 = callbacks.calc_load_vector(an, np.zeros_like(fext), fext, 0.)
|
|
157
|
+
u_ref = solve(kC0, q0, silent=True)
|
|
158
|
+
else:
|
|
159
|
+
u_ref = solve(kC0, fext, silent=True)
|
|
140
160
|
scale2 = u_ref.dot(u_ref)
|
|
141
161
|
if not scale2 > 0:
|
|
142
162
|
scale2 = 1.
|
|
@@ -175,7 +195,8 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
175
195
|
level=1, silent=silent)
|
|
176
196
|
break
|
|
177
197
|
# load vector of the converged configuration, dR/dlbd
|
|
178
|
-
|
|
198
|
+
q_conv = callbacks.calc_load_vector(an, c, fext, lbd)
|
|
199
|
+
dc_q_conv = solve_kT_conv(q_conv)
|
|
179
200
|
norm_q = np.sqrt(inner(dc_q_conv, 1., dc_q_conv, 1.))
|
|
180
201
|
if arc_length is None:
|
|
181
202
|
arc_length = an.initialInc*norm_q
|
|
@@ -204,8 +225,15 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
204
225
|
fint = callbacks.calc_fint(an, c + dc, lbd + dlbd)
|
|
205
226
|
fext_total = (lbd + dlbd)*fext
|
|
206
227
|
R = fext_total - fint
|
|
228
|
+
q_ref = None
|
|
229
|
+
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
|
+
q_ref = (lbd + dlbd)*q_iter
|
|
207
234
|
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
|
|
208
|
-
fext_ref=dlbd*fext
|
|
235
|
+
fext_ref=dlbd*fext,
|
|
236
|
+
q_ref=q_ref)
|
|
209
237
|
msg('Iteration: %d, lbd %1.5f, max(|R|) = %1.3e, relative ||R|| = %1.3e'
|
|
210
238
|
% (iteration, lbd + dlbd, np.abs(R).max(), Rrel), level=2, silent=silent)
|
|
211
239
|
if conv:
|
|
@@ -225,7 +253,11 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
225
253
|
except RuntimeError:
|
|
226
254
|
warn('Singular tangent stiffness matrix', level=2, silent=silent)
|
|
227
255
|
break
|
|
228
|
-
|
|
256
|
+
if lbd_dependent:
|
|
257
|
+
dc_q = solve_kT(q_iter)
|
|
258
|
+
else:
|
|
259
|
+
dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc,
|
|
260
|
+
fext))
|
|
229
261
|
iter_kT = 1
|
|
230
262
|
dc_R = solve_kT(R)
|
|
231
263
|
|
|
@@ -261,7 +293,8 @@ def _solver_arc_length(an, method, silent=False):
|
|
|
261
293
|
if converged and lbd + dlbd > 1. and lbd < 1.:
|
|
262
294
|
msg('Load factor 1.0 exceeded, correcting to load factor 1.0...', level=1, silent=silent)
|
|
263
295
|
t = (1. - lbd)/dlbd
|
|
264
|
-
converged, dc1 = _NR_iterations(an, c, t*dc, 1., fext, kT0=None,
|
|
296
|
+
converged, dc1 = _NR_iterations(an, c, t*dc, 1., fext, kT0=None,
|
|
297
|
+
silent=silent, lbd_dependent=lbd_dependent)
|
|
265
298
|
if converged:
|
|
266
299
|
msg('Converged at load factor 1.00000', level=2, silent=silent)
|
|
267
300
|
an.cs.append((c + dc1).copy())
|
|
@@ -11,7 +11,9 @@ MKL 2025.0.1 or newer is not affected, neither are the non-symmetric drivers
|
|
|
11
11
|
of :func:`scipy.sparse.linalg.eigs`, which do not call ``dsteqr``.
|
|
12
12
|
|
|
13
13
|
:func:`capped_eigsh` keeps ``ncv <= 32`` when :func:`arpack_max_ncv` says so,
|
|
14
|
-
computing more than 15 eigenpairs in several calls with deflation
|
|
14
|
+
computing more than 15 eigenpairs in several calls with deflation, whose
|
|
15
|
+
eigenvectors are combined with a Rayleigh-Ritz projection such that they
|
|
16
|
+
are orthonormal also across the calls.
|
|
15
17
|
|
|
16
18
|
The cap is decided by :func:`arpack_max_ncv` and can be overridden with
|
|
17
19
|
the module attribute :data:`ARPACK_MAX_NCV` or the environment variable
|
|
@@ -32,6 +34,7 @@ import sys
|
|
|
32
34
|
import warnings
|
|
33
35
|
|
|
34
36
|
import numpy as np
|
|
37
|
+
from scipy.linalg import cholesky, eigh, solve_triangular
|
|
35
38
|
from scipy.sparse import csc_matrix
|
|
36
39
|
from scipy.sparse.linalg import LinearOperator, splu
|
|
37
40
|
from scipy.sparse.linalg import eigsh as _scipy_eigsh
|
|
@@ -281,6 +284,34 @@ def _deflation_shift(w, far_shift):
|
|
|
281
284
|
return 0.5*(last[i] + last[i + 1])
|
|
282
285
|
|
|
283
286
|
|
|
287
|
+
def _rayleigh_ritz(A, M, V):
|
|
288
|
+
r"""Rayleigh-Ritz projection of `[A]\{x\} = w [M]\{x\}` onto `[V]`
|
|
289
|
+
|
|
290
|
+
The eigenvectors of different passes of :func:`capped_eigsh` are only
|
|
291
|
+
`[M]`-orthogonal to the accuracy of ARPACK, e.g. `10^{-6}` between
|
|
292
|
+
close eigenvalues of different passes. `[V]` is `[M]`-orthonormalized
|
|
293
|
+
with the Cholesky factor `[L]` of `[V]^T[M][V]`, and the projected
|
|
294
|
+
problem `[W]^T[A][W]\{y\} = w\{y\}`, with `[W] = [V][L]^{-T}`, is
|
|
295
|
+
solved with the LAPACK driver ``dsyevr`` (``driver='evr'``), which does
|
|
296
|
+
not use the ``dsteqr`` of Intel MKL broken above 32 x 32 matrices, see
|
|
297
|
+
:func:`dsteqr_is_faulty`. The returned eigenvectors `[W]\{y\}` are
|
|
298
|
+
`[M]`-orthonormal to the rounding errors, and the Ritz values are at
|
|
299
|
+
least as accurate as the eigenvalues of the passes.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
w, x : ndarray
|
|
304
|
+
The Ritz values by increasing value and the Ritz vectors.
|
|
305
|
+
|
|
306
|
+
"""
|
|
307
|
+
MV = M @ V
|
|
308
|
+
L = cholesky(0.5*(V.T @ MV + MV.T @ V), lower=True)
|
|
309
|
+
W = solve_triangular(L, V.T, lower=True).T
|
|
310
|
+
H = W.T @ (A @ W)
|
|
311
|
+
w, Y = eigh(0.5*(H + H.T), driver='evr')
|
|
312
|
+
return w, W @ Y
|
|
313
|
+
|
|
314
|
+
|
|
284
315
|
def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
|
|
285
316
|
r""":func:`scipy.sparse.linalg.eigsh` with ``ncv`` from :func:`arpack_ncv`
|
|
286
317
|
|
|
@@ -312,7 +343,9 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
|
|
|
312
343
|
in the study of the panels cylinders it was 30 to 45% faster and gave
|
|
313
344
|
eigenvalues 5 to 10 times more accurate. Each pass factorizes
|
|
314
345
|
`[A] - \sigma_d [M]` with SuperLU, and every call starts from
|
|
315
|
-
:func:`start_vector`.
|
|
346
|
+
:func:`start_vector`. The eigenvectors of all the passes are finally
|
|
347
|
+
combined with a Rayleigh-Ritz projection, see :func:`_rayleigh_ritz`,
|
|
348
|
+
such that they are `[M]`-orthonormal also across the passes.
|
|
316
349
|
|
|
317
350
|
Returns
|
|
318
351
|
-------
|
|
@@ -359,5 +392,4 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
|
|
|
359
392
|
w = np.concatenate((w, wi))
|
|
360
393
|
V = np.hstack((V, Vi))
|
|
361
394
|
release_memory()
|
|
362
|
-
|
|
363
|
-
return w[order], V[:, order]
|
|
395
|
+
return _rayleigh_ritz(A, M, V)
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
|
|
57
|
+
``lbd`` is never passed by the linear static analysis,
|
|
58
|
+
``Analysis.static(NLgeom=False)``.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
import inspect
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def accepts_kwarg(func, name):
|
|
65
|
+
"""Tell whether ``func`` declares the keyword argument ``name``"""
|
|
66
|
+
try:
|
|
67
|
+
sig = inspect.signature(func)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
return False
|
|
70
|
+
par = sig.parameters.get(name)
|
|
71
|
+
return par is not None and par.kind in (par.POSITIONAL_OR_KEYWORD,
|
|
72
|
+
par.KEYWORD_ONLY)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def calc_fint(an, c, lbd):
|
|
76
|
+
r"""``an.calc_fint`` at ``c``, with ``inc=lbd`` when accepted"""
|
|
77
|
+
kwargs = dict(inc=lbd) if accepts_kwarg(an.calc_fint, 'inc') else {}
|
|
78
|
+
return an.calc_fint(c=c, silent=True, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def calc_kT(an, c, lbd):
|
|
82
|
+
r"""Tangent stiffness matrix ``calc_kC + calc_kG`` at ``c``, with
|
|
83
|
+
``inc=lbd`` passed to the callables that accept it"""
|
|
84
|
+
kwC = dict(inc=lbd) if accepts_kwarg(an.calc_kC, 'inc') else {}
|
|
85
|
+
kwG = dict(inc=lbd) if accepts_kwarg(an.calc_kG, 'inc') else {}
|
|
86
|
+
kC = an.calc_kC(c=c, NLgeom=True, silent=True, **kwC)
|
|
87
|
+
kG = an.calc_kG(c=c, NLgeom=True, silent=True, **kwG)
|
|
88
|
+
return kC + kG
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_load_configuration_dependent(an):
|
|
92
|
+
"""Tell whether ``an.calc_fext`` accepts the configuration ``c``"""
|
|
93
|
+
return accepts_kwarg(an.calc_fext, 'c')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_load_factor_dependent(an):
|
|
97
|
+
"""Tell whether ``an.calc_fext`` accepts the load factor ``lbd``"""
|
|
98
|
+
return accepts_kwarg(an.calc_fext, 'lbd')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def calc_load_vector(an, c, fext, lbd=None):
|
|
102
|
+
r"""Derivative of the residual with respect to the load factor
|
|
103
|
+
|
|
104
|
+
Returns `\{q\} = \partial R/\partial \lambda` at the configuration ``c``
|
|
105
|
+
and the load factor ``lbd``, used by the solvers in the predictors, in
|
|
106
|
+
the arc-length constraint and in the convergence criterion. It is the
|
|
107
|
+
external load vector only for loads that do not depend on the
|
|
108
|
+
configuration; with prescribed displacements imposed exactly it also
|
|
109
|
+
contains `-[T]^T [K_T] \{c_{p1}\}`, see the module docstring.
|
|
110
|
+
|
|
111
|
+
With prescribed displacements, `\{q\}` is not their reaction force: it
|
|
112
|
+
has the size of the reduced problem and acts on the free degrees of
|
|
113
|
+
freedom. It is the out-of-balance force produced by a unit increment of
|
|
114
|
+
the load factor at the current configuration, with the free degrees of
|
|
115
|
+
freedom held fixed. The reaction is a quantity of the full problem,
|
|
116
|
+
e.g. the generalized reaction work-conjugate to `\lambda`, `P =
|
|
117
|
+
\{c_{p1}\}^T (\{F_{int}(c)\} - \lambda \{F_{ext}\})`. ``structsolve``
|
|
118
|
+
does not compute it; see for instance ``MultiDomain.reaction_pd`` of
|
|
119
|
+
`panels`.
|
|
120
|
+
|
|
121
|
+
``an.calc_fext`` is called with ``inc=1.`` and only with the keyword
|
|
122
|
+
arguments that it declares:
|
|
123
|
+
|
|
124
|
+
- ``c=c``, when it declares ``c``;
|
|
125
|
+
- ``lbd=lbd``, when it declares ``lbd`` and ``lbd`` is not ``None``.
|
|
126
|
+
|
|
127
|
+
When none of them is passed, `\{q\}` is the constant reference vector,
|
|
128
|
+
and ``fext`` is returned without calling ``an.calc_fext``.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
an : :class:`.Analysis`
|
|
133
|
+
Analysis object.
|
|
134
|
+
c : array-like
|
|
135
|
+
Configuration.
|
|
136
|
+
fext : array-like
|
|
137
|
+
Reference vector ``an.calc_fext(inc=1.)``.
|
|
138
|
+
lbd : float or None, optional
|
|
139
|
+
Load factor.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
q : array-like
|
|
144
|
+
`\partial R/\partial \lambda`.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
kwargs = {}
|
|
148
|
+
if is_load_configuration_dependent(an):
|
|
149
|
+
kwargs['c'] = c
|
|
150
|
+
if lbd is not None and is_load_factor_dependent(an):
|
|
151
|
+
kwargs['lbd'] = lbd
|
|
152
|
+
if not kwargs:
|
|
153
|
+
return fext
|
|
154
|
+
return an.calc_fext(inc=1., silent=True, **kwargs)
|
|
@@ -862,9 +862,10 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
862
862
|
MKL versions returns wrong eigenvectors for matrices larger than
|
|
863
863
|
32 x 32, which ARPACK uses with the size ``ncv``. When such a LAPACK is
|
|
864
864
|
detected, ``ncv`` is capped at 32 and more than 15 eigenpairs are
|
|
865
|
-
computed in several passes,
|
|
866
|
-
the cap can be overridden,
|
|
867
|
-
``STRUCTSOLVE_ARPACK_MAX_NCV=0`` to
|
|
865
|
+
computed in several passes, combined with a Rayleigh-Ritz projection,
|
|
866
|
+
see :mod:`structsolve.arpackutils`, where the cap can be overridden,
|
|
867
|
+
e.g. with the environment variable ``STRUCTSOLVE_ARPACK_MAX_NCV=0`` to
|
|
868
|
+
lift it with MKL 2025.0.1 or newer.
|
|
868
869
|
The non-symmetric drivers of :func:`scipy.sparse.linalg.eigs` are not
|
|
869
870
|
affected.
|
|
870
871
|
|
|
@@ -32,13 +32,14 @@ INC_GROWTH_FACTOR = 1.1111
|
|
|
32
32
|
INC_CUT_FACTOR = 0.5
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _check_convergence(an, R, fint, fext_total, fext_ref=None):
|
|
36
|
-
"""Check the convergence of the residual force vector
|
|
35
|
+
def _check_convergence(an, R, fint, fext_total, fext_ref=None, q_ref=None):
|
|
36
|
+
r"""Check the convergence of the residual force vector
|
|
37
37
|
|
|
38
38
|
The convergence is achieved when either:
|
|
39
39
|
|
|
40
|
-
- ``||R|| <= relTOL*max(||fext_total||, ||fint||, ||fext_ref
|
|
41
|
-
the Euclidean norm, if ``an.relTOL`` is not
|
|
40
|
+
- ``||R|| <= relTOL*max(||fext_total||, ||fint||, ||fext_ref||,
|
|
41
|
+
||q_ref||)``, using the Euclidean norm, if ``an.relTOL`` is not
|
|
42
|
+
``None``
|
|
42
43
|
- ``max(|R|) <= absTOL``, if ``an.absTOL`` is not ``None``
|
|
43
44
|
|
|
44
45
|
Parameters
|
|
@@ -54,6 +55,12 @@ def _check_convergence(an, R, fint, fext_total, fext_ref=None):
|
|
|
54
55
|
fext_ref : array-like or None, optional
|
|
55
56
|
Additional reference force vector, e.g. the external force vector of
|
|
56
57
|
the current increment, useful when the load factor is close to zero.
|
|
58
|
+
q_ref : array-like or None, optional
|
|
59
|
+
Additional reference force vector, the load factor times
|
|
60
|
+
`\partial R/\partial \lambda` when ``calc_fext`` accepts ``lbd``,
|
|
61
|
+
see :mod:`.callbacks`. Under a prescribed displacement alone, the
|
|
62
|
+
external and the internal force vectors of the reduced problem both
|
|
63
|
+
vanish at equilibrium and cannot be the reference.
|
|
57
64
|
|
|
58
65
|
Returns
|
|
59
66
|
-------
|
|
@@ -62,7 +69,8 @@ def _check_convergence(an, R, fint, fext_total, fext_ref=None):
|
|
|
62
69
|
Rnorm : float
|
|
63
70
|
Euclidean norm of the residual.
|
|
64
71
|
Rrel : float
|
|
65
|
-
Relative residual ``||R||/max(||fext_total||, ||fint||, ||fext_ref
|
|
72
|
+
Relative residual ``||R||/max(||fext_total||, ||fint||, ||fext_ref||,
|
|
73
|
+
||q_ref||)``.
|
|
66
74
|
|
|
67
75
|
"""
|
|
68
76
|
if an.relTOL is None and an.absTOL is None:
|
|
@@ -71,6 +79,8 @@ def _check_convergence(an, R, fint, fext_total, fext_ref=None):
|
|
|
71
79
|
ref = max(np.linalg.norm(fext_total), np.linalg.norm(fint))
|
|
72
80
|
if fext_ref is not None:
|
|
73
81
|
ref = max(ref, np.linalg.norm(fext_ref))
|
|
82
|
+
if q_ref is not None:
|
|
83
|
+
ref = max(ref, np.linalg.norm(q_ref))
|
|
74
84
|
Rrel = Rnorm/ref if ref > 0 else Rnorm
|
|
75
85
|
converged = False
|
|
76
86
|
if an.relTOL is not None and Rnorm <= an.relTOL*ref:
|
|
@@ -128,8 +138,9 @@ def _check_divergence(an, Rnorm, Rnorms, iteration, silent=False):
|
|
|
128
138
|
return False
|
|
129
139
|
|
|
130
140
|
|
|
131
|
-
def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False
|
|
132
|
-
|
|
141
|
+
def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False,
|
|
142
|
+
lbd_dependent=False):
|
|
143
|
+
r"""Newton-Raphson iterations at a fixed load factor
|
|
133
144
|
|
|
134
145
|
Parameters
|
|
135
146
|
----------
|
|
@@ -148,6 +159,11 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False):
|
|
|
148
159
|
``an.modified_NR=True``. If ``None`` it is calculated at ``c + dc``.
|
|
149
160
|
silent : bool, optional
|
|
150
161
|
A boolean to tell whether the log messages should be printed.
|
|
162
|
+
lbd_dependent : bool, optional
|
|
163
|
+
Whether ``calc_fext`` accepts ``lbd``, see :mod:`.callbacks`;
|
|
164
|
+
``total*q``, with `\{q\} = \partial R/\partial \lambda` evaluated
|
|
165
|
+
at the current iterate, is then an additional reference of the
|
|
166
|
+
relative convergence criterion, see :func:`._check_convergence`.
|
|
151
167
|
|
|
152
168
|
Returns
|
|
153
169
|
-------
|
|
@@ -159,6 +175,7 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False):
|
|
|
159
175
|
"""
|
|
160
176
|
modified_NR = an.modified_NR
|
|
161
177
|
fext_total = total*fext
|
|
178
|
+
q_ref = None
|
|
162
179
|
Rnorms = []
|
|
163
180
|
iter_kT = 0
|
|
164
181
|
kT = None
|
|
@@ -170,8 +187,11 @@ def _NR_iterations(an, c, dc, total, fext, kT0=None, silent=False):
|
|
|
170
187
|
if fint is None:
|
|
171
188
|
fint = callbacks.calc_fint(an, c + dc, total)
|
|
172
189
|
R = fext_total - fint
|
|
190
|
+
if lbd_dependent:
|
|
191
|
+
q_ref = total*callbacks.calc_load_vector(an, c + dc, fext, total)
|
|
173
192
|
|
|
174
|
-
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total
|
|
193
|
+
conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
|
|
194
|
+
q_ref=q_ref)
|
|
175
195
|
msg('Iteration: %d, max(|R|) = %1.3e, relative ||R|| = %1.3e'
|
|
176
196
|
% (iteration, np.abs(R).max(), Rrel), level=2, silent=silent)
|
|
177
197
|
if conv:
|
|
@@ -235,6 +255,17 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
235
255
|
factor `\lambda` of the step, see :mod:`.callbacks`, which allows
|
|
236
256
|
configuration-dependent loads such as follower pressures.
|
|
237
257
|
|
|
258
|
+
When ``calc_fext`` accepts the keyword argument ``lbd``, e.g. for
|
|
259
|
+
prescribed displacements imposed exactly, see :mod:`.callbacks`, the
|
|
260
|
+
load vector `\{q\} = \partial R/\partial \lambda` at `c = 0`,
|
|
261
|
+
`\lambda = 0` gives the predictor of the first increment, its linear
|
|
262
|
+
solution `[K_0]^{-1}\{q\}`, instead of the linear solution of the
|
|
263
|
+
reference vector, which is null under a prescribed displacement alone,
|
|
264
|
+
and `\lambda \|q\|`, with `\{q\}` evaluated at the current iterate
|
|
265
|
+
before each convergence check, is an additional reference of the
|
|
266
|
+
relative convergence criterion. With ``an.modified_NR = True`` this is
|
|
267
|
+
one call to ``calc_fext`` per iteration.
|
|
268
|
+
|
|
238
269
|
By default full Newton-Raphson is used, i.e. the tangent stiffness matrix
|
|
239
270
|
``kT = kC + kG`` is rebuilt at every iteration, which gives quadratic
|
|
240
271
|
convergence when ``kT`` is the exact Jacobian of ``fint``. With
|
|
@@ -283,8 +314,12 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
283
314
|
|
|
284
315
|
fext = an.calc_fext(inc=1., silent=True)
|
|
285
316
|
kC0 = an.calc_kC(silent=silent)
|
|
317
|
+
q = None
|
|
318
|
+
if callbacks.is_load_factor_dependent(an):
|
|
319
|
+
# dR/dlbd at c = 0 and lbd = 0
|
|
320
|
+
q = callbacks.calc_load_vector(an, np.zeros_like(fext), fext, 0.)
|
|
286
321
|
# predictor of the first step: linear solution scaled by the load factor
|
|
287
|
-
dc_last = solve(kC0, fext, silent=True)
|
|
322
|
+
dc_last = solve(kC0, fext if q is None else q, silent=True)
|
|
288
323
|
inc_last = 1.
|
|
289
324
|
c = np.zeros_like(dc_last)
|
|
290
325
|
total_last = 0.
|
|
@@ -305,7 +340,8 @@ def _solver_NR(an, silent=False, initialInc=None):
|
|
|
305
340
|
if an.modified_NR and step_num == 1 and not an.kT_initial_state:
|
|
306
341
|
kT0 = kC0
|
|
307
342
|
converged, dc = _NR_iterations(an, c, dc, total, fext, kT0=kT0,
|
|
308
|
-
silent=silent
|
|
343
|
+
silent=silent,
|
|
344
|
+
lbd_dependent=q is not None)
|
|
309
345
|
|
|
310
346
|
if converged:
|
|
311
347
|
msg('Converged at load factor %1.5f' % total, level=2, silent=silent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Operating System :: Microsoft :: Windows
|
|
23
23
|
Classifier: Operating System :: Unix
|
|
24
|
+
Classifier: Environment :: WebAssembly :: Emscripten
|
|
24
25
|
Requires-Python: >=3.8
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
License-File: LICENSE
|
|
@@ -53,6 +54,8 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
53
54
|
- Nonlinear statics using Newton-Raphson
|
|
54
55
|
- Nonlinear statics using the Arc-Length method
|
|
55
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
|
|
56
59
|
|
|
57
60
|
Currently these solvers are pretty much compatible with my other repositories
|
|
58
61
|
[panels](https://github.com/saullocastro/panels),
|
|
@@ -62,7 +65,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
62
65
|
Citing this library
|
|
63
66
|
===================
|
|
64
67
|
|
|
65
|
-
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.7.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
Documentation
|
|
@@ -77,9 +80,12 @@ History
|
|
|
77
80
|
|
|
78
81
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
79
82
|
|
|
80
|
-
* version 0.
|
|
83
|
+
* version 0.7.0 (2026-10-13)
|
|
84
|
+
- `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
|
+
* version 0.6.1 (2026-10-08)
|
|
86
|
+
- Pyodide support: `await micropip.install("structsolve")` in the browser, and the whole test suite runs in Pyodide in GitHub Actions
|
|
81
87
|
- `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`
|
|
82
|
-
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`)
|
|
88
|
+
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`), computing more than 15 eigenpairs in passes whose eigenvectors are made orthonormal with a Rayleigh-Ritz projection
|
|
83
89
|
- Unsymmetric `lb`: a shift search finds the lowest positive real load multiplier, instead of silently returning negative or complex ones
|
|
84
90
|
- The test extra no longer requires `buckling` and `composites`
|
|
85
91
|
* version 0.5.3 (2026-10-06)
|
|
@@ -27,6 +27,7 @@ tests/test_follower_loads.py
|
|
|
27
27
|
tests/test_freq.py
|
|
28
28
|
tests/test_linear_buckling.py
|
|
29
29
|
tests/test_linear_buckling_unsymmetric.py
|
|
30
|
+
tests/test_load_factor_dependent_load.py
|
|
30
31
|
tests/test_newton_raphson.py
|
|
31
32
|
tests/test_sparse_first.py
|
|
32
33
|
tests/test_sparseutils.py
|
|
@@ -217,6 +217,9 @@ def test_lb_nearly_symmetric(sparse_solver, max_dense_size):
|
|
|
217
217
|
assert np.all(relative_residuals(Kn, KG, eigvals, eigvecs) < 1e-8)
|
|
218
218
|
|
|
219
219
|
|
|
220
|
+
@pytest.mark.skipif(sys.platform == 'emscripten',
|
|
221
|
+
reason='no subprocesses in Pyodide, whose SciPy is not '
|
|
222
|
+
'linked to MKL')
|
|
220
223
|
def test_eig_complex_mkl_crash_regression():
|
|
221
224
|
"""Regression test: with Intel MKL 2025.0.0, scipy.linalg.eig on these
|
|
222
225
|
real 48 x 48 matrices (LAPACK dggev) corrupts the heap and the process
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
r"""Load vector depending on the load factor: prescribed displacements
|
|
2
|
+
|
|
3
|
+
Three hardening springs in series, `f_i(e) = k_i e + a_i e^3`, between a
|
|
4
|
+
wall and the nodes 1, 2 and 3, with the displacement of node 3 prescribed,
|
|
5
|
+
`x_3 = d_0 + \lambda d_1`, and an optional load `\lambda F` on node 1. The
|
|
6
|
+
constraint is imposed exactly, as in the null-space method of `panels`:
|
|
7
|
+
|
|
8
|
+
.. math::
|
|
9
|
+
|
|
10
|
+
\{c\} = [T]\{c_r\} + \{c_{p0}\} + \lambda \{c_{p1}\}, \qquad
|
|
11
|
+
[T] = \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ 0 & 0 \end{bmatrix}, \qquad
|
|
12
|
+
\{c_{p0}\} = \{0, 0, d_0\}, \qquad \{c_{p1}\} = \{0, 0, d_1\}
|
|
13
|
+
|
|
14
|
+
such that the reduced problem has two degrees of freedom and the residual
|
|
15
|
+
|
|
16
|
+
.. math::
|
|
17
|
+
|
|
18
|
+
\{R_r(c_r, \lambda)\} = [T]^T \left(\lambda \{F_{ext}\} -
|
|
19
|
+
\{F_{int}(c)\}\right), \qquad
|
|
20
|
+
\frac{\partial R_r}{\partial \lambda} = [T]^T \left(\{F_{ext}\} -
|
|
21
|
+
[K_T(c)] \{c_{p1}\}\right)
|
|
22
|
+
|
|
23
|
+
whose derivative with respect to the load factor depends on the
|
|
24
|
+
configuration and on the load factor, passed to :class:`structsolve.Analysis`
|
|
25
|
+
with ``calc_fext(inc=1., c=c_r, lbd=lbd)``, see :mod:`structsolve.callbacks`.
|
|
26
|
+
Under the prescribed displacement alone, `F = 0`, the reference vector
|
|
27
|
+
``calc_fext(inc=1.)`` is null.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
import numpy as np
|
|
31
|
+
import pytest
|
|
32
|
+
from scipy.optimize import brentq
|
|
33
|
+
from scipy.sparse import csr_matrix
|
|
34
|
+
|
|
35
|
+
from structsolve import Analysis
|
|
36
|
+
from structsolve import callbacks
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
T = np.array([[1., 0.], [0., 1.], [0., 0.]])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SpringsProblem(object):
|
|
43
|
+
def __init__(self, k=(100., 150., 200.), a=(20., 40., 60.), d0=0.,
|
|
44
|
+
d1=1., F=0., with_lbd=True):
|
|
45
|
+
self.k = np.asarray(k)
|
|
46
|
+
self.a = np.asarray(a)
|
|
47
|
+
self.cp0 = np.array([0., 0., d0])
|
|
48
|
+
self.cp1 = np.array([0., 0., d1])
|
|
49
|
+
self.Fext = np.array([F, 0., 0.])
|
|
50
|
+
self.with_lbd = with_lbd
|
|
51
|
+
self.an = None
|
|
52
|
+
# (lbd, c_r) of each call to calc_fext with lbd, and (inc, ||R_r||,
|
|
53
|
+
# number of converged increments) of each call to calc_fint
|
|
54
|
+
self.fext_calls = []
|
|
55
|
+
self.fint_calls = []
|
|
56
|
+
|
|
57
|
+
def full(self, c_r, lbd):
|
|
58
|
+
return T @ c_r + self.cp0 + lbd*self.cp1
|
|
59
|
+
|
|
60
|
+
def elongations(self, c):
|
|
61
|
+
return np.array([c[0], c[1] - c[0], c[2] - c[1]])
|
|
62
|
+
|
|
63
|
+
def fint_full(self, c):
|
|
64
|
+
k, a = self.k, self.a
|
|
65
|
+
f = k*self.elongations(c) + a*self.elongations(c)**3
|
|
66
|
+
return np.array([f[0] - f[1], f[1] - f[2], f[2]])
|
|
67
|
+
|
|
68
|
+
def kT_full(self, c):
|
|
69
|
+
ke = self.k + 3*self.a*self.elongations(c)**2
|
|
70
|
+
return np.array([[ke[0] + ke[1], -ke[1], 0.],
|
|
71
|
+
[-ke[1], ke[1] + ke[2], -ke[2]],
|
|
72
|
+
[0., -ke[2], ke[2]]])
|
|
73
|
+
|
|
74
|
+
def residual(self, c_r, lbd):
|
|
75
|
+
return T.T @ (lbd*self.Fext - self.fint_full(self.full(c_r, lbd)))
|
|
76
|
+
|
|
77
|
+
def dR_dlbd(self, c_r, lbd):
|
|
78
|
+
c = self.full(c_r, lbd)
|
|
79
|
+
return T.T @ (self.Fext - self.kT_full(c) @ self.cp1)
|
|
80
|
+
|
|
81
|
+
def reaction(self, c_r, lbd):
|
|
82
|
+
r"""Force of the support on node 3, `c_{p1}^T r / d_1`, with `r =
|
|
83
|
+
F_{int}(c) - \lambda F_{ext}`"""
|
|
84
|
+
c = self.full(c_r, lbd)
|
|
85
|
+
r = self.fint_full(c) - lbd*self.Fext
|
|
86
|
+
return self.cp1 @ r/self.cp1[2]
|
|
87
|
+
|
|
88
|
+
# callables of structsolve.Analysis
|
|
89
|
+
def calc_fext(self, inc=1., silent=True, c=None, lbd=None):
|
|
90
|
+
if c is None:
|
|
91
|
+
return inc*(T.T @ self.Fext)
|
|
92
|
+
self.fext_calls.append((lbd, c.copy()))
|
|
93
|
+
return inc*self.dR_dlbd(c, lbd)
|
|
94
|
+
|
|
95
|
+
def calc_fint(self, c, inc=1., silent=True):
|
|
96
|
+
step = len(self.an.increments) if self.an is not None else 0
|
|
97
|
+
self.fint_calls.append((inc, np.linalg.norm(self.residual(c, inc)),
|
|
98
|
+
step))
|
|
99
|
+
return T.T @ self.fint_full(self.full(c, inc))
|
|
100
|
+
|
|
101
|
+
def calc_kC(self, c=None, NLgeom=False, inc=1., silent=True):
|
|
102
|
+
if c is None or not NLgeom:
|
|
103
|
+
return csr_matrix(T.T @ self.kT_full(np.zeros(3)) @ T)
|
|
104
|
+
return csr_matrix(T.T @ self.kT_full(self.full(c, inc)) @ T)
|
|
105
|
+
|
|
106
|
+
def calc_kG(self, c=None, NLgeom=False, silent=True):
|
|
107
|
+
return csr_matrix((2, 2))
|
|
108
|
+
|
|
109
|
+
def analysis(self, **kwargs):
|
|
110
|
+
if self.with_lbd:
|
|
111
|
+
calc_fext = self.calc_fext
|
|
112
|
+
else:
|
|
113
|
+
calc_fext = lambda inc=1., silent=True: self.calc_fext(inc)
|
|
114
|
+
an = Analysis(calc_fext, self.calc_fint, self.calc_kC, self.calc_kG)
|
|
115
|
+
for key, value in kwargs.items():
|
|
116
|
+
setattr(an, key, value)
|
|
117
|
+
self.an = an
|
|
118
|
+
return an
|
|
119
|
+
|
|
120
|
+
def exact(self, lbd):
|
|
121
|
+
r"""Solution without the load on node 1: the three springs carry the
|
|
122
|
+
same force `P`, found from `\sum_i e_i(P) = x_3`"""
|
|
123
|
+
assert self.Fext[0] == 0.
|
|
124
|
+
x3 = self.cp0[2] + lbd*self.cp1[2]
|
|
125
|
+
|
|
126
|
+
def e(P, i):
|
|
127
|
+
return brentq(lambda ei: self.k[i]*ei + self.a[i]*ei**3 - P,
|
|
128
|
+
-1e4, 1e4, xtol=1e-15, rtol=1e-15)
|
|
129
|
+
|
|
130
|
+
def gap(P):
|
|
131
|
+
return sum(e(P, i) for i in range(3)) - x3
|
|
132
|
+
|
|
133
|
+
P = 0. if x3 == 0 else brentq(gap, -1e4, 1e4, xtol=1e-14,
|
|
134
|
+
rtol=1e-15)
|
|
135
|
+
e1, e2 = e(P, 0), e(P, 1)
|
|
136
|
+
return np.array([e1, e1 + e2]), P
|
|
137
|
+
|
|
138
|
+
def solve_reference(self, lbd, tol=1e-14):
|
|
139
|
+
"""Dense Newton-Raphson with continuation on the load factor"""
|
|
140
|
+
c_r = np.zeros(2)
|
|
141
|
+
for l in np.linspace(0, lbd, 21)[1:]:
|
|
142
|
+
for _ in range(100):
|
|
143
|
+
R = self.residual(c_r, l)
|
|
144
|
+
if np.linalg.norm(R) <= tol*max(1., np.linalg.norm(
|
|
145
|
+
self.dR_dlbd(c_r, l))):
|
|
146
|
+
break
|
|
147
|
+
kT = T.T @ self.kT_full(self.full(c_r, l)) @ T
|
|
148
|
+
c_r = c_r + np.linalg.solve(kT, R)
|
|
149
|
+
return c_r
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_calc_load_vector_passes_lbd_only_when_declared():
|
|
153
|
+
fext = np.array([1., 2.])
|
|
154
|
+
calls = []
|
|
155
|
+
|
|
156
|
+
class A(object):
|
|
157
|
+
calc_fext = None
|
|
158
|
+
|
|
159
|
+
def with_c(inc=1., silent=True, c=None):
|
|
160
|
+
calls.append(('c', c))
|
|
161
|
+
return fext
|
|
162
|
+
|
|
163
|
+
def with_c_lbd(inc=1., silent=True, c=None, lbd=None):
|
|
164
|
+
calls.append(('c, lbd', c, lbd))
|
|
165
|
+
return fext
|
|
166
|
+
|
|
167
|
+
def with_kwargs(inc=1., silent=True, c=None, **kwargs):
|
|
168
|
+
calls.append(('c, **kwargs', c, kwargs))
|
|
169
|
+
return fext
|
|
170
|
+
|
|
171
|
+
def without(inc=1., silent=True):
|
|
172
|
+
calls.append('without')
|
|
173
|
+
return fext
|
|
174
|
+
|
|
175
|
+
an = A()
|
|
176
|
+
c = np.array([3., 4.])
|
|
177
|
+
for func in (with_c, with_c_lbd, with_kwargs, without):
|
|
178
|
+
an.calc_fext = func
|
|
179
|
+
callbacks.calc_load_vector(an, c, fext, 0.5)
|
|
180
|
+
assert calls[0] == ('c', c)
|
|
181
|
+
assert calls[1] == ('c, lbd', c, 0.5)
|
|
182
|
+
# a **kwargs does not count, as for inc
|
|
183
|
+
assert calls[2] == ('c, **kwargs', c, {})
|
|
184
|
+
# the constant reference vector is returned without calling calc_fext
|
|
185
|
+
assert len(calls) == 3
|
|
186
|
+
assert not callbacks.is_load_factor_dependent(A())
|
|
187
|
+
an.calc_fext = with_c_lbd
|
|
188
|
+
assert callbacks.is_load_factor_dependent(an)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
|
|
192
|
+
'arc_length_crisfield'])
|
|
193
|
+
def test_displacement_control_relative_criterion(method):
|
|
194
|
+
r"""The prescribed displacement alone, with the relative convergence
|
|
195
|
+
criterion only: every converged increment is the exact solution, and
|
|
196
|
+
``calc_fext`` receives the load factor of the configuration"""
|
|
197
|
+
pb = SpringsProblem(d1=0.8)
|
|
198
|
+
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-10,
|
|
199
|
+
absTOL=None)
|
|
200
|
+
an.static(NLgeom=True, silent=True)
|
|
201
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
202
|
+
assert len(an.increments) > 2
|
|
203
|
+
for lbd, c_r in zip(an.increments, an.cs):
|
|
204
|
+
ref, P = pb.exact(lbd)
|
|
205
|
+
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
|
|
206
|
+
assert np.isclose(pb.reaction(c_r, lbd), P, rtol=1e-8)
|
|
207
|
+
assert (np.linalg.norm(pb.residual(c_r, lbd))
|
|
208
|
+
<= 1e-9*np.linalg.norm(lbd*pb.dR_dlbd(c_r, lbd)))
|
|
209
|
+
# the arc-length solvers evaluate dR/dlbd at the configurations and load
|
|
210
|
+
# factors of their iterations
|
|
211
|
+
if method != 'NR':
|
|
212
|
+
assert len(pb.fext_calls) > len(an.increments)
|
|
213
|
+
lbds = [lbd for lbd, _ in pb.fext_calls]
|
|
214
|
+
assert lbds[0] == 0.
|
|
215
|
+
assert all(lbd is not None for lbd in lbds)
|
|
216
|
+
assert len(set(lbds)) > 2
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_displacement_control_without_lbd_does_not_converge():
|
|
220
|
+
r"""The reason for ``lbd``: without it, the reference vector and the
|
|
221
|
+
reduced internal force vector vanish at equilibrium, and the relative
|
|
222
|
+
criterion cannot be satisfied"""
|
|
223
|
+
pb = SpringsProblem(d1=0.8, with_lbd=False)
|
|
224
|
+
an = pb.analysis(initialInc=0.2, relTOL=1e-10, absTOL=None,
|
|
225
|
+
maxNumIter=10, minInc=0.01)
|
|
226
|
+
an.static(NLgeom=True, silent=True)
|
|
227
|
+
assert len(an.increments) == 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_newton_raphson_first_predictor_is_linear_solution():
|
|
231
|
+
r"""With linear springs the predictor of the first increment,
|
|
232
|
+
`[K_0]^{-1} \partial R/\partial \lambda`, is the solution: each
|
|
233
|
+
increment converges at its first evaluation of the residual"""
|
|
234
|
+
pb = SpringsProblem(a=(0., 0., 0.), d1=0.5)
|
|
235
|
+
an = pb.analysis(initialInc=0.3, relTOL=1e-12)
|
|
236
|
+
an.static(NLgeom=True, silent=True)
|
|
237
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
238
|
+
steps = [step for _, _, step in pb.fint_calls]
|
|
239
|
+
assert steps.count(0) == 1, pb.fint_calls
|
|
240
|
+
ref, _ = pb.exact(1.)
|
|
241
|
+
assert np.allclose(an.cs[-1], ref, rtol=1e-12)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def orders(errors, floor=1e-12):
|
|
245
|
+
"""Convergence orders log(e2/e1)/log(e1/e0) above the round-off floor"""
|
|
246
|
+
return [np.log(e2/e1)/np.log(e1/e0)
|
|
247
|
+
for e0, e1, e2 in zip(errors[:-2], errors[1:-1], errors[2:])
|
|
248
|
+
if e2 > floor and e0 < 0.1]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_riks_quadratic_with_load_factor_dependent_load_vector():
|
|
252
|
+
r"""The bordered system of Riks is Newton-Raphson applied to the
|
|
253
|
+
augmented system with the exact `\partial R/\partial \lambda`"""
|
|
254
|
+
pb = SpringsProblem(k=(10., 15., 20.), a=(200., 400., 600.), d1=1.)
|
|
255
|
+
an = pb.analysis(NL_method='arc_length_riks', initialInc=0.5,
|
|
256
|
+
relTOL=1e-13, maxNumIter=60)
|
|
257
|
+
an.static(NLgeom=True, silent=True)
|
|
258
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
259
|
+
first = [e for _, e, step in pb.fint_calls if step == 0]
|
|
260
|
+
ps = orders(first)
|
|
261
|
+
assert len(ps) >= 1, first
|
|
262
|
+
assert min(ps) > 1.7, first
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
|
|
266
|
+
'arc_length_crisfield'])
|
|
267
|
+
def test_constant_and_incremented_prescriptions(method):
|
|
268
|
+
r"""A constant prescribed displacement `d_0`, held during the analysis,
|
|
269
|
+
with a load `\lambda F` on node 1 and an incremented displacement
|
|
270
|
+
`\lambda d_1`"""
|
|
271
|
+
pb = SpringsProblem(d0=-0.3, d1=0.6, F=40.)
|
|
272
|
+
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-11)
|
|
273
|
+
an.static(NLgeom=True, silent=True)
|
|
274
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
275
|
+
for lbd, c_r in zip(an.increments, an.cs):
|
|
276
|
+
ref = pb.solve_reference(lbd)
|
|
277
|
+
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
|
|
278
|
+
# at lbd = 0 the constant displacement alone is not in equilibrium with
|
|
279
|
+
# c_r = 0: the first increment starts from a non-null residual
|
|
280
|
+
assert np.linalg.norm(pb.residual(np.zeros(2), 0.)) > 1.
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@pytest.mark.parametrize('method', ['NR', 'arc_length_riks',
|
|
284
|
+
'arc_length_crisfield'])
|
|
285
|
+
def test_null_load_vector_at_the_origin(method):
|
|
286
|
+
r"""The last spring is purely cubic, `f_3 = a_3 e^3`, such that
|
|
287
|
+
`\partial R/\partial \lambda = 0` at `c = 0`, `\lambda = 0`: the
|
|
288
|
+
reference of the relative criterion is `\lambda \partial R/\partial
|
|
289
|
+
\lambda` at the current iterate, not at the origin"""
|
|
290
|
+
pb = SpringsProblem(k=(100., 150., 0.), a=(20., 40., 600.), d1=0.5)
|
|
291
|
+
assert np.linalg.norm(pb.dR_dlbd(np.zeros(2), 0.)) == 0.
|
|
292
|
+
an = pb.analysis(NL_method=method, initialInc=0.2, relTOL=1e-10,
|
|
293
|
+
absTOL=None)
|
|
294
|
+
an.static(NLgeom=True, silent=True)
|
|
295
|
+
assert np.isclose(an.increments[-1], 1.)
|
|
296
|
+
for lbd, c_r in zip(an.increments, an.cs):
|
|
297
|
+
ref, P = pb.exact(lbd)
|
|
298
|
+
assert np.allclose(c_r, ref, rtol=1e-8, atol=1e-12), (lbd, c_r, ref)
|
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
"""
|
|
28
|
-
import inspect
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def accepts_kwarg(func, name):
|
|
32
|
-
"""Tell whether ``func`` declares the keyword argument ``name``"""
|
|
33
|
-
try:
|
|
34
|
-
sig = inspect.signature(func)
|
|
35
|
-
except (TypeError, ValueError):
|
|
36
|
-
return False
|
|
37
|
-
par = sig.parameters.get(name)
|
|
38
|
-
return par is not None and par.kind in (par.POSITIONAL_OR_KEYWORD,
|
|
39
|
-
par.KEYWORD_ONLY)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def calc_fint(an, c, lbd):
|
|
43
|
-
r"""``an.calc_fint`` at ``c``, with ``inc=lbd`` when accepted"""
|
|
44
|
-
kwargs = dict(inc=lbd) if accepts_kwarg(an.calc_fint, 'inc') else {}
|
|
45
|
-
return an.calc_fint(c=c, silent=True, **kwargs)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def calc_kT(an, c, lbd):
|
|
49
|
-
r"""Tangent stiffness matrix ``calc_kC + calc_kG`` at ``c``, with
|
|
50
|
-
``inc=lbd`` passed to the callables that accept it"""
|
|
51
|
-
kwC = dict(inc=lbd) if accepts_kwarg(an.calc_kC, 'inc') else {}
|
|
52
|
-
kwG = dict(inc=lbd) if accepts_kwarg(an.calc_kG, 'inc') else {}
|
|
53
|
-
kC = an.calc_kC(c=c, NLgeom=True, silent=True, **kwC)
|
|
54
|
-
kG = an.calc_kG(c=c, NLgeom=True, silent=True, **kwG)
|
|
55
|
-
return kC + kG
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def is_load_configuration_dependent(an):
|
|
59
|
-
"""Tell whether ``an.calc_fext`` accepts the configuration ``c``"""
|
|
60
|
-
return accepts_kwarg(an.calc_fext, 'c')
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def calc_load_vector(an, c, fext):
|
|
64
|
-
r"""`\partial R/\partial \lambda` at ``c``: ``calc_fext(inc=1., c=c)``
|
|
65
|
-
when ``calc_fext`` accepts ``c``, otherwise the constant ``fext``"""
|
|
66
|
-
if is_load_configuration_dependent(an):
|
|
67
|
-
return an.calc_fext(inc=1., c=c, silent=True)
|
|
68
|
-
return fext
|
|
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
|