structsolve 0.4.0__tar.gz → 0.4.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.
- {structsolve-0.4.0/structsolve.egg-info → structsolve-0.4.2}/PKG-INFO +15 -5
- {structsolve-0.4.0 → structsolve-0.4.2}/README.md +14 -4
- {structsolve-0.4.0 → structsolve-0.4.2}/pyproject.toml +1 -1
- structsolve-0.4.2/structsolve/linear_buckling.py +357 -0
- {structsolve-0.4.0 → structsolve-0.4.2/structsolve.egg-info}/PKG-INFO +15 -5
- structsolve-0.4.2/tests/test_linear_buckling.py +352 -0
- structsolve-0.4.0/structsolve/linear_buckling.py +0 -132
- structsolve-0.4.0/tests/test_linear_buckling.py +0 -155
- {structsolve-0.4.0 → structsolve-0.4.2}/LICENSE +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/setup.cfg +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/__init__.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/analysis.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/arc_length.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/freq.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/logger.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/newton_raphson.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/sparseutils.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve/static.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve.egg-info/SOURCES.txt +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_analysis.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_arc_length.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_freq.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_sparseutils.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_static.py +0 -0
- {structsolve-0.4.0 → structsolve-0.4.2}/tests/test_static_deflection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -41,8 +41,7 @@ Github Actions status:
|
|
|
41
41
|
|
|
42
42
|
Coverage status:
|
|
43
43
|
[](https://github.com/saullocastro/structsolve/actions/workflows/coverage.yml)
|
|
44
|
-
[](https://codecov.io/gh/saullocastro/structsolve)
|
|
46
45
|
|
|
47
46
|
Structural analysis solvers tailored for semi-analytical models
|
|
48
47
|
===============================================================
|
|
@@ -61,7 +60,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
61
60
|
Citing this library
|
|
62
61
|
===================
|
|
63
62
|
|
|
64
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.
|
|
63
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.2). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
Documentation
|
|
@@ -76,7 +75,18 @@ History
|
|
|
76
75
|
|
|
77
76
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
78
77
|
|
|
79
|
-
* version 0.4.
|
|
78
|
+
* version 0.4.2 (2026-09-17)
|
|
79
|
+
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
80
|
+
return buckling loads higher than the critical ones
|
|
81
|
+
- Fixed wrong buckling loads of the sparse solver of `lb` when SciPy is
|
|
82
|
+
linked against Intel MKL 2024.2.0 to 2025.0.0 (e.g. Anaconda on
|
|
83
|
+
Windows), whose `dsteqr` routine breaks ARPACK
|
|
84
|
+
- `lb` condenses out the dofs where `KG` is null and solves small
|
|
85
|
+
condensed problems with a dense solver
|
|
86
|
+
- `lb` verifies the eigenpairs of the sparse solver, raising an error
|
|
87
|
+
instead of returning wrong results
|
|
88
|
+
- The sparse solver of `lb` returns the load multipliers sorted as the
|
|
89
|
+
dense solver
|
|
80
90
|
- Newton-Raphson with full Newton iterations and a relative convergence
|
|
81
91
|
criterion by default, reaching quadratic convergence with exact tangent
|
|
82
92
|
stiffness matrices
|
|
@@ -4,8 +4,7 @@ Github Actions status:
|
|
|
4
4
|
|
|
5
5
|
Coverage status:
|
|
6
6
|
[](https://github.com/saullocastro/structsolve/actions/workflows/coverage.yml)
|
|
7
|
-
[](https://codecov.io/gh/saullocastro/structsolve)
|
|
9
8
|
|
|
10
9
|
Structural analysis solvers tailored for semi-analytical models
|
|
11
10
|
===============================================================
|
|
@@ -24,7 +23,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
24
23
|
Citing this library
|
|
25
24
|
===================
|
|
26
25
|
|
|
27
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.
|
|
26
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.2). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
Documentation
|
|
@@ -39,7 +38,18 @@ History
|
|
|
39
38
|
|
|
40
39
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
41
40
|
|
|
42
|
-
* version 0.4.
|
|
41
|
+
* version 0.4.2 (2026-09-17)
|
|
42
|
+
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
43
|
+
return buckling loads higher than the critical ones
|
|
44
|
+
- Fixed wrong buckling loads of the sparse solver of `lb` when SciPy is
|
|
45
|
+
linked against Intel MKL 2024.2.0 to 2025.0.0 (e.g. Anaconda on
|
|
46
|
+
Windows), whose `dsteqr` routine breaks ARPACK
|
|
47
|
+
- `lb` condenses out the dofs where `KG` is null and solves small
|
|
48
|
+
condensed problems with a dense solver
|
|
49
|
+
- `lb` verifies the eigenpairs of the sparse solver, raising an error
|
|
50
|
+
instead of returning wrong results
|
|
51
|
+
- The sparse solver of `lb` returns the load multipliers sorted as the
|
|
52
|
+
dense solver
|
|
43
53
|
- Newton-Raphson with full Newton iterations and a relative convergence
|
|
44
54
|
criterion by default, reaching quadratic convergence with exact tangent
|
|
45
55
|
stiffness matrices
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy.sparse import csc_matrix, csr_matrix
|
|
5
|
+
from scipy.sparse.linalg import eigsh, splu
|
|
6
|
+
from scipy.linalg import eigh, LinAlgError
|
|
7
|
+
|
|
8
|
+
from .logger import msg, warn
|
|
9
|
+
from .sparseutils import remove_null_cols
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _estimate_sigma(K, KG, safety=10., max_iter=50, rel_tol=1e-3):
|
|
13
|
+
r"""Shift of the Cayley mode used by :func:`lb`
|
|
14
|
+
|
|
15
|
+
With ``sigma > 0``, ``eigsh(A=KG, M=K, sigma=sigma, mode='cayley',
|
|
16
|
+
which='SM')`` returns the eigenvalues `\mu` of ``KG u = mu K u`` with the
|
|
17
|
+
smallest `|(\mu + \sigma)/(\mu - \sigma)|`. This ordering selects the most
|
|
18
|
+
negative `\mu`, i.e. the lowest positive load multipliers `-1/\mu`, only
|
|
19
|
+
when `\sigma` is larger than `|\mu|` of these critical eigenvalues.
|
|
20
|
+
Otherwise the eigenvalues closest to `-\sigma` are returned.
|
|
21
|
+
|
|
22
|
+
The largest `|\mu|` is estimated with power iterations on
|
|
23
|
+
`[K]^{-1}[K_G]`, whose norm ratio in the `[K]`-norm increases towards the
|
|
24
|
+
largest `|\mu|`, and multiplied by ``safety``. The default shift ``1.`` is
|
|
25
|
+
returned when ``K`` is singular, not positive definite, or the linear
|
|
26
|
+
solution is inaccurate.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
with warnings.catch_warnings():
|
|
31
|
+
warnings.simplefilter("ignore")
|
|
32
|
+
lu = splu(csc_matrix(K))
|
|
33
|
+
x = np.random.RandomState(42).randn(K.shape[0])
|
|
34
|
+
Kx = K @ x
|
|
35
|
+
mu_max = 0.
|
|
36
|
+
for i in range(max_iter):
|
|
37
|
+
xKx = x @ Kx
|
|
38
|
+
if not np.isfinite(xKx) or xKx <= 0:
|
|
39
|
+
return 1.
|
|
40
|
+
x = x / np.sqrt(xKx)
|
|
41
|
+
rhs = KG @ x
|
|
42
|
+
y = lu.solve(rhs)
|
|
43
|
+
Ky = K @ y
|
|
44
|
+
if i == 0:
|
|
45
|
+
residual = np.linalg.norm(Ky - rhs)
|
|
46
|
+
if not residual <= 1e-6 * np.linalg.norm(rhs):
|
|
47
|
+
return 1.
|
|
48
|
+
# ||y||_K / ||x||_K, with ||x||_K = 1
|
|
49
|
+
ratio = np.sqrt(abs(y @ Ky))
|
|
50
|
+
if not np.isfinite(ratio) or ratio == 0:
|
|
51
|
+
return 1.
|
|
52
|
+
converged = i > 0 and ratio - mu_max <= rel_tol * ratio
|
|
53
|
+
mu_max = max(mu_max, ratio)
|
|
54
|
+
if converged:
|
|
55
|
+
break
|
|
56
|
+
x, Kx = y, Ky
|
|
57
|
+
return safety * mu_max
|
|
58
|
+
except Exception:
|
|
59
|
+
return 1.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_positive_definite(A):
|
|
63
|
+
"""Tells if the symmetric sparse matrix ``A`` is positive definite
|
|
64
|
+
|
|
65
|
+
``A`` is factorized with a symmetric permutation and without row
|
|
66
|
+
interchanges. The Gaussian elimination of a positive definite matrix
|
|
67
|
+
never finds a zero or negative pivot, and by Sylvester's law of inertia
|
|
68
|
+
any negative pivot means a negative eigenvalue of ``A``.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
with warnings.catch_warnings():
|
|
73
|
+
warnings.simplefilter("ignore")
|
|
74
|
+
lu = splu(csc_matrix(A), permc_spec='MMD_AT_PLUS_A',
|
|
75
|
+
diag_pivot_thresh=0., options=dict(SymmetricMode=True))
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
# exactly singular
|
|
78
|
+
return False
|
|
79
|
+
if not np.array_equal(lu.perm_r, lu.perm_c):
|
|
80
|
+
# a zero pivot forced a row interchange
|
|
81
|
+
return False
|
|
82
|
+
return bool(np.all(lu.U.diagonal() > 0))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4):
|
|
86
|
+
r"""Verify the eigenpairs of `([K] + \lambda [K_G])\{u\} = \{0\}`
|
|
87
|
+
|
|
88
|
+
- The relative residual
|
|
89
|
+
`||K u + \lambda K_G u|| / (||K u|| + |\lambda| ||K_G u||)` of every
|
|
90
|
+
eigenpair with a finite `\lambda` must not be larger than ``rtol``.
|
|
91
|
+
- No load multiplier is missing below the lowest positive `\lambda_1`
|
|
92
|
+
of ``eigvals``: when ``K`` is positive definite, `[K] + s [K_G]` with
|
|
93
|
+
`s = \lambda_1 (1 - min\_rel\_gap)` must also be positive definite.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
error : str or None
|
|
98
|
+
Description of the failed check, ``None`` when all checks passed.
|
|
99
|
+
|
|
100
|
+
"""
|
|
101
|
+
finite = np.isfinite(eigvals)
|
|
102
|
+
if not np.any(finite):
|
|
103
|
+
return 'no finite load multiplier was found'
|
|
104
|
+
lam = eigvals[finite]
|
|
105
|
+
u = eigvecs[:, finite]
|
|
106
|
+
Ku = K @ u
|
|
107
|
+
KGu = KG @ u
|
|
108
|
+
num = np.linalg.norm(Ku + KGu*lam, axis=0)
|
|
109
|
+
den = np.linalg.norm(Ku, axis=0) + np.abs(lam)*np.linalg.norm(KGu, axis=0)
|
|
110
|
+
residual = np.full(lam.shape, np.inf)
|
|
111
|
+
np.divide(num, den, out=residual, where=den > 0)
|
|
112
|
+
if not np.all(residual <= rtol):
|
|
113
|
+
i = np.argmax(np.nan_to_num(residual, nan=np.inf))
|
|
114
|
+
return ('relative residual {0:.2e} > {1:.1e} for the load multiplier '
|
|
115
|
+
'{2}'.format(residual[i], rtol, lam[i]))
|
|
116
|
+
positive = lam[lam > 0]
|
|
117
|
+
if positive.size and _is_positive_definite(K):
|
|
118
|
+
s = positive.min()*(1 - min_rel_gap)
|
|
119
|
+
if not _is_positive_definite(K + s*KG):
|
|
120
|
+
return ('at least one load multiplier lower than {0} is missing'
|
|
121
|
+
.format(positive.min()))
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _sort_eigenpairs(mu, eigvecs):
|
|
126
|
+
r"""Sort by increasing `\mu`, i.e. the positive `\lambda = -1/\mu` first
|
|
127
|
+
in increasing order, as returned by :func:`scipy.linalg.eigh`"""
|
|
128
|
+
order = np.argsort(mu, kind='stable')
|
|
129
|
+
return mu[order], eigvecs[:, order]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _eigh_condensed(K, KG, k):
|
|
133
|
+
r"""Dense solution after condensing out the dofs where ``KG`` is null
|
|
134
|
+
|
|
135
|
+
With the dofs split in `b`, where `[K_G]` has non-null rows, and `a`,
|
|
136
|
+
where it does not, the static condensation of the `a` dofs gives:
|
|
137
|
+
|
|
138
|
+
.. math::
|
|
139
|
+
|
|
140
|
+
([K_{bb}] - [K_{ba}][K_{aa}]^{-1}[K_{ab}]
|
|
141
|
+
+ \lambda [K_{G_{bb}}])\{u_b\} = \{0\}, \qquad
|
|
142
|
+
\{u_a\} = -[K_{aa}]^{-1}[K_{ab}]\{u_b\}
|
|
143
|
+
|
|
144
|
+
solved with :func:`scipy.linalg.eigh`, which requires a positive
|
|
145
|
+
definite Schur complement. Returns the ``k`` lowest eigenvalues `\mu` of
|
|
146
|
+
`K_G u = \mu K u` and the corresponding eigenvectors.
|
|
147
|
+
|
|
148
|
+
"""
|
|
149
|
+
active = np.asarray(abs(KG).sum(axis=1)).ravel() > 0
|
|
150
|
+
b = np.flatnonzero(active)
|
|
151
|
+
a = np.flatnonzero(~active)
|
|
152
|
+
if b.size == 0:
|
|
153
|
+
raise ValueError('KG is a null matrix')
|
|
154
|
+
K = csr_matrix(K)
|
|
155
|
+
KGbb = csr_matrix(KG)[b, :][:, b].toarray()
|
|
156
|
+
S = K[b, :][:, b].toarray()
|
|
157
|
+
if a.size:
|
|
158
|
+
Kab = csc_matrix(K[a, :][:, b])
|
|
159
|
+
Kba = K[b, :][:, a]
|
|
160
|
+
with warnings.catch_warnings():
|
|
161
|
+
warnings.simplefilter("ignore")
|
|
162
|
+
lu = splu(csc_matrix(K[a, :][:, a]))
|
|
163
|
+
# solving blocks of right-hand sides with at most ~1e7 entries
|
|
164
|
+
chunk = max(1, int(1e7) // a.size)
|
|
165
|
+
for j0 in range(0, b.size, chunk):
|
|
166
|
+
j1 = min(j0 + chunk, b.size)
|
|
167
|
+
X = lu.solve(Kab[:, j0:j1].toarray())
|
|
168
|
+
S[:, j0:j1] -= Kba @ X
|
|
169
|
+
mu, ub = eigh(a=0.5*(KGbb + KGbb.T), b=0.5*(S + S.T))
|
|
170
|
+
k = min(k, b.size)
|
|
171
|
+
ub = ub[:, :k]
|
|
172
|
+
eigvecs = np.zeros((K.shape[0], k), dtype=ub.dtype)
|
|
173
|
+
eigvecs[b, :] = ub
|
|
174
|
+
if a.size:
|
|
175
|
+
eigvecs[a, :] = -lu.solve(np.asarray(Kab @ ub))
|
|
176
|
+
return mu[:k], eigvecs
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _eigsh_cayley(K, KG, k, tol, silent):
|
|
180
|
+
"""Solution with :func:`scipy.sparse.linalg.eigsh` in Cayley mode"""
|
|
181
|
+
sigma = _estimate_sigma(K, KG)
|
|
182
|
+
msg('sigma={0}'.format(sigma), level=4, silent=silent)
|
|
183
|
+
k = min(k, K.shape[0] - 2)
|
|
184
|
+
mu, eigvecs = eigsh(A=KG, k=k, which='SM', M=K, tol=tol, sigma=sigma,
|
|
185
|
+
mode='cayley')
|
|
186
|
+
return _sort_eigenpairs(mu, eigvecs)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
190
|
+
num_eigvalues=25, num_eigvalues_print=5,
|
|
191
|
+
skip_null_cols=False, max_dense_size=2000, check_rtol=1e-3):
|
|
192
|
+
r"""Linear buckling analysis
|
|
193
|
+
|
|
194
|
+
Calculates the eigenvalues `\lambda` and eigenvectors `\{u\}` of the
|
|
195
|
+
eigenvalue problem:
|
|
196
|
+
|
|
197
|
+
.. math::
|
|
198
|
+
|
|
199
|
+
([K] + \lambda [K_G])\{u\} = \{0\}
|
|
200
|
+
|
|
201
|
+
where `\lambda` is the load multiplier of the load state that generated
|
|
202
|
+
`[K_G]`. It can also be used for more general eigenvalue analyses, e.g.
|
|
203
|
+
if ``K`` is the tangent stiffness matrix of a given load state. The null
|
|
204
|
+
rows and columns of ``K`` are removed from both matrices before solving
|
|
205
|
+
the eigenvalue problem, unless ``skip_null_cols=True``.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
K : sparse_matrix
|
|
210
|
+
Stiffness matrix. Should include all constant terms of the initial
|
|
211
|
+
stress stiffness matrix, aerodynamic matrix and so forth when
|
|
212
|
+
applicable.
|
|
213
|
+
KG : sparse_matrix
|
|
214
|
+
Initial stress stiffness matrix that multiplies the load multiplier
|
|
215
|
+
`\lambda` of the eigenvalue problem.
|
|
216
|
+
tol : float, optional
|
|
217
|
+
A float tolerance passed to :func:`scipy.sparse.linalg.eigsh`.
|
|
218
|
+
sparse_solver : bool, optional
|
|
219
|
+
With ``True``, when ``KG`` has at most ``max_dense_size`` non-null
|
|
220
|
+
rows, the other dofs (typically the in-plane dofs) are condensed out
|
|
221
|
+
with a sparse factorization of ``K`` and the condensed problem is
|
|
222
|
+
solved with :func:`scipy.linalg.eigh`. Otherwise, or when this
|
|
223
|
+
solution fails, :func:`scipy.sparse.linalg.eigsh` solves the full
|
|
224
|
+
problem in Cayley mode, with a shift estimated from the matrices.
|
|
225
|
+
The eigenpairs are verified as described in the Notes, and
|
|
226
|
+
``num_eigvalues`` load multipliers are returned.
|
|
227
|
+
With ``False``, :func:`scipy.linalg.eigh` solves the full problem,
|
|
228
|
+
returning all load multipliers, and requires a positive definite
|
|
229
|
+
``K``.
|
|
230
|
+
silent : bool, optional
|
|
231
|
+
A boolean to tell whether the log messages should be printed.
|
|
232
|
+
num_eigvalues : int, optional
|
|
233
|
+
Number of load multipliers calculated with the sparse solver, and
|
|
234
|
+
number of returned eigenvectors.
|
|
235
|
+
num_eigvalues_print : int, optional
|
|
236
|
+
Number of eigenvalues to print.
|
|
237
|
+
skip_null_cols : bool, optional
|
|
238
|
+
If True, skip the removal of null columns from the matrices.
|
|
239
|
+
Use only when K is known to be non-singular.
|
|
240
|
+
max_dense_size : int, optional
|
|
241
|
+
Maximum number of non-null rows of ``KG`` for which the sparse solver
|
|
242
|
+
solves the condensed problem with :func:`scipy.linalg.eigh`. Use
|
|
243
|
+
``0`` to always use :func:`scipy.sparse.linalg.eigsh`.
|
|
244
|
+
check_rtol : float, optional
|
|
245
|
+
Maximum relative residual of the eigenpairs of the sparse solver,
|
|
246
|
+
see the Notes.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
eigvals : ndarray
|
|
251
|
+
The load multipliers `\lambda`, calculated as ``-1/eigval`` from the
|
|
252
|
+
eigenvalues ``eigval`` of ``KG u = eigval K u``. The positive load
|
|
253
|
+
multipliers come first in increasing order, followed by the negative
|
|
254
|
+
ones, for both solvers.
|
|
255
|
+
eigvecs : ndarray
|
|
256
|
+
The `i^{th}` eigenvector is ``eigvecs[:, i]``, with the size of the
|
|
257
|
+
original matrices. Only ``num_eigvalues`` eigenvectors are returned.
|
|
258
|
+
|
|
259
|
+
Raises
|
|
260
|
+
------
|
|
261
|
+
RuntimeError
|
|
262
|
+
When no eigenpairs of the sparse solver pass the verification.
|
|
263
|
+
|
|
264
|
+
Notes
|
|
265
|
+
-----
|
|
266
|
+
The eigenpairs of the sparse solver are verified as follows:
|
|
267
|
+
|
|
268
|
+
- the relative residual
|
|
269
|
+
`||K u + \lambda K_G u|| / (||K u|| + |\lambda| ||K_G u||)` of each
|
|
270
|
+
eigenpair must not be larger than ``check_rtol``;
|
|
271
|
+
- when ``K`` is positive definite, `[K] + s [K_G]` must also be positive
|
|
272
|
+
definite for `s` slightly lower than the lowest positive load
|
|
273
|
+
multiplier found, i.e. no lower load multiplier was missed. The
|
|
274
|
+
factorization is done with SuperLU, without row interchanges.
|
|
275
|
+
|
|
276
|
+
When the condensed dense solution fails the verification,
|
|
277
|
+
:func:`scipy.sparse.linalg.eigsh` is tried, and a ``RuntimeError`` is
|
|
278
|
+
raised when it also fails.
|
|
279
|
+
|
|
280
|
+
ARPACK, used by :func:`scipy.sparse.linalg.eigsh`, returns wrong
|
|
281
|
+
eigenpairs at random, without any error, when linked against Intel MKL
|
|
282
|
+
2024.2.0 to 2025.0.0, e.g. in some Anaconda builds of SciPy: the
|
|
283
|
+
``dsteqr`` routine of these MKL versions returns wrong eigenvectors for
|
|
284
|
+
matrices larger than 32 x 32. MKL 2025.0.1 or newer is not affected.
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
msg('Running linear buckling analysis...', silent=silent)
|
|
288
|
+
|
|
289
|
+
msg('Eigenvalue solver... ', level=2, silent=silent)
|
|
290
|
+
|
|
291
|
+
size = KG.shape[0]
|
|
292
|
+
if skip_null_cols:
|
|
293
|
+
used_cols = None
|
|
294
|
+
else:
|
|
295
|
+
K, KG, used_cols = remove_null_cols(K, KG, silent=silent)
|
|
296
|
+
if sparse_solver:
|
|
297
|
+
K = csr_matrix(K)
|
|
298
|
+
KG = csr_matrix(KG)
|
|
299
|
+
num_active = int(np.count_nonzero(abs(KG).sum(axis=1)))
|
|
300
|
+
solvers = []
|
|
301
|
+
if num_active <= max_dense_size:
|
|
302
|
+
solvers.append(('eigh() with condensed dofs',
|
|
303
|
+
lambda: _eigh_condensed(K, KG, num_eigvalues)))
|
|
304
|
+
solvers.append(('eigsh()',
|
|
305
|
+
lambda: _eigsh_cayley(K, KG, num_eigvalues, tol,
|
|
306
|
+
silent)))
|
|
307
|
+
errors = []
|
|
308
|
+
for name, solver in solvers:
|
|
309
|
+
msg('{0} solver ({1} of {2} rows of KG are non-null)...'.format(
|
|
310
|
+
name, num_active, K.shape[0]), level=3, silent=silent)
|
|
311
|
+
try:
|
|
312
|
+
eigvals, peigvecs = solver()
|
|
313
|
+
with np.errstate(divide='ignore'):
|
|
314
|
+
error = _check_eigenpairs(K, KG, -1./eigvals, peigvecs,
|
|
315
|
+
check_rtol)
|
|
316
|
+
except (LinAlgError, RuntimeError, ValueError) as e:
|
|
317
|
+
error = '{0}: {1}'.format(type(e).__name__, e)
|
|
318
|
+
if error is None:
|
|
319
|
+
msg('finished!', level=3, silent=silent)
|
|
320
|
+
break
|
|
321
|
+
warn('{0} solver failed: {1}'.format(name, error), level=3,
|
|
322
|
+
silent=silent)
|
|
323
|
+
errors.append('{0} solver: {1}'.format(name, error))
|
|
324
|
+
else:
|
|
325
|
+
raise RuntimeError(
|
|
326
|
+
'Linear buckling analysis failed, no eigenvalue solver passed '
|
|
327
|
+
'the verification ({0}). When ARPACK is linked against Intel '
|
|
328
|
+
'MKL 2024.2.0 to 2025.0.0, update MKL to 2025.0.1 or newer. '
|
|
329
|
+
'Otherwise, try sparse_solver=False or a larger '
|
|
330
|
+
'max_dense_size.'.format('; '.join(errors)))
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
K = K.toarray()
|
|
334
|
+
KG = KG.toarray()
|
|
335
|
+
msg('eigh() solver...', level=3, silent=silent)
|
|
336
|
+
eigvals, peigvecs = eigh(a=KG, b=K)
|
|
337
|
+
msg('finished!', level=3, silent=silent)
|
|
338
|
+
|
|
339
|
+
num_eigvecs = min(num_eigvalues, peigvecs.shape[1])
|
|
340
|
+
if used_cols is not None:
|
|
341
|
+
eigvecs = np.zeros((size, num_eigvecs), dtype=peigvecs.dtype)
|
|
342
|
+
eigvecs[used_cols, :] = peigvecs[:, :num_eigvecs]
|
|
343
|
+
else:
|
|
344
|
+
eigvecs = peigvecs[:, :num_eigvecs]
|
|
345
|
+
|
|
346
|
+
with np.errstate(divide='ignore'):
|
|
347
|
+
eigvals = -1./eigvals
|
|
348
|
+
|
|
349
|
+
msg('finished!', level=2, silent=silent)
|
|
350
|
+
|
|
351
|
+
msg('first {0} eigenvalues:'.format(num_eigvalues_print), level=1,
|
|
352
|
+
silent=silent)
|
|
353
|
+
|
|
354
|
+
for eig in eigvals[:num_eigvalues_print]:
|
|
355
|
+
msg('{0}'.format(eig), level=2, silent=silent)
|
|
356
|
+
|
|
357
|
+
return eigvals, eigvecs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -41,8 +41,7 @@ Github Actions status:
|
|
|
41
41
|
|
|
42
42
|
Coverage status:
|
|
43
43
|
[](https://github.com/saullocastro/structsolve/actions/workflows/coverage.yml)
|
|
44
|
-
[](https://codecov.io/gh/saullocastro/structsolve)
|
|
46
45
|
|
|
47
46
|
Structural analysis solvers tailored for semi-analytical models
|
|
48
47
|
===============================================================
|
|
@@ -61,7 +60,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
61
60
|
Citing this library
|
|
62
61
|
===================
|
|
63
62
|
|
|
64
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.
|
|
63
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.4.2). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
Documentation
|
|
@@ -76,7 +75,18 @@ History
|
|
|
76
75
|
|
|
77
76
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
78
77
|
|
|
79
|
-
* version 0.4.
|
|
78
|
+
* version 0.4.2 (2026-09-17)
|
|
79
|
+
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
80
|
+
return buckling loads higher than the critical ones
|
|
81
|
+
- Fixed wrong buckling loads of the sparse solver of `lb` when SciPy is
|
|
82
|
+
linked against Intel MKL 2024.2.0 to 2025.0.0 (e.g. Anaconda on
|
|
83
|
+
Windows), whose `dsteqr` routine breaks ARPACK
|
|
84
|
+
- `lb` condenses out the dofs where `KG` is null and solves small
|
|
85
|
+
condensed problems with a dense solver
|
|
86
|
+
- `lb` verifies the eigenpairs of the sparse solver, raising an error
|
|
87
|
+
instead of returning wrong results
|
|
88
|
+
- The sparse solver of `lb` returns the load multipliers sorted as the
|
|
89
|
+
dense solver
|
|
80
90
|
- Newton-Raphson with full Newton iterations and a relative convergence
|
|
81
91
|
criterion by default, reaching quadratic convergence with exact tangent
|
|
82
92
|
stiffness matrices
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Tests for structsolve.linear_buckling: lb()"""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
from scipy.sparse import csc_matrix
|
|
7
|
+
|
|
8
|
+
from structsolve import lb
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_lb_simple_eigenvalue():
|
|
12
|
+
"""Test linear buckling with a well-conditioned system
|
|
13
|
+
|
|
14
|
+
lb solves: [K] + lambda[KG] = 0
|
|
15
|
+
For K = diag(2,4,6,...) and KG = I, lambda_i = -K_ii
|
|
16
|
+
lb returns -1/eigvals from eigsh(A=KG, M=K), giving positive load factors.
|
|
17
|
+
"""
|
|
18
|
+
n = 10
|
|
19
|
+
k_diag = np.arange(2, 2 * n + 2, 2, dtype=float)
|
|
20
|
+
K = csc_matrix(np.diag(k_diag))
|
|
21
|
+
KG = csc_matrix(np.eye(n))
|
|
22
|
+
|
|
23
|
+
eigvals, eigvecs = lb(K, KG, silent=True, num_eigvalues=5,
|
|
24
|
+
sparse_solver=True)
|
|
25
|
+
# lb returns eigenvalues (load multipliers), verify we get results
|
|
26
|
+
assert len(eigvals) >= 5
|
|
27
|
+
assert eigvecs.shape[0] == n
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_lb_dense_solver():
|
|
31
|
+
"""Test lb with sparse_solver=False
|
|
32
|
+
|
|
33
|
+
lb solves [K] + lambda[KG] = 0, returning -1/eigvals.
|
|
34
|
+
For K=2I, KG=I: eigsh gives eigval=0.5, so lb returns -1/0.5 = -2.
|
|
35
|
+
The dense solver (eigh) with b=K gives eigvals with the same sign convention.
|
|
36
|
+
"""
|
|
37
|
+
n = 10
|
|
38
|
+
K = csc_matrix(np.eye(n) * 2.0)
|
|
39
|
+
KG = csc_matrix(np.eye(n))
|
|
40
|
+
|
|
41
|
+
eigvals, eigvecs = lb(K, KG, silent=True, num_eigvalues=5,
|
|
42
|
+
sparse_solver=False)
|
|
43
|
+
np.testing.assert_allclose(eigvals[:5], -2.0, rtol=1e-6)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize('max_dense_size', [0, 2000])
|
|
47
|
+
def test_lb_sparse_shift_mixed_spectrum(max_dense_size):
|
|
48
|
+
"""Sparse and dense lb agree when the load multipliers have mixed signs
|
|
49
|
+
|
|
50
|
+
Regression test: the shift of the sparse solver was a single Rayleigh
|
|
51
|
+
quotient, in which the positive and negative eigenvalues of
|
|
52
|
+
``KG u = mu K u`` cancel. The shift landed below the critical ``|mu|``
|
|
53
|
+
and eigsh returned the load multipliers near ``1/sigma`` instead of the
|
|
54
|
+
lowest ones. With ``max_dense_size=0`` eigsh is always used.
|
|
55
|
+
"""
|
|
56
|
+
from scipy.sparse import diags
|
|
57
|
+
from structsolve.linear_buckling import _estimate_sigma
|
|
58
|
+
|
|
59
|
+
n = 80
|
|
60
|
+
k = np.linspace(1e6, 5e6, n)
|
|
61
|
+
lambdas = np.concatenate([[1.0, 1.0, 1.1, 1.2],
|
|
62
|
+
np.linspace(1.5, 4., 36),
|
|
63
|
+
-np.linspace(1.5, 4., 40)])
|
|
64
|
+
mu = -1. / lambdas
|
|
65
|
+
K = diags(k).tocsc()
|
|
66
|
+
KG = diags(mu * k).tocsc()
|
|
67
|
+
|
|
68
|
+
# the shift must bound the largest |mu| of the critical eigenvalues
|
|
69
|
+
assert _estimate_sigma(K, KG) >= np.abs(mu).max()
|
|
70
|
+
|
|
71
|
+
eig_sparse, _ = lb(K, KG, silent=True, num_eigvalues=10,
|
|
72
|
+
max_dense_size=max_dense_size)
|
|
73
|
+
eig_dense, _ = lb(K, KG, silent=True, sparse_solver=False)
|
|
74
|
+
np.testing.assert_allclose(eig_sparse[:4], [1.0, 1.0, 1.1, 1.2],
|
|
75
|
+
rtol=1e-8)
|
|
76
|
+
np.testing.assert_allclose(eig_sparse[:4], eig_dense[:4], rtol=1e-8)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
DATA = os.path.join(os.path.dirname(__file__), 'data')
|
|
80
|
+
|
|
81
|
+
# K and KG of panels models, null rows and columns removed, with the
|
|
82
|
+
# critical load multipliers of the dense solver
|
|
83
|
+
SAVED_MATRICES = [
|
|
84
|
+
('plate_ssss_Nxx', [157.5386, 226.3470, 232.5623]),
|
|
85
|
+
('cylinder_5panels', [473.1924, 473.1924, 478.6168]),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_saved_matrices(name):
|
|
90
|
+
from scipy.sparse import load_npz
|
|
91
|
+
K = load_npz(os.path.join(DATA, '%s_K.npz' % name)).tocsr()
|
|
92
|
+
KG = load_npz(os.path.join(DATA, '%s_KG.npz' % name)).tocsr()
|
|
93
|
+
return K, KG
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def relative_residuals(K, KG, eigvals, eigvecs):
|
|
97
|
+
Ku = K @ eigvecs
|
|
98
|
+
KGu = KG @ eigvecs
|
|
99
|
+
return (np.linalg.norm(Ku + KGu*eigvals, axis=0)
|
|
100
|
+
/ (np.linalg.norm(Ku, axis=0)
|
|
101
|
+
+ np.abs(eigvals)*np.linalg.norm(KGu, axis=0)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize('name, ref', SAVED_MATRICES)
|
|
105
|
+
def test_lb_saved_matrices_sparse_solver(name, ref):
|
|
106
|
+
"""Regression test: ARPACK linked against Intel MKL 2024.2.0 to 2025.0.0
|
|
107
|
+
returned wrong load multipliers at random, e.g. with the Anaconda builds
|
|
108
|
+
of SciPy. The default sparse solver condenses out the dofs where KG is
|
|
109
|
+
null and verifies the eigenpairs"""
|
|
110
|
+
K, KG = load_saved_matrices(name)
|
|
111
|
+
eig_dense, _ = lb(K, KG, silent=True, sparse_solver=False)
|
|
112
|
+
np.testing.assert_allclose(eig_dense[:3], ref, rtol=1e-5)
|
|
113
|
+
for trial in range(3):
|
|
114
|
+
eigvals, eigvecs = lb(K, KG, silent=True)
|
|
115
|
+
assert eigvals.shape == (25,)
|
|
116
|
+
assert eigvecs.shape == (K.shape[0], 25)
|
|
117
|
+
np.testing.assert_allclose(eigvals, eig_dense[:25], rtol=1e-6)
|
|
118
|
+
assert np.all(relative_residuals(K, KG, eigvals, eigvecs) < 1e-6)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize('name, ref', SAVED_MATRICES)
|
|
122
|
+
def test_lb_saved_matrices_eigsh_never_wrong(name, ref):
|
|
123
|
+
"""With max_dense_size=0 only eigsh is used, which either returns the
|
|
124
|
+
correct load multipliers or raises a RuntimeError, when the eigenpairs
|
|
125
|
+
returned by ARPACK are wrong"""
|
|
126
|
+
K, KG = load_saved_matrices(name)
|
|
127
|
+
for trial in range(3):
|
|
128
|
+
try:
|
|
129
|
+
eigvals, eigvecs = lb(K, KG, silent=True, max_dense_size=0)
|
|
130
|
+
except RuntimeError as e:
|
|
131
|
+
assert 'verification' in str(e)
|
|
132
|
+
continue
|
|
133
|
+
np.testing.assert_allclose(eigvals[:3], ref, rtol=1e-5)
|
|
134
|
+
assert np.all(eigvals[:-1] <= eigvals[1:])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_lb_check_eigenpairs():
|
|
138
|
+
"""The verification detects wrong eigenpairs and a missing lowest load
|
|
139
|
+
multiplier"""
|
|
140
|
+
from structsolve.linear_buckling import _check_eigenpairs, _eigh_condensed
|
|
141
|
+
|
|
142
|
+
K, KG = load_saved_matrices('plate_ssss_Nxx')
|
|
143
|
+
mu, eigvecs = _eigh_condensed(K, KG, 10)
|
|
144
|
+
eigvals = -1./mu
|
|
145
|
+
assert _check_eigenpairs(K, KG, eigvals, eigvecs, 1e-3) is None
|
|
146
|
+
|
|
147
|
+
# eigenvectors of other eigenvalues
|
|
148
|
+
error = _check_eigenpairs(K, KG, eigvals, eigvecs[:, ::-1], 1e-3)
|
|
149
|
+
assert 'relative residual' in error
|
|
150
|
+
|
|
151
|
+
# wrong eigenvalues
|
|
152
|
+
error = _check_eigenpairs(K, KG, eigvals*1.01, eigvecs, 1e-3)
|
|
153
|
+
assert 'relative residual' in error
|
|
154
|
+
|
|
155
|
+
# lowest load multiplier missing
|
|
156
|
+
error = _check_eigenpairs(K, KG, eigvals[1:], eigvecs[:, 1:], 1e-3)
|
|
157
|
+
assert 'missing' in error
|
|
158
|
+
|
|
159
|
+
# both equal critical load multipliers missing
|
|
160
|
+
K, KG = load_saved_matrices('cylinder_5panels')
|
|
161
|
+
mu, eigvecs = _eigh_condensed(K, KG, 10)
|
|
162
|
+
eigvals = -1./mu
|
|
163
|
+
assert _check_eigenpairs(K, KG, eigvals, eigvecs, 1e-3) is None
|
|
164
|
+
error = _check_eigenpairs(K, KG, eigvals[2:], eigvecs[:, 2:], 1e-3)
|
|
165
|
+
assert 'missing' in error
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_lb_sparse_raises_when_verification_fails(monkeypatch):
|
|
169
|
+
"""A RuntimeError is raised instead of returning wrong eigenpairs"""
|
|
170
|
+
import structsolve.linear_buckling as linear_buckling
|
|
171
|
+
|
|
172
|
+
K, KG = load_saved_matrices('plate_ssss_Nxx')
|
|
173
|
+
|
|
174
|
+
def wrong_eigsh(K, KG, k, tol, silent):
|
|
175
|
+
rng = np.random.RandomState(0)
|
|
176
|
+
return -rng.rand(k), rng.randn(K.shape[0], k)
|
|
177
|
+
|
|
178
|
+
monkeypatch.setattr(linear_buckling, '_eigsh_cayley', wrong_eigsh)
|
|
179
|
+
with pytest.raises(RuntimeError, match='verification'):
|
|
180
|
+
lb(K, KG, silent=True, max_dense_size=0)
|
|
181
|
+
# the condensed dense solution is used first
|
|
182
|
+
eigvals, _ = lb(K, KG, silent=True)
|
|
183
|
+
np.testing.assert_allclose(eigvals[0], 157.5386, rtol=1e-5)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.parametrize('max_dense_size', [0, 2000])
|
|
187
|
+
def test_lb_sparse_sorted_as_dense(max_dense_size):
|
|
188
|
+
"""The sparse solver returns the positive load multipliers first, in
|
|
189
|
+
increasing order, followed by the negative ones, as the dense solver"""
|
|
190
|
+
from scipy.sparse import diags
|
|
191
|
+
|
|
192
|
+
n = 40
|
|
193
|
+
k = np.linspace(1., 3., n)
|
|
194
|
+
rng = np.random.RandomState(1)
|
|
195
|
+
lambdas = rng.permutation(np.concatenate([np.linspace(1., 20., 5),
|
|
196
|
+
-np.linspace(1., 20., n - 5)]))
|
|
197
|
+
K = diags(k).tocsr()
|
|
198
|
+
KG = diags(-k/lambdas).tocsr()
|
|
199
|
+
eig_sparse, eigvecs = lb(K, KG, silent=True, num_eigvalues=10,
|
|
200
|
+
max_dense_size=max_dense_size)
|
|
201
|
+
eig_dense, _ = lb(K, KG, silent=True, sparse_solver=False)
|
|
202
|
+
np.testing.assert_allclose(eig_sparse, eig_dense[:10], rtol=1e-8)
|
|
203
|
+
np.testing.assert_allclose(eig_sparse[:5], np.linspace(1., 20., 5),
|
|
204
|
+
rtol=1e-8)
|
|
205
|
+
assert np.all(eig_sparse[5:] < 0)
|
|
206
|
+
assert np.all(relative_residuals(K, KG, eig_sparse, eigvecs) < 1e-8)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_lb_sparse_null_KG_rows_and_null_cols():
|
|
210
|
+
"""Condensed solution with null rows of KG and null rows of K, returning
|
|
211
|
+
eigenvectors with the size of the original matrices"""
|
|
212
|
+
from scipy.sparse import random as sprandom, identity, bmat
|
|
213
|
+
|
|
214
|
+
n = 60
|
|
215
|
+
A = sprandom(n, n, density=0.1, random_state=2)
|
|
216
|
+
K = (A @ A.T + identity(n)).tocsr()
|
|
217
|
+
B = sprandom(n//3, n//3, density=0.3, random_state=3)
|
|
218
|
+
KG = bmat([[-(B @ B.T + 0.1*identity(n//3)), None],
|
|
219
|
+
[None, csc_matrix((n - n//3, n - n//3))]]).tocsr()
|
|
220
|
+
# adding two null dofs
|
|
221
|
+
Z = csc_matrix((2, 2))
|
|
222
|
+
K2 = bmat([[K, None], [None, Z]]).tocsr()
|
|
223
|
+
KG2 = bmat([[KG, None], [None, Z]]).tocsr()
|
|
224
|
+
eig_sparse, eigvecs = lb(K2, KG2, silent=True, num_eigvalues=10)
|
|
225
|
+
eig_dense, _ = lb(K2, KG2, silent=True, sparse_solver=False)
|
|
226
|
+
np.testing.assert_allclose(eig_sparse, eig_dense[:10], rtol=1e-8)
|
|
227
|
+
assert eigvecs.shape == (n + 2, 10)
|
|
228
|
+
assert np.all(eigvecs[n:, :] == 0)
|
|
229
|
+
assert np.all(relative_residuals(K2, KG2, eig_sparse, eigvecs) < 1e-8)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_lb_sparse_singular_K():
|
|
233
|
+
"""The shift falls back to 1 when K is singular"""
|
|
234
|
+
from structsolve.linear_buckling import _estimate_sigma
|
|
235
|
+
|
|
236
|
+
K = csc_matrix(np.diag([1., 2., 0., 4., 5.]))
|
|
237
|
+
KG = csc_matrix(np.eye(5))
|
|
238
|
+
assert _estimate_sigma(K, KG) == 1.
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_lb_plate_buckling_fsdt():
|
|
242
|
+
"""Test plate buckling (FSDT) based on semi-analytical Ritz method.
|
|
243
|
+
|
|
244
|
+
Reference: notebook BucklingPlates-FSDT.ipynb from the buckling repository.
|
|
245
|
+
Uses Legendre polynomials as basis functions with Gauss-Legendre quadrature.
|
|
246
|
+
"""
|
|
247
|
+
from scipy.special import roots_legendre
|
|
248
|
+
from composites import isotropic_plate
|
|
249
|
+
from buckling.legendre import vecf, vecfxi
|
|
250
|
+
|
|
251
|
+
m1 = 20
|
|
252
|
+
m2 = 10
|
|
253
|
+
N = 3 * m1 * m2
|
|
254
|
+
|
|
255
|
+
pts1, weights1 = roots_legendre(2 * m1 - 1)
|
|
256
|
+
pts2, weights2 = roots_legendre(2 * m2 - 1)
|
|
257
|
+
|
|
258
|
+
E = 200.e9
|
|
259
|
+
nu = 0.3
|
|
260
|
+
|
|
261
|
+
# BCs: w simply supported, phi free
|
|
262
|
+
wxit1, wxir1, wxit2, wxir2 = 0, 1, 0, 1
|
|
263
|
+
wetat1, wetar1, wetat2, wetar2 = 0, 1, 0, 1
|
|
264
|
+
xit1, xir1, xit2, xir2 = 1, 1, 1, 1
|
|
265
|
+
etat1, etar1, etat2, etar2 = 1, 1, 1, 1
|
|
266
|
+
|
|
267
|
+
a, b, h = 0.3, 0.1, 0.003
|
|
268
|
+
prop = isotropic_plate(thickness=h, E=E, nu=nu)
|
|
269
|
+
|
|
270
|
+
Nxxhat = -100.
|
|
271
|
+
|
|
272
|
+
Swx = np.zeros(N)
|
|
273
|
+
Swy = np.zeros(N)
|
|
274
|
+
Sphix = np.zeros(N)
|
|
275
|
+
Sphiy = np.zeros(N)
|
|
276
|
+
Sphixx = np.zeros(N)
|
|
277
|
+
Sphixy = np.zeros(N)
|
|
278
|
+
Sphiyx = np.zeros(N)
|
|
279
|
+
Sphiyy = np.zeros(N)
|
|
280
|
+
|
|
281
|
+
buff = np.zeros((N, N))
|
|
282
|
+
K = np.zeros((N, N))
|
|
283
|
+
KG = np.zeros((N, N))
|
|
284
|
+
|
|
285
|
+
def addouter(matrix, vec1, vec2):
|
|
286
|
+
np.outer(vec1, vec2, out=buff)
|
|
287
|
+
matrix += buff
|
|
288
|
+
|
|
289
|
+
for xi, wxi in zip(pts1, weights1):
|
|
290
|
+
wP_xi = vecf(m1, xi, wxit1, wxir1, wxit2, wxir2)
|
|
291
|
+
wPx_xi = vecfxi(m1, xi, wxit1, wxir1, wxit2, wxir2)
|
|
292
|
+
P_xi = vecf(m1, xi, xit1, xir1, xit2, xir2)
|
|
293
|
+
Px_xi = vecfxi(m1, xi, xit1, xir1, xit2, xir2)
|
|
294
|
+
|
|
295
|
+
for eta, weta in zip(pts2, weights2):
|
|
296
|
+
wP_eta = vecf(m2, eta, wetat1, wetar1, wetat2, wetar2)
|
|
297
|
+
wPx_eta = vecfxi(m2, eta, wetat1, wetar1, wetat2, wetar2)
|
|
298
|
+
P_eta = vecf(m2, eta, etat1, etar1, etat2, etar2)
|
|
299
|
+
Px_eta = vecfxi(m2, eta, etat1, etar1, etat2, etar2)
|
|
300
|
+
|
|
301
|
+
weight = wxi * weta
|
|
302
|
+
|
|
303
|
+
Pi, Pj = np.meshgrid(P_xi, P_eta, indexing='ij')
|
|
304
|
+
Sphix[m1*m2:2*m1*m2] = (Pi * Pj).flatten()
|
|
305
|
+
Sphiy[2*m1*m2:] = (Pi * Pj).flatten()
|
|
306
|
+
|
|
307
|
+
Pxi, Pj = np.meshgrid(wPx_xi, wP_eta, indexing='ij')
|
|
308
|
+
Swx[:m1*m2] = (Pxi * Pj * (2 / a)).flatten()
|
|
309
|
+
|
|
310
|
+
Pxi, Pj = np.meshgrid(Px_xi, P_eta, indexing='ij')
|
|
311
|
+
Sphixx[m1*m2:2*m1*m2] = (Pxi * Pj * (2 / a)).flatten()
|
|
312
|
+
Sphiyx[2*m1*m2:] = (Pxi * Pj * (2 / a)).flatten()
|
|
313
|
+
|
|
314
|
+
Pi, Pxj = np.meshgrid(wP_xi, wPx_eta, indexing='ij')
|
|
315
|
+
Swy[:m1*m2] = (Pi * Pxj * (2 / b)).flatten()
|
|
316
|
+
|
|
317
|
+
Pi, Pxj = np.meshgrid(P_xi, Px_eta, indexing='ij')
|
|
318
|
+
Sphixy[m1*m2:2*m1*m2] = (Pi * Pxj * (2 / b)).flatten()
|
|
319
|
+
Sphiyy[2*m1*m2:] = (Pi * Pxj * (2 / b)).flatten()
|
|
320
|
+
|
|
321
|
+
e1xx = Sphixx
|
|
322
|
+
e1yy = Sphiyy
|
|
323
|
+
e1xy = Sphixy + Sphiyx
|
|
324
|
+
|
|
325
|
+
g0yz = Sphiy + Swy
|
|
326
|
+
g0xz = Sphix + Swx
|
|
327
|
+
|
|
328
|
+
Mxx = prop.D11 * e1xx + prop.D12 * e1yy + prop.D16 * e1xy
|
|
329
|
+
Myy = prop.D12 * e1xx + prop.D22 * e1yy + prop.D26 * e1xy
|
|
330
|
+
Mxy = prop.D16 * e1xx + prop.D26 * e1yy + prop.D66 * e1xy
|
|
331
|
+
|
|
332
|
+
Qy = prop.A44 * g0yz + prop.A45 * g0xz
|
|
333
|
+
Qx = prop.A45 * g0yz + prop.A55 * g0xz
|
|
334
|
+
|
|
335
|
+
detJ = a * b / 4
|
|
336
|
+
|
|
337
|
+
addouter(K, detJ * weight * Mxx, e1xx)
|
|
338
|
+
addouter(K, detJ * weight * Myy, e1yy)
|
|
339
|
+
addouter(K, detJ * weight * Mxy, e1xy)
|
|
340
|
+
addouter(K, detJ * weight * Qy, g0yz)
|
|
341
|
+
addouter(K, detJ * weight * Qx, g0xz)
|
|
342
|
+
|
|
343
|
+
addouter(KG, detJ * weight * Nxxhat * Swx, Swx)
|
|
344
|
+
|
|
345
|
+
eigvals, eigvecs = lb(csc_matrix(K), csc_matrix(KG), silent=True)
|
|
346
|
+
|
|
347
|
+
D = E * h**3 / (12 * (1 - nu**2))
|
|
348
|
+
Ncr_analytical = 4 * np.pi**2 * D / b**2
|
|
349
|
+
Ncr_computed = eigvals[0] * abs(Nxxhat)
|
|
350
|
+
|
|
351
|
+
# FSDT gives slightly lower Ncr than CLPT due to shear deformation
|
|
352
|
+
np.testing.assert_allclose(Ncr_computed, Ncr_analytical, rtol=0.02)
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import warnings
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from scipy.sparse.linalg import eigsh, spsolve
|
|
5
|
-
from scipy.linalg import eigh
|
|
6
|
-
|
|
7
|
-
from .logger import msg, warn
|
|
8
|
-
from .sparseutils import remove_null_cols
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _estimate_sigma(K, KG):
|
|
12
|
-
try:
|
|
13
|
-
rhs = KG @ np.random.RandomState(42).randn(K.shape[0])
|
|
14
|
-
with warnings.catch_warnings(record=True) as caught:
|
|
15
|
-
warnings.simplefilter("always")
|
|
16
|
-
y = spsolve(K, rhs)
|
|
17
|
-
if any(issubclass(w.category, (RuntimeWarning, Warning))
|
|
18
|
-
and "singular" in str(w.message).lower() for w in caught):
|
|
19
|
-
return 1.
|
|
20
|
-
residual = np.linalg.norm(K @ y - rhs)
|
|
21
|
-
if residual > 1e-6 * np.linalg.norm(rhs):
|
|
22
|
-
return 1.
|
|
23
|
-
sigma = abs((y @ KG @ y) / (y @ K @ y))
|
|
24
|
-
if not np.isfinite(sigma) or sigma == 0:
|
|
25
|
-
return 1.
|
|
26
|
-
return sigma
|
|
27
|
-
except Exception:
|
|
28
|
-
return 1.
|
|
29
|
-
|
|
30
|
-
def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
31
|
-
num_eigvalues=25, num_eigvalues_print=5,
|
|
32
|
-
skip_null_cols=False):
|
|
33
|
-
r"""Linear buckling analysis
|
|
34
|
-
|
|
35
|
-
Calculates the eigenvalues `\lambda` and eigenvectors `\{u\}` of the
|
|
36
|
-
eigenvalue problem:
|
|
37
|
-
|
|
38
|
-
.. math::
|
|
39
|
-
|
|
40
|
-
([K] + \lambda [K_G])\{u\} = \{0\}
|
|
41
|
-
|
|
42
|
-
where `\lambda` is the load multiplier of the load state that generated
|
|
43
|
-
`[K_G]`. It can also be used for more general eigenvalue analyses, e.g.
|
|
44
|
-
if ``K`` is the tangent stiffness matrix of a given load state. The null
|
|
45
|
-
rows and columns of ``K`` are removed from both matrices before solving
|
|
46
|
-
the eigenvalue problem, unless ``skip_null_cols=True``.
|
|
47
|
-
|
|
48
|
-
Parameters
|
|
49
|
-
----------
|
|
50
|
-
K : sparse_matrix
|
|
51
|
-
Stiffness matrix. Should include all constant terms of the initial
|
|
52
|
-
stress stiffness matrix, aerodynamic matrix and so forth when
|
|
53
|
-
applicable.
|
|
54
|
-
KG : sparse_matrix
|
|
55
|
-
Initial stress stiffness matrix that multiplies the load multiplier
|
|
56
|
-
`\lambda` of the eigenvalue problem.
|
|
57
|
-
tol : float, optional
|
|
58
|
-
A float tolerance passed to the eigenvalue solver.
|
|
59
|
-
sparse_solver : bool, optional
|
|
60
|
-
Tells if solver :func:`scipy.sparse.linalg.eigsh` (``True``) or
|
|
61
|
-
:func:`scipy.linalg.eigh` (``False``) should be used. The sparse
|
|
62
|
-
solver uses the Cayley mode with a shift estimated from the matrices
|
|
63
|
-
and calculates ``num_eigvalues`` eigenvalues. The dense solver
|
|
64
|
-
calculates all eigenvalues and requires a positive definite ``K``.
|
|
65
|
-
silent : bool, optional
|
|
66
|
-
A boolean to tell whether the log messages should be printed.
|
|
67
|
-
num_eigvalues : int, optional
|
|
68
|
-
Number of calculated eigenvalues with the sparse solver, limited to
|
|
69
|
-
the size of ``KG`` minus 2, and number of returned eigenvectors.
|
|
70
|
-
num_eigvalues_print : int, optional
|
|
71
|
-
Number of eigenvalues to print.
|
|
72
|
-
skip_null_cols : bool, optional
|
|
73
|
-
If True, skip the removal of null columns from the matrices.
|
|
74
|
-
Use only when K is known to be non-singular.
|
|
75
|
-
|
|
76
|
-
Returns
|
|
77
|
-
-------
|
|
78
|
-
eigvals : ndarray
|
|
79
|
-
The load multipliers `\lambda`, calculated as ``-1/eigval`` from the
|
|
80
|
-
eigenvalues ``eigval`` of ``KG u = eigval K u``. The dense solver
|
|
81
|
-
returns all load multipliers, the positive ones first in increasing
|
|
82
|
-
order.
|
|
83
|
-
eigvecs : ndarray
|
|
84
|
-
The `i^{th}` eigenvector is ``eigvecs[:, i]``, with the size of the
|
|
85
|
-
original matrices. Only ``num_eigvalues`` eigenvectors are returned.
|
|
86
|
-
|
|
87
|
-
"""
|
|
88
|
-
msg('Running linear buckling analysis...', silent=silent)
|
|
89
|
-
|
|
90
|
-
msg('Eigenvalue solver... ', level=2, silent=silent)
|
|
91
|
-
|
|
92
|
-
k = min(num_eigvalues, KG.shape[0]-2)
|
|
93
|
-
size = KG.shape[0]
|
|
94
|
-
if skip_null_cols:
|
|
95
|
-
used_cols = None
|
|
96
|
-
else:
|
|
97
|
-
K, KG, used_cols = remove_null_cols(K, KG, silent=silent)
|
|
98
|
-
if sparse_solver:
|
|
99
|
-
mode = 'cayley'
|
|
100
|
-
sigma = _estimate_sigma(K, KG)
|
|
101
|
-
msg('eigsh() solver (sigma={0})...'.format(sigma), level=3, silent=silent)
|
|
102
|
-
eigvals, peigvecs = eigsh(A=KG, k=k,
|
|
103
|
-
which='SM', M=K, tol=tol, sigma=sigma, mode=mode)
|
|
104
|
-
msg('finished!', level=3, silent=silent)
|
|
105
|
-
|
|
106
|
-
else:
|
|
107
|
-
K = K.toarray()
|
|
108
|
-
KG = KG.toarray()
|
|
109
|
-
msg('eigh() solver...', level=3, silent=silent)
|
|
110
|
-
eigvals, peigvecs = eigh(a=KG, b=K)
|
|
111
|
-
msg('finished!', level=3, silent=silent)
|
|
112
|
-
|
|
113
|
-
if used_cols is not None:
|
|
114
|
-
eigvecs = np.zeros((size, num_eigvalues), dtype=peigvecs.dtype)
|
|
115
|
-
eigvecs[used_cols, :] = peigvecs[:, :num_eigvalues]
|
|
116
|
-
else:
|
|
117
|
-
eigvecs = peigvecs[:, :num_eigvalues]
|
|
118
|
-
|
|
119
|
-
eigvals = -1./eigvals
|
|
120
|
-
|
|
121
|
-
eigvals = eigvals
|
|
122
|
-
eigvecs = eigvecs
|
|
123
|
-
|
|
124
|
-
msg('finished!', level=2, silent=silent)
|
|
125
|
-
|
|
126
|
-
msg('first {0} eigenvalues:'.format(num_eigvalues_print), level=1,
|
|
127
|
-
silent=silent)
|
|
128
|
-
|
|
129
|
-
for eig in eigvals[:num_eigvalues_print]:
|
|
130
|
-
msg('{0}'.format(eig), level=2, silent=silent)
|
|
131
|
-
|
|
132
|
-
return eigvals, eigvecs
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
"""Tests for structsolve.linear_buckling: lb()"""
|
|
2
|
-
import numpy as np
|
|
3
|
-
import pytest
|
|
4
|
-
from scipy.sparse import csc_matrix
|
|
5
|
-
|
|
6
|
-
from structsolve import lb
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_lb_simple_eigenvalue():
|
|
10
|
-
"""Test linear buckling with a well-conditioned system
|
|
11
|
-
|
|
12
|
-
lb solves: [K] + lambda[KG] = 0
|
|
13
|
-
For K = diag(2,4,6,...) and KG = I, lambda_i = -K_ii
|
|
14
|
-
lb returns -1/eigvals from eigsh(A=KG, M=K), giving positive load factors.
|
|
15
|
-
"""
|
|
16
|
-
n = 10
|
|
17
|
-
k_diag = np.arange(2, 2 * n + 2, 2, dtype=float)
|
|
18
|
-
K = csc_matrix(np.diag(k_diag))
|
|
19
|
-
KG = csc_matrix(np.eye(n))
|
|
20
|
-
|
|
21
|
-
eigvals, eigvecs = lb(K, KG, silent=True, num_eigvalues=5,
|
|
22
|
-
sparse_solver=True)
|
|
23
|
-
# lb returns eigenvalues (load multipliers), verify we get results
|
|
24
|
-
assert len(eigvals) >= 5
|
|
25
|
-
assert eigvecs.shape[0] == n
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_lb_dense_solver():
|
|
29
|
-
"""Test lb with sparse_solver=False
|
|
30
|
-
|
|
31
|
-
lb solves [K] + lambda[KG] = 0, returning -1/eigvals.
|
|
32
|
-
For K=2I, KG=I: eigsh gives eigval=0.5, so lb returns -1/0.5 = -2.
|
|
33
|
-
The dense solver (eigh) with b=K gives eigvals with the same sign convention.
|
|
34
|
-
"""
|
|
35
|
-
n = 10
|
|
36
|
-
K = csc_matrix(np.eye(n) * 2.0)
|
|
37
|
-
KG = csc_matrix(np.eye(n))
|
|
38
|
-
|
|
39
|
-
eigvals, eigvecs = lb(K, KG, silent=True, num_eigvalues=5,
|
|
40
|
-
sparse_solver=False)
|
|
41
|
-
np.testing.assert_allclose(eigvals[:5], -2.0, rtol=1e-6)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_lb_plate_buckling_fsdt():
|
|
45
|
-
"""Test plate buckling (FSDT) based on semi-analytical Ritz method.
|
|
46
|
-
|
|
47
|
-
Reference: notebook BucklingPlates-FSDT.ipynb from the buckling repository.
|
|
48
|
-
Uses Legendre polynomials as basis functions with Gauss-Legendre quadrature.
|
|
49
|
-
"""
|
|
50
|
-
from scipy.special import roots_legendre
|
|
51
|
-
from composites import isotropic_plate
|
|
52
|
-
from buckling.legendre import vecf, vecfxi
|
|
53
|
-
|
|
54
|
-
m1 = 20
|
|
55
|
-
m2 = 10
|
|
56
|
-
N = 3 * m1 * m2
|
|
57
|
-
|
|
58
|
-
pts1, weights1 = roots_legendre(2 * m1 - 1)
|
|
59
|
-
pts2, weights2 = roots_legendre(2 * m2 - 1)
|
|
60
|
-
|
|
61
|
-
E = 200.e9
|
|
62
|
-
nu = 0.3
|
|
63
|
-
|
|
64
|
-
# BCs: w simply supported, phi free
|
|
65
|
-
wxit1, wxir1, wxit2, wxir2 = 0, 1, 0, 1
|
|
66
|
-
wetat1, wetar1, wetat2, wetar2 = 0, 1, 0, 1
|
|
67
|
-
xit1, xir1, xit2, xir2 = 1, 1, 1, 1
|
|
68
|
-
etat1, etar1, etat2, etar2 = 1, 1, 1, 1
|
|
69
|
-
|
|
70
|
-
a, b, h = 0.3, 0.1, 0.003
|
|
71
|
-
prop = isotropic_plate(thickness=h, E=E, nu=nu)
|
|
72
|
-
|
|
73
|
-
Nxxhat = -100.
|
|
74
|
-
|
|
75
|
-
Swx = np.zeros(N)
|
|
76
|
-
Swy = np.zeros(N)
|
|
77
|
-
Sphix = np.zeros(N)
|
|
78
|
-
Sphiy = np.zeros(N)
|
|
79
|
-
Sphixx = np.zeros(N)
|
|
80
|
-
Sphixy = np.zeros(N)
|
|
81
|
-
Sphiyx = np.zeros(N)
|
|
82
|
-
Sphiyy = np.zeros(N)
|
|
83
|
-
|
|
84
|
-
buff = np.zeros((N, N))
|
|
85
|
-
K = np.zeros((N, N))
|
|
86
|
-
KG = np.zeros((N, N))
|
|
87
|
-
|
|
88
|
-
def addouter(matrix, vec1, vec2):
|
|
89
|
-
np.outer(vec1, vec2, out=buff)
|
|
90
|
-
matrix += buff
|
|
91
|
-
|
|
92
|
-
for xi, wxi in zip(pts1, weights1):
|
|
93
|
-
wP_xi = vecf(m1, xi, wxit1, wxir1, wxit2, wxir2)
|
|
94
|
-
wPx_xi = vecfxi(m1, xi, wxit1, wxir1, wxit2, wxir2)
|
|
95
|
-
P_xi = vecf(m1, xi, xit1, xir1, xit2, xir2)
|
|
96
|
-
Px_xi = vecfxi(m1, xi, xit1, xir1, xit2, xir2)
|
|
97
|
-
|
|
98
|
-
for eta, weta in zip(pts2, weights2):
|
|
99
|
-
wP_eta = vecf(m2, eta, wetat1, wetar1, wetat2, wetar2)
|
|
100
|
-
wPx_eta = vecfxi(m2, eta, wetat1, wetar1, wetat2, wetar2)
|
|
101
|
-
P_eta = vecf(m2, eta, etat1, etar1, etat2, etar2)
|
|
102
|
-
Px_eta = vecfxi(m2, eta, etat1, etar1, etat2, etar2)
|
|
103
|
-
|
|
104
|
-
weight = wxi * weta
|
|
105
|
-
|
|
106
|
-
Pi, Pj = np.meshgrid(P_xi, P_eta, indexing='ij')
|
|
107
|
-
Sphix[m1*m2:2*m1*m2] = (Pi * Pj).flatten()
|
|
108
|
-
Sphiy[2*m1*m2:] = (Pi * Pj).flatten()
|
|
109
|
-
|
|
110
|
-
Pxi, Pj = np.meshgrid(wPx_xi, wP_eta, indexing='ij')
|
|
111
|
-
Swx[:m1*m2] = (Pxi * Pj * (2 / a)).flatten()
|
|
112
|
-
|
|
113
|
-
Pxi, Pj = np.meshgrid(Px_xi, P_eta, indexing='ij')
|
|
114
|
-
Sphixx[m1*m2:2*m1*m2] = (Pxi * Pj * (2 / a)).flatten()
|
|
115
|
-
Sphiyx[2*m1*m2:] = (Pxi * Pj * (2 / a)).flatten()
|
|
116
|
-
|
|
117
|
-
Pi, Pxj = np.meshgrid(wP_xi, wPx_eta, indexing='ij')
|
|
118
|
-
Swy[:m1*m2] = (Pi * Pxj * (2 / b)).flatten()
|
|
119
|
-
|
|
120
|
-
Pi, Pxj = np.meshgrid(P_xi, Px_eta, indexing='ij')
|
|
121
|
-
Sphixy[m1*m2:2*m1*m2] = (Pi * Pxj * (2 / b)).flatten()
|
|
122
|
-
Sphiyy[2*m1*m2:] = (Pi * Pxj * (2 / b)).flatten()
|
|
123
|
-
|
|
124
|
-
e1xx = Sphixx
|
|
125
|
-
e1yy = Sphiyy
|
|
126
|
-
e1xy = Sphixy + Sphiyx
|
|
127
|
-
|
|
128
|
-
g0yz = Sphiy + Swy
|
|
129
|
-
g0xz = Sphix + Swx
|
|
130
|
-
|
|
131
|
-
Mxx = prop.D11 * e1xx + prop.D12 * e1yy + prop.D16 * e1xy
|
|
132
|
-
Myy = prop.D12 * e1xx + prop.D22 * e1yy + prop.D26 * e1xy
|
|
133
|
-
Mxy = prop.D16 * e1xx + prop.D26 * e1yy + prop.D66 * e1xy
|
|
134
|
-
|
|
135
|
-
Qy = prop.A44 * g0yz + prop.A45 * g0xz
|
|
136
|
-
Qx = prop.A45 * g0yz + prop.A55 * g0xz
|
|
137
|
-
|
|
138
|
-
detJ = a * b / 4
|
|
139
|
-
|
|
140
|
-
addouter(K, detJ * weight * Mxx, e1xx)
|
|
141
|
-
addouter(K, detJ * weight * Myy, e1yy)
|
|
142
|
-
addouter(K, detJ * weight * Mxy, e1xy)
|
|
143
|
-
addouter(K, detJ * weight * Qy, g0yz)
|
|
144
|
-
addouter(K, detJ * weight * Qx, g0xz)
|
|
145
|
-
|
|
146
|
-
addouter(KG, detJ * weight * Nxxhat * Swx, Swx)
|
|
147
|
-
|
|
148
|
-
eigvals, eigvecs = lb(csc_matrix(K), csc_matrix(KG), silent=True)
|
|
149
|
-
|
|
150
|
-
D = E * h**3 / (12 * (1 - nu**2))
|
|
151
|
-
Ncr_analytical = 4 * np.pi**2 * D / b**2
|
|
152
|
-
Ncr_computed = eigvals[0] * abs(Nxxhat)
|
|
153
|
-
|
|
154
|
-
# FSDT gives slightly lower Ncr than CLPT due to shear deformation
|
|
155
|
-
np.testing.assert_allclose(Ncr_computed, Ncr_analytical, rtol=0.02)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|