structsolve 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structsolve-0.4.2/structsolve.egg-info → structsolve-0.5.0}/PKG-INFO +7 -4
- {structsolve-0.4.2 → structsolve-0.5.0}/README.md +5 -2
- {structsolve-0.4.2 → structsolve-0.5.0}/pyproject.toml +2 -2
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/analysis.py +69 -3
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/arc_length.py +16 -9
- structsolve-0.5.0/structsolve/callbacks.py +68 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/freq.py +78 -6
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/linear_buckling.py +205 -10
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/newton_raphson.py +9 -6
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/sparseutils.py +14 -5
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/static.py +8 -3
- {structsolve-0.4.2 → structsolve-0.5.0/structsolve.egg-info}/PKG-INFO +7 -4
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve.egg-info/SOURCES.txt +3 -0
- structsolve-0.5.0/tests/test_follower_freq_static.py +232 -0
- structsolve-0.5.0/tests/test_follower_loads.py +254 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/LICENSE +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/setup.cfg +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/__init__.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve/logger.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_analysis.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_arc_length.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_freq.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_linear_buckling.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_sparseutils.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_static.py +0 -0
- {structsolve-0.4.2 → structsolve-0.5.0}/tests/test_static_deflection.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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 ::
|
|
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.
|
|
64
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
Documentation
|
|
@@ -75,7 +76,9 @@ History
|
|
|
75
76
|
|
|
76
77
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
77
78
|
|
|
78
|
-
* version 0.
|
|
79
|
+
* version 0.5.0 (2026-10-01)
|
|
80
|
+
- Configuration-dependent loads and unsymmetric eigenproblems
|
|
81
|
+
* version 0.4.3 (2026-09-17)
|
|
79
82
|
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
80
83
|
return buckling loads higher than the critical ones
|
|
81
84
|
- Fixed wrong buckling loads of the sparse solver of `lb` when SciPy is
|
|
@@ -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.
|
|
27
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
Documentation
|
|
@@ -38,7 +39,9 @@ History
|
|
|
38
39
|
|
|
39
40
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
40
41
|
|
|
41
|
-
* version 0.
|
|
42
|
+
* version 0.5.0 (2026-10-01)
|
|
43
|
+
- Configuration-dependent loads and unsymmetric eigenproblems
|
|
44
|
+
* version 0.4.3 (2026-09-17)
|
|
42
45
|
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
43
46
|
return buckling loads higher than the critical ones
|
|
44
47
|
- Fixed wrong buckling loads of the sparse solver of `lb` when SciPy is
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structsolve"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
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 ::
|
|
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`.
|
|
193
|
-
the ``
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
6
|
-
from scipy.linalg import
|
|
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
|
|
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
|
-
|
|
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
|