structsolve 0.9.1__tar.gz → 0.9.3__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 (41) hide show
  1. {structsolve-0.9.1/structsolve.egg-info → structsolve-0.9.3}/PKG-INFO +9 -6
  2. {structsolve-0.9.1 → structsolve-0.9.3}/README.md +8 -5
  3. {structsolve-0.9.1 → structsolve-0.9.3}/pyproject.toml +1 -1
  4. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/analysis.py +34 -30
  5. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/arc_length.py +68 -42
  6. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/arpackutils.py +20 -23
  7. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/callbacks.py +21 -19
  8. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/convergence.py +159 -32
  9. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/freq.py +73 -31
  10. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/linear_buckling.py +228 -119
  11. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/newton_raphson.py +78 -45
  12. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/static.py +1 -1
  13. {structsolve-0.9.1 → structsolve-0.9.3/structsolve.egg-info}/PKG-INFO +9 -6
  14. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve.egg-info/SOURCES.txt +3 -1
  15. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_analysis.py +55 -5
  16. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_arc_length.py +28 -1
  17. structsolve-0.9.3/tests/test_arc_length_fung_kaplan.py +261 -0
  18. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_follower_freq_static.py +2 -2
  19. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_follower_loads.py +11 -9
  20. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_freq.py +100 -0
  21. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_linear_buckling.py +32 -32
  22. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_load_factor_dependent_load.py +37 -31
  23. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_newton_raphson.py +6 -3
  24. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_reuse.py +3 -3
  25. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_static.py +6 -6
  26. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_static_deflection.py +19 -16
  27. structsolve-0.9.3/tests/test_warnings_registry.py +159 -0
  28. {structsolve-0.9.1 → structsolve-0.9.3}/LICENSE +0 -0
  29. {structsolve-0.9.1 → structsolve-0.9.3}/setup.cfg +0 -0
  30. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/__init__.py +0 -0
  31. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/arc_length_crisfield.py +0 -0
  32. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/arc_length_riks.py +0 -0
  33. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/logger.py +0 -0
  34. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve/sparseutils.py +0 -0
  35. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve.egg-info/dependency_links.txt +0 -0
  36. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve.egg-info/requires.txt +0 -0
  37. {structsolve-0.9.1 → structsolve-0.9.3}/structsolve.egg-info/top_level.txt +0 -0
  38. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_arpackutils.py +0 -0
  39. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_linear_buckling_unsymmetric.py +0 -0
  40. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_sparse_first.py +0 -0
  41. {structsolve-0.9.1 → structsolve-0.9.3}/tests/test_sparseutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structsolve
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: Structural analysis solvers
5
5
  Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -63,7 +63,7 @@ Currently these solvers are pretty much compatible with my other repository
63
63
  Citing this library
64
64
  ===================
65
65
 
66
- Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.9.1). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
66
+ Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.9.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
67
67
 
68
68
 
69
69
  Documentation
@@ -78,12 +78,15 @@ History
78
78
 
79
79
  See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
80
80
 
81
- * version 0.9.1 (2026-10-24)
82
- - documentation published again: pandoc installed in the documentation workflow
83
- * version 0.9.0 (2026-10-24)
81
+ * version 0.9.3 (2026-10-26)
84
82
  - Crisfield's convergence criterion is the default of the non-linear analyses, `Analysis.convergence = 'crisfield'`, with `relTOL = 1e-9`: the residual and the reference forces are measured with the norm scaled by the diagonal of `calc_kC()` over the free degrees of freedom; the former check is kept with `convergence = 'relative'`
83
+ - With both convergence criteria, an iteration also converges at the round-off floor of its residual, `3 eps ||(|K_C0| |c|)||`: the arc-length methods trace the snap-through of a low arch at `relTOL = 1e-9` through the null load where they stopped
85
84
  - `lb` returns the negative load multipliers closest to zero after the positive ones, and a tensile reference load no longer needs the dense fallback
86
- - `freq(sort=True)` sorts and filters `lambda2` together with the eigenvectors, and rigid-body modes no longer fail the verification of the residuals
85
+ - `freq(sort=True)` sorts and filters `lambda2` together with the eigenvectors, and neither rigid-body modes nor eigenpairs with an eigenvalue close to zero whose backward error is at the round-off, e.g. pre-stressed close to buckling, fail the verification of the residuals any more
86
+ - arc-length solvers: a converged step that points back along the path is repeated with half the arc length, instead of being accepted
87
+ - documentation published again: pandoc installed in the documentation workflow
88
+ - `lb` and `freq` no longer reset the registry of the warnings already shown, which made a warning of the caller under the `'default'` action be shown again after each call
89
+ - The residual is `R = Fint - lbd*Fext`, the convention of `pyfe3d.solver` and `panels`, with `K_T dc = -R` in the Newton-Raphson corrections and `dR/dlbd = -Fext`; the solutions are bitwise identical
87
90
  * version 0.8.0 (2026-10-14)
88
91
  - 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
89
92
  * version 0.7.0 (2026-10-13)
@@ -28,7 +28,7 @@ Currently these solvers are pretty much compatible with my other repository
28
28
  Citing this library
29
29
  ===================
30
30
 
31
- Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.9.1). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
31
+ Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.9.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
32
32
 
33
33
 
34
34
  Documentation
@@ -43,12 +43,15 @@ History
43
43
 
44
44
  See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
45
45
 
