structsolve 0.5.3__tar.gz → 0.6.1__tar.gz

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