structsolve 0.5.2__tar.gz → 0.6.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.5.2/structsolve.egg-info → structsolve-0.6.0}/PKG-INFO +14 -5
- {structsolve-0.5.2 → structsolve-0.6.0}/README.md +13 -2
- {structsolve-0.5.2 → structsolve-0.6.0}/pyproject.toml +1 -3
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/__init__.py +3 -1
- structsolve-0.6.0/structsolve/arpackutils.py +363 -0
- structsolve-0.6.0/structsolve/freq.py +398 -0
- structsolve-0.6.0/structsolve/linear_buckling.py +996 -0
- {structsolve-0.5.2 → structsolve-0.6.0/structsolve.egg-info}/PKG-INFO +14 -5
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve.egg-info/SOURCES.txt +4 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve.egg-info/requires.txt +0 -2
- structsolve-0.6.0/tests/test_arpackutils.py +282 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_follower_loads.py +7 -1
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_freq.py +30 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_linear_buckling.py +54 -27
- structsolve-0.6.0/tests/test_linear_buckling_unsymmetric.py +354 -0
- structsolve-0.6.0/tests/test_sparse_first.py +217 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_static_deflection.py +2 -2
- structsolve-0.5.2/structsolve/freq.py +0 -241
- structsolve-0.5.2/structsolve/linear_buckling.py +0 -552
- {structsolve-0.5.2 → structsolve-0.6.0}/LICENSE +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/setup.cfg +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/analysis.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/arc_length.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/arc_length_crisfield.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/arc_length_riks.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/callbacks.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/logger.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/newton_raphson.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/sparseutils.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve/static.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve.egg-info/dependency_links.txt +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/structsolve.egg-info/top_level.txt +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_analysis.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_arc_length.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_follower_freq_static.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_newton_raphson.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_sparseutils.py +0 -0
- {structsolve-0.5.2 → structsolve-0.6.0}/tests/test_static.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structsolve
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Structural analysis solvers
|
|
5
5
|
Author-email: "Saullo G. P. Castro" <castrosaullo@gmail.com>
|
|
6
6
|
License: BSD-2-Clause
|
|
@@ -31,8 +31,6 @@ Requires-Dist: pytest; extra == "test"
|
|
|
31
31
|
Requires-Dist: pytest-cov; extra == "test"
|
|
32
32
|
Requires-Dist: flake8; extra == "test"
|
|
33
33
|
Requires-Dist: coverage; extra == "test"
|
|
34
|
-
Requires-Dist: composites; extra == "test"
|
|
35
|
-
Requires-Dist: buckling>=2026b4; extra == "test"
|
|
36
34
|
Dynamic: license-file
|
|
37
35
|
|
|
38
36
|
Github Actions status:
|
|
@@ -49,6 +47,9 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
49
47
|
- Linear statics: [K]{u} = {f}
|
|
50
48
|
- Eigensolver for Linear buckling: ([K] + lambda[KG]){u} = 0
|
|
51
49
|
- Eigensolver for dynamics: ([K] + lambda^2[M]){u} = 0
|
|
50
|
+
- Sparse eigenvalue solvers first, verified, with dense solvers only as a
|
|
51
|
+
last resort, also in the browser (Pyodide) and with the Intel MKL versions
|
|
52
|
+
that break ARPACK
|
|
52
53
|
- Nonlinear statics using Newton-Raphson
|
|
53
54
|
- Nonlinear statics using the Arc-Length method
|
|
54
55
|
- Support for unsymmetric problems, such as those involving follower forces
|
|
@@ -61,7 +62,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
61
62
|
Citing this library
|
|
62
63
|
===================
|
|
63
64
|
|
|
64
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.
|
|
65
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.6.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
Documentation
|
|
@@ -76,12 +77,20 @@ History
|
|
|
76
77
|
|
|
77
78
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
78
79
|
|
|
79
|
-
* version 0.
|
|
80
|
+
* version 0.6.0 (2026-10-08)
|
|
81
|
+
- `lb` and `freq` always run the sparse solvers first, whatever the size of the problem, with verified results; the dense solvers are only a last resort within `max_dense_size`, with a `DenseFallbackWarning`
|
|
82
|
+
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`)
|
|
83
|
+
- Unsymmetric `lb`: a shift search finds the lowest positive real load multiplier, instead of silently returning negative or complex ones
|
|
84
|
+
- The test extra no longer requires `buckling` and `composites`
|
|
85
|
+
* version 0.5.3 (2026-10-06)
|
|
80
86
|
- Configuration-dependent loads, e.g. follower pressures: the load factor
|
|
81
87
|
is passed to the callables that accept `inc`, and the arc-length
|
|
82
88
|
methods use the load vector of the current configuration
|
|
83
89
|
- Unsymmetric matrices in `lb` (automatic detection), `freq` (residual
|
|
84
90
|
check, optional symmetric solvers, kinetic criterion) and `solve`
|
|
91
|
+
- The unsymmetric solvers of `lb` and `freq` use the complex LAPACK
|
|
92
|
+
drivers of `scipy.linalg.eig`, avoiding crashes of the Python process
|
|
93
|
+
with Intel MKL 2025.0.0
|
|
85
94
|
- Linear static analysis with configuration-dependent loads in
|
|
86
95
|
`Analysis.static(NLgeom=False)`
|
|
87
96
|
* version 0.4.3 (2026-09-17)
|
|
@@ -12,6 +12,9 @@ Structural analysis solvers tailored for semi-analytical models
|
|
|
12
12
|
- Linear statics: [K]{u} = {f}
|
|
13
13
|
- Eigensolver for Linear buckling: ([K] + lambda[KG]){u} = 0
|
|
14
14
|
- Eigensolver for dynamics: ([K] + lambda^2[M]){u} = 0
|
|
15
|
+
- Sparse eigenvalue solvers first, verified, with dense solvers only as a
|
|
16
|
+
last resort, also in the browser (Pyodide) and with the Intel MKL versions
|
|
17
|
+
that break ARPACK
|
|
15
18
|
- Nonlinear statics using Newton-Raphson
|
|
16
19
|
- Nonlinear statics using the Arc-Length method
|
|
17
20
|
- Support for unsymmetric problems, such as those involving follower forces
|
|
@@ -24,7 +27,7 @@ Currently these solvers are pretty much compatible with my other repositories
|
|
|
24
27
|
Citing this library
|
|
25
28
|
===================
|
|
26
29
|
|
|
27
|
-
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.
|
|
30
|
+
Saullo G. P. Castro (2026). Structural analysis solvers tailored for semi-analytical models (Version 0.6.0). Zenodo. DOI: https://doi.org/10.5281/zenodo.2581212.
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
Documentation
|
|
@@ -39,12 +42,20 @@ History
|
|
|
39
42
|
|
|
40
43
|
See [CHANGELOG.md](CHANGELOG.md) for the details of each version.
|
|
41
44
|
|
|
42
|
-
* version 0.
|
|
45
|
+
* version 0.6.0 (2026-10-08)
|
|
46
|
+
- `lb` and `freq` always run the sparse solvers first, whatever the size of the problem, with verified results; the dense solvers are only a last resort within `max_dense_size`, with a `DenseFallbackWarning`
|
|
47
|
+
- ARPACK's `ncv` is capped at 32 when SciPy is linked against the Intel MKL versions whose `dsteqr` is faulty (new `structsolve.arpackutils`)
|
|
48
|
+
- Unsymmetric `lb`: a shift search finds the lowest positive real load multiplier, instead of silently returning negative or complex ones
|
|
49
|
+
- The test extra no longer requires `buckling` and `composites`
|
|
50
|
+
* version 0.5.3 (2026-10-06)
|
|
43
51
|
- Configuration-dependent loads, e.g. follower pressures: the load factor
|
|
44
52
|
is passed to the callables that accept `inc`, and the arc-length
|
|
45
53
|
methods use the load vector of the current configuration
|
|
46
54
|
- Unsymmetric matrices in `lb` (automatic detection), `freq` (residual
|
|
47
55
|
check, optional symmetric solvers, kinetic criterion) and `solve`
|
|
56
|
+
- The unsymmetric solvers of `lb` and `freq` use the complex LAPACK
|
|
57
|
+
drivers of `scipy.linalg.eig`, avoiding crashes of the Python process
|
|
58
|
+
with Intel MKL 2025.0.0
|
|
48
59
|
- Linear static analysis with configuration-dependent loads in
|
|
49
60
|
`Analysis.static(NLgeom=False)`
|
|
50
61
|
* version 0.4.3 (2026-09-17)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structsolve"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Structural analysis solvers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "BSD-2-Clause"}
|
|
@@ -43,8 +43,6 @@ test = [
|
|
|
43
43
|
"pytest-cov",
|
|
44
44
|
"flake8",
|
|
45
45
|
"coverage",
|
|
46
|
-
"composites",
|
|
47
|
-
"buckling>=2026b4",
|
|
48
46
|
]
|
|
49
47
|
|
|
50
48
|
[project.urls]
|
|
@@ -14,11 +14,13 @@ directly from the ``structsolve`` namespace:
|
|
|
14
14
|
- :func:`.static`: linear static analysis
|
|
15
15
|
- :func:`.lb`: linear buckling analysis
|
|
16
16
|
- :func:`.freq`: frequency analysis
|
|
17
|
+
- :class:`.DenseFallbackWarning`: warning of :func:`.lb` and :func:`.freq`
|
|
18
|
+
when a dense solver was used as a last resort
|
|
17
19
|
|
|
18
20
|
"""
|
|
19
21
|
from __future__ import absolute_import
|
|
20
22
|
|
|
21
23
|
from .analysis import Analysis
|
|
22
24
|
from .freq import freq
|
|
23
|
-
from .linear_buckling import lb
|
|
25
|
+
from .linear_buckling import DenseFallbackWarning, lb
|
|
24
26
|
from .static import solve, static
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
r"""Utilities for ARPACK, against the Intel MKL bug of ``dsteqr``
|
|
2
|
+
|
|
3
|
+
The symmetric drivers of ARPACK, used by :func:`scipy.sparse.linalg.eigsh`,
|
|
4
|
+
compute the Ritz vectors with the LAPACK routine ``dsteqr`` applied to the
|
|
5
|
+
``ncv x ncv`` tridiagonal matrix of the Lanczos process. The ``dsteqr`` of
|
|
6
|
+
Intel MKL 2024.2.0 to 2025.0.0 returns wrong eigenvectors for matrices
|
|
7
|
+
larger than 32 x 32, such that :func:`scipy.sparse.linalg.eigsh` returns
|
|
8
|
+
wrong eigenpairs, without any error, or raises ``ArpackError -8``, whenever
|
|
9
|
+
``ncv > 32``, e.g. with SciPy's default ``ncv = 2 k + 1`` for ``k >= 16``.
|
|
10
|
+
MKL 2025.0.1 or newer is not affected, neither are the non-symmetric drivers
|
|
11
|
+
of :func:`scipy.sparse.linalg.eigs`, which do not call ``dsteqr``.
|
|
12
|
+
|
|
13
|
+
:func:`capped_eigsh` keeps ``ncv <= 32`` when :func:`arpack_max_ncv` says so,
|
|
14
|
+
computing more than 15 eigenpairs in several calls with deflation.
|
|
15
|
+
|
|
16
|
+
The cap is decided by :func:`arpack_max_ncv` and can be overridden with
|
|
17
|
+
the module attribute :data:`ARPACK_MAX_NCV` or the environment variable
|
|
18
|
+
``STRUCTSOLVE_ARPACK_MAX_NCV``, e.g. to lift it with a fixed MKL that cannot
|
|
19
|
+
be identified as such::
|
|
20
|
+
|
|
21
|
+
import structsolve.arpackutils
|
|
22
|
+
structsolve.arpackutils.ARPACK_MAX_NCV = 0 # no cap
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
import contextlib
|
|
26
|
+
import functools
|
|
27
|
+
import gc
|
|
28
|
+
import io
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
import warnings
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
from scipy.sparse import csc_matrix
|
|
36
|
+
from scipy.sparse.linalg import LinearOperator, splu
|
|
37
|
+
from scipy.sparse.linalg import eigsh as _scipy_eigsh
|
|
38
|
+
|
|
39
|
+
#: Maximum ``ncv`` of :func:`capped_eigsh`. ``None``, the default, reads the
|
|
40
|
+
#: environment variable ``STRUCTSOLVE_ARPACK_MAX_NCV`` and, when it is not
|
|
41
|
+
#: defined, uses ``'auto'``: the cap :data:`DSTEQR_MAX_SIZE` is applied when
|
|
42
|
+
#: the LAPACK of SciPy is detected as faulty, see :func:`arpack_max_ncv`.
|
|
43
|
+
#: ``0`` (or ``'none'``) disables the cap, an integer of at least ``3`` sets
|
|
44
|
+
#: it.
|
|
45
|
+
ARPACK_MAX_NCV = None
|
|
46
|
+
|
|
47
|
+
#: Largest tridiagonal matrix for which the faulty ``dsteqr`` is correct
|
|
48
|
+
DSTEQR_MAX_SIZE = 32
|
|
49
|
+
|
|
50
|
+
#: First and last Intel MKL versions with the faulty ``dsteqr``
|
|
51
|
+
MKL_FAULTY_VERSIONS = ((2024, 2, 0), (2025, 0, 0))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _lapack_name_from_config(module):
|
|
55
|
+
"""Name of the LAPACK library in the build configuration of ``module``
|
|
56
|
+
(``scipy`` or ``numpy``), ``None`` when unknown"""
|
|
57
|
+
try:
|
|
58
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
59
|
+
config = module.show_config(mode='dicts')
|
|
60
|
+
name = config['Build Dependencies']['lapack']['name']
|
|
61
|
+
return str(name).lower() if name else None
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
# builds with numpy.distutils, e.g. SciPy < 1.9
|
|
65
|
+
config = getattr(module, '__config__', None)
|
|
66
|
+
if config is None:
|
|
67
|
+
return None
|
|
68
|
+
for key in ('lapack_mkl_info', 'blas_mkl_info'):
|
|
69
|
+
info = getattr(config, key, None)
|
|
70
|
+
if info is None and hasattr(config, 'get_info'):
|
|
71
|
+
try:
|
|
72
|
+
info = config.get_info(key)
|
|
73
|
+
except Exception:
|
|
74
|
+
info = None
|
|
75
|
+
if info:
|
|
76
|
+
return 'mkl'
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@functools.lru_cache(maxsize=None)
|
|
81
|
+
def lapack_is_mkl():
|
|
82
|
+
"""Tell whether the LAPACK of SciPy is Intel MKL
|
|
83
|
+
|
|
84
|
+
``scipy.show_config(mode='dicts')`` is read first, NumPy's build
|
|
85
|
+
configuration only when SciPy's does not name the LAPACK library. Older
|
|
86
|
+
versions of SciPy without ``mode='dicts'`` are checked through
|
|
87
|
+
``scipy.__config__``.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
is_mkl : bool or None
|
|
92
|
+
``None`` when the build configuration does not tell.
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
import numpy
|
|
96
|
+
import scipy
|
|
97
|
+
for module in (scipy, numpy):
|
|
98
|
+
name = _lapack_name_from_config(module)
|
|
99
|
+
if name is not None:
|
|
100
|
+
return 'mkl' in name
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _parse_mkl_version(text):
|
|
105
|
+
found = re.search(r'(\d{4})\.(\d+)(?:\.(\d+))?', str(text))
|
|
106
|
+
if found is None:
|
|
107
|
+
return None
|
|
108
|
+
return tuple(int(g) if g is not None else 0 for g in found.groups())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@functools.lru_cache(maxsize=None)
|
|
112
|
+
def mkl_version():
|
|
113
|
+
"""Version of the Intel MKL loaded in the process, as a tuple
|
|
114
|
+
``(major, minor, update)``
|
|
115
|
+
|
|
116
|
+
The build configuration of SciPy only gives the major version, e.g.
|
|
117
|
+
``'2025'``, therefore the version is read at runtime from the optional
|
|
118
|
+
packages ``mkl-service`` (``mkl.get_version()``) or ``threadpoolctl``.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
version : tuple or None
|
|
123
|
+
``None`` when neither package is installed or no MKL is loaded.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
import mkl
|
|
128
|
+
v = mkl.get_version()
|
|
129
|
+
return (int(v['MajorVersion']), int(v['MinorVersion']),
|
|
130
|
+
int(v['UpdateVersion']))
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
try:
|
|
134
|
+
import threadpoolctl
|
|
135
|
+
for info in threadpoolctl.threadpool_info():
|
|
136
|
+
if info.get('internal_api') == 'mkl':
|
|
137
|
+
return _parse_mkl_version(info.get('version'))
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@functools.lru_cache(maxsize=None)
|
|
144
|
+
def dsteqr_is_faulty():
|
|
145
|
+
"""Tell whether the LAPACK ``dsteqr`` of SciPy returns wrong eigenvectors
|
|
146
|
+
|
|
147
|
+
``scipy.linalg.lapack.dstev`` computes the eigenvectors of a symmetric
|
|
148
|
+
tridiagonal matrix with ``dsteqr``. It is called for three random
|
|
149
|
+
matrices of sizes 33, 40 and 64, whose eigenpairs must have relative
|
|
150
|
+
residuals and an orthogonality error below ``1e-10``. With Intel MKL
|
|
151
|
+
2025.0.0 every size above 32 fails, with rare exceptions for sizes close
|
|
152
|
+
to 32, hence the three sizes.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
faulty : bool or None
|
|
157
|
+
``None`` when ``dstev`` is not available.
|
|
158
|
+
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
from scipy.linalg.lapack import dstev
|
|
162
|
+
except ImportError:
|
|
163
|
+
return None
|
|
164
|
+
rng = np.random.RandomState(0)
|
|
165
|
+
for n in (33, 40, 64):
|
|
166
|
+
d = rng.randn(n)
|
|
167
|
+
e = rng.randn(n - 1)
|
|
168
|
+
try:
|
|
169
|
+
with np.errstate(all='ignore'):
|
|
170
|
+
w, z, info = dstev(d, e, compute_v=1)
|
|
171
|
+
T = np.diag(d) + np.diag(e, 1) + np.diag(e, -1)
|
|
172
|
+
residual = np.linalg.norm(T @ z - z*w)/np.linalg.norm(T)
|
|
173
|
+
orthogonality = np.linalg.norm(z.T @ z - np.eye(n))
|
|
174
|
+
except Exception:
|
|
175
|
+
return True
|
|
176
|
+
if not (info == 0 and residual <= 1e-10 and orthogonality <= 1e-10):
|
|
177
|
+
return True
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@functools.lru_cache(maxsize=None)
|
|
182
|
+
def _auto_max_ncv():
|
|
183
|
+
if dsteqr_is_faulty():
|
|
184
|
+
return DSTEQR_MAX_SIZE
|
|
185
|
+
if lapack_is_mkl():
|
|
186
|
+
version = mkl_version()
|
|
187
|
+
first, last = MKL_FAULTY_VERSIONS
|
|
188
|
+
#NOTE the probe of dsteqr_is_faulty() passed, but the version of an
|
|
189
|
+
# MKL build must be known to lift the cap
|
|
190
|
+
if version is None or first <= version <= last:
|
|
191
|
+
return DSTEQR_MAX_SIZE
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def arpack_max_ncv():
|
|
196
|
+
r"""Maximum ``ncv`` of :func:`capped_eigsh`, ``None`` when not capped
|
|
197
|
+
|
|
198
|
+
The value of :data:`ARPACK_MAX_NCV` is used, or, when it is ``None``,
|
|
199
|
+
the environment variable ``STRUCTSOLVE_ARPACK_MAX_NCV``. Both accept
|
|
200
|
+
``'auto'``, ``0`` or ``'none'`` (no cap), or an integer of at least
|
|
201
|
+
``3``. In the ``'auto'`` mode, the default, ``ncv`` is capped at
|
|
202
|
+
:data:`DSTEQR_MAX_SIZE` when:
|
|
203
|
+
|
|
204
|
+
- :func:`dsteqr_is_faulty` is true; or
|
|
205
|
+
- :func:`lapack_is_mkl` is true and :func:`mkl_version` is unknown or
|
|
206
|
+
within :data:`MKL_FAULTY_VERSIONS`.
|
|
207
|
+
|
|
208
|
+
The detection runs once per process.
|
|
209
|
+
|
|
210
|
+
"""
|
|
211
|
+
value = ARPACK_MAX_NCV
|
|
212
|
+
if value is None:
|
|
213
|
+
value = os.environ.get('STRUCTSOLVE_ARPACK_MAX_NCV', 'auto')
|
|
214
|
+
if isinstance(value, str):
|
|
215
|
+
value = value.strip().lower()
|
|
216
|
+
if value in ('', 'auto'):
|
|
217
|
+
return _auto_max_ncv()
|
|
218
|
+
if value in ('none', 'off', 'no', 'false'):
|
|
219
|
+
return None
|
|
220
|
+
value = int(value)
|
|
221
|
+
if not value:
|
|
222
|
+
return None
|
|
223
|
+
if value < 3:
|
|
224
|
+
raise ValueError('The maximum ncv of ARPACK must be 0 (no cap) or at '
|
|
225
|
+
'least 3, got {0}'.format(value))
|
|
226
|
+
return int(value)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def arpack_ncv(k, n):
|
|
230
|
+
r"""``ncv`` of :func:`scipy.sparse.linalg.eigsh` for ``k`` eigenpairs of
|
|
231
|
+
a problem of size ``n``
|
|
232
|
+
|
|
233
|
+
SciPy's default ``min(n, max(2 k + 1, 20))``, capped at
|
|
234
|
+
:func:`arpack_max_ncv`. With a cap, ``k`` should not exceed
|
|
235
|
+
``(max_ncv - 1)//2``, see :func:`capped_eigsh`.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
ncv = min(n, max(2*k + 1, 20))
|
|
239
|
+
max_ncv = arpack_max_ncv()
|
|
240
|
+
if max_ncv is not None:
|
|
241
|
+
ncv = min(ncv, max_ncv)
|
|
242
|
+
return ncv
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def release_memory():
|
|
246
|
+
"""Collect the reference cycles of SciPy's ARPACK wrappers in the browser
|
|
247
|
+
|
|
248
|
+
The objects of :func:`scipy.sparse.linalg.eigsh` and
|
|
249
|
+
:func:`scipy.sparse.linalg.eigs` keep the SuperLU factorizations of the
|
|
250
|
+
shift-invert modes in reference cycles, freed only by the cyclic garbage
|
|
251
|
+
collector. Natively this is harmless, but the WebAssembly heap of
|
|
252
|
+
Pyodide (``sys.platform == 'emscripten'``) is exhausted before the
|
|
253
|
+
collector runs, e.g. after about ten ``eigs`` calls on a problem with
|
|
254
|
+
1,000 dofs, raising ``MemoryError``. :func:`gc.collect` is then called
|
|
255
|
+
after each ARPACK call; it does nothing on other platforms.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
if sys.platform == 'emscripten':
|
|
259
|
+
gc.collect()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def start_vector(n):
|
|
263
|
+
"""Fixed starting vector of ARPACK, uniform in ``[-1, 1]`` as ARPACK's
|
|
264
|
+
random one, such that the results are reproducible"""
|
|
265
|
+
return np.random.RandomState(42).uniform(-1., 1., n)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _deflation_shift(w, far_shift):
|
|
269
|
+
"""Shift of the passes after the first one of :func:`capped_eigsh`
|
|
270
|
+
|
|
271
|
+
The middle of the largest gap between the last five eigenvalues found,
|
|
272
|
+
sorted by increasing value, i.e. close to the next eigenvalues and away
|
|
273
|
+
from the deflated ones. ``far_shift`` when they are all equal.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
last = np.sort(w)[-5:]
|
|
277
|
+
gaps = np.diff(last)
|
|
278
|
+
if not gaps.size or gaps.max() <= 1e-8*np.abs(last).max():
|
|
279
|
+
return far_shift
|
|
280
|
+
i = np.argmax(gaps)
|
|
281
|
+
return 0.5*(last[i] + last[i + 1])
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def capped_eigsh(A, k, M, sigma, which, mode='normal', tol=0):
|
|
285
|
+
r""":func:`scipy.sparse.linalg.eigsh` with ``ncv`` from :func:`arpack_ncv`
|
|
286
|
+
|
|
287
|
+
Solves `[A]\{x\} = w [M]\{x\}` with a positive definite ``M`` about the
|
|
288
|
+
shift ``sigma``, in the shift-invert (``mode='normal'``) or Cayley
|
|
289
|
+
(``mode='cayley'``) mode, selecting the eigenvalues by increasing `w`,
|
|
290
|
+
e.g. the Cayley mode with ``which='SM'`` and ``sigma`` larger than all
|
|
291
|
+
`|w|`, or the shift-invert mode with ``which='LM'`` and ``sigma`` lower
|
|
292
|
+
than all `w`.
|
|
293
|
+
|
|
294
|
+
When ``ncv`` is capped at ``max_ncv``, see :func:`arpack_max_ncv`, and
|
|
295
|
+
``k`` is larger than ``(max_ncv - 1)//2``, i.e. 15, the eigenpairs are
|
|
296
|
+
computed in passes of at most 15 eigenpairs, each with ``ncv=max_ncv``.
|
|
297
|
+
With ``k`` close to ``ncv`` ARPACK converges very slowly, not at all, or
|
|
298
|
+
misses eigenvalues, e.g. ``k=31`` and ``ncv=32``. The first pass is a
|
|
299
|
+
call in the requested mode. The following passes use the shift-invert
|
|
300
|
+
mode about `\sigma_d`, the middle of the largest gap between the last
|
|
301
|
+
five eigenvalues found, with the eigenvectors `[V]` of the previous
|
|
302
|
+
passes deflated from the operator:
|
|
303
|
+
|
|
304
|
+
.. math::
|
|
305
|
+
|
|
306
|
+
\{y\} = ([I] - [V][V]^T[M])([A] - \sigma_d [M])^{-1}\{x\}
|
|
307
|
+
|
|
308
|
+
such that the eigenvalues of `[V]`, already found, become null and are
|
|
309
|
+
not found again. Each pass therefore returns the eigenvalues nearest
|
|
310
|
+
`\sigma_d` among those not found yet, i.e. the next ones by increasing
|
|
311
|
+
`w`. A shift close to them, instead of ``sigma``, separates them better:
|
|
312
|
+
in the study of the panels cylinders it was 30 to 45% faster and gave
|
|
313
|
+
eigenvalues 5 to 10 times more accurate. Each pass factorizes
|
|
314
|
+
`[A] - \sigma_d [M]` with SuperLU, and every call starts from
|
|
315
|
+
:func:`start_vector`.
|
|
316
|
+
|
|
317
|
+
Returns
|
|
318
|
+
-------
|
|
319
|
+
w, x : ndarray
|
|
320
|
+
The eigenvalues and the `[M]`-orthonormal eigenvectors, by
|
|
321
|
+
increasing `w` when computed in passes.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
n = A.shape[0]
|
|
325
|
+
max_ncv = arpack_max_ncv()
|
|
326
|
+
max_k = n if max_ncv is None else (max_ncv - 1)//2
|
|
327
|
+
if k <= max_k or min(n, max(2*k + 1, 20)) <= max_ncv:
|
|
328
|
+
result = _scipy_eigsh(A=A, k=k, M=M, sigma=sigma, which=which,
|
|
329
|
+
mode=mode, tol=tol, ncv=arpack_ncv(k, n),
|
|
330
|
+
v0=start_vector(n))
|
|
331
|
+
release_memory()
|
|
332
|
+
return result
|
|
333
|
+
num_passes = -(-k//max_k)
|
|
334
|
+
k_pass = -(-k//num_passes)
|
|
335
|
+
ncv = min(n, max_ncv)
|
|
336
|
+
w, V = _scipy_eigsh(A=A, k=k_pass, M=M, sigma=sigma, which=which,
|
|
337
|
+
mode=mode, tol=tol, ncv=ncv, v0=start_vector(n))
|
|
338
|
+
release_memory()
|
|
339
|
+
far_shift = -sigma if mode == 'cayley' else sigma
|
|
340
|
+
dtype = np.result_type(A.dtype, M.dtype)
|
|
341
|
+
while w.shape[0] < k:
|
|
342
|
+
order = np.argsort(w, kind='stable')
|
|
343
|
+
w, V = w[order], V[:, order]
|
|
344
|
+
sigma_d = _deflation_shift(w, far_shift)
|
|
345
|
+
with warnings.catch_warnings():
|
|
346
|
+
warnings.simplefilter("ignore")
|
|
347
|
+
lu = splu(csc_matrix(A - sigma_d*M))
|
|
348
|
+
MV = M @ V
|
|
349
|
+
|
|
350
|
+
def matvec(x, V=V, MV=MV, lu=lu):
|
|
351
|
+
y = lu.solve(np.asarray(x, dtype=dtype).ravel())
|
|
352
|
+
return y - V @ (MV.T @ y)
|
|
353
|
+
|
|
354
|
+
OPinv = LinearOperator((n, n), matvec=matvec, dtype=dtype)
|
|
355
|
+
wi, Vi = _scipy_eigsh(A=A, k=min(k_pass, k - w.shape[0]), M=M,
|
|
356
|
+
sigma=sigma_d, which='LM', mode='normal',
|
|
357
|
+
tol=tol, ncv=ncv, OPinv=OPinv,
|
|
358
|
+
v0=start_vector(n))
|
|
359
|
+
w = np.concatenate((w, wi))
|
|
360
|
+
V = np.hstack((V, Vi))
|
|
361
|
+
release_memory()
|
|
362
|
+
order = np.argsort(w, kind='stable')
|
|
363
|
+
return w[order], V[:, order]
|