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.
Files changed (39) hide show
  1. {structsolve-0.6.1/structsolve.egg-info → structsolve-0.8.0}/PKG-INFO +8 -2
  2. {structsolve-0.6.1 → structsolve-0.8.0}/README.md +7 -1
  3. {structsolve-0.6.1 → structsolve-0.8.0}/pyproject.toml +1 -1
  4. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/analysis.py +21 -3
  5. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length.py +48 -38
  6. structsolve-0.8.0/structsolve/callbacks.py +160 -0
  7. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/linear_buckling.py +68 -29
  8. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/newton_raphson.py +86 -26
  9. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/static.py +32 -1
  10. {structsolve-0.6.1 → structsolve-0.8.0/structsolve.egg-info}/PKG-INFO +8 -2
  11. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/SOURCES.txt +2 -0
  12. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_linear_buckling.py +1 -1
  13. structsolve-0.8.0/tests/test_load_factor_dependent_load.py +298 -0
  14. structsolve-0.8.0/tests/test_reuse.py +174 -0
  15. structsolve-0.6.1/structsolve/callbacks.py +0 -68
  16. {structsolve-0.6.1 → structsolve-0.8.0}/LICENSE +0 -0
  17. {structsolve-0.6.1 → structsolve-0.8.0}/setup.cfg +0 -0
  18. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/__init__.py +0 -0
  19. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length_crisfield.py +0 -0
  20. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arc_length_riks.py +0 -0
  21. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/arpackutils.py +0 -0
  22. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/freq.py +0 -0
  23. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/logger.py +0 -0
  24. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve/sparseutils.py +0 -0
  25. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/dependency_links.txt +0 -0
  26. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/requires.txt +0 -0
  27. {structsolve-0.6.1 → structsolve-0.8.0}/structsolve.egg-info/top_level.txt +0 -0
  28. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_analysis.py +0 -0
  29. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_arc_length.py +0 -0
  30. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_arpackutils.py +0 -0
  31. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_follower_freq_static.py +0 -0
  32. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_follower_loads.py +0 -0
  33. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_freq.py +0 -0
  34. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_linear_buckling_unsymmetric.py +0 -0
  35. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_newton_raphson.py +0 -0
  36. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_sparse_first.py +0 -0
  37. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_sparseutils.py +0 -0
  38. {structsolve-0.6.1 → structsolve-0.8.0}/tests/test_static.py +0 -0
  39. {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.6.1
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.6.1). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
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.6.1). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
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`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structsolve"
7
- version = "0.6.1"
7
+ version = "0.8.0"
8
8
  description = "Structural analysis solvers"
9
9
  readme = "README.md"
10
10
  license = {text = "BSD-2-Clause"}
@@ -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
- c = c + solve(k0, R, silent=True)
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
- u_ref = solve(kC0, fext, silent=True)
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
- # load vector of the converged configuration, dR/dlbd
178
- dc_q_conv = solve_kT_conv(callbacks.calc_load_vector(an, c, fext))
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
- dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc, fext))
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, kT0=None, silent=silent)
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 _estimate_sigma(K, KG, safety=10., max_iter=50, rel_tol=1e-3):
31
- r"""Shift of the Cayley mode used by :func:`lb`
32
-
33
- With ``sigma > 0``, ``eigsh(A=KG, M=K, sigma=sigma, mode='cayley',
34
- which='SM')`` returns the eigenvalues `\mu` of ``KG u = mu K u`` with the
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 1.
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 1.
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 1.
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 safety * mu_max
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 positive.size and _is_positive_definite(K):
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. ARPACK's ``ncv`` is chosen by
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))