46
- * version 0.9.1 (2026-10-24)
47
- - documentation published again: pandoc installed in the documentation workflow
48
- * version 0.9.0 (2026-10-24)
46
+ * version 0.9.3 (2026-10-26)
49
47
  - Crisfield's convergence criterion is the default of the non-linear analyses, `Analysis.convergence = 'crisfield'`, with `relTOL = 1e-9`: the residual and the reference forces are measured with the norm scaled by the diagonal of `calc_kC()` over the free degrees of freedom; the former check is kept with `convergence = 'relative'`
48
+ - With both convergence criteria, an iteration also converges at the round-off floor of its residual, `3 eps ||(|K_C0| |c|)||`: the arc-length methods trace the snap-through of a low arch at `relTOL = 1e-9` through the null load where they stopped
50
49
  - `lb` returns the negative load multipliers closest to zero after the positive ones, and a tensile reference load no longer needs the dense fallback
51
- - `freq(sort=True)` sorts and filters `lambda2` together with the eigenvectors, and rigid-body modes no longer fail the verification of the residuals
50
+ - `freq(sort=True)` sorts and filters `lambda2` together with the eigenvectors, and neither rigid-body modes nor eigenpairs with an eigenvalue close to zero whose backward error is at the round-off, e.g. pre-stressed close to buckling, fail the verification of the residuals any more
51
+ - arc-length solvers: a converged step that points back along the path is repeated with half the arc length, instead of being accepted
52
+ - documentation published again: pandoc installed in the documentation workflow
53
+ - `lb` and `freq` no longer reset the registry of the warnings already shown, which made a warning of the caller under the `'default'` action be shown again after each call
54
+ - The residual is `R = Fint - lbd*Fext`, the convention of `pyfe3d.solver` and `panels`, with `K_T dc = -R` in the Newton-Raphson corrections and `dR/dlbd = -Fext`; the solutions are bitwise identical
52
55
  * version 0.8.0 (2026-10-14)
53
56
  - 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
54
57
  * version 0.7.0 (2026-10-13)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structsolve"
7
- version = "0.9.1"
7
+ version = "0.9.3"
8
8
  description = "Structural analysis solvers"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -15,7 +15,7 @@ import numpy as np
15
15
  from .static import solve, _factorize
16
16
  from .logger import msg, warn
17
17
  from . import callbacks
18
- from .newton_raphson import _check_convergence, _weights
18
+ from .newton_raphson import _check_convergence, _weights, _abs_matrix
19
19
  from .newton_raphson import _solver_NR
20
20
  from .arc_length_riks import _solver_arc_length_riks
21
21
  from .arc_length_crisfield import _solver_arc_length_crisfield
@@ -172,19 +172,20 @@ class Analysis(object):
172
172
  Notes
173
173
  -----
174
174
  For non-linear analyses, the internal force vector
175
- `\{F_{int}(c)\}` must be in equilibrium with the external force
175
+ `\boldsymbol{F}_{int}(\boldsymbol{c})` must be in equilibrium with the external force
176
176
  vector scaled by the load factor `\lambda`:
177
177
 
178
178
  .. math::
179
179
 
180
- \{R\} = \lambda \{F_{ext}\} - \{F_{int}(c)\} = \{0\}
180
+ \boldsymbol{R} = \boldsymbol{F}_{int}(\boldsymbol{c}) - \lambda \boldsymbol{F}_{ext} = \boldsymbol{0}
181
181
 
182
- and the tangent stiffness matrix `[K_T] = [K_C] + [K_G]`, given by the
182
+ and the tangent stiffness matrix `\boldsymbol{K}_T = \boldsymbol{K}_C + \boldsymbol{K}_G`, given by the
183
183
  callables ``calc_kC`` and ``calc_kG``, should be the exact derivative of
184
- `\{F_{int}\}` with respect to `\{c\}`, such that the Newton-Raphson
185
- iterations converge quadratically. The convergence is checked by
186
- default with Crisfield's criterion, the residual and the reference
187
- force vectors scaled with the diagonal of the linear stiffness matrix
184
+ `\boldsymbol{F}_{int}` with respect to `\boldsymbol{c}`, `\boldsymbol{K}_T = \partial
185
+ \boldsymbol{R}/\partial \boldsymbol{c}`, such that the Newton-Raphson
186
+ iterations, `\boldsymbol{K}_T \delta \boldsymbol{c} = -\boldsymbol{R}`, converge
187
+ quadratically. The convergence is checked by default with Crisfield's
188
+ criterion, the residual and the reference force vectors scaled with the diagonal of the linear stiffness matrix
188
189
  ``calc_kC()`` over the free degrees of freedom, such that it does not
189
190
  depend on the units of each degree of freedom, e.g. translations and
190
191
  rotations, see :mod:`.convergence`; ``convergence = 'relative'`` keeps
@@ -209,15 +210,15 @@ class Analysis(object):
209
210
  such that ``calc_fint`` may contain the configuration-dependent part of
210
211
  the load, scaled by ``inc``, and ``calc_kC + calc_kG`` its derivative.
211
212
  - ``calc_fext(inc=1., c=c)`` must return the load vector of the
212
- configuration ``c`` for a unit load factor, the derivative of the
213
- residual with respect to the load factor, used by the arc-length
214
- solvers.
213
+ configuration ``c`` for a unit load factor, minus the derivative of
214
+ the residual with respect to the load factor, `-\partial
215
+ \boldsymbol{R}/\partial \lambda`, used by the arc-length solvers.
215
216
  - ``calc_fext(inc=1., c=c, lbd=lbd)`` also receives the current load
