structsolve 0.4.3__tar.gz → 0.5.2__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 (32) hide show
  1. {structsolve-0.4.3/structsolve.egg-info → structsolve-0.5.2}/PKG-INFO +12 -3
  2. {structsolve-0.4.3 → structsolve-0.5.2}/README.md +10 -1
  3. {structsolve-0.4.3 → structsolve-0.5.2}/pyproject.toml +2 -2
  4. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/analysis.py +69 -3
  5. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/arc_length.py +16 -9
  6. structsolve-0.5.2/structsolve/callbacks.py +68 -0
  7. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/freq.py +78 -6
  8. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/linear_buckling.py +205 -10
  9. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/newton_raphson.py +9 -6
  10. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/sparseutils.py +14 -5
  11. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/static.py +8 -3
  12. {structsolve-0.4.3 → structsolve-0.5.2/structsolve.egg-info}/PKG-INFO +12 -3
  13. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve.egg-info/SOURCES.txt +3 -0
  14. structsolve-0.5.2/tests/test_follower_freq_static.py +232 -0
  15. structsolve-0.5.2/tests/test_follower_loads.py +254 -0
  16. {structsolve-0.4.3 → structsolve-0.5.2}/LICENSE +0 -0
  17. {structsolve-0.4.3 → structsolve-0.5.2}/setup.cfg +0 -0
  18. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/__init__.py +0 -0
  19. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/arc_length_crisfield.py +0 -0
  20. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/arc_length_riks.py +0 -0
  21. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve/logger.py +0 -0
  22. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve.egg-info/dependency_links.txt +0 -0
  23. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve.egg-info/requires.txt +0 -0
  24. {structsolve-0.4.3 → structsolve-0.5.2}/structsolve.egg-info/top_level.txt +0 -0
  25. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_analysis.py +0 -0
  26. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_arc_length.py +0 -0
  27. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_freq.py +0 -0
  28. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_linear_buckling.py +0 -0
  29. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_newton_raphson.py +0 -0
  30. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_sparseutils.py +0 -0
  31. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_static.py +0 -0
  32. {structsolve-0.4.3 → structsolve-0.5.2}/tests/test_static_deflection.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structsolve
3
- Version: 0.4.3
3
+ Version: 0.5.2
4
4
  Summary: Structural analysis solvers
5
5
  Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
6
6
  License: BSD-2-Clause
7
7
  Project-URL: Homepage, https://github.com/compmech/structsolve
8
8
  Project-URL: Repository, https://github.com/compmech/structsolve
9
9
  Keywords: structural analysis,stress,vibration,linear buckling,newton raphson,arc length
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Intended Audience :: Education
@@ -51,6 +51,7 @@ Structural analysis solvers tailored for semi-analytical models
51
51
  - Eigensolver for dynamics: ([K] + lambda^2[M]){u} = 0
52
52
  - Nonlinear statics using Newton-Raphson
53
53
  - Nonlinear statics using the Arc-Length method
54
+ - Support for unsymmetric problems, such as those involving follower forces
54
55
 
55
56
  Currently these solvers are pretty much compatible with my other repositories
56
57
  [panels](https://github.com/saullocastro/panels),
@@ -60,7 +61,7 @@ Currently these solvers are pretty much compatible with my other repositories
60
61
  Citing this library
61
62
  ===================
62
63
 
63
- Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
64
+ Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.2). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
64
65
 
65
66
 
66
67
  Documentation
@@ -75,6 +76,14 @@ History
75
76
 
76
77
  See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
77
78
 
79
+ * version 0.5.2 (2026-10-02)
80
+ - Configuration-dependent loads, e.g. follower pressures: the load factor
81
+ is passed to the callables that accept `inc`, and the arc-length
82
+ methods use the load vector of the current configuration
83
+ - Unsymmetric matrices in `lb` (automatic detection), `freq` (residual
84
+ check, optional symmetric solvers, kinetic criterion) and `solve`
85
+ - Linear static analysis with configuration-dependent loads in
86
+ `Analysis.static(NLgeom=False)`
78
87
  * version 0.4.3 (2026-09-17)
