structsolve 0.5.0__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structsolve-0.5.0/structsolve.egg-info → structsolve-0.5.3}/PKG-INFO +13 -4
- {structsolve-0.5.0 → structsolve-0.5.3}/README.md +12 -3
- {structsolve-0.5.0 → structsolve-0.5.3}/pyproject.toml +1 -1
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/freq.py +6 -2
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/linear_buckling.py +112 -34
- {structsolve-0.5.0 → structsolve-0.5.3/structsolve.egg-info}/PKG-INFO +13 -4
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve.egg-info/SOURCES.txt +1 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_follower_loads.py +7 -1
- structsolve-0.5.3/tests/test_linear_buckling_unsymmetric.py +241 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/LICENSE +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/setup.cfg +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/__init__.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/analysis.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/arc_length.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/callbacks.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/logger.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/newton_raphson.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/sparseutils.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve/static.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve.egg-info/requires.txt +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_analysis.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_arc_length.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_follower_freq_static.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_freq.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_linear_buckling.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_sparseutils.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_static.py +0 -0
- {structsolve-0.5.0 → structsolve-0.5.3}/tests/test_static_deflection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -61,7 +61,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
61
61
|
Citing this library
|
|
62
62
|
===================
|
|
63
63
|
|
|
64
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.
|
|
64
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
Documentation
|
|
@@ -76,8 +76,17 @@ History
|
|
|
76
76
|
|
|
77
77
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
78
78
|
|
|
79
|
-
* version 0.5.
|
|
80
|
-
- Configuration-dependent loads
|
|
79
|
+
* version 0.5.3 (2026-10-06)
|
|
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
|
+
- The unsymmetric solvers of `lb` and `freq` use the complex LAPACK
|
|
86
|
+
drivers of `scipy.linalg.eig`, avoiding crashes of the Python process
|
|
87
|
+
with Intel MKL 2025.0.0
|
|
88
|
+
- Linear static analysis with configuration-dependent loads in
|
|
89
|
+
`Analysis.static(NLgeom=False)`
|
|
81
90
|
* version 0.4.3 (2026-09-17)
|
|
82
91
|
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
83
92
|
return buckling loads higher than the critical ones
|
|
@@ -24,7 +24,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
24
24
|
Citing this library
|
|
25
25
|
===================
|
|
26
26
|
|
|
27
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.
|
|
27
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
Documentation
|
|
@@ -39,8 +39,17 @@ History
|
|
|
39
39
|
|
|
40
40
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
41
41
|
|
|
42
|
-
* version 0.5.
|
|
43
|
-
- Configuration-dependent loads
|
|
42
|
+
* version 0.5.3 (2026-10-06)
|
|
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
|
+
- The unsymmetric solvers of `lb` and `freq` use the complex LAPACK
|
|
49
|
+
drivers of `scipy.linalg.eig`, avoiding crashes of the Python process
|
|
50
|
+
with Intel MKL 2025.0.0
|
|
51
|
+
- Linear static analysis with configuration-dependent loads in
|
|
52
|
+
`Analysis.static(NLgeom=False)`
|
|
44
53
|
* version 0.4.3 (2026-09-17)
|
|
45
54
|
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
46
55
|
return buckling loads higher than the critical ones
|
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
import scipy
|
|
5
5
|
from scipy.sparse import csr_matrix
|
|
6
6
|
from scipy.sparse.linalg import eigs, eigsh, spsolve
|
|
7
|
-
from scipy.linalg import
|
|
7
|
+
from scipy.linalg import eigh
|
|
8
8
|
|
|
9
9
|
from .logger import msg, warn
|
|
10
10
|
from .sparseutils import remove_null_cols
|
|
@@ -193,8 +193,12 @@ def freq(K, M, tol=0, sparse_solver=True,
|
|
|
193
193
|
# for effiency reasons, solving:
|
|
194
194
|
# [M]{u} = (-1/lambda2)[K]{u}
|
|
195
195
|
# [M]{u} = eigval [K]{u}
|
|
196
|
+
#NOTE complex matrices to call zggev instead of dggev, which crashes
|
|
197
|
+
# the process for some matrices with Intel MKL 2025.0.0, see
|
|
198
|
+
# linear_buckling._eig_complex
|
|
199
|
+
from .linear_buckling import _eig_complex
|
|
196
200
|
msg('eig() solver...', level=3, silent=silent)
|
|
197
|
-
eigvals, peigvecs =
|
|
201
|
+
eigvals, peigvecs = _eig_complex(a=Meff, b=Keff)
|
|
198
202
|
lambda2 = -1./eigvals
|
|
199
203
|
|
|
200
204
|
if check_rtol is not None and peigvecs.shape[1]:
|
|
@@ -82,17 +82,34 @@ def _is_positive_definite(A):
|
|
|
82
82
|
return bool(np.all(lu.U.diagonal() > 0))
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _lu_solve(lu, b):
|
|
86
|
+
"""``lu.solve(b)`` for a real or complex right-hand side ``b``
|
|
87
|
+
|
|
88
|
+
The SuperLU factorization of a real matrix rejects a complex ``b``, whose
|
|
89
|
+
real and imaginary parts are then solved separately.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
b = np.asarray(b)
|
|
93
|
+
if np.iscomplexobj(b):
|
|
94
|
+
return (lu.solve(np.ascontiguousarray(b.real))
|
|
95
|
+
+ 1j*lu.solve(np.ascontiguousarray(b.imag)))
|
|
96
|
+
return lu.solve(b)
|
|
97
|
+
|
|
98
|
+
|
|
85
99
|
def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
86
100
|
check_inertia=True):
|
|
87
101
|
r"""Verify the eigenpairs of `([K] + \lambda [K_G])\{u\} = \{0\}`
|
|
88
102
|
|
|
103
|
+
The eigenpairs may be real or complex.
|
|
104
|
+
|
|
89
105
|
- The relative residual
|
|
90
106
|
`||K u + \lambda K_G u|| / (||K u|| + |\lambda| ||K_G u||)` of every
|
|
91
107
|
eigenpair with a finite `\lambda` must not be larger than ``rtol``.
|
|
92
108
|
- No load multiplier is missing below the lowest positive `\lambda_1`
|
|
93
109
|
of ``eigvals``: when ``K`` is positive definite, `[K] + s [K_G]` with
|
|
94
110
|
`s = \lambda_1 (1 - min\_rel\_gap)` must also be positive definite.
|
|
95
|
-
Only for symmetric matrices, ``check_inertia=True
|
|
111
|
+
Only for symmetric matrices, ``check_inertia=True``, the complex
|
|
112
|
+
`\lambda` are ignored.
|
|
96
113
|
|
|
97
114
|
Returns
|
|
98
115
|
-------
|
|
@@ -100,6 +117,7 @@ def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
|
100
117
|
Description of the failed check, ``None`` when all checks passed.
|
|
101
118
|
|
|
102
119
|
"""
|
|
120
|
+
eigvals = np.asarray(eigvals)
|
|
103
121
|
finite = np.isfinite(eigvals)
|
|
104
122
|
if not np.any(finite):
|
|
105
123
|
return 'no finite load multiplier was found'
|
|
@@ -117,6 +135,7 @@ def _check_eigenpairs(K, KG, eigvals, eigvecs, rtol, min_rel_gap=1e-4,
|
|
|
117
135
|
'{2}'.format(residual[i], rtol, lam[i]))
|
|
118
136
|
if not check_inertia:
|
|
119
137
|
return None
|
|
138
|
+
lam = lam[np.isreal(lam)].real
|
|
120
139
|
positive = lam[lam > 0]
|
|
121
140
|
if positive.size and _is_positive_definite(K):
|
|
122
141
|
s = positive.min()*(1 - min_rel_gap)
|
|
@@ -200,20 +219,21 @@ def _real_if_close(mu, eigvecs, rtol=1e-8):
|
|
|
200
219
|
|
|
201
220
|
Each eigenvector of an eigenvalue whose imaginary part is below ``rtol``
|
|
202
221
|
of its modulus is rotated in the complex plane to make its largest
|
|
203
|
-
component real
|
|
222
|
+
component real, and its imaginary part is dropped, as the imaginary part
|
|
223
|
+
of the eigenvalue. Complex arrays are returned, see
|
|
224
|
+
:func:`_general_eigenpairs`.
|
|
204
225
|
|
|
205
226
|
"""
|
|
206
|
-
mu = np.
|
|
207
|
-
eigvecs = np.
|
|
227
|
+
mu = np.array(mu, dtype=complex)
|
|
228
|
+
eigvecs = np.array(eigvecs, dtype=complex)
|
|
208
229
|
real = np.abs(mu.imag) <= rtol*np.abs(mu)
|
|
209
|
-
mu =
|
|
230
|
+
mu[real] = mu[real].real
|
|
210
231
|
for i in np.flatnonzero(real):
|
|
211
232
|
v = eigvecs[:, i]
|
|
212
233
|
j = np.argmax(np.abs(v))
|
|
213
234
|
if v[j] != 0:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return mu.real, eigvecs.real
|
|
235
|
+
v = v*(abs(v[j])/v[j])
|
|
236
|
+
eigvecs[:, i] = v.real
|
|
217
237
|
return mu, eigvecs
|
|
218
238
|
|
|
219
239
|
|
|
@@ -230,12 +250,61 @@ def _sort_eigenpairs_general(mu, eigvecs):
|
|
|
230
250
|
return mu[order], eigvecs[:, order]
|
|
231
251
|
|
|
232
252
|
|
|
253
|
+
def _general_eigenpairs(mu, eigvecs, k=None):
|
|
254
|
+
r"""The first ``k`` finite eigenpairs of a general eigenvalue solver
|
|
255
|
+
|
|
256
|
+
The eigenvalues with a negligible imaginary part are made real with
|
|
257
|
+
:func:`_real_if_close`, the finite eigenpairs are sorted with
|
|
258
|
+
:func:`_sort_eigenpairs_general` and the first ``k`` are kept, all of
|
|
259
|
+
them with ``k=None``. Real arrays are returned when the kept eigenpairs
|
|
260
|
+
are real, even when discarded ones, e.g. spurious eigenvalues far from
|
|
261
|
+
the critical ones, are complex.
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
mu = np.asarray(mu)
|
|
265
|
+
finite = np.isfinite(mu)
|
|
266
|
+
mu, eigvecs = _real_if_close(mu[finite], eigvecs[:, finite])
|
|
267
|
+
mu, eigvecs = _sort_eigenpairs_general(mu, eigvecs)
|
|
268
|
+
if k is not None:
|
|
269
|
+
mu = mu[:k]
|
|
270
|
+
eigvecs = eigvecs[:, :k]
|
|
271
|
+
if np.all(mu.imag == 0):
|
|
272
|
+
return mu.real, eigvecs.real
|
|
273
|
+
return mu, eigvecs
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _eig_complex(a, b=None):
|
|
277
|
+
r"""All eigenpairs of `[a]\{v\} = w [b]\{v\}` with :func:`scipy.linalg.eig`
|
|
278
|
+
|
|
279
|
+
The matrices are converted to complex, such that LAPACK ``zgeev`` or
|
|
280
|
+
``zggev`` is used instead of ``dgeev`` or ``dggev``. With Intel MKL
|
|
281
|
+
2025.0.0 the real drivers crash the Python process when computing the
|
|
282
|
+
eigenvectors of some matrices, with an access violation or a heap
|
|
283
|
+
corruption that cannot be caught, deterministically for a given matrix,
|
|
284
|
+
already for 48 x 48 matrices and with any number of threads. The complex
|
|
285
|
+
drivers are not affected, at about twice the cost of the real ones. The
|
|
286
|
+
complex eigenvectors of real eigenvalues are made real by
|
|
287
|
+
:func:`_general_eigenpairs`.
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
a = np.asarray(a, dtype=complex)
|
|
291
|
+
if b is not None:
|
|
292
|
+
b = np.asarray(b, dtype=complex)
|
|
293
|
+
return eig(a=a, b=b)
|
|
294
|
+
|
|
295
|
+
|
|
233
296
|
def _eig_condensed(K, KG, k):
|
|
234
297
|
r"""As :func:`_eigh_condensed`, for unsymmetric matrices
|
|
235
298
|
|
|
236
299
|
The dofs where both the row and the column of ``KG`` are null are
|
|
237
300
|
condensed out with an LU factorization of ``K``, and the condensed
|
|
238
|
-
problem is solved with :func:`scipy.linalg.eig
|
|
301
|
+
problem is solved with :func:`scipy.linalg.eig`, through the complex
|
|
302
|
+
LAPACK driver ``zggev``, see :func:`_eig_complex`. Neither converting to
|
|
303
|
+
the standard problem `[S]^{-1}[K_{G_{bb}}]` (``dgeev``) nor limiting the
|
|
304
|
+
threads of BLAS/LAPACK avoids the crash of ``dggev`` with Intel MKL
|
|
305
|
+
2025.0.0. The ``k`` returned eigenpairs, see :func:`_general_eigenpairs`,
|
|
306
|
+
are real when they are all real, and complex eigenvectors are
|
|
307
|
+
back-substituted with the real factorization of `[K_{aa}]`.
|
|
239
308
|
|
|
240
309
|
"""
|
|
241
310
|
KG = csr_matrix(KG)
|
|
@@ -259,17 +328,12 @@ def _eig_condensed(K, KG, k):
|
|
|
259
328
|
j1 = min(j0 + chunk, b.size)
|
|
260
329
|
X = lu.solve(Kab[:, j0:j1].toarray())
|
|
261
330
|
S[:, j0:j1] -= Kba @ X
|
|
262
|
-
mu, ub =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
mu, ub = _sort_eigenpairs_general(mu, ub)
|
|
266
|
-
k = min(k, mu.shape[0])
|
|
267
|
-
mu = mu[:k]
|
|
268
|
-
ub = ub[:, :k]
|
|
269
|
-
eigvecs = np.zeros((K.shape[0], k), dtype=ub.dtype)
|
|
331
|
+
mu, ub = _eig_complex(a=KGbb, b=S)
|
|
332
|
+
mu, ub = _general_eigenpairs(mu, ub, k)
|
|
333
|
+
eigvecs = np.zeros((K.shape[0], mu.shape[0]), dtype=ub.dtype)
|
|
270
334
|
eigvecs[b, :] = ub
|
|
271
335
|
if a.size:
|
|
272
|
-
eigvecs[a, :] = -lu
|
|
336
|
+
eigvecs[a, :] = -_lu_solve(lu, np.asarray(Kab @ ub))
|
|
273
337
|
return mu, eigvecs
|
|
274
338
|
|
|
275
339
|
|
|
@@ -278,20 +342,21 @@ def _eigs_inverse(K, KG, k, tol):
|
|
|
278
342
|
|
|
279
343
|
The largest `|\mu|` of `[K]^{-1}[K_G] u = \mu u` are the load
|
|
280
344
|
multipliers `\lambda = -1/\mu` of smallest modulus. ``K`` may be
|
|
281
|
-
unsymmetric, it is factorized once with SuperLU.
|
|
345
|
+
unsymmetric, it is factorized once with SuperLU. The operator is real
|
|
346
|
+
for real matrices, a complex vector is solved with :func:`_lu_solve`.
|
|
282
347
|
|
|
283
348
|
"""
|
|
349
|
+
K = csc_matrix(K)
|
|
284
350
|
with warnings.catch_warnings():
|
|
285
351
|
warnings.simplefilter("ignore")
|
|
286
|
-
lu = splu(
|
|
352
|
+
lu = splu(K)
|
|
287
353
|
KG = csr_matrix(KG)
|
|
288
354
|
n = K.shape[0]
|
|
289
|
-
OP = LinearOperator((n, n), matvec=lambda x: lu
|
|
290
|
-
dtype=np.result_type(
|
|
355
|
+
OP = LinearOperator((n, n), matvec=lambda x: _lu_solve(lu, KG @ x),
|
|
356
|
+
dtype=np.result_type(K.dtype, KG.dtype))
|
|
291
357
|
k = min(k, n - 2)
|
|
292
358
|
mu, eigvecs = eigs(OP, k=k, which='LM', tol=tol)
|
|
293
|
-
|
|
294
|
-
return _sort_eigenpairs_general(mu, eigvecs)
|
|
359
|
+
return _general_eigenpairs(mu, eigvecs)
|
|
295
360
|
|
|
296
361
|
|
|
297
362
|
def _eigsh_cayley(K, KG, k, tol, silent):
|
|
@@ -418,7 +483,9 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
418
483
|
The relative residual of each eigenpair is verified as above, but not
|
|
419
484
|
the inertia, which requires symmetric matrices. Eigenvalues whose
|
|
420
485
|
imaginary part is below ``1e-8`` of their modulus are returned as real,
|
|
421
|
-
with real eigenvectors.
|
|
486
|
+
with real eigenvectors. The arrays are real when the returned eigenpairs
|
|
487
|
+
are real, even if other eigenvalues of the solver are complex. A solver
|
|
488
|
+
that raises an error is skipped, as one that fails the verification.
|
|
422
489
|
|
|
423
490
|
ARPACK, used by :func:`scipy.sparse.linalg.eigsh`, returns wrong
|
|
424
491
|
eigenpairs at random, without any error, when linked against Intel MKL
|
|
@@ -426,6 +493,13 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
426
493
|
``dsteqr`` routine of these MKL versions returns wrong eigenvectors for
|
|
427
494
|
matrices larger than 32 x 32. MKL 2025.0.1 or newer is not affected.
|
|
428
495
|
|
|
496
|
+
With Intel MKL 2025.0.0, the real LAPACK drivers ``dgeev`` and ``dggev``
|
|
497
|
+
of :func:`scipy.linalg.eig` crash the Python process for some matrices
|
|
498
|
+
when computing the eigenvectors, with any number of threads. The
|
|
499
|
+
solvers of unsymmetric matrices therefore call :func:`scipy.linalg.eig`
|
|
500
|
+
with complex matrices, i.e. ``zgeev`` and ``zggev``, which are not
|
|
501
|
+
affected.
|
|
502
|
+
|
|
429
503
|
"""
|
|
430
504
|
msg('Running linear buckling analysis...', silent=silent)
|
|
431
505
|
|
|
@@ -461,10 +535,10 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
461
535
|
name, num_active, K.shape[0]), level=3, silent=silent)
|
|
462
536
|
try:
|
|
463
537
|
eigvals, peigvecs = solver()
|
|
464
|
-
with np.errstate(divide='ignore'):
|
|
538
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
465
539
|
error = _check_eigenpairs(K, KG, -1./eigvals, peigvecs,
|
|
466
540
|
check_rtol)
|
|
467
|
-
except (LinAlgError, RuntimeError, ValueError) as e:
|
|
541
|
+
except (LinAlgError, RuntimeError, ValueError, TypeError) as e:
|
|
468
542
|
error = '{0}: {1}'.format(type(e).__name__, e)
|
|
469
543
|
if error is None:
|
|
470
544
|
msg('finished!', level=3, silent=silent)
|
|
@@ -488,13 +562,18 @@ def lb(K, KG, tol=0, sparse_solver=True, silent=False,
|
|
|
488
562
|
msg('finished!', level=3, silent=silent)
|
|
489
563
|
|
|
490
564
|
num_eigvecs = min(num_eigvalues, peigvecs.shape[1])
|
|
565
|
+
if (np.iscomplexobj(peigvecs)
|
|
566
|
+
and np.all(np.imag(eigvals[:num_eigvecs]) == 0)):
|
|
567
|
+
# all eigenvalues are returned by eig(), only the returned
|
|
568
|
+
# eigenvectors must be real
|
|
569
|
+
peigvecs = peigvecs[:, :num_eigvecs].real
|
|
491
570
|
if used_cols is not None:
|
|
492
571
|
eigvecs = np.zeros((size, num_eigvecs), dtype=peigvecs.dtype)
|
|
493
572
|
eigvecs[used_cols, :] = peigvecs[:, :num_eigvecs]
|
|
494
573
|
else:
|
|
495
574
|
eigvecs = peigvecs[:, :num_eigvecs]
|
|
496
575
|
|
|
497
|
-
with np.errstate(divide='ignore'):
|
|
576
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
498
577
|
eigvals = -1./eigvals
|
|
499
578
|
|
|
500
579
|
msg('finished!', level=2, silent=silent)
|
|
@@ -514,10 +593,9 @@ def _lb_unsymmetric(K, KG, tol, sparse_solver, silent, num_eigvalues,
|
|
|
514
593
|
the Notes of :func:`lb`"""
|
|
515
594
|
if not sparse_solver:
|
|
516
595
|
msg('eig() solver...', level=3, silent=silent)
|
|
517
|
-
mu, eigvecs =
|
|
518
|
-
|
|
519
|
-
mu, eigvecs =
|
|
520
|
-
mu, eigvecs = _sort_eigenpairs_general(mu, eigvecs)
|
|
596
|
+
mu, eigvecs = _eig_complex(a=csr_matrix(KG).toarray(),
|
|
597
|
+
b=csr_matrix(K).toarray())
|
|
598
|
+
mu, eigvecs = _general_eigenpairs(mu, eigvecs)
|
|
521
599
|
msg('finished!', level=3, silent=silent)
|
|
522
600
|
return mu, eigvecs
|
|
523
601
|
K = csr_matrix(K)
|
|
@@ -536,10 +614,10 @@ def _lb_unsymmetric(K, KG, tol, sparse_solver, silent, num_eigvalues,
|
|
|
536
614
|
.format(name, num_active, K.shape[0]), level=3, silent=silent)
|
|
537
615
|
try:
|
|
538
616
|
mu, eigvecs = solver()
|
|
539
|
-
with np.errstate(divide='ignore'):
|
|
617
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
540
618
|
error = _check_eigenpairs(K, KG, -1./mu, eigvecs, check_rtol,
|
|
541
619
|
check_inertia=False)
|
|
542
|
-
except (LinAlgError, RuntimeError, ValueError) as e:
|
|
620
|
+
except (LinAlgError, RuntimeError, ValueError, TypeError) as e:
|
|
543
621
|
error = '{0}: {1}'.format(type(e).__name__, e)
|
|
544
622
|
if error is None:
|
|
545
623
|
msg('finished!', level=3, silent=silent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -61,7 +61,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
61
61
|
Citing this library
|
|
62
62
|
===================
|
|
63
63
|
|
|
64
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.
|
|
64
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.5.3). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
Documentation
|
|
@@ -76,8 +76,17 @@ History
|
|
|
76
76
|
|
|
77
77
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
78
78
|
|
|
79
|
-
* version 0.5.
|
|
80
|
-
- Configuration-dependent loads
|
|
79
|
+
* version 0.5.3 (2026-10-06)
|
|
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
|
+
- The unsymmetric solvers of `lb` and `freq` use the complex LAPACK
|
|
86
|
+
drivers of `scipy.linalg.eig`, avoiding crashes of the Python process
|
|
87
|
+
with Intel MKL 2025.0.0
|
|
88
|
+
- Linear static analysis with configuration-dependent loads in
|
|
89
|
+
`Analysis.static(NLgeom=False)`
|
|
81
90
|
* version 0.4.3 (2026-09-17)
|
|
82
91
|
- Fixed the eigenvalue shift of the sparse solver of `lb`, which could
|
|
83
92
|
return buckling loads higher than the critical ones
|
|
@@ -234,7 +234,13 @@ def test_lb_unsymmetric_complex_pairs():
|
|
|
234
234
|
assert np.iscomplexobj(eigvals)
|
|
235
235
|
mu = eig(KG, K, right=False)
|
|
236
236
|
ref = -1/mu
|
|
237
|
-
|
|
237
|
+
# the complex driver zggev, used by lb(), does not return conjugate
|
|
238
|
+
# pairs with identical real parts, which changes the order of
|
|
239
|
+
# np.sort_complex within a pair: nearest reference of each eigenvalue
|
|
240
|
+
assert eigvals.shape == ref.shape
|
|
241
|
+
dist = np.abs(eigvals[:, None] - ref[None, :])
|
|
242
|
+
assert np.all(dist.min(axis=1) <= 1e-8*np.abs(eigvals))
|
|
243
|
+
assert np.all(dist.min(axis=0) <= 1e-8*np.abs(ref))
|
|
238
244
|
real = np.isreal(eigvals)
|
|
239
245
|
# the real ones come first
|
|
240
246
|
assert not np.any(real[np.argmin(real):]) or np.all(real)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Tests for structsolve.linear_buckling: lb() with unsymmetric matrices"""
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pytest
|
|
9
|
+
from scipy.linalg import eig
|
|
10
|
+
from scipy.sparse import csr_matrix, diags, random as sprandom, identity
|
|
11
|
+
|
|
12
|
+
from structsolve import lb
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def relative_residuals(K, KG, eigvals, eigvecs):
|
|
16
|
+
Ku = K @ eigvecs
|
|
17
|
+
KGu = KG @ eigvecs
|
|
18
|
+
return (np.linalg.norm(Ku + KGu*eigvals, axis=0)
|
|
19
|
+
/ (np.linalg.norm(Ku, axis=0)
|
|
20
|
+
+ np.abs(eigvals)*np.linalg.norm(KGu, axis=0)))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dense_load_multipliers(K, KG):
|
|
24
|
+
"""Finite load multipliers of the full problem with a direct dense eig"""
|
|
25
|
+
mu = eig(a=np.asarray(KG, dtype=complex), b=np.asarray(K, dtype=complex),
|
|
26
|
+
right=False)
|
|
27
|
+
mu = mu[np.isfinite(mu)]
|
|
28
|
+
# the eigenvalues of the null rows and columns of KG
|
|
29
|
+
mu = mu[np.abs(mu) > 1e-10*np.abs(mu).max()]
|
|
30
|
+
return -1./mu
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def matrices_complex_KG_block():
|
|
34
|
+
"""Dofs 3 to 5 have null rows and columns of KG, which are condensed out,
|
|
35
|
+
and the KG block has a complex pair of eigenvalues"""
|
|
36
|
+
n = 6
|
|
37
|
+
K = np.diag([4., 5., 6., 7., 8., 9.]) + 0.1*(np.eye(n, k=1)
|
|
38
|
+
+ np.eye(n, k=-1))
|
|
39
|
+
KG = np.zeros((n, n))
|
|
40
|
+
KG[:3, :3] = [[-1., 0.5, 0.], [-0.5, -1., 0.], [0., 0., -2.]]
|
|
41
|
+
return K, KG
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_lb_unsymmetric_complex_pair_condensed():
|
|
45
|
+
"""Regression test: a complex eigenpair of the condensed problem, even
|
|
46
|
+
when not returned, made the back-substitution of the condensed dofs
|
|
47
|
+
raise a TypeError in the real SuperLU factorization"""
|
|
48
|
+
K, KG = matrices_complex_KG_block()
|
|
49
|
+
ref = dense_load_multipliers(K, KG)
|
|
50
|
+
real = np.abs(ref.imag) <= 1e-8*np.abs(ref)
|
|
51
|
+
assert np.sum(real) == 1 and np.sum(~real) == 2
|
|
52
|
+
lam_real = ref[real].real[0]
|
|
53
|
+
lam_cplx = ref[~real]
|
|
54
|
+
|
|
55
|
+
# only the real eigenpair is returned: real arrays
|
|
56
|
+
eigvals, eigvecs = lb(csr_matrix(K), csr_matrix(KG), num_eigvalues=1,
|
|
57
|
+
symmetric=False, silent=True)
|
|
58
|
+
assert eigvals.dtype == np.float64 and eigvecs.dtype == np.float64
|
|
59
|
+
assert eigvecs.shape == (6, 1)
|
|
60
|
+
np.testing.assert_allclose(eigvals, [lam_real], rtol=1e-10)
|
|
61
|
+
assert np.all(relative_residuals(K, KG, eigvals, eigvecs) < 1e-10)
|
|
62
|
+
|
|
63
|
+
# the real eigenpair first, followed by one of the complex pair
|
|
64
|
+
eigvals, eigvecs = lb(csr_matrix(K), csr_matrix(KG), num_eigvalues=2,
|
|
65
|
+
symmetric=False, silent=True)
|
|
66
|
+
assert eigvals.shape == (2,) and eigvecs.shape == (6, 2)
|
|
67
|
+
assert np.iscomplexobj(eigvals) and np.iscomplexobj(eigvecs)
|
|
68
|
+
assert eigvals[0].imag == 0 and np.all(eigvecs[:, 0].imag == 0)
|
|
69
|
+
np.testing.assert_allclose(eigvals[0].real, lam_real, rtol=1e-10)
|
|
70
|
+
assert np.min(np.abs(lam_cplx - eigvals[1])) <= 1e-10*abs(eigvals[1])
|
|
71
|
+
assert np.all(relative_residuals(K, KG, eigvals, eigvecs) < 1e-10)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_lb_unsymmetric_general_eigenpairs_real_when_kept_real():
|
|
75
|
+
"""The real/complex decision applies to the returned eigenpairs"""
|
|
76
|
+
from structsolve.linear_buckling import _general_eigenpairs
|
|
77
|
+
|
|
78
|
+
mu = np.array([-0.5 + 0.2j, -0.5 - 0.2j, -0.25 + 1e-14j, -0.1, np.inf])
|
|
79
|
+
v = np.random.RandomState(0).randn(4, 5)*(1 + 1j)
|
|
80
|
+
m, u = _general_eigenpairs(mu, v, 2)
|
|
81
|
+
assert m.dtype == np.float64 and u.dtype == np.float64
|
|
82
|
+
np.testing.assert_allclose(m, [-0.25, -0.1])
|
|
83
|
+
# rotated eigenvector with a positive largest component
|
|
84
|
+
r = v[:, 2].real
|
|
85
|
+
np.testing.assert_allclose(u[:, 0],
|
|
86
|
+
np.sqrt(2)*r*np.sign(r[np.argmax(abs(r))]))
|
|
87
|
+
m, u = _general_eigenpairs(mu, v)
|
|
88
|
+
assert np.iscomplexobj(m) and m.shape == (4,)
|
|
89
|
+
np.testing.assert_allclose(m[:2], [-0.25, -0.1])
|
|
90
|
+
assert np.all(u[:, :2].imag == 0)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize('max_dense_size', [2000, 0])
|
|
94
|
+
def test_lb_unsymmetric_complex_eigenpairs(max_dense_size):
|
|
95
|
+
"""Genuinely complex eigenpairs, with condensed dofs that receive complex
|
|
96
|
+
eigenvectors from the back-substitution"""
|
|
97
|
+
n, nb = 30, 12
|
|
98
|
+
K = (diags(np.linspace(4., 8., n)) + diags(0.5*np.ones(n - 1), 1)
|
|
99
|
+
+ diags(0.5*np.ones(n - 1), -1)).toarray()
|
|
100
|
+
rng = np.random.RandomState(4)
|
|
101
|
+
A = rng.randn(nb, nb)
|
|
102
|
+
KG = np.zeros((n, n))
|
|
103
|
+
KG[:nb, :nb] = -np.eye(nb) + 0.5*(A - A.T)
|
|
104
|
+
ref = dense_load_multipliers(K, KG)
|
|
105
|
+
assert np.all(np.abs(ref.imag) > 1e-3*np.abs(ref))
|
|
106
|
+
k = 4
|
|
107
|
+
eigvals, eigvecs = lb(csr_matrix(K), csr_matrix(KG), num_eigvalues=k,
|
|
108
|
+
symmetric=False, silent=True,
|
|
109
|
+
max_dense_size=max_dense_size)
|
|
110
|
+
assert np.iscomplexobj(eigvals) and np.iscomplexobj(eigvecs)
|
|
111
|
+
assert eigvals.shape == (k,) and eigvecs.shape == (n, k)
|
|
112
|
+
assert np.all(np.abs(eigvals.imag) > 1e-3*np.abs(eigvals))
|
|
113
|
+
# the k load multipliers of smallest modulus, by increasing modulus
|
|
114
|
+
np.testing.assert_allclose(np.abs(eigvals), np.sort(np.abs(ref))[:k],
|
|
115
|
+
rtol=1e-8)
|
|
116
|
+
for lam in eigvals:
|
|
117
|
+
assert np.min(np.abs(ref - lam)) <= 1e-8*abs(lam)
|
|
118
|
+
res = relative_residuals(K, KG, eigvals, eigvecs)
|
|
119
|
+
assert np.all(res < 1e-8)
|
|
120
|
+
# the condensed dofs are not null
|
|
121
|
+
assert np.all(np.linalg.norm(eigvecs[nb:, :], axis=0) > 0)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_lb_check_eigenpairs_real_and_complex():
|
|
125
|
+
"""_check_eigenpairs with check_inertia=False accepts real and complex
|
|
126
|
+
eigenpairs, and detects wrong ones"""
|
|
127
|
+
from structsolve.linear_buckling import _check_eigenpairs, _eig_condensed
|
|
128
|
+
|
|
129
|
+
K, KG = matrices_complex_KG_block()
|
|
130
|
+
K = csr_matrix(K)
|
|
131
|
+
KG = csr_matrix(KG)
|
|
132
|
+
mu, eigvecs = _eig_condensed(K, KG, 3)
|
|
133
|
+
assert np.iscomplexobj(mu) and np.iscomplexobj(eigvecs)
|
|
134
|
+
lam = -1./mu
|
|
135
|
+
assert _check_eigenpairs(K, KG, lam, eigvecs, 1e-8,
|
|
136
|
+
check_inertia=False) is None
|
|
137
|
+
error = _check_eigenpairs(K, KG, lam, eigvecs[:, ::-1], 1e-8,
|
|
138
|
+
check_inertia=False)
|
|
139
|
+
assert 'relative residual' in error
|
|
140
|
+
# complex load multipliers are ignored by the inertia check
|
|
141
|
+
assert _check_eigenpairs(K, KG, lam, eigvecs, 1e-8) is None
|
|
142
|
+
|
|
143
|
+
mu, eigvecs = _eig_condensed(K, KG, 1)
|
|
144
|
+
assert mu.dtype == np.float64 and eigvecs.dtype == np.float64
|
|
145
|
+
lam = -1./mu
|
|
146
|
+
assert _check_eigenpairs(K, KG, lam, eigvecs, 1e-8,
|
|
147
|
+
check_inertia=False) is None
|
|
148
|
+
error = _check_eigenpairs(K, KG, lam*1.01, eigvecs, 1e-8,
|
|
149
|
+
check_inertia=False)
|
|
150
|
+
assert 'relative residual' in error
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize('symmetric, solver', [
|
|
154
|
+
(False, '_eig_condensed'), (True, '_eigh_condensed')])
|
|
155
|
+
def test_lb_solver_type_error_falls_back(monkeypatch, symmetric, solver):
|
|
156
|
+
"""A TypeError of the first solver is recorded and the next solver,
|
|
157
|
+
eigs() or eigsh(), is used"""
|
|
158
|
+
import structsolve.linear_buckling as linear_buckling
|
|
159
|
+
|
|
160
|
+
n = 40
|
|
161
|
+
k = np.linspace(1., 3., n)
|
|
162
|
+
lambdas = np.linspace(1., 20., n)
|
|
163
|
+
K = diags(k).tocsr()
|
|
164
|
+
KG = diags(-k/lambdas).tocsr()
|
|
165
|
+
|
|
166
|
+
def raise_type_error(*args, **kwargs):
|
|
167
|
+
raise TypeError('Cannot cast array data from dtype complex128')
|
|
168
|
+
|
|
169
|
+
monkeypatch.setattr(linear_buckling, solver, raise_type_error)
|
|
170
|
+
eigvals, eigvecs = lb(K, KG, silent=True, num_eigvalues=5,
|
|
171
|
+
symmetric=symmetric)
|
|
172
|
+
np.testing.assert_allclose(eigvals, lambdas[:5], rtol=1e-8)
|
|
173
|
+
assert eigvals.dtype == np.float64 and eigvecs.dtype == np.float64
|
|
174
|
+
assert np.all(relative_residuals(K, KG, eigvals, eigvecs) < 1e-8)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.parametrize('sparse_solver, max_dense_size',
|
|
178
|
+
[(True, 2000), (True, 0), (False, 2000)])
|
|
179
|
+
def test_lb_nearly_symmetric(sparse_solver, max_dense_size):
|
|
180
|
+
"""A symmetric positive definite K with 1e-12 asymmetric noise, forced
|
|
181
|
+
to the solvers of unsymmetric matrices, gives the load multipliers of
|
|
182
|
+
the symmetric solver, with real eigenvectors"""
|
|
183
|
+
n = 60
|
|
184
|
+
A = sprandom(n, n, density=0.1, random_state=2)
|
|
185
|
+
K = (A @ A.T + identity(n)).tocsr()
|
|
186
|
+
B = sprandom(n//3, n//3, density=0.3, random_state=3)
|
|
187
|
+
KG = np.zeros((n, n))
|
|
188
|
+
KG[:n//3, :n//3] = -(B @ B.T + 0.1*identity(n//3)).toarray()
|
|
189
|
+
KG = csr_matrix(KG)
|
|
190
|
+
noise = sprandom(n, n, density=0.2, random_state=5).toarray()
|
|
191
|
+
Kn = csr_matrix(K.toarray() + 1e-12*abs(K).max()*noise)
|
|
192
|
+
assert np.linalg.norm((Kn - Kn.T).toarray()) > 1e-13*abs(K).max()
|
|
193
|
+
|
|
194
|
+
ref, _ = lb(K, KG, silent=True, num_eigvalues=10)
|
|
195
|
+
eigvals, eigvecs = lb(Kn, KG, silent=True, num_eigvalues=10,
|
|
196
|
+
symmetric=False, sparse_solver=sparse_solver,
|
|
197
|
+
max_dense_size=max_dense_size)
|
|
198
|
+
if not sparse_solver:
|
|
199
|
+
# all eigenvalues are returned, also the spurious ones of the null
|
|
200
|
+
# rows of KG, which may be complex
|
|
201
|
+
assert np.all(eigvals[:10].imag == 0)
|
|
202
|
+
eigvals = eigvals[:10].real
|
|
203
|
+
assert eigvals.dtype == np.float64 and eigvecs.dtype == np.float64
|
|
204
|
+
assert eigvecs.shape == (n, 10)
|
|
205
|
+
np.testing.assert_allclose(eigvals, ref, rtol=1e-8)
|
|
206
|
+
assert np.all(relative_residuals(Kn, KG, eigvals, eigvecs) < 1e-8)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_eig_complex_mkl_crash_regression():
|
|
210
|
+
"""Regression test: with Intel MKL 2025.0.0, scipy.linalg.eig on these
|
|
211
|
+
real 48 x 48 matrices (LAPACK dggev) corrupts the heap and the process
|
|
212
|
+
exits with 0xc0000374, deterministically and with any number of threads.
|
|
213
|
+
lb() calls the complex driver zggev. Run in a subprocess, which a crash
|
|
214
|
+
does not take down with the test session"""
|
|
215
|
+
code = textwrap.dedent("""
|
|
216
|
+
import numpy as np
|
|
217
|
+
from scipy.sparse import csr_matrix
|
|
218
|
+
from structsolve import lb
|
|
219
|
+
rng = np.random.RandomState(4)
|
|
220
|
+
KG = rng.randn(48, 48)
|
|
221
|
+
K = rng.randn(48, 48) + 48*np.eye(48)
|
|
222
|
+
eigvals, eigvecs = lb(csr_matrix(K), csr_matrix(KG), silent=True,
|
|
223
|
+
num_eigvalues=5, symmetric=False)
|
|
224
|
+
Ku = K @ eigvecs
|
|
225
|
+
KGu = KG @ eigvecs
|
|
226
|
+
res = (np.linalg.norm(Ku + KGu*eigvals, axis=0)
|
|
227
|
+
/ (np.linalg.norm(Ku, axis=0)
|
|
228
|
+
+ np.abs(eigvals)*np.linalg.norm(KGu, axis=0)))
|
|
229
|
+
assert eigvecs.shape == (48, 5), eigvecs.shape
|
|
230
|
+
assert np.all(res < 1e-8), res
|
|
231
|
+
print('ok')
|
|
232
|
+
""")
|
|
233
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
234
|
+
env = dict(os.environ)
|
|
235
|
+
env['PYTHONPATH'] = os.pathsep.join(
|
|
236
|
+
[root] + [p for p in [env.get('PYTHONPATH')] if p])
|
|
237
|
+
result = subprocess.run([sys.executable, '-c', code], env=env,
|
|
238
|
+
capture_output=True, text=True, timeout=300)
|
|
239
|
+
assert result.returncode == 0, (hex(result.returncode & 0xffffffff),
|
|
240
|
+
result.stderr[-2000:])
|
|
241
|
+
assert result.stdout.strip().endswith('ok')
|
|
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
|
|
File without changes
|