216
217
  factor when it declares ``lbd``, for a derivative `\partial
217
- R/\partial \lambda` that depends on the load factor, e.g. prescribed
218
+ \boldsymbol{R}/\partial \lambda` that depends on the load factor, e.g. prescribed
218
219
  displacements imposed exactly as linear constraints, where the
219
220
  reference vector ``calc_fext(inc=1.)`` may be null. The solvers then
220
- use `\partial R/\partial \lambda` for the displacement scale of the
221
+ use `-\partial \boldsymbol{R}/\partial \lambda` for the displacement scale of the
221
222
  arc-length methods, for the predictor of the first Newton-Raphson
222
223
  increment and as a reference of the convergence criterion, see
223
224
  :mod:`.callbacks`. The linear static analysis never passes
@@ -313,29 +314,29 @@ class Analysis(object):
313
314
 
314
315
  Notes
315
316
  -----
316
- The linear analysis solves `[K_C]\{c\} = \{F_{ext}\}`. When the load
317
+ The linear analysis solves `\boldsymbol{K}_C \boldsymbol{c} = \boldsymbol{F}_{ext}`. When the load
317
318
  depends on the configuration, the structure is still geometrically
318
319
  linear but the load is evaluated in the current configuration,
319
- `[K_C]\{c\} = \{F_{ext}(c)\}`, which is solved by the load
320
+ `\boldsymbol{K}_C \boldsymbol{c} = \boldsymbol{F}_{ext}(\boldsymbol{c})`, which is solved by the load
320
321
  correction iterations
321
322
 
322
323
  .. math::
323
324
 
324
- \{c_{k+1}\} = \{c_k\} + [K_C]^{-1}(\{F_{ext}(c_k)\}
325
- - [K_C]\{c_k\})
325
+ \boldsymbol{c}_{k+1} = \boldsymbol{c}_k + \boldsymbol{K}_C^{-1}(\boldsymbol{F}_{ext}(\boldsymbol{c}_k)
326
+ - \boldsymbol{K}_C \boldsymbol{c}_k)
326
327
 
327
- starting from `\{c_0\} = [K_C]^{-1}\{F_{ext}\}`, the solution of the
328
- reference load ``calc_fext()``, with `\{F_{ext}(c_k)\}` given by
328
+ starting from `\boldsymbol{c}_0 = \boldsymbol{K}_C^{-1} \boldsymbol{F}_{ext}`, the solution of the
329
+ reference load ``calc_fext()``, with `\boldsymbol{F}_{ext}(\boldsymbol{c}_k)` given by
329
330
  ``calc_fext(inc=1., c=c_k)``. This is a fixed-point iteration
330
331
  without the load stiffness in the iteration matrix. For a follower
331
- load whose force vector is affine in `c`, `\{F(c)\} = \{F_0\} -
332
- [K_f]\{c\}`, this is the solution of `([K_C] + [K_f])\{c\} =
333
- \{F_0\}`, with the convergence rate of the spectral radius of
334
- `[K_C]^{-1}[K_f]`, i.e. of the ratio of the load to the lowest
332
+ load whose force vector is affine in `c`, `\boldsymbol{F}(\boldsymbol{c}) = \boldsymbol{F}_0 -
333
+ \boldsymbol{K}_f \boldsymbol{c}`, this is the solution of `(\boldsymbol{K}_C + \boldsymbol{K}_f) \boldsymbol{c} =
334
+ \boldsymbol{F}_0`, with the convergence rate of the spectral radius of
335
+ `\boldsymbol{K}_C^{-1} \boldsymbol{K}_f`, i.e. of the ratio of the load to the lowest
335
336
  critical load of the linearized problem; beyond it the iterations
336
- diverge. The error is multiplied by `-[K_C]^{-1}[K_f]` at each
337
+ diverge. The error is multiplied by `-\boldsymbol{K}_C^{-1} \boldsymbol{K}_f` at each
337
338
  iteration, whose spectral radius is `1/\min|\lambda|` over the load
338
- multipliers `\lambda` of `([K_C] + \lambda [K_f])\{u\} = \{0\}`,
339
+ multipliers `\lambda` of `(\boldsymbol{K}_C + \lambda \boldsymbol{K}_f) \boldsymbol{u} = \boldsymbol{0}`,
339
340
  including the negative and complex ones.
340
341
 
341
342
  """
@@ -378,15 +379,18 @@ class Analysis(object):
378
379
  msg('Configuration-dependent load: load correction iterations',
379
380
  level=2, silent=silent)
380
381
  w = _weights(self, k0)
382
+ k0_abs = _abs_matrix(k0)
381
383
  # k0 is factorized once for all the corrections
382
384
  solve_k0 = None
383
385
  for iteration in range(1, self.maxNumIter + 1):
384
386
  fext = self.calc_fext(inc=1., c=c, silent=True)
385
387
  fint = k0 @ c
386
- R = fext - fint
387
- conv, Rnorm, Rrel = _check_convergence(self, R, fint, fext, w=w)
388
- msg('Iteration: %d, %s = %1.3e' % (iteration, 'zeta' if
389
- self.convergence == 'crisfield' else 'relative ||R||', Rrel),
388
+ R = fint - fext
389
+ conv, Rnorm, Rrel, roundoff = _check_convergence(self, R, fint,
390
+ fext, w=w, Kc_abs=k0_abs @ np.abs(c), full_output=True)
391
+ msg('Iteration: %d, %s = %1.3e%s' % (iteration, 'zeta' if
392
+ self.convergence == 'crisfield' else 'relative ||R||', Rrel,
393
+ ', converged at the round-off floor' if roundoff else ''),
390
394
  level=3, silent=silent)
391
395
  if conv:
392
396
  return c
@@ -397,7 +401,7 @@ class Analysis(object):
397
401
  solve_k0 = _factorize(k0)
398
402
  except RuntimeError:
399
403
  break
400
- c = c + solve_k0(R)
404
+ c = c + solve_k0(-R)
401
405
  warn('The load correction iterations did not converge, the load may '
402
406
  'exceed the critical load of the linearized problem', level=1,
403
407
  silent=silent)
@@ -14,7 +14,7 @@ from .static import solve, _factorize
14
14
  from . import callbacks
15
15
  from .newton_raphson import (_check_convergence, _check_divergence,
16
16
  _NR_iterations, _log_iteration, _weights,
17
- INC_CUT_FACTOR)
17
+ _abs_matrix, INC_CUT_FACTOR)
18
18
 
19
19
 
20
20
  #: Scaling factor of the load-factor term in the arc-length constraint
@@ -75,65 +75,67 @@ def _solver_arc_length(an, method, silent=False):
75
75
 
76
76
  Notes
77
77
  -----
78
- The equilibrium path `\{F_{int}(c)\} = \lambda \{F_{ext}\}` is traced with
78
+ The equilibrium path `\boldsymbol{F}_{int}(\boldsymbol{c}) = \lambda \boldsymbol{F}_{ext}` is traced with
79
79
  the arc-length constraint
80
80
 
81
81
  .. math::
82
82
 
83
- \frac{\{\Delta c\}^T\{\Delta c\}}{\{u_{ref}\}^T\{u_{ref}\}}
83
+ \frac{\Delta \boldsymbol{c}^\top \Delta \boldsymbol{c}}{\boldsymbol{u}_{ref}^\top \boldsymbol{u}_{ref}}
84
84
  + \psi^2 \Delta\lambda^2 = \Delta s^2
85
85
 
86
- where `\{\Delta c\}` and `\Delta\lambda` are the increments of the current
87
- step and `\{u_{ref}\}` is the linear solution for `\lambda=1`, which makes
86
+ where `\Delta \boldsymbol{c}` and `\Delta\lambda` are the increments of the current
87
+ step and `\boldsymbol{u}_{ref}` is the linear solution for `\lambda=1`, which makes
88
88
  the arc length `\Delta s` independent of the units of the model. In the
89
89
  linear regime `\Delta s \approx \sqrt{1 + \psi^2} \Delta\lambda`. It is
90
90
  the spherical arc-length constraint of [4]_ (Section 9.3.2, Eq. (9.19),
91
91
  p. 271), where the displacement term is not scaled and the load term is
92
- `\psi^2 \Delta\lambda^2 \{F_{ext}\}^T\{F_{ext}\}`. `\psi` is the
92
+ `\psi^2 \Delta\lambda^2 \boldsymbol{F}_{ext}^\top \boldsymbol{F}_{ext}`. `\psi` is the
93
93
  constant ``PSI``,
94
- and `\{u_{ref}\}^T\{u_{ref}\}` is
94
+ and `\boldsymbol{u}_{ref}^\top \boldsymbol{u}_{ref}` is
95
95
  replaced by 1 when it is null. With the inner product
96
96
 
97
97
  .. math::
98
98
 
99
99
  \langle (a, \alpha), (b, \beta) \rangle =
100
- \frac{\{a\}^T\{b\}}{\{u_{ref}\}^T\{u_{ref}\}} + \psi^2 \alpha \beta
100
+ \frac{\boldsymbol{a}^\top \boldsymbol{b}}{\boldsymbol{u}_{ref}^\top \boldsymbol{u}_{ref}} + \psi^2 \alpha \beta
101
101
 
102
102
  the constraint reads `g = \frac{1}{2}(\langle (\Delta c, \Delta\lambda),
103
103
  (\Delta c, \Delta\lambda) \rangle - \Delta s^2) = 0`.
104
104
 
105
105
  Each step starts from a tangent predictor, whose direction follows the
106
106
  previous increment, allowing limit points to be passed. With
107
- `\{\delta c_q\} = [K_T]^{-1}\{F_{ext}\}` at the last converged state,
107
+ `\delta \boldsymbol{c}_q = \boldsymbol{K}_T^{-1} \boldsymbol{F}_{ext}` at the last converged state,
108
108
  the predictor
109
109
 
110
110
  .. math::
111
111
 
112
112
  \Delta\lambda = \pm \frac{\Delta s}{\sqrt{\langle (\delta c_q, 1),
113
113
  (\delta c_q, 1) \rangle}}, \qquad
114
- \{\Delta c\} = \Delta\lambda \{\delta c_q\}
114
+ \Delta \boldsymbol{c} = \Delta\lambda \delta \boldsymbol{c}_q
115
115
 
116
116
  satisfies the constraint, with the sign of `\langle (\delta c_q, 1),