79
88
  - Fixed the eigenvalue shift of the sparse solver of `lb`, which could
80
89
  return buckling loads higher than the critical ones
@@ -14,6 +14,7 @@ Structural analysis solvers tailored for semi-analytical models
14
14
  - Eigensolver for dynamics: ([K] + lambda^2[M]){u} = 0
15
15
  - Nonlinear statics using Newton-Raphson
16
16
  - Nonlinear statics using the Arc-Length method
17
+ - Support for unsymmetric problems, such as those involving follower forces
17
18
 
18
19
  Currently these solvers are pretty much compatible with my other repositories
19
20
  [panels](https://github.com/saullocastro/panels),
@@ -23,7 +24,7 @@ Currently these solvers are pretty much compatible with my other repositories
23
24
  Citing this library
24
25
  ===================
25
26
 
26
- Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
27
+ Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.2). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
27
28
 
28
29
 
29
30
  Documentation
@@ -38,6 +39,14 @@ History
38
39
 
39
40
  See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
40
41
 
42
+ * version 0.5.2 (2026-10-02)
43
+ - Configuration-dependent loads, e.g. follower pressures: the load factor
44
+ is passed to the callables that accept `inc`, and the arc-length
45
+ methods use the load vector of the current configuration
46
+ - Unsymmetric matrices in `lb` (automatic detection), `freq` (residual
47
+ check, optional symmetric solvers, kinetic criterion) and `solve`
48
+ - Linear static analysis with configuration-dependent loads in
49
+ `Analysis.static(NLgeom=False)`
41
50
  * version 0.4.3 (2026-09-17)
42
51
  - Fixed the eigenvalue shift of the sparse solver of `lb`, which could
43
52
  return buckling loads higher than the critical ones
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "structsolve"
7
- version = "0.4.3"
7
+ version = "0.5.2"
8
8
  description = "Structural analysis solvers"
9
9
  readme = "README.md"
10
10
  license = {text = "BSD-2-Clause"}
@@ -17,7 +17,7 @@ keywords = [
17
17
  "newton raphson", "arc length",
18
18
  ]
19
19
  classifiers = [
20
- "Development Status :: 3 - Alpha",
20
+ "Development Status :: 4 - Beta",
21
21
  "Intended Audience :: Science/Research",
22
22
  "Intended Audience :: Developers",
23
23
  "Intended Audience :: Education",
@@ -1,7 +1,11 @@
1
1
  from __future__ import absolute_import
2
2
 
3
+ import numpy as np
4
+
3
5
  from .static import solve
4
- from .logger import msg
6
+ from .logger import msg, warn
7
+ from . import callbacks
8
+ from .newton_raphson import _check_convergence
5
9
  from .newton_raphson import _solver_NR
6
10
  from .arc_length_riks import _solver_arc_length_riks
7
11
  from .arc_length_crisfield import _solver_arc_length_crisfield
@@ -132,6 +136,24 @@ class Analysis(object):
132
136
  matrix with the geometric stiffness matrix at ``c``. It is called
133
137
  with ``NLgeom=True``. Required for non-linear analyses.
134
138
 
139
+ Notes
140
+ -----
141
+ Configuration-dependent loads, e.g. follower pressures, are supported
142
+ through optional keyword arguments, passed only to the callables that
143
+ declare them explicitly, see :mod:`.callbacks`:
144
+
145
+ - ``calc_fint(c, inc=lbd)``, ``calc_kC(c, NLgeom=True, inc=lbd)`` and
146
+ ``calc_kG(c, NLgeom=True, inc=lbd)`` receive the current load factor,
147
+ such that ``calc_fint`` may contain the configuration-dependent part of
148
+ the load, scaled by ``inc``, and ``calc_kC + calc_kG`` its derivative.
149
+ - ``calc_fext(inc=1., c=c)`` must return the load vector of the
150
+ configuration ``c`` for a unit load factor, the derivative of the
151
+ residual with respect to the load factor, used by the arc-length
152
+ solvers.
153
+
154
+ The tangent stiffness matrix may then be unsymmetric, which the solvers
155
+ support, since they use LU factorizations.
156
+
135
157
  Attributes
136
158
  ----------
137
159
  increments : list
@@ -189,8 +211,28 @@ class Analysis(object):
189
211
  r"""General solver for static analyses
190
212
 
191
213
  The linear analysis solves `[K_C]\{c\} = \{F_{ext}\}` using
192
- :func:`.solve`. The non-linear analysis uses the solver selected by
193
- the ``NL_method`` attribute.
214
+ :func:`.solve`. When the load depends on the configuration, i.e.
215
+ ``calc_fext`` declares the keyword argument ``c``, see
216
+ :mod:`.callbacks`, the structure is still geometrically linear but
217
+ the load is evaluated in the current configuration,
218
+ `[K_C]\{c\} = \{F_{ext}(c)\}`, which is solved by the load
219
+ correction iterations
220
+
221
+ .. math::
222
+
223
+ \{c_{k+1}\} = \{c_k\} + [K_C]^{-1}(\{F_{ext}(c_k)\}
224
+ - [K_C]\{c_k\})
225
+
226
+ starting from the solution of the reference load, until the relative
227
+ criterion ``relTOL`` is satisfied, see
228
+ :func:`.newton_raphson._check_convergence`, or ``maxNumIter``
229
+ iterations are done. For a follower load whose force vector is
230
+ affine in `c`, `\{F(c)\} = \{F_0\} - [K_f]\{c\}`, this is the
231
+ solution of `([K_C] + [K_f])\{c\} = \{F_0\}`, with the
232
+ convergence rate of the spectral radius of `[K_C]^{-1}[K_f]`, i.e.
233
+ of the ratio of the load to the lowest critical load of the
234
+ linearized problem; beyond it the iterations diverge. The non-linear
235
+ analysis uses the solver selected by the ``NL_method`` attribute.
194
236
 
195
237
  Parameters
196
238
  ----------
@@ -232,6 +274,8 @@ class Analysis(object):
232
274
  k0 = self.calc_kC(silent=silent)
233
275
 
234
276
  c = solve(k0, fext, silent=silent)
277
+ if callbacks.is_load_configuration_dependent(self):
278
+ c = self._linear_static_follower(k0, c, silent)
235
279
 
236
280
  self.cs.append(c)
237
281
  self.increments.append(1.)
@@ -241,3 +285,25 @@ class Analysis(object):
241
285
 
242
286
  return self.increments, self.cs
243
287
 
288
+
289
+ def _linear_static_follower(self, k0, c, silent):
290
+ r"""Load correction iterations of the linear static analysis with a
291
+ configuration-dependent load, see :meth:`.static`"""
292
+ msg('Configuration-dependent load: load correction iterations',
293
+ level=2, silent=silent)
294
+ for iteration in range(1, self.maxNumIter + 1):
295
+ fext = self.calc_fext(inc=1., c=c, silent=True)
296
+ fint = k0 @ c
297
+ R = fext - fint
298
+ conv, Rnorm, Rrel = _check_convergence(self, R, fint, fext)
299
+ msg('Iteration: %d, relative ||R|| = %1.3e' % (iteration, Rrel),
300
+ level=3, silent=silent)
301
+ if conv:
302
+ return c
303
+ if not np.isfinite(Rnorm):
304
+ break
305
+ c = c + solve(k0, R, silent=True)
306
+ warn('The load correction iterations did not converge, the load may '
307
+ 'exceed the critical load of the linearized problem', level=1,
308
+ silent=silent)
309
+ return c
@@ -14,6 +14,7 @@ from scipy.sparse.linalg import splu
14
14
  from .logger import msg, warn
15
15
  from .static import solve
16
16
  from .sparseutils import remove_null_cols
17
+ from . import callbacks
17
18
  from .newton_raphson import (_check_convergence, _check_divergence,
18
19
  _NR_iterations, INC_CUT_FACTOR)
19
20
 
@@ -101,6 +102,14 @@ def _solver_arc_length(an, method, silent=False):
101
102
  - the arc length becomes smaller than ``an.minInc``
102
103
  - ``MAX_NUM_STEPS`` steps converged
103
104
 
105
+ When the load depends on the configuration, i.e. ``calc_fext`` accepts
106
+ the keyword argument ``c``, `\{F_{ext}\}` in `\{\delta c_q\}` and in the
107
+ tangent predictor is the load vector of the current configuration
108
+ ``calc_fext(inc=1., c=c)``, the derivative `\partial R/\partial
109
+ \lambda` of the residual, whereas `\{u_{ref}\}` remains the linear
110
+ solution of the reference vector. The callables that accept ``inc``
111
+ receive the current load factor, see :mod:`.callbacks`.
112
+
104
113
  The initial arc length corresponds to a load factor increment of
105
114
  ``an.initialInc`` along the initial tangent. The convergence and
106
115
  divergence checks are the same as in the Newton-Raphson solver, see
@@ -158,16 +167,15 @@ def _solver_arc_length(an, method, silent=False):
158
167
  if step_num == 1 and modified_NR and not an.kT_initial_state:
159
168
  kT = kC0
160
169
  else:
161
- kC = an.calc_kC(c=c, NLgeom=True, silent=True)
162
- kG = an.calc_kG(c=c, NLgeom=True, silent=True)
163
- kT = kC + kG
170
+ kT = callbacks.calc_kT(an, c, lbd)
164
171
  try:
165
172
  solve_kT_conv = _factorize(kT)
166
173
  except RuntimeError:
167
174
  warn('Singular tangent stiffness matrix at the last converged state! Analysis stopped',
168
175
  level=1, silent=silent)
169
176
  break
170
- dc_q_conv = solve_kT_conv(fext)
177
+ # load vector of the converged configuration, dR/dlbd
178
+ dc_q_conv = solve_kT_conv(callbacks.calc_load_vector(an, c, fext))
171
179
  norm_q = np.sqrt(inner(dc_q_conv, 1., dc_q_conv, 1.))
172
180
  if arc_length is None:
173
181
  arc_length = an.initialInc*norm_q
@@ -193,7 +201,7 @@ def _solver_arc_length(an, method, silent=False):
193
201
  iteration = 0
194
202
  while True:
195
203
  iteration += 1
196
- fint = an.calc_fint(c=(c + dc), silent=True)
204
+ fint = callbacks.calc_fint(an, c + dc, lbd + dlbd)
197
205
  fext_total = (lbd + dlbd)*fext
198
206
  R = fext_total - fint
199
207
  conv, Rnorm, Rrel = _check_convergence(an, R, fint, fext_total,
@@ -211,14 +219,13 @@ def _solver_arc_length(an, method, silent=False):
211
219
  if modified_NR and iter_kT < an.compute_every_n:
212
220
  iter_kT += 1
213
221
  else:
214
- kC = an.calc_kC(c=(c + dc), NLgeom=True, silent=True)
215
- kG = an.calc_kG(c=(c + dc), NLgeom=True, silent=True)
216
222
  try:
217
- solve_kT = _factorize(kC + kG)
223
+ solve_kT = _factorize(callbacks.calc_kT(an, c + dc,
224
+ lbd + dlbd))
218
225
  except RuntimeError:
219
226
  warn('Singular tangent stiffness matrix', level=2, silent=silent)
220
227
  break
221
- dc_q = solve_kT(fext)
228
+ dc_q = solve_kT(callbacks.calc_load_vector(an, c + dc, fext))
222
229
  iter_kT = 1
223
230
  dc_R = solve_kT(R)
224
231
 
@@ -0,0 +1,68 @@
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
@@ -2,8 +2,9 @@ import warnings
2
2
 
3
3
  import numpy as np
4
4
  import scipy
5
- from scipy.sparse.linalg import eigs, spsolve
6
- from scipy.linalg import eig
5
+ from scipy.sparse import csr_matrix
6
+ from scipy.sparse.linalg import eigs, eigsh, spsolve
7
+ from scipy.linalg import eig, eigh
7
8
 
8
9
  from .logger import msg, warn
9
10
  from .sparseutils import remove_null_cols
@@ -29,9 +30,25 @@ def _estimate_sigma(K, M):
29
30
  return -1.
30
31
 
31
32
 
33
+ def _residuals(K, M, lambda2, eigvecs):
34
+ r"""Relative residuals `||K u + \lambda^2 M u||/(||K u|| + |\lambda^2|
35
+ ||M u||)` of the finite eigenpairs"""
36
+ finite = np.isfinite(lambda2)
37
+ u = eigvecs[:, finite]
38
+ lam = lambda2[finite]
39
+ Ku = K @ u
40
+ Mu = M @ u
41
+ num = np.linalg.norm(Ku + Mu*lam, axis=0)
42
+ den = np.linalg.norm(Ku, axis=0) + np.abs(lam)*np.linalg.norm(Mu, axis=0)
43
+ out = np.zeros(num.shape)
44
+ np.divide(num, den, out=out, where=den > 0)
45
+ return out
46
+
47
+
32
48
  def freq(K, M, tol=0, sparse_solver=True,
33
49
  silent=False, sort=True, num_eigvalues=25,
34
- num_eigvalues_print=5, skip_null_cols=False):
50
+ num_eigvalues_print=5, skip_null_cols=False, symmetric=False,
51
+ check_rtol=1e-6):
35
52
  r"""Frequency analysis
36
53
 
37
54
  Calculates the eigenvalues `\lambda^2` and eigenvectors `\{u\}` of the
@@ -80,12 +97,27 @@ def freq(K, M, tol=0, sparse_solver=True,
80
97
  skip_null_cols : bool, optional
81
98
  If True, skip the removal of null columns from the matrices.
82
99
  Use only when ``K`` and ``M`` are known to be non-singular.
100
+ symmetric : bool or None, optional
101
+ ``False``, the default, uses the general solvers described above,
102
+ valid for unsymmetric matrices, e.g. ``K`` with an aerodynamic matrix
103
+ or with the load stiffness of follower loads. ``True`` uses the
104
+ solvers of symmetric matrices, :func:`scipy.sparse.linalg.eigsh` in
105
+ shift-invert mode or :func:`scipy.linalg.eigh`, which require
106
+ symmetric ``K`` and ``M`` and a positive definite ``M``, and return
107
+ real eigenvalues and eigenvectors normalized with respect to ``M``.
108
+ ``None`` selects them when :func:`.linear_buckling.is_symmetric` is
109
+ true for both matrices.
110
+ check_rtol : float or None, optional
111
+ A warning is issued when the relative residual of an eigenpair,
112
+ `||K u + \lambda^2 M u||/(||K u|| + |\lambda^2| ||M u||)`, is larger
113
+ than ``check_rtol``. ``None`` skips the check.
83
114
 
84
115
  Returns
85
116
  -------
86
117
  lambda2 : ndarray
87
118
  Complex array with the eigenvalues `\lambda^2 = -\omega_n^2`, in the
88
- order returned by the eigenvalue solver.
119
+ order returned by the eigenvalue solver, real with
120
+ ``symmetric=True``.
89
121
  eigvecs : ndarray
90
122
  The `i^{th}` eigenvector is ``eigvecs[:, i]``, with the size of the
91
123
  original matrices. The sparse solver returns eigenvectors normalized
@@ -95,6 +127,16 @@ def freq(K, M, tol=0, sparse_solver=True,
95
127
  eigenvalues in ``lambda2`` only if the solver already returned them
96
128
  in the order of increasing natural frequency.
97
129
 
130
+ Notes
131
+ -----
132
+ Non-conservative systems, e.g. under follower loads, may lose stability
133
+ by flutter, which the static criterion of :func:`.lb` does not detect:
134
+ with ``K`` the tangent stiffness matrix at a load level, including the
135
+ load stiffness, the eigenvalues `\lambda^2` of the general solvers are
136
+ real and negative below the flutter load and become complex conjugate
137
+ pairs above it, when two natural frequencies coalesce (the kinetic
138
+ criterion). A real positive `\lambda^2` indicates divergence.
139
+
98
140
  """
99
141
  msg('Running frequency analysis...', silent=silent)
100
142
 
@@ -108,7 +150,23 @@ def freq(K, M, tol=0, sparse_solver=True,
108
150
  else:
109
151
  Keff, Meff, used_cols = remove_null_cols(K, M, silent=silent,
110
152
  level=3)
111
- if sparse_solver:
153
+ if symmetric is None:
154
+ from .linear_buckling import is_symmetric
155
+ symmetric = is_symmetric(Keff) and is_symmetric(Meff)
156
+ if symmetric and sparse_solver:
157
+ sigma = _estimate_sigma(Keff, Meff)
158
+ msg('eigsh() solver (sigma={0})...'.format(sigma), level=3,
159
+ silent=silent)
160
+ eigvals, peigvecs = eigsh(A=csr_matrix(Keff), M=csr_matrix(Meff),
161
+ k=k, which='LM', tol=tol, sigma=sigma)
162
+ lambda2 = -eigvals
163
+ elif symmetric:
164
+ msg('eigh() solver...', level=3, silent=silent)
165
+ Kd = Keff.toarray() if hasattr(Keff, 'toarray') else np.asarray(Keff)
166
+ Md = Meff.toarray() if hasattr(Meff, 'toarray') else np.asarray(Meff)
167
+ eigvals, peigvecs = eigh(a=Kd, b=Md)
168
+ lambda2 = -eigvals
169
+ elif sparse_solver:
112
170
  #NOTE Looking for better performance with symmetric matrices, I tried
113
171
  # using sparseutils.sparse.is_symmetric and eigsh, but it seems not
114
172
  # to improve speed (I did not try passing only half of the sparse
@@ -139,6 +197,13 @@ def freq(K, M, tol=0, sparse_solver=True,
139
197
  eigvals, peigvecs = eig(a=Meff, b=Keff)
140
198
  lambda2 = -1./eigvals
141
199
 
200
+ if check_rtol is not None and peigvecs.shape[1]:
201
+ res = _residuals(Keff, Meff, lambda2, peigvecs)
202
+ if res.size and res.max() > check_rtol:
203
+ warn('freq: relative residual {0:.1e} of an eigenpair is larger '
204
+ 'than check_rtol={1:.1e}'.format(res.max(), check_rtol),
205
+ level=2, silent=silent)
206
+
142
207
  if used_cols is not None:
143
208
  eigvecs = np.zeros((size, peigvecs.shape[1]), dtype=peigvecs.dtype)
144
209
  eigvecs[used_cols, :] = peigvecs
@@ -164,6 +229,13 @@ def freq(K, M, tol=0, sparse_solver=True,
164
229
  msg('first {0} eigenvalues:'.format(num_eigvalues_print), level=1,
165
230
  silent=silent)
166
231
  for lambda2i in lambda2[:num_eigvalues_print]:
167
- msg('lambda**2: %1.5f, natural frequency: %1.5f rad/s' % (lambda2i, (-lambda2i)**0.5), level=2, silent=silent)
232
+ #NOTE complex for non-conservative systems, e.g. beyond flutter
233
+ omegan = np.sqrt(-complex(lambda2i))
234
+ if np.iscomplexobj(lambda2) and abs(complex(lambda2i).imag) > 0:
235
+ msg('lambda**2: {0:.5g}, natural frequency: {1:.5g} rad/s'.format(
236
+ complex(lambda2i), omegan), level=2, silent=silent)
237
+ else:
238
+ msg('lambda**2: %1.5f, natural frequency: %1.5f rad/s'
239
+ % (np.real(lambda2i), omegan.real), level=2, silent=silent)
168
240
 
169
241
  return lambda2, eigvecs