117
117
  (\Delta c_{prev}, \Delta\lambda_{prev}) \rangle`, where
118
118
  `(\Delta c_{prev}, \Delta\lambda_{prev})` is the increment of the
119
119
  previous converged step, and a positive sign at the first step. The
120
- initial arc length, `\Delta s_0 = initialInc \sqrt{\langle (\delta c_q,
121
- 1), (\delta c_q, 1) \rangle}` at the origin, corresponds to a load factor
120
+ initial arc length, `\Delta s_0 = initialInc \sqrt{\langle (\delta \boldsymbol{c}_q,
121
+ 1), (\delta \boldsymbol{c}_q, 1) \rangle}` at the origin, corresponds to a load factor
122
122
  increment of ``an.initialInc`` along the initial tangent.
123
123
 
124
- At each iteration, the residual is `\{R\} = (\lambda + \Delta\lambda)
125
- \{F_{ext}\} - \{F_{int}(c + \Delta c)\}`, and the bordered system is
124
+ At each iteration, the residual is `\boldsymbol{R} = \boldsymbol{F}_{int}(\boldsymbol{c} +
125
+ \Delta \boldsymbol{c}) - (\lambda + \Delta\lambda) \boldsymbol{F}_{ext}`, with `\partial
126
+ \boldsymbol{R}/\partial \lambda = -\boldsymbol{F}_{ext}`, and the bordered system
127
+ `\boldsymbol{K}_T \delta \boldsymbol{c} - \delta\lambda \, \boldsymbol{F}_{ext} = -\boldsymbol{R}` is
126
128
  solved with two solutions of the tangent stiffness matrix,
127
- `\{\delta c_R\} = [K_T]^{-1}\{R\}` and `\{\delta c_q\} =
128
- [K_T]^{-1}\{F_{ext}\}`; the increment is updated to `(\Delta c +
129
- \delta c_R + \delta\lambda \, \delta c_q, \Delta\lambda +
129
+ `\delta \boldsymbol{c}_R = -\boldsymbol{K}_T^{-1} \boldsymbol{R}` and `\delta \boldsymbol{c}_q =
130
+ \boldsymbol{K}_T^{-1} \boldsymbol{F}_{ext}`; the increment is updated to `(\Delta \boldsymbol{c} +
131
+ \delta \boldsymbol{c}_R + \delta\lambda \, \delta \boldsymbol{c}_q, \Delta\lambda +
130
132
  \delta\lambda)`, where the correction of the load factor
131
133
  `\delta\lambda` comes from the constraint:
132
134
 
133
135
  - ``method='riks'``: the constraint is linearized at the current
134
136
  increment (updated normal plane), which is Newton-Raphson applied to
135
137
  the augmented system of the equilibrium equations and the constraint
136
- ([4]_, Section 9.3.2, p. 272), with quadratic convergence when `[K_T]`
138
+ ([4]_, Section 9.3.2, p. 272), with quadratic convergence when `\boldsymbol{K}_T`
137
139
  is updated at every iteration and is the exact Jacobian. It follows
138
140
  Riks [1]_, who applies Newton's method to the equilibrium equations
139
141
  augmented with an equation that controls the arc length, and states
@@ -158,7 +160,7 @@ def _solver_arc_length(an, method, silent=False):
158
160
  - ``method='crisfield'``: the quadratic (spherical) constraint is solved
159
161
  exactly, choosing the root with the smallest angle to the current
160
162
  increment. When the roots are complex, the linearized constraint is
161
- used, see [2]_. With `\{u\} = \{\Delta c\} + \{\delta c_R\}`, the
163
+ used, see [2]_. With `\boldsymbol{u} = \Delta \boldsymbol{c} + \delta \boldsymbol{c}_R`, the
162
164
  constraint
163
165
  `\langle (u + \delta\lambda \, \delta c_q, \Delta\lambda +
164
166
  \delta\lambda), (u + \delta\lambda \, \delta c_q, \Delta\lambda +
@@ -174,8 +176,20 @@ def _solver_arc_length(an, method, silent=False):
174
176
  angle, since both have the length `\Delta s`.
175
177
 
176
178
  The step fails when `\delta\lambda` cannot be computed or is not
177
- finite. With full Newton-Raphson, `[K_T]` is updated at every iteration
178
- at the current iterate `(c + \Delta c, \lambda + \Delta\lambda)`; with
179
+ finite, and when the converged increment points against the tangent
180
+ predictor `(\Delta c_{pred}, \Delta\lambda_{pred})` of the step,
181
+ `\langle (\Delta c, \Delta\lambda), (\Delta c_{pred},
182
+ \Delta\lambda_{pred}) \rangle \le 0`. The previous converged point is
183
+ also at the distance `\Delta s` from the last one, and with long steps
184
+ or near a limit point the iterations may converge to it and then trace
185
+ the path backwards, the "doubling back" that [2]_ (p. 56) avoids with
186
+ the choice of the root. The accepted points are thus on the side of the
187
+ tangent predictor of the hyperplane normal to it, in the inner product
188
+ above, through the last converged point, as the points of the plane of
189
+ Riks [1]_, normal to the tangent at the distance `\Delta s` from the
190
+ last converged point. With full Newton-Raphson, `\boldsymbol{K}_T` is updated at
191
+ every iteration at the current iterate `(c + \Delta c, \lambda +
192
+ \Delta\lambda)`; with
179
193
  ``an.modified_NR = True`` the factorization at the last converged state
180
194
  is used for the first ``an.compute_every_n`` iterations and the tangent
181
195
  is then updated every ``an.compute_every_n`` iterations; at the first
@@ -198,27 +212,27 @@ def _solver_arc_length(an, method, silent=False):
198
212
  `\lambda < 1` to `\lambda + \Delta\lambda > 1`, the step is replaced by
199
213
  Newton-Raphson iterations at exactly `\lambda = 1`, see
200
214
  :func:`.newton_raphson._NR_iterations`, from the predictor
201
- `t \{\Delta c\}` with `t = (1 - \lambda)/\Delta\lambda`. When these
215
+ `t \Delta \boldsymbol{c}` with `t = (1 - \lambda)/\Delta\lambda`. When these
202
216
  iterations do not converge, the step is repeated with a shorter arc
203
217
  length.
204
218
 
205
219
  When the load depends on the configuration, i.e. ``calc_fext`` accepts
206
- the keyword argument ``c``, `\{F_{ext}\}` in `\{\delta c_q\}` and in the
220
+ the keyword argument ``c``, `\boldsymbol{F}_{ext}` in `\delta \boldsymbol{c}_q` and in the
207
221
  tangent predictor is the load vector of the current configuration
208
- ``calc_fext(inc=1., c=c)``, the derivative `\partial R/\partial
209
- \lambda` of the residual, whereas `\{u_{ref}\}` remains the linear
222
+ ``calc_fext(inc=1., c=c)``, minus the derivative `\partial \boldsymbol{R}/\partial
223
+ \lambda` of the residual, whereas `\boldsymbol{u}_{ref}` remains the linear
210
224
  solution of the reference vector. The callables that accept ``inc``
211
225
  receive the current load factor, see :mod:`.callbacks`.
212
226
 
213
227
  When ``calc_fext`` also accepts the keyword argument ``lbd``, e.g. for
214
228
  prescribed displacements imposed exactly, it receives the load factor
215
- together with the configuration, `\{F_{ext}\}` above being then
216
- `\partial R/\partial \lambda` at the current `c` and `\lambda`;
217
- `\{u_{ref}\}` is the linear solution of `\partial R/\partial \lambda`
229
+ together with the configuration, `\boldsymbol{F}_{ext}` above being then
230
+ `-\partial \boldsymbol{R}/\partial \lambda` at the current `c` and `\lambda`;
231
+ `\boldsymbol{u}_{ref}` is the linear solution of `-\partial \boldsymbol{R}/\partial \lambda`
218
232
  at `c = 0`, `\lambda = 0`, since the reference vector may be null, e.g.
219
- under a prescribed displacement alone; and `\lambda \, \partial
220
- R/\partial \lambda` is an additional reference of the relative
221
- convergence criterion. `\partial R/\partial \lambda` is evaluated
233
+ under a prescribed displacement alone; and `-\lambda \, \partial
234
+ \boldsymbol{R}/\partial \lambda` is an additional reference of the relative
235
+ convergence criterion. `-\partial \boldsymbol{R}/\partial \lambda` is evaluated
222
236
  together with the tangent stiffness matrix, at the same iterate, i.e.
223
237
  once per update of the tangent stiffness matrix, and used in the
224
238
  bordered solution; the last one evaluated is the reference of the next
@@ -249,9 +263,10 @@ def _solver_arc_length(an, method, silent=False):
249
263
  fext = an.calc_fext(inc=1., silent=True)
250
264
  kC0 = an.calc_kC(silent=silent)
251
265
  w = _weights(an, kC0)
266
+ K0_abs = _abs_matrix(kC0)
252
267
  lbd_dependent = callbacks.is_load_factor_dependent(an)
253
268
  if lbd_dependent:
254
- # dR/dlbd at c = 0 and lbd = 0
269
+ # -dR/dlbd at c = 0 and lbd = 0
255
270
  q0 = callbacks.calc_load_vector(an, np.zeros_like(fext), fext, 0.)
256
271
  u_ref = solve(kC0, q0, silent=True)
257
272
  else:
@@ -293,7 +308,7 @@ def _solver_arc_length(an, method, silent=False):
293
308
  warn('Singular tangent stiffness matrix at the last converged state! Analysis stopped',
294
309
  level=1, silent=silent)
295
310
  break
296
- # load vector of the converged configuration, dR/dlbd, kept with
311
+ # load vector of the converged configuration, -dR/dlbd, kept with
297
312
  # the tangent stiffness matrix when the step is repeated
298
313
  if step_num == 1 and lbd_dependent:
299
314
  q_conv = q0
@@ -313,6 +328,9 @@ def _solver_arc_length(an, method, silent=False):
313
328
  sign = 1.
314
329
  dlbd = sign*arc_length/norm_q
315
330
  dc = dlbd*dc_q_conv
331
+ # tangent predictor, the forward direction of the step
332
+ dc_pred = dc
333
+ dlbd_pred = dlbd
316
334
 
317
335
  msg('Step %d, lbd %1.5f, arc-length increment %1.5f' % (step_num, lbd, arc_length),
318
336
  level=1, silent=silent)
@@ -321,7 +339,7 @@ def _solver_arc_length(an, method, silent=False):
321
339
  Rnorms = []
322
340
  solve_kT = solve_kT_conv
323
341
  dc_q = dc_q_conv
324
- # the last dR/dlbd evaluated, with the tangent stiffness matrix
342
+ # the last -dR/dlbd evaluated, with the tangent stiffness matrix
325
343
  q_iter = q_conv
326
344
  iter_kT = 0
327
345
  iteration = 0
@@ -329,14 +347,15 @@ def _solver_arc_length(an, method, silent=False):
329
347
  iteration += 1
330
348
  fint = callbacks.calc_fint(an, c + dc, lbd + dlbd)
331
349
  fext_total = (lbd + dlbd)*fext
332
- R = fext_total - fint
350
+ R = fint - fext_total
333
351
  q_ref = None
334
352
  if lbd_dependent:
335
353
  q_ref = (lbd + dlbd)*q_iter
336
- conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
337
- fext_ref=dlbd*fext,
338
- q_ref=q_ref, w=w)
339
- _log_iteration(an, iteration, R, Rrel, silent, lbd=lbd + dlbd)
354
+ conv, Rnorm, Rrel, roundoff = _check_convergence(an, R, fint,
355
+ fext_total, fext_ref=dlbd*fext, q_ref=q_ref, w=w,
356
+ Kc_abs=K0_abs @ np.abs(c + dc), full_output=True)
357
+ _log_iteration(an, iteration, R, Rrel, silent, lbd=lbd + dlbd,
358
+ roundoff=roundoff)
340
359
  if conv:
341
360
  converged = True
342
361
  break
@@ -355,7 +374,7 @@ def _solver_arc_length(an, method, silent=False):
355
374
  warn('Singular tangent stiffness matrix', level=2, silent=silent)
356
375
  break
357
376
  if lbd_dependent:
358
- # dR/dlbd at the iterate of the tangent stiffness matrix
377
+ # -dR/dlbd at the iterate of the tangent stiffness matrix
359
378
  q_iter = callbacks.calc_load_vector(an, c + dc, fext,
360
379
  lbd + dlbd)
361
380
  dc_q = solve_kT(q_iter)
@@ -363,7 +382,7 @@ def _solver_arc_length(an, method, silent=False):
363
382
  dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc,
364
383
  fext))
365
384
  iter_kT = 1
366
- dc_R = solve_kT(R)
385
+ dc_R = solve_kT(-R)
367
386
 
368
387
  # linearized constraint
369
388
  den = inner(dc, dlbd, dc_q, 1.)
@@ -393,13 +412,20 @@ def _solver_arc_length(an, method, silent=False):
393
412
  dc = dc + dc_R + varlbd*dc_q
394
413
  dlbd = dlbd + varlbd
395
414
 
415
+ if converged and inner(dc, dlbd, dc_pred, dlbd_pred) <= 0:
416
+ # the increment points backwards, e.g. to the previous converged
417
+ # point, which is also at the distance arc_length
418
+ warn('Diverged - the step returned along the path already traced',
419
+ level=2, silent=silent)
420
+ converged = False
421
+
396
422
  finished = False
397
423
  if converged and lbd + dlbd > 1. and lbd < 1.:
398
424
  msg('Load factor 1.0 exceeded, correcting to load factor 1.0...', level=1, silent=silent)
399
425
  t = (1. - lbd)/dlbd
400
426
  converged, dc1, _ = _NR_iterations(an, c, t*dc, 1., fext,
401
427
  kT0=None, silent=silent, lbd_dependent=lbd_dependent,
402
- q=q_iter, w=w)
428
+ q=q_iter, w=w, K0_abs=K0_abs)
403
429
  if converged:
404
430
  msg('Converged at load factor 1.00000', level=2, silent=silent)
405
431
  an.cs.append((c + dc1).copy())
@@ -37,7 +37,6 @@ import io
37
37
  import os
38
38
  import re
39
39
  import sys
40
- import warnings
41
40
 
42
41
  import numpy as np
43
42
  from scipy.linalg import cholesky, eigh, solve_triangular
@@ -407,27 +406,27 @@ def _rayleigh_ritz(A, M, V):
407
406
  Notes
408
407
  -----
409
408
  The eigenvectors of different passes of :func:`capped_eigsh` are only
410
- `[M]`-orthogonal to the accuracy of ARPACK, e.g. `10^{-6}` between
411
- close eigenvalues of different passes. `[V]` is `[M]`-orthonormalized
412
- with the Cholesky factor `[L]` of `[V]^T[M][V]`, and the projected
413
- problem `[W]^T[A][W]\{y\} = w\{y\}`, with `[W] = [V][L]^{-T}`, is
409
+ `\boldsymbol{M}`-orthogonal to the accuracy of ARPACK, e.g. `10^{-6}` between
410
+ close eigenvalues of different passes. `\boldsymbol{V}` is `\boldsymbol{M}`-orthonormalized
411
+ with the Cholesky factor `\boldsymbol{L}` of `\boldsymbol{V}^\top \boldsymbol{M} \boldsymbol{V}`, and the projected
412
+ problem `\boldsymbol{W}^\top \boldsymbol{A} \boldsymbol{W} \boldsymbol{y} = w \boldsymbol{y}`, with `\boldsymbol{W} = \boldsymbol{V} \boldsymbol{L}^{-\top}`, is
414
413
  solved with the LAPACK driver ``dsyevr`` (``driver='evr'``), which does
415
414
  not use the ``dsteqr`` of Intel MKL broken above 32 x 32 matrices, see
416
- :func:`dsteqr_is_faulty`. The returned eigenvectors `[W]\{y\}` are
417
- `[M]`-orthonormal to the rounding errors, and the Ritz values are at
418
- least as accurate as the eigenvalues of the passes. `[V]^T[M][V]` and
419
- `[W]^T[A][W]` are symmetrized, `\frac{1}{2}([B] + [B]^T)`, before the
415
+ :func:`dsteqr_is_faulty`. The returned eigenvectors `\boldsymbol{W} \boldsymbol{y}` are
416
+ `\boldsymbol{M}`-orthonormal to the rounding errors, and the Ritz values are at
417
+ least as accurate as the eigenvalues of the passes. `\boldsymbol{V}^\top \boldsymbol{M} \boldsymbol{V}` and
418
+ `\boldsymbol{W}^\top \boldsymbol{A} \boldsymbol{W}` are symmetrized, `\frac{1}{2}(\boldsymbol{B} + \boldsymbol{B}^\top)`, before the
420
419
  Cholesky factorization and the eigenvalue solution.
421
420
 
422
421
  The Ritz values are the optimal approximations of the eigenvalues that
423
- can be obtained from the subspace spanned by the columns of `[V]`
422
+ can be obtained from the subspace spanned by the columns of `\boldsymbol{V}`
424
423
  ([1]_, Chapter 11, "Approximations from a Subspace", Section 11.4,
425
424
  "Optimality", Eq. (11.5) and Theorem 11.4.1, p. 236): the `j`-th
426
425
  Ritz value is the minimum over the `j`-dimensional subspaces of the span
427
426
  of the maximum of the Rayleigh quotient, so it can only improve when the
428
427
  span grows, and the span contains the eigenvectors of every pass. The
429
428
  theorem is stated for the standard problem; it holds for the pencil in
430
- the `[M]` inner product, through the Cholesky factor of `[V]^T[M][V]`.
429
+ the `\boldsymbol{M}` inner product, through the Cholesky factor of `\boldsymbol{V}^\top \boldsymbol{M} \boldsymbol{V}`.
431
430
 
432
431
  References
433
432
  ----------
@@ -446,7 +445,7 @@ def _rayleigh_ritz(A, M, V):
446
445
  def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
447
446
  r""":func:`scipy.sparse.linalg.eigsh` with ``ncv`` from :func:`arpack_ncv`
448
447
 
449
- Solves `[A]\{x\} = w [M]\{x\}` with a positive definite ``M`` about the
448
+ Solves `\boldsymbol{A} \boldsymbol{x} = w \boldsymbol{M} \boldsymbol{x}` with a positive definite ``M`` about the
450
449
  shift ``sigma``, in the shift-invert (``mode='normal'``) or Cayley
451
450
  (``mode='cayley'``) mode, selecting the eigenvalues by increasing `w`,
452
451
  e.g. the Cayley mode with ``which='SM'`` and ``sigma`` larger than all
@@ -490,7 +489,7 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
490
489
  Returns
491
490
  -------
492
491
  w, x : ndarray
493
- The eigenvalues and the `[M]`-orthonormal eigenvectors, by
492
+ The eigenvalues and the `\boldsymbol{M}`-orthonormal eigenvectors, by
494
493
  increasing `w` when computed in passes. ``w`` is a 1-D array of size
495
494
  ``k`` and ``x`` an array of shape ``(n, k)``.
496
495
 
@@ -502,7 +501,7 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
502
501
  -----
503
502
  In the shift-invert mode ARPACK computes the eigenvalues
504
503
  `\nu = 1/(w - \sigma)` of largest modulus of the operator
505
- `([A] - \sigma [M])^{-1}[M]`, i.e. the `w` nearest `\sigma` ([1]_,
504
+ `(\boldsymbol{A} - \sigma \boldsymbol{M})^{-1} \boldsymbol{M}`, i.e. the `w` nearest `\sigma` ([1]_,
506
505
  Chapter 3, Section 3.2, Eq. (3.2.2)).
507
506
  The passes keep ``k <= (max_ncv - 1)//2``, i.e. ``ncv >= 2 k + 1``.
508
507
  With ``k`` close to ``ncv`` ARPACK converges very slowly, not at all, or
@@ -510,22 +509,22 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
510
509
 
511
510
  The passes after the first one use the shift-invert mode about
512
511
  `\sigma_d`, the middle of the largest gap between the last five
513
- eigenvalues found, with the eigenvectors `[V]` of the previous passes
512
+ eigenvalues found, with the eigenvectors `\boldsymbol{V}` of the previous passes
514
513
  deflated from the operator:
515
514
 
516
515
  .. math::
517
516
 
518
- \{y\} = ([I] - [V][V]^T[M])([A] - \sigma_d [M])^{-1}\{x\}
517
+ \boldsymbol{y} = (\boldsymbol{I} - \boldsymbol{V} \boldsymbol{V}^\top \boldsymbol{M})(\boldsymbol{A} - \sigma_d \boldsymbol{M})^{-1} \boldsymbol{x}
519
518
 
520
- such that the eigenvalues of `[V]`, already found, become null and are
519
+ such that the eigenvalues of `\boldsymbol{V}`, already found, become null and are
521
520
  not found again. Each pass therefore returns the eigenvalues nearest
522
521
  `\sigma_d` among those not found yet, i.e. the next ones by increasing
523
522
  `w`. A shift close to them, instead of ``sigma``, separates them better:
524
523
  in the study of the panels cylinders it was 30 to 45% faster and gave
525
524
  eigenvalues 5 to 10 times more accurate. ARPACK applies the operator
526
- to `[M]\{x\}`; for an eigenvector `\{v_i\}` of `[V]`,
527
- `([A] - \sigma_d [M])^{-1}[M]\{v_i\} = \{v_i\}/(w_i - \sigma_d)` and,
528
- `[V]` being `[M]`-orthonormal, `([I] - [V][V]^T[M])\{v_i\} = \{0\}`.
525
+ to `\boldsymbol{M} \boldsymbol{x}`; for an eigenvector `\boldsymbol{v}_i` of `\boldsymbol{V}`,
526
+ `(\boldsymbol{A} - \sigma_d \boldsymbol{M})^{-1} \boldsymbol{M} \boldsymbol{v}_i = \boldsymbol{v}_i/(w_i - \sigma_d)` and,
527
+ `\boldsymbol{V}` being `\boldsymbol{M}`-orthonormal, `(\boldsymbol{I} - \boldsymbol{V} \boldsymbol{V}^\top \boldsymbol{M}) \boldsymbol{v}_i = \boldsymbol{0}`.
529
528
 
530
529
  References
531
530
  ----------
@@ -557,9 +556,7 @@ def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
557
556
  order = np.argsort(w, kind='stable')
558
557
  w, V = w[order], V[:, order]
559
558
  sigma_d = _deflation_shift(w, far_shift)
560
- with warnings.catch_warnings():
561
- warnings.simplefilter("ignore")
562
- lu = splu(csc_matrix(A - sigma_d*M))
559
+ lu = splu(csc_matrix(A - sigma_d*M))
563
560
  MV = M @ V
564
561
 
565
562
  def matvec(x, V=V, MV=MV, lu=lu):