blocksolver 0.8.0__cp38-cp38-win_amd64.whl
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.
blocksolver/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BLIT - Block Iterative Sparse Linear Solvers
|
|
3
|
+
|
|
4
|
+
A Python interface to the BLIT Fortran library for solving sparse linear systems.
|
|
5
|
+
Falls back to pure-Python implementation when Fortran extension is unavailable.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
>>> from blocksolver import blqmr_solve
|
|
10
|
+
>>> result = blqmr_solve(Ap, Ai, Ax, b)
|
|
11
|
+
>>> print(result.x, result.converged)
|
|
12
|
+
|
|
13
|
+
>>> # With scipy sparse matrices:
|
|
14
|
+
>>> from blocksolver import blqmr_scipy
|
|
15
|
+
>>> x, flag = blqmr_scipy(A, b)
|
|
16
|
+
|
|
17
|
+
>>> # Direct block QMR with custom preconditioner:
|
|
18
|
+
>>> from blocksolver import blqmr, make_preconditioner
|
|
19
|
+
>>> M1 = make_preconditioner(A, 'ilu')
|
|
20
|
+
>>> x, flag, relres, niter, resv = blqmr(A, b, M1=M1)
|
|
21
|
+
|
|
22
|
+
>>> # Check which backend is being used:
|
|
23
|
+
>>> from blocksolver import BLQMR_EXT
|
|
24
|
+
>>> print("Using Fortran" if BLQMR_EXT else "Using pure Python")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .blqmr import (
|
|
28
|
+
blqmr_solve,
|
|
29
|
+
blqmr_solve_multi,
|
|
30
|
+
blqmr_scipy,
|
|
31
|
+
blqmr,
|
|
32
|
+
BLQMRResult,
|
|
33
|
+
BLQMR_EXT,
|
|
34
|
+
qqr,
|
|
35
|
+
BLQMRWorkspace,
|
|
36
|
+
SparsePreconditioner,
|
|
37
|
+
DensePreconditioner,
|
|
38
|
+
make_preconditioner,
|
|
39
|
+
HAS_NUMBA,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__version__ = "0.8.0"
|
|
43
|
+
__author__ = "Qianqian Fang"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"blqmr_solve",
|
|
47
|
+
"blqmr_solve_multi",
|
|
48
|
+
"blqmr_scipy",
|
|
49
|
+
"blqmr",
|
|
50
|
+
"BLQMRResult",
|
|
51
|
+
"BLQMR_EXT",
|
|
52
|
+
"HAS_NUMBA",
|
|
53
|
+
"qqr",
|
|
54
|
+
"BLQMRWorkspace",
|
|
55
|
+
"SparsePreconditioner",
|
|
56
|
+
"DensePreconditioner",
|
|
57
|
+
"make_preconditioner",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test():
|
|
62
|
+
"""Run basic tests to verify installation."""
|
|
63
|
+
from .blqmr import _test
|
|
64
|
+
|
|
65
|
+
return _test()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_backend_info():
|
|
69
|
+
"""Return information about the active backend.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
dict
|
|
74
|
+
Dictionary containing:
|
|
75
|
+
- 'backend': 'binary' or 'native'
|
|
76
|
+
- 'has_fortran': bool
|
|
77
|
+
- 'has_numba': bool (for Python backend acceleration)
|
|
78
|
+
"""
|
|
79
|
+
return {
|
|
80
|
+
"backend": "binary" if BLQMR_EXT else "native",
|
|
81
|
+
"has_fortran": BLQMR_EXT,
|
|
82
|
+
"has_numba": HAS_NUMBA,
|
|
83
|
+
}
|
|
Binary file
|
|
Binary file
|
blocksolver/blqmr.py
ADDED
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BLQMR - Block Quasi-Minimal-Residual sparse linear solver.
|
|
3
|
+
|
|
4
|
+
This module provides a unified interface that uses the Fortran extension
|
|
5
|
+
when available, falling back to a pure-Python implementation otherwise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import sparse
|
|
10
|
+
from scipy.sparse.linalg import splu, spilu
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Optional, Tuple, Union
|
|
13
|
+
import warnings
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"blqmr_solve",
|
|
17
|
+
"blqmr_solve_multi",
|
|
18
|
+
"blqmr_scipy",
|
|
19
|
+
"blqmr",
|
|
20
|
+
"BLQMRResult",
|
|
21
|
+
"BLQMR_EXT",
|
|
22
|
+
"qqr",
|
|
23
|
+
"BLQMRWorkspace",
|
|
24
|
+
"SparsePreconditioner",
|
|
25
|
+
"DensePreconditioner",
|
|
26
|
+
"make_preconditioner",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Backend Detection
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
BLQMR_EXT = False
|
|
34
|
+
_blqmr = None
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from blocksolver import _blqmr
|
|
38
|
+
|
|
39
|
+
BLQMR_EXT = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
try:
|
|
42
|
+
import _blqmr
|
|
43
|
+
|
|
44
|
+
BLQMR_EXT = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# Optional Numba acceleration
|
|
49
|
+
try:
|
|
50
|
+
from numba import njit
|
|
51
|
+
|
|
52
|
+
HAS_NUMBA = True
|
|
53
|
+
except ImportError:
|
|
54
|
+
HAS_NUMBA = False
|
|
55
|
+
|
|
56
|
+
def njit(*args, **kwargs):
|
|
57
|
+
def decorator(func):
|
|
58
|
+
return func
|
|
59
|
+
|
|
60
|
+
return decorator if not args or callable(args[0]) else decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# =============================================================================
|
|
64
|
+
# Result Container
|
|
65
|
+
# =============================================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class BLQMRResult:
|
|
70
|
+
"""Result container for BLQMR solver."""
|
|
71
|
+
|
|
72
|
+
x: np.ndarray
|
|
73
|
+
flag: int
|
|
74
|
+
iter: int
|
|
75
|
+
relres: float
|
|
76
|
+
resv: Optional[np.ndarray] = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def converged(self) -> bool:
|
|
80
|
+
return self.flag == 0
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
status = "converged" if self.converged else f"flag={self.flag}"
|
|
84
|
+
backend = "fortran" if BLQMR_EXT else "python"
|
|
85
|
+
return f"BLQMRResult({status}, iter={self.iter}, relres={self.relres:.2e}, backend={backend})"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# Quasi-QR Decomposition
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@njit(cache=True)
|
|
94
|
+
def _qqr_kernel_complex(Q, R, n, m):
|
|
95
|
+
"""Numba-accelerated quasi-QR kernel for complex arrays."""
|
|
96
|
+
for j in range(m):
|
|
97
|
+
r_jj_sq = 0.0j
|
|
98
|
+
for i in range(n):
|
|
99
|
+
r_jj_sq += Q[i, j] * Q[i, j]
|
|
100
|
+
r_jj = np.sqrt(r_jj_sq)
|
|
101
|
+
R[j, j] = r_jj
|
|
102
|
+
if abs(r_jj) > 1e-14:
|
|
103
|
+
inv_r_jj = 1.0 / r_jj
|
|
104
|
+
for i in range(n):
|
|
105
|
+
Q[i, j] *= inv_r_jj
|
|
106
|
+
for k in range(j + 1, m):
|
|
107
|
+
dot = 0.0j
|
|
108
|
+
for i in range(n):
|
|
109
|
+
dot += Q[i, j] * Q[i, k]
|
|
110
|
+
R[j, k] = dot
|
|
111
|
+
for i in range(n):
|
|
112
|
+
Q[i, k] -= Q[i, j] * dot
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@njit(cache=True)
|
|
116
|
+
def _qqr_kernel_real(Q, R, n, m):
|
|
117
|
+
"""Numba-accelerated quasi-QR kernel for real arrays."""
|
|
118
|
+
for j in range(m):
|
|
119
|
+
r_jj_sq = 0.0
|
|
120
|
+
for i in range(n):
|
|
121
|
+
r_jj_sq += Q[i, j] * Q[i, j]
|
|
122
|
+
r_jj = np.sqrt(r_jj_sq)
|
|
123
|
+
R[j, j] = r_jj
|
|
124
|
+
if abs(r_jj) > 1e-14:
|
|
125
|
+
inv_r_jj = 1.0 / r_jj
|
|
126
|
+
for i in range(n):
|
|
127
|
+
Q[i, j] *= inv_r_jj
|
|
128
|
+
for k in range(j + 1, m):
|
|
129
|
+
dot = 0.0
|
|
130
|
+
for i in range(n):
|
|
131
|
+
dot += Q[i, j] * Q[i, k]
|
|
132
|
+
R[j, k] = dot
|
|
133
|
+
for i in range(n):
|
|
134
|
+
Q[i, k] -= Q[i, j] * dot
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def qqr(
|
|
138
|
+
A: np.ndarray, tol: float = 0, use_numba: bool = True
|
|
139
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
140
|
+
"""
|
|
141
|
+
Quasi-QR decomposition using modified Gram-Schmidt with quasi inner product.
|
|
142
|
+
|
|
143
|
+
For complex symmetric systems, uses <x,y>_Q = sum(x_k * y_k) without conjugation.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
A : ndarray
|
|
148
|
+
Input matrix (n x m)
|
|
149
|
+
tol : float
|
|
150
|
+
Tolerance (unused, for API compatibility)
|
|
151
|
+
use_numba : bool
|
|
152
|
+
If True and Numba available, use JIT-compiled kernel
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
Q : ndarray
|
|
157
|
+
Quasi-orthonormal columns (n x m)
|
|
158
|
+
R : ndarray
|
|
159
|
+
Upper triangular matrix (m x m)
|
|
160
|
+
"""
|
|
161
|
+
n, m = A.shape
|
|
162
|
+
is_complex = np.iscomplexobj(A)
|
|
163
|
+
dtype = np.complex128 if is_complex else np.float64
|
|
164
|
+
|
|
165
|
+
Q = np.ascontiguousarray(A, dtype=dtype)
|
|
166
|
+
R = np.zeros((m, m), dtype=dtype)
|
|
167
|
+
|
|
168
|
+
if use_numba and HAS_NUMBA:
|
|
169
|
+
if is_complex:
|
|
170
|
+
_qqr_kernel_complex(Q, R, n, m)
|
|
171
|
+
else:
|
|
172
|
+
_qqr_kernel_real(Q, R, n, m)
|
|
173
|
+
else:
|
|
174
|
+
for j in range(m):
|
|
175
|
+
qj = Q[:, j]
|
|
176
|
+
r_jj_sq = np.dot(qj, qj)
|
|
177
|
+
r_jj = np.sqrt(r_jj_sq)
|
|
178
|
+
R[j, j] = r_jj
|
|
179
|
+
if np.abs(r_jj) > 1e-14:
|
|
180
|
+
Q[:, j] *= 1.0 / r_jj
|
|
181
|
+
if j < m - 1:
|
|
182
|
+
R[j, j + 1 :] = np.dot(Q[:, j], Q[:, j + 1 :])
|
|
183
|
+
Q[:, j + 1 :] -= np.outer(Q[:, j], R[j, j + 1 :])
|
|
184
|
+
|
|
185
|
+
return Q, R
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Preconditioner Classes
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _ILUPreconditioner:
|
|
194
|
+
"""Wrapper for ILU preconditioner to work with blqmr."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, ilu_factor):
|
|
197
|
+
self.ilu = ilu_factor
|
|
198
|
+
self.shape = (ilu_factor.shape[0], ilu_factor.shape[1])
|
|
199
|
+
self.dtype = ilu_factor.L.dtype
|
|
200
|
+
|
|
201
|
+
def solve(self, b):
|
|
202
|
+
# Convert to real if needed for real ILU
|
|
203
|
+
b_solve = b.real if np.isrealobj(self.ilu.L.data) and np.iscomplexobj(b) else b
|
|
204
|
+
if b_solve.ndim == 1:
|
|
205
|
+
return self.ilu.solve(b_solve)
|
|
206
|
+
else:
|
|
207
|
+
x = np.zeros_like(b_solve)
|
|
208
|
+
for i in range(b_solve.shape[1]):
|
|
209
|
+
x[:, i] = self.ilu.solve(b_solve[:, i])
|
|
210
|
+
return x
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class SparsePreconditioner:
|
|
214
|
+
"""Efficient sparse preconditioner using LU factorization."""
|
|
215
|
+
|
|
216
|
+
__slots__ = ("lu1", "lu2", "is_two_part", "is_ilu1", "is_ilu2")
|
|
217
|
+
|
|
218
|
+
def __init__(self, M1, M2=None):
|
|
219
|
+
self.is_two_part = M2 is not None
|
|
220
|
+
self.is_ilu1 = isinstance(M1, _ILUPreconditioner)
|
|
221
|
+
self.is_ilu2 = isinstance(M2, _ILUPreconditioner) if M2 is not None else False
|
|
222
|
+
|
|
223
|
+
if M1 is not None:
|
|
224
|
+
if self.is_ilu1:
|
|
225
|
+
self.lu1 = M1
|
|
226
|
+
else:
|
|
227
|
+
M1_csc = sparse.csc_matrix(M1) if not sparse.isspmatrix_csc(M1) else M1
|
|
228
|
+
self.lu1 = splu(M1_csc)
|
|
229
|
+
else:
|
|
230
|
+
self.lu1 = None
|
|
231
|
+
|
|
232
|
+
if M2 is not None:
|
|
233
|
+
if self.is_ilu2:
|
|
234
|
+
self.lu2 = M2
|
|
235
|
+
else:
|
|
236
|
+
M2_csc = sparse.csc_matrix(M2) if not sparse.isspmatrix_csc(M2) else M2
|
|
237
|
+
self.lu2 = splu(M2_csc)
|
|
238
|
+
else:
|
|
239
|
+
self.lu2 = None
|
|
240
|
+
|
|
241
|
+
def solve(self, b: np.ndarray, out: Optional[np.ndarray] = None) -> np.ndarray:
|
|
242
|
+
if self.lu1 is None:
|
|
243
|
+
return b
|
|
244
|
+
if out is None:
|
|
245
|
+
out = np.empty_like(b)
|
|
246
|
+
|
|
247
|
+
# Handle dtype conversion for ILU with real data
|
|
248
|
+
if self.is_ilu1:
|
|
249
|
+
result = self.lu1.solve(b)
|
|
250
|
+
if out.dtype != result.dtype:
|
|
251
|
+
out = np.asarray(out, dtype=result.dtype)
|
|
252
|
+
out[:] = result
|
|
253
|
+
else:
|
|
254
|
+
if b.ndim == 1:
|
|
255
|
+
out[:] = self.lu1.solve(b)
|
|
256
|
+
else:
|
|
257
|
+
for i in range(b.shape[1]):
|
|
258
|
+
out[:, i] = self.lu1.solve(b[:, i])
|
|
259
|
+
|
|
260
|
+
if self.is_two_part:
|
|
261
|
+
if self.is_ilu2:
|
|
262
|
+
out[:] = self.lu2.solve(out)
|
|
263
|
+
else:
|
|
264
|
+
if b.ndim == 1:
|
|
265
|
+
out[:] = self.lu2.solve(out)
|
|
266
|
+
else:
|
|
267
|
+
for i in range(b.shape[1]):
|
|
268
|
+
out[:, i] = self.lu2.solve(out[:, i])
|
|
269
|
+
return out
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class DensePreconditioner:
|
|
273
|
+
"""Efficient dense preconditioner using LU factorization."""
|
|
274
|
+
|
|
275
|
+
__slots__ = ("lu1", "piv1", "lu2", "piv2", "is_two_part")
|
|
276
|
+
|
|
277
|
+
def __init__(self, M1: Optional[np.ndarray], M2: Optional[np.ndarray] = None):
|
|
278
|
+
from scipy.linalg import lu_factor
|
|
279
|
+
|
|
280
|
+
self.is_two_part = M2 is not None
|
|
281
|
+
if M1 is not None:
|
|
282
|
+
self.lu1, self.piv1 = lu_factor(M1)
|
|
283
|
+
else:
|
|
284
|
+
self.lu1 = self.piv1 = None
|
|
285
|
+
if M2 is not None:
|
|
286
|
+
self.lu2, self.piv2 = lu_factor(M2)
|
|
287
|
+
else:
|
|
288
|
+
self.lu2 = self.piv2 = None
|
|
289
|
+
|
|
290
|
+
def solve(self, b: np.ndarray, out: Optional[np.ndarray] = None) -> np.ndarray:
|
|
291
|
+
from scipy.linalg import lu_solve
|
|
292
|
+
|
|
293
|
+
if self.lu1 is None:
|
|
294
|
+
return b
|
|
295
|
+
result = lu_solve((self.lu1, self.piv1), b)
|
|
296
|
+
if self.is_two_part:
|
|
297
|
+
result = lu_solve((self.lu2, self.piv2), result)
|
|
298
|
+
if out is not None:
|
|
299
|
+
out[:] = result
|
|
300
|
+
return out
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# =============================================================================
|
|
305
|
+
# BL-QMR Workspace
|
|
306
|
+
# =============================================================================
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class BLQMRWorkspace:
|
|
310
|
+
"""Pre-allocated workspace for BL-QMR iterations."""
|
|
311
|
+
|
|
312
|
+
__slots__ = (
|
|
313
|
+
"v",
|
|
314
|
+
"vt",
|
|
315
|
+
"beta",
|
|
316
|
+
"alpha",
|
|
317
|
+
"omega",
|
|
318
|
+
"theta",
|
|
319
|
+
"Qa",
|
|
320
|
+
"Qb",
|
|
321
|
+
"Qc",
|
|
322
|
+
"Qd",
|
|
323
|
+
"zeta",
|
|
324
|
+
"zetat",
|
|
325
|
+
"eta",
|
|
326
|
+
"tau",
|
|
327
|
+
"taot",
|
|
328
|
+
"p",
|
|
329
|
+
"stacked",
|
|
330
|
+
"QQ_full",
|
|
331
|
+
"tmp0",
|
|
332
|
+
"tmp1",
|
|
333
|
+
"tmp2",
|
|
334
|
+
"Av",
|
|
335
|
+
"precond_tmp",
|
|
336
|
+
"n",
|
|
337
|
+
"m",
|
|
338
|
+
"dtype",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def __init__(self, n: int, m: int, dtype=np.float64):
|
|
342
|
+
self.n, self.m = n, m
|
|
343
|
+
self.dtype = dtype
|
|
344
|
+
self.v = np.zeros((n, m, 3), dtype=dtype)
|
|
345
|
+
self.vt = np.zeros((n, m), dtype=dtype)
|
|
346
|
+
self.beta = np.zeros((m, m, 3), dtype=dtype)
|
|
347
|
+
self.alpha = np.zeros((m, m), dtype=dtype)
|
|
348
|
+
self.omega = np.zeros((m, m, 3), dtype=dtype)
|
|
349
|
+
self.theta = np.zeros((m, m), dtype=dtype)
|
|
350
|
+
self.Qa = np.zeros((m, m, 3), dtype=dtype)
|
|
351
|
+
self.Qb = np.zeros((m, m, 3), dtype=dtype)
|
|
352
|
+
self.Qc = np.zeros((m, m, 3), dtype=dtype)
|
|
353
|
+
self.Qd = np.zeros((m, m, 3), dtype=dtype)
|
|
354
|
+
self.zeta = np.zeros((m, m), dtype=dtype)
|
|
355
|
+
self.zetat = np.zeros((m, m), dtype=dtype)
|
|
356
|
+
self.eta = np.zeros((m, m), dtype=dtype)
|
|
357
|
+
self.tau = np.zeros((m, m), dtype=dtype)
|
|
358
|
+
self.taot = np.zeros((m, m), dtype=dtype)
|
|
359
|
+
self.p = np.zeros((n, m, 3), dtype=dtype)
|
|
360
|
+
self.stacked = np.zeros((2 * m, m), dtype=dtype)
|
|
361
|
+
self.QQ_full = np.zeros((2 * m, 2 * m), dtype=dtype)
|
|
362
|
+
self.tmp0 = np.zeros((m, m), dtype=dtype)
|
|
363
|
+
self.tmp1 = np.zeros((m, m), dtype=dtype)
|
|
364
|
+
self.tmp2 = np.zeros((m, m), dtype=dtype)
|
|
365
|
+
self.Av = np.zeros((n, m), dtype=dtype)
|
|
366
|
+
self.precond_tmp = np.zeros((n, m), dtype=dtype)
|
|
367
|
+
|
|
368
|
+
def reset(self):
|
|
369
|
+
self.v.fill(0)
|
|
370
|
+
self.beta.fill(0)
|
|
371
|
+
self.omega.fill(0)
|
|
372
|
+
self.Qa.fill(0)
|
|
373
|
+
self.Qb.fill(0)
|
|
374
|
+
self.Qc.fill(0)
|
|
375
|
+
self.Qd.fill(0)
|
|
376
|
+
self.p.fill(0)
|
|
377
|
+
self.taot.fill(0)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# =============================================================================
|
|
381
|
+
# Preconditioner Factory
|
|
382
|
+
# =============================================================================
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def make_preconditioner(A: sparse.spmatrix, precond_type: str = "diag"):
|
|
386
|
+
"""
|
|
387
|
+
Create a preconditioner for iterative solvers.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
A : sparse matrix
|
|
392
|
+
System matrix
|
|
393
|
+
precond_type : str
|
|
394
|
+
'diag' or 'jacobi': Diagonal (Jacobi) preconditioner
|
|
395
|
+
'ilu' or 'ilu0': Incomplete LU
|
|
396
|
+
'ssor': Symmetric SOR
|
|
397
|
+
|
|
398
|
+
Returns
|
|
399
|
+
-------
|
|
400
|
+
M : preconditioner object
|
|
401
|
+
Preconditioner (use as M1 in blqmr)
|
|
402
|
+
"""
|
|
403
|
+
if precond_type in ("diag", "jacobi"):
|
|
404
|
+
diag = A.diagonal().copy()
|
|
405
|
+
diag[np.abs(diag) < 1e-14] = 1.0
|
|
406
|
+
return sparse.diags(diag, format="csr")
|
|
407
|
+
|
|
408
|
+
elif precond_type in ("ilu", "ilu0"):
|
|
409
|
+
try:
|
|
410
|
+
ilu = spilu(A.tocsc(), drop_tol=0, fill_factor=1)
|
|
411
|
+
return _ILUPreconditioner(ilu)
|
|
412
|
+
except Exception as e:
|
|
413
|
+
warnings.warn(f"ILU factorization failed: {e}, falling back to diagonal")
|
|
414
|
+
return make_preconditioner(A, "diag")
|
|
415
|
+
|
|
416
|
+
elif precond_type == "ssor":
|
|
417
|
+
omega = 1.0
|
|
418
|
+
D = sparse.diags(A.diagonal(), format="csr")
|
|
419
|
+
L = sparse.tril(A, k=-1, format="csr")
|
|
420
|
+
return (D + omega * L).tocsr()
|
|
421
|
+
|
|
422
|
+
else:
|
|
423
|
+
raise ValueError(f"Unknown preconditioner type: {precond_type}")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# =============================================================================
|
|
427
|
+
# Pure-Python Block QMR Solver
|
|
428
|
+
# =============================================================================
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _blqmr_python_impl(
|
|
432
|
+
A: Union[np.ndarray, sparse.spmatrix],
|
|
433
|
+
B: np.ndarray,
|
|
434
|
+
tol: float = 1e-6,
|
|
435
|
+
maxiter: Optional[int] = None,
|
|
436
|
+
M1=None,
|
|
437
|
+
M2=None,
|
|
438
|
+
x0: Optional[np.ndarray] = None,
|
|
439
|
+
residual: bool = False,
|
|
440
|
+
workspace: Optional[BLQMRWorkspace] = None,
|
|
441
|
+
) -> Tuple[np.ndarray, int, float, int, np.ndarray]:
|
|
442
|
+
"""Native Python Block QMR implementation (internal)."""
|
|
443
|
+
if B.ndim == 1:
|
|
444
|
+
B = B.reshape(-1, 1)
|
|
445
|
+
|
|
446
|
+
n, m = B.shape
|
|
447
|
+
is_complex_input = np.iscomplexobj(A) or np.iscomplexobj(B)
|
|
448
|
+
dtype = np.complex128 if is_complex_input else np.float64
|
|
449
|
+
|
|
450
|
+
if maxiter is None:
|
|
451
|
+
maxiter = min(n, 20)
|
|
452
|
+
|
|
453
|
+
if (
|
|
454
|
+
workspace is None
|
|
455
|
+
or workspace.n != n
|
|
456
|
+
or workspace.m != m
|
|
457
|
+
or workspace.dtype != dtype
|
|
458
|
+
):
|
|
459
|
+
ws = BLQMRWorkspace(n, m, dtype)
|
|
460
|
+
else:
|
|
461
|
+
ws = workspace
|
|
462
|
+
ws.reset()
|
|
463
|
+
|
|
464
|
+
# Setup preconditioner
|
|
465
|
+
if M1 is not None:
|
|
466
|
+
if isinstance(M1, _ILUPreconditioner):
|
|
467
|
+
precond = SparsePreconditioner(M1, M2)
|
|
468
|
+
elif sparse.issparse(M1):
|
|
469
|
+
precond = SparsePreconditioner(M1, M2)
|
|
470
|
+
else:
|
|
471
|
+
precond = DensePreconditioner(M1, M2)
|
|
472
|
+
else:
|
|
473
|
+
precond = None
|
|
474
|
+
|
|
475
|
+
if x0 is None:
|
|
476
|
+
x = np.zeros((n, m), dtype=dtype)
|
|
477
|
+
else:
|
|
478
|
+
x = np.asarray(x0, dtype=dtype).reshape(n, m).copy()
|
|
479
|
+
|
|
480
|
+
t3, t3n, t3p, t3nn = 0, 2, 1, 1
|
|
481
|
+
ws.Qa[:, :, t3] = np.eye(m, dtype=dtype)
|
|
482
|
+
ws.Qd[:, :, t3n] = np.eye(m, dtype=dtype)
|
|
483
|
+
ws.Qd[:, :, t3] = np.eye(m, dtype=dtype)
|
|
484
|
+
|
|
485
|
+
A_is_sparse = sparse.issparse(A)
|
|
486
|
+
if A_is_sparse:
|
|
487
|
+
ws.vt[:] = B - A @ x
|
|
488
|
+
else:
|
|
489
|
+
np.subtract(B, A @ x, out=ws.vt)
|
|
490
|
+
|
|
491
|
+
if precond is not None:
|
|
492
|
+
precond.solve(ws.vt, out=ws.vt)
|
|
493
|
+
if np.any(np.isnan(ws.vt)):
|
|
494
|
+
return x, 2, 1.0, 0, np.array([])
|
|
495
|
+
|
|
496
|
+
Q, R = qqr(ws.vt)
|
|
497
|
+
ws.v[:, :, t3p] = Q
|
|
498
|
+
ws.beta[:, :, t3p] = R
|
|
499
|
+
|
|
500
|
+
col_norms = np.sqrt(np.einsum("ij,ij->j", Q.conj(), Q).real)
|
|
501
|
+
ws.omega[:, :, t3p] = np.diag(col_norms)
|
|
502
|
+
np.matmul(ws.omega[:, :, t3p], ws.beta[:, :, t3p], out=ws.taot)
|
|
503
|
+
|
|
504
|
+
isquasires = not residual
|
|
505
|
+
if isquasires:
|
|
506
|
+
Qres0 = np.sqrt(np.einsum("ij,ij->j", ws.taot.conj(), ws.taot).real).max()
|
|
507
|
+
else:
|
|
508
|
+
omegat = Q @ np.diag(1.0 / (col_norms + 1e-16))
|
|
509
|
+
Qres0 = np.sqrt(np.einsum("ij,ij->j", ws.vt.conj(), ws.vt).real).max()
|
|
510
|
+
|
|
511
|
+
if Qres0 < 1e-16:
|
|
512
|
+
result = x.real if not is_complex_input else x
|
|
513
|
+
return result, 0, 0.0, 0, np.array([0.0])
|
|
514
|
+
|
|
515
|
+
flag, resv, Qres1, relres, iter_count = 1, np.zeros(maxiter), None, 1.0, 0
|
|
516
|
+
omegat = None if isquasires else Q @ np.diag(1.0 / (col_norms + 1e-16))
|
|
517
|
+
|
|
518
|
+
for k in range(1, maxiter + 1):
|
|
519
|
+
t3, t3n, t3p, t3nn = k % 3, (k - 1) % 3, (k + 1) % 3, (k - 2) % 3
|
|
520
|
+
|
|
521
|
+
if A_is_sparse:
|
|
522
|
+
ws.Av[:] = A @ ws.v[:, :, t3]
|
|
523
|
+
else:
|
|
524
|
+
np.matmul(A, ws.v[:, :, t3], out=ws.Av)
|
|
525
|
+
|
|
526
|
+
if precond is not None:
|
|
527
|
+
precond.solve(ws.Av, out=ws.vt)
|
|
528
|
+
ws.vt -= ws.v[:, :, t3n] @ ws.beta[:, :, t3].T
|
|
529
|
+
else:
|
|
530
|
+
np.matmul(ws.v[:, :, t3n], ws.beta[:, :, t3].T, out=ws.vt)
|
|
531
|
+
np.subtract(ws.Av, ws.vt, out=ws.vt)
|
|
532
|
+
|
|
533
|
+
np.matmul(ws.v[:, :, t3].T, ws.vt, out=ws.alpha)
|
|
534
|
+
ws.vt -= ws.v[:, :, t3] @ ws.alpha
|
|
535
|
+
|
|
536
|
+
Q, R = qqr(ws.vt)
|
|
537
|
+
ws.v[:, :, t3p] = Q
|
|
538
|
+
ws.beta[:, :, t3p] = R
|
|
539
|
+
|
|
540
|
+
col_norms = np.sqrt(np.einsum("ij,ij->j", Q.conj(), Q).real)
|
|
541
|
+
ws.omega[:, :, t3p] = np.diag(col_norms)
|
|
542
|
+
|
|
543
|
+
np.matmul(ws.omega[:, :, t3n], ws.beta[:, :, t3].T, out=ws.tmp0)
|
|
544
|
+
np.matmul(ws.Qb[:, :, t3nn], ws.tmp0, out=ws.theta)
|
|
545
|
+
|
|
546
|
+
np.matmul(ws.Qd[:, :, t3nn], ws.tmp0, out=ws.tmp1)
|
|
547
|
+
np.matmul(ws.omega[:, :, t3], ws.alpha, out=ws.tmp2)
|
|
548
|
+
np.matmul(ws.Qa[:, :, t3n], ws.tmp1, out=ws.eta)
|
|
549
|
+
ws.eta += ws.Qb[:, :, t3n] @ ws.tmp2
|
|
550
|
+
|
|
551
|
+
np.matmul(ws.Qc[:, :, t3n], ws.tmp1, out=ws.zetat)
|
|
552
|
+
ws.zetat += ws.Qd[:, :, t3n] @ ws.tmp2
|
|
553
|
+
|
|
554
|
+
ws.stacked[:m, :] = ws.zetat
|
|
555
|
+
np.matmul(ws.omega[:, :, t3p], ws.beta[:, :, t3p], out=ws.stacked[m:, :])
|
|
556
|
+
|
|
557
|
+
QQ, zeta_full = np.linalg.qr(ws.stacked, mode="complete")
|
|
558
|
+
ws.zeta[:] = zeta_full[:m, :]
|
|
559
|
+
ws.QQ_full[:] = QQ.conj().T
|
|
560
|
+
|
|
561
|
+
ws.Qa[:, :, t3] = ws.QQ_full[:m, :m]
|
|
562
|
+
ws.Qb[:, :, t3] = ws.QQ_full[:m, m : 2 * m]
|
|
563
|
+
ws.Qc[:, :, t3] = ws.QQ_full[m : 2 * m, :m]
|
|
564
|
+
ws.Qd[:, :, t3] = ws.QQ_full[m : 2 * m, m : 2 * m]
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
zeta_inv = np.linalg.inv(ws.zeta)
|
|
568
|
+
except np.linalg.LinAlgError:
|
|
569
|
+
zeta_inv = np.linalg.pinv(ws.zeta)
|
|
570
|
+
|
|
571
|
+
ws.p[:, :, t3] = (
|
|
572
|
+
ws.v[:, :, t3] - ws.p[:, :, t3n] @ ws.eta - ws.p[:, :, t3nn] @ ws.theta
|
|
573
|
+
) @ zeta_inv
|
|
574
|
+
|
|
575
|
+
np.matmul(ws.Qa[:, :, t3], ws.taot, out=ws.tau)
|
|
576
|
+
x += ws.p[:, :, t3] @ ws.tau
|
|
577
|
+
|
|
578
|
+
taot_copy = ws.taot.copy()
|
|
579
|
+
np.matmul(ws.Qc[:, :, t3], taot_copy, out=ws.taot)
|
|
580
|
+
|
|
581
|
+
if isquasires:
|
|
582
|
+
Qres = np.sqrt(np.einsum("ij,ij->j", ws.taot.conj(), ws.taot).real).max()
|
|
583
|
+
else:
|
|
584
|
+
omega_diag_inv = np.diag(1.0 / (col_norms + 1e-16))
|
|
585
|
+
omegat = (
|
|
586
|
+
omegat @ ws.Qc[:, :, t3].conj().T
|
|
587
|
+
+ ws.v[:, :, t3p] @ (ws.Qd[:, :, t3] @ omega_diag_inv).conj().T
|
|
588
|
+
)
|
|
589
|
+
R_resid = omegat @ ws.taot
|
|
590
|
+
Qres = np.sqrt(np.einsum("ij,ij->j", R_resid.conj(), R_resid).real).max()
|
|
591
|
+
|
|
592
|
+
resv[k - 1] = Qres
|
|
593
|
+
|
|
594
|
+
if Qres1 is not None and Qres == Qres1:
|
|
595
|
+
flag, iter_count = 3, k
|
|
596
|
+
break
|
|
597
|
+
|
|
598
|
+
Qres1, relres, iter_count = Qres, Qres / Qres0, k
|
|
599
|
+
|
|
600
|
+
if relres <= tol:
|
|
601
|
+
flag = 0
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
resv = resv[:iter_count]
|
|
605
|
+
result = x.real if not is_complex_input else x
|
|
606
|
+
return result, flag, relres, iter_count, resv
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# =============================================================================
|
|
610
|
+
# High-Level Solver Interface
|
|
611
|
+
# =============================================================================
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def blqmr_solve(
|
|
615
|
+
Ap: np.ndarray,
|
|
616
|
+
Ai: np.ndarray,
|
|
617
|
+
Ax: np.ndarray,
|
|
618
|
+
b: np.ndarray,
|
|
619
|
+
*,
|
|
620
|
+
x0: Optional[np.ndarray] = None,
|
|
621
|
+
tol: float = 1e-6,
|
|
622
|
+
maxiter: Optional[int] = None,
|
|
623
|
+
droptol: float = 0.001,
|
|
624
|
+
use_precond: bool = True,
|
|
625
|
+
zero_based: bool = True,
|
|
626
|
+
) -> BLQMRResult:
|
|
627
|
+
"""
|
|
628
|
+
Solve sparse linear system Ax = b using Block QMR algorithm.
|
|
629
|
+
|
|
630
|
+
Uses Fortran extension if available, otherwise falls back to pure Python.
|
|
631
|
+
|
|
632
|
+
Parameters
|
|
633
|
+
----------
|
|
634
|
+
Ap : ndarray of int32
|
|
635
|
+
Column pointers for CSC format. Length n+1.
|
|
636
|
+
Ai : ndarray of int32
|
|
637
|
+
Row indices for CSC format. Length nnz.
|
|
638
|
+
Ax : ndarray of float64
|
|
639
|
+
Non-zero values. Length nnz.
|
|
640
|
+
b : ndarray of float64
|
|
641
|
+
Right-hand side vector. Length n.
|
|
642
|
+
x0 : ndarray, optional
|
|
643
|
+
Initial guess.
|
|
644
|
+
tol : float, default 1e-6
|
|
645
|
+
Convergence tolerance for relative residual.
|
|
646
|
+
maxiter : int, optional
|
|
647
|
+
Maximum iterations. Default is n.
|
|
648
|
+
droptol : float, default 0.001
|
|
649
|
+
Drop tolerance for ILU preconditioner (Fortran only).
|
|
650
|
+
use_precond : bool, default True
|
|
651
|
+
Whether to use ILU preconditioning.
|
|
652
|
+
zero_based : bool, default True
|
|
653
|
+
If True, Ap and Ai use 0-based indexing (Python/C convention).
|
|
654
|
+
If False, uses 1-based indexing (Fortran convention).
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
BLQMRResult
|
|
659
|
+
Result object containing solution and convergence info.
|
|
660
|
+
"""
|
|
661
|
+
n = len(Ap) - 1
|
|
662
|
+
|
|
663
|
+
if maxiter is None:
|
|
664
|
+
maxiter = n
|
|
665
|
+
|
|
666
|
+
if BLQMR_EXT:
|
|
667
|
+
return _blqmr_solve_fortran(
|
|
668
|
+
Ap,
|
|
669
|
+
Ai,
|
|
670
|
+
Ax,
|
|
671
|
+
b,
|
|
672
|
+
x0=x0,
|
|
673
|
+
tol=tol,
|
|
674
|
+
maxiter=maxiter,
|
|
675
|
+
droptol=droptol,
|
|
676
|
+
use_precond=use_precond,
|
|
677
|
+
zero_based=zero_based,
|
|
678
|
+
)
|
|
679
|
+
else:
|
|
680
|
+
return _blqmr_solve_native_csc(
|
|
681
|
+
Ap,
|
|
682
|
+
Ai,
|
|
683
|
+
Ax,
|
|
684
|
+
b,
|
|
685
|
+
x0=x0,
|
|
686
|
+
tol=tol,
|
|
687
|
+
maxiter=maxiter,
|
|
688
|
+
use_precond=use_precond,
|
|
689
|
+
zero_based=zero_based,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _blqmr_solve_fortran(
|
|
694
|
+
Ap, Ai, Ax, b, *, x0, tol, maxiter, droptol, use_precond, zero_based
|
|
695
|
+
) -> BLQMRResult:
|
|
696
|
+
"""Fortran backend for blqmr_solve."""
|
|
697
|
+
n = len(Ap) - 1
|
|
698
|
+
nnz = len(Ax)
|
|
699
|
+
|
|
700
|
+
Ap = np.asfortranarray(Ap, dtype=np.int32)
|
|
701
|
+
Ai = np.asfortranarray(Ai, dtype=np.int32)
|
|
702
|
+
Ax = np.asfortranarray(Ax, dtype=np.float64)
|
|
703
|
+
b = np.asfortranarray(b, dtype=np.float64)
|
|
704
|
+
|
|
705
|
+
if len(Ai) != nnz:
|
|
706
|
+
raise ValueError(f"Ai length ({len(Ai)}) must match Ax length ({nnz})")
|
|
707
|
+
if len(b) != n:
|
|
708
|
+
raise ValueError(f"b length ({len(b)}) must match matrix size ({n})")
|
|
709
|
+
|
|
710
|
+
if zero_based:
|
|
711
|
+
Ap = Ap + 1
|
|
712
|
+
Ai = Ai + 1
|
|
713
|
+
|
|
714
|
+
dopcond = 1 if use_precond else 0
|
|
715
|
+
|
|
716
|
+
x, flag, niter, relres = _blqmr.blqmr_solve_real(
|
|
717
|
+
n, nnz, Ap, Ai, Ax, b, maxiter, tol, droptol, dopcond
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
return BLQMRResult(
|
|
721
|
+
x=x.copy(), flag=int(flag), iter=int(niter), relres=float(relres)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _blqmr_solve_native_csc(
|
|
726
|
+
Ap, Ai, Ax, b, *, x0, tol, maxiter, use_precond, zero_based
|
|
727
|
+
) -> BLQMRResult:
|
|
728
|
+
"""Native Python backend for blqmr_solve with CSC input."""
|
|
729
|
+
n = len(Ap) - 1
|
|
730
|
+
|
|
731
|
+
if not zero_based:
|
|
732
|
+
Ap = Ap - 1
|
|
733
|
+
Ai = Ai - 1
|
|
734
|
+
|
|
735
|
+
A = sparse.csc_matrix((Ax, Ai, Ap), shape=(n, n))
|
|
736
|
+
|
|
737
|
+
M1 = None
|
|
738
|
+
if use_precond:
|
|
739
|
+
try:
|
|
740
|
+
M1 = make_preconditioner(A, "ilu")
|
|
741
|
+
except Exception:
|
|
742
|
+
M1 = make_preconditioner(A, "diag")
|
|
743
|
+
|
|
744
|
+
x, flag, relres, niter, resv = _blqmr_python_impl(
|
|
745
|
+
A, b, tol=tol, maxiter=maxiter, M1=M1, x0=x0
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if x.ndim > 1:
|
|
749
|
+
x = x.ravel()
|
|
750
|
+
|
|
751
|
+
return BLQMRResult(x=x, flag=flag, iter=niter, relres=relres, resv=resv)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def blqmr_solve_multi(
|
|
755
|
+
Ap: np.ndarray,
|
|
756
|
+
Ai: np.ndarray,
|
|
757
|
+
Ax: np.ndarray,
|
|
758
|
+
B: np.ndarray,
|
|
759
|
+
*,
|
|
760
|
+
tol: float = 1e-6,
|
|
761
|
+
maxiter: Optional[int] = None,
|
|
762
|
+
droptol: float = 0.001,
|
|
763
|
+
use_precond: bool = True,
|
|
764
|
+
zero_based: bool = True,
|
|
765
|
+
) -> BLQMRResult:
|
|
766
|
+
"""
|
|
767
|
+
Solve sparse linear system AX = B with multiple right-hand sides.
|
|
768
|
+
|
|
769
|
+
Uses Fortran extension if available, otherwise falls back to pure Python.
|
|
770
|
+
"""
|
|
771
|
+
n = len(Ap) - 1
|
|
772
|
+
|
|
773
|
+
if maxiter is None:
|
|
774
|
+
maxiter = n
|
|
775
|
+
|
|
776
|
+
if BLQMR_EXT:
|
|
777
|
+
return _blqmr_solve_multi_fortran(
|
|
778
|
+
Ap,
|
|
779
|
+
Ai,
|
|
780
|
+
Ax,
|
|
781
|
+
B,
|
|
782
|
+
tol=tol,
|
|
783
|
+
maxiter=maxiter,
|
|
784
|
+
droptol=droptol,
|
|
785
|
+
use_precond=use_precond,
|
|
786
|
+
zero_based=zero_based,
|
|
787
|
+
)
|
|
788
|
+
else:
|
|
789
|
+
return _blqmr_solve_multi_native(
|
|
790
|
+
Ap,
|
|
791
|
+
Ai,
|
|
792
|
+
Ax,
|
|
793
|
+
B,
|
|
794
|
+
tol=tol,
|
|
795
|
+
maxiter=maxiter,
|
|
796
|
+
use_precond=use_precond,
|
|
797
|
+
zero_based=zero_based,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _blqmr_solve_multi_fortran(
|
|
802
|
+
Ap, Ai, Ax, B, *, tol, maxiter, droptol, use_precond, zero_based
|
|
803
|
+
) -> BLQMRResult:
|
|
804
|
+
"""Fortran backend for blqmr_solve_multi."""
|
|
805
|
+
n = len(Ap) - 1
|
|
806
|
+
nnz = len(Ax)
|
|
807
|
+
|
|
808
|
+
Ap = np.asfortranarray(Ap, dtype=np.int32)
|
|
809
|
+
Ai = np.asfortranarray(Ai, dtype=np.int32)
|
|
810
|
+
Ax = np.asfortranarray(Ax, dtype=np.float64)
|
|
811
|
+
B = np.asfortranarray(B, dtype=np.float64)
|
|
812
|
+
|
|
813
|
+
if B.ndim == 1:
|
|
814
|
+
B = B.reshape(-1, 1, order="F")
|
|
815
|
+
nrhs = B.shape[1]
|
|
816
|
+
|
|
817
|
+
if zero_based:
|
|
818
|
+
Ap = Ap + 1
|
|
819
|
+
Ai = Ai + 1
|
|
820
|
+
|
|
821
|
+
dopcond = 1 if use_precond else 0
|
|
822
|
+
|
|
823
|
+
X, flag, niter, relres = _blqmr.blqmr_solve_real_multi(
|
|
824
|
+
n, nnz, nrhs, Ap, Ai, Ax, B, maxiter, tol, droptol, dopcond
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
return BLQMRResult(
|
|
828
|
+
x=X.copy(), flag=int(flag), iter=int(niter), relres=float(relres)
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _blqmr_solve_multi_native(
|
|
833
|
+
Ap, Ai, Ax, B, *, tol, maxiter, use_precond, zero_based
|
|
834
|
+
) -> BLQMRResult:
|
|
835
|
+
"""Native Python backend for blqmr_solve_multi."""
|
|
836
|
+
n = len(Ap) - 1
|
|
837
|
+
|
|
838
|
+
if not zero_based:
|
|
839
|
+
Ap = Ap - 1
|
|
840
|
+
Ai = Ai - 1
|
|
841
|
+
|
|
842
|
+
A = sparse.csc_matrix((Ax, Ai, Ap), shape=(n, n))
|
|
843
|
+
|
|
844
|
+
M1 = None
|
|
845
|
+
if use_precond:
|
|
846
|
+
try:
|
|
847
|
+
M1 = make_preconditioner(A, "ilu")
|
|
848
|
+
except Exception:
|
|
849
|
+
M1 = make_preconditioner(A, "diag")
|
|
850
|
+
|
|
851
|
+
if B.ndim == 1:
|
|
852
|
+
B = B.reshape(-1, 1)
|
|
853
|
+
|
|
854
|
+
x, flag, relres, niter, resv = _blqmr_python_impl(
|
|
855
|
+
A, B, tol=tol, maxiter=maxiter, M1=M1
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
return BLQMRResult(x=x, flag=flag, iter=niter, relres=relres, resv=resv)
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def blqmr_scipy(
|
|
862
|
+
A,
|
|
863
|
+
b: np.ndarray,
|
|
864
|
+
x0: Optional[np.ndarray] = None,
|
|
865
|
+
tol: float = 1e-6,
|
|
866
|
+
maxiter: Optional[int] = None,
|
|
867
|
+
M=None,
|
|
868
|
+
**kwargs,
|
|
869
|
+
) -> Tuple[np.ndarray, int]:
|
|
870
|
+
"""
|
|
871
|
+
SciPy-compatible interface for BLQMR solver.
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
A : sparse matrix or ndarray
|
|
876
|
+
System matrix
|
|
877
|
+
b : ndarray
|
|
878
|
+
Right-hand side vector
|
|
879
|
+
x0 : ndarray, optional
|
|
880
|
+
Initial guess
|
|
881
|
+
tol : float
|
|
882
|
+
Convergence tolerance
|
|
883
|
+
maxiter : int, optional
|
|
884
|
+
Maximum iterations
|
|
885
|
+
M : preconditioner, optional
|
|
886
|
+
Preconditioner (used as M1 for Python backend)
|
|
887
|
+
**kwargs
|
|
888
|
+
Additional arguments passed to blqmr()
|
|
889
|
+
|
|
890
|
+
Returns
|
|
891
|
+
-------
|
|
892
|
+
x : ndarray
|
|
893
|
+
Solution vector
|
|
894
|
+
flag : int
|
|
895
|
+
Convergence flag (0 = converged)
|
|
896
|
+
"""
|
|
897
|
+
result = blqmr(A, b, x0=x0, tol=tol, maxiter=maxiter, M1=M, **kwargs)
|
|
898
|
+
return result.x, result.flag
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def blqmr(
|
|
902
|
+
A: Union[np.ndarray, sparse.spmatrix],
|
|
903
|
+
B: np.ndarray,
|
|
904
|
+
tol: float = 1e-6,
|
|
905
|
+
maxiter: Optional[int] = None,
|
|
906
|
+
M1=None,
|
|
907
|
+
M2=None,
|
|
908
|
+
x0: Optional[np.ndarray] = None,
|
|
909
|
+
residual: bool = False,
|
|
910
|
+
workspace: Optional[BLQMRWorkspace] = None,
|
|
911
|
+
droptol: float = 0.001,
|
|
912
|
+
use_precond: bool = True,
|
|
913
|
+
) -> BLQMRResult:
|
|
914
|
+
"""
|
|
915
|
+
Block Quasi-Minimal-Residual (BL-QMR) solver - main interface.
|
|
916
|
+
|
|
917
|
+
Uses Fortran extension if available, otherwise falls back to pure Python.
|
|
918
|
+
|
|
919
|
+
Parameters
|
|
920
|
+
----------
|
|
921
|
+
A : ndarray or sparse matrix
|
|
922
|
+
Symmetric n x n matrix (can be complex)
|
|
923
|
+
B : ndarray
|
|
924
|
+
Right-hand side vector/matrix (n,) or (n x m)
|
|
925
|
+
tol : float
|
|
926
|
+
Convergence tolerance (default: 1e-6)
|
|
927
|
+
maxiter : int, optional
|
|
928
|
+
Maximum iterations (default: n for Fortran, min(n, 20) for Python)
|
|
929
|
+
M1, M2 : preconditioner, optional
|
|
930
|
+
Preconditioner M = M1 @ M2 (Python backend only)
|
|
931
|
+
x0 : ndarray, optional
|
|
932
|
+
Initial guess
|
|
933
|
+
residual : bool
|
|
934
|
+
If True, use true residual for convergence (Python backend only)
|
|
935
|
+
workspace : BLQMRWorkspace, optional
|
|
936
|
+
Pre-allocated workspace (Python backend only)
|
|
937
|
+
droptol : float, default 0.001
|
|
938
|
+
Drop tolerance for ILU preconditioner (Fortran backend only)
|
|
939
|
+
use_precond : bool, default True
|
|
940
|
+
Whether to use ILU preconditioning (Fortran backend only)
|
|
941
|
+
|
|
942
|
+
Returns
|
|
943
|
+
-------
|
|
944
|
+
BLQMRResult
|
|
945
|
+
Result object containing:
|
|
946
|
+
- x: Solution array
|
|
947
|
+
- flag: 0 = converged, 1 = max iterations, 2 = preconditioner singular, 3 = stagnated
|
|
948
|
+
- iter: Number of iterations
|
|
949
|
+
- relres: Final relative residual
|
|
950
|
+
- resv: Residual history (Python backend only)
|
|
951
|
+
"""
|
|
952
|
+
if BLQMR_EXT:
|
|
953
|
+
return _blqmr_fortran(
|
|
954
|
+
A,
|
|
955
|
+
B,
|
|
956
|
+
tol=tol,
|
|
957
|
+
maxiter=maxiter,
|
|
958
|
+
x0=x0,
|
|
959
|
+
droptol=droptol,
|
|
960
|
+
use_precond=use_precond,
|
|
961
|
+
)
|
|
962
|
+
else:
|
|
963
|
+
return _blqmr_native(
|
|
964
|
+
A,
|
|
965
|
+
B,
|
|
966
|
+
tol=tol,
|
|
967
|
+
maxiter=maxiter,
|
|
968
|
+
M1=M1,
|
|
969
|
+
M2=M2,
|
|
970
|
+
x0=x0,
|
|
971
|
+
residual=residual,
|
|
972
|
+
workspace=workspace,
|
|
973
|
+
use_precond=use_precond,
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _blqmr_fortran(
|
|
978
|
+
A: Union[np.ndarray, sparse.spmatrix],
|
|
979
|
+
B: np.ndarray,
|
|
980
|
+
*,
|
|
981
|
+
tol: float,
|
|
982
|
+
maxiter: Optional[int],
|
|
983
|
+
x0: Optional[np.ndarray],
|
|
984
|
+
droptol: float,
|
|
985
|
+
use_precond: bool,
|
|
986
|
+
) -> BLQMRResult:
|
|
987
|
+
"""Fortran backend for blqmr()."""
|
|
988
|
+
A_csc = sparse.csc_matrix(A)
|
|
989
|
+
Ap = A_csc.indptr.astype(np.int32)
|
|
990
|
+
Ai = A_csc.indices.astype(np.int32)
|
|
991
|
+
Ax = A_csc.data.astype(np.float64)
|
|
992
|
+
|
|
993
|
+
n = A_csc.shape[0]
|
|
994
|
+
nnz = len(Ax)
|
|
995
|
+
|
|
996
|
+
if maxiter is None:
|
|
997
|
+
maxiter = n
|
|
998
|
+
|
|
999
|
+
# Convert to Fortran format
|
|
1000
|
+
Ap_f = np.asfortranarray(Ap + 1, dtype=np.int32) # 1-based
|
|
1001
|
+
Ai_f = np.asfortranarray(Ai + 1, dtype=np.int32) # 1-based
|
|
1002
|
+
Ax_f = np.asfortranarray(Ax, dtype=np.float64)
|
|
1003
|
+
|
|
1004
|
+
dopcond = 1 if use_precond else 0
|
|
1005
|
+
|
|
1006
|
+
if B.ndim == 1 or (B.ndim == 2 and B.shape[1] == 1):
|
|
1007
|
+
b = np.asfortranarray(B.ravel(), dtype=np.float64)
|
|
1008
|
+
x, flag, niter, relres = _blqmr.blqmr_solve_real(
|
|
1009
|
+
n, nnz, Ap_f, Ai_f, Ax_f, b, maxiter, tol, droptol, dopcond
|
|
1010
|
+
)
|
|
1011
|
+
return BLQMRResult(
|
|
1012
|
+
x=x.copy(), flag=int(flag), iter=int(niter), relres=float(relres)
|
|
1013
|
+
)
|
|
1014
|
+
else:
|
|
1015
|
+
B_f = np.asfortranarray(B, dtype=np.float64)
|
|
1016
|
+
nrhs = B_f.shape[1]
|
|
1017
|
+
X, flag, niter, relres = _blqmr.blqmr_solve_real_multi(
|
|
1018
|
+
n, nnz, nrhs, Ap_f, Ai_f, Ax_f, B_f, maxiter, tol, droptol, dopcond
|
|
1019
|
+
)
|
|
1020
|
+
return BLQMRResult(
|
|
1021
|
+
x=X.copy(), flag=int(flag), iter=int(niter), relres=float(relres)
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _blqmr_native(
|
|
1026
|
+
A: Union[np.ndarray, sparse.spmatrix],
|
|
1027
|
+
B: np.ndarray,
|
|
1028
|
+
*,
|
|
1029
|
+
tol: float,
|
|
1030
|
+
maxiter: Optional[int],
|
|
1031
|
+
M1,
|
|
1032
|
+
M2,
|
|
1033
|
+
x0: Optional[np.ndarray],
|
|
1034
|
+
residual: bool,
|
|
1035
|
+
workspace: Optional[BLQMRWorkspace],
|
|
1036
|
+
use_precond: bool,
|
|
1037
|
+
) -> BLQMRResult:
|
|
1038
|
+
"""Native Python backend for blqmr()."""
|
|
1039
|
+
# Auto-create preconditioner if requested and not provided
|
|
1040
|
+
if use_precond and M1 is None:
|
|
1041
|
+
A_sp = sparse.csc_matrix(A) if not sparse.issparse(A) else A
|
|
1042
|
+
try:
|
|
1043
|
+
M1 = make_preconditioner(A_sp, "ilu")
|
|
1044
|
+
except Exception:
|
|
1045
|
+
M1 = make_preconditioner(A_sp, "diag")
|
|
1046
|
+
|
|
1047
|
+
x, flag, relres, niter, resv = _blqmr_python_impl(
|
|
1048
|
+
A,
|
|
1049
|
+
B,
|
|
1050
|
+
tol=tol,
|
|
1051
|
+
maxiter=maxiter,
|
|
1052
|
+
M1=M1,
|
|
1053
|
+
M2=M2,
|
|
1054
|
+
x0=x0,
|
|
1055
|
+
residual=residual,
|
|
1056
|
+
workspace=workspace,
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
# Flatten x if single RHS
|
|
1060
|
+
if x.ndim > 1 and x.shape[1] == 1:
|
|
1061
|
+
x = x.ravel()
|
|
1062
|
+
|
|
1063
|
+
return BLQMRResult(x=x, flag=flag, iter=niter, relres=relres, resv=resv)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
# =============================================================================
|
|
1067
|
+
# Test Function
|
|
1068
|
+
# =============================================================================
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _test():
|
|
1072
|
+
"""Quick test to verify installation."""
|
|
1073
|
+
print("BLIT BLQMR Test")
|
|
1074
|
+
print("=" * 40)
|
|
1075
|
+
print(f"Fortran backend available: {BLQMR_EXT}")
|
|
1076
|
+
print(f"Numba acceleration available: {HAS_NUMBA}")
|
|
1077
|
+
print(f"Using backend: {'Fortran' if BLQMR_EXT else 'Pure Python'}")
|
|
1078
|
+
print()
|
|
1079
|
+
|
|
1080
|
+
# Build test matrix from CSC components
|
|
1081
|
+
n = 5
|
|
1082
|
+
Ap = np.array([0, 2, 5, 9, 10, 12], dtype=np.int32)
|
|
1083
|
+
Ai = np.array([0, 1, 0, 2, 4, 1, 2, 3, 4, 2, 1, 4], dtype=np.int32)
|
|
1084
|
+
Ax = np.array(
|
|
1085
|
+
[2.0, 3.0, 3.0, -1.0, 4.0, 4.0, -3.0, 1.0, 2.0, 2.0, 6.0, 1.0], dtype=np.float64
|
|
1086
|
+
)
|
|
1087
|
+
b = np.array([8.0, 45.0, -3.0, 3.0, 19.0], dtype=np.float64)
|
|
1088
|
+
|
|
1089
|
+
# Create sparse matrix
|
|
1090
|
+
A = sparse.csc_matrix((Ax, Ai, Ap), shape=(n, n))
|
|
1091
|
+
|
|
1092
|
+
print(f"Matrix: {n}x{n}, nnz={len(Ax)}")
|
|
1093
|
+
print(f"b: {b}")
|
|
1094
|
+
print("\nCalling blqmr()...")
|
|
1095
|
+
|
|
1096
|
+
result = blqmr(A, b, tol=1e-8)
|
|
1097
|
+
|
|
1098
|
+
print(f"\n{result}")
|
|
1099
|
+
print(f"Solution: {result.x}")
|
|
1100
|
+
|
|
1101
|
+
# Verify
|
|
1102
|
+
res = np.linalg.norm(A @ result.x - b)
|
|
1103
|
+
print(f"||Ax - b|| = {res:.2e}")
|
|
1104
|
+
|
|
1105
|
+
return result.converged
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
if __name__ == "__main__":
|
|
1109
|
+
_test()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: blocksolver
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Block Quasi-Minimal-Residual sparse linear solver
|
|
5
|
+
Keywords: sparse,linear-algebra,iterative-solver,qmr,fortran,umfpack
|
|
6
|
+
Author-Email: Qianqian Fang <q.fang@neu.edu>
|
|
7
|
+
License: BSD-3-Clause OR LGPL-3.0-or-later OR GPL-3.0-or-later
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Programming Language :: Fortran
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
26
|
+
Project-URL: Homepage, https://blit.sourceforge.net
|
|
27
|
+
Project-URL: Repository, https://github.com/fangq/blocksolver
|
|
28
|
+
Project-URL: Documentation, https://blit.sourceforge.net
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/fangq/blocksolver/issues
|
|
30
|
+
Requires-Python: >=3.8
|
|
31
|
+
Requires-Dist: numpy>=1.20
|
|
32
|
+
Requires-Dist: scipy>=1.0
|
|
33
|
+
Provides-Extra: fast
|
|
34
|
+
Requires-Dist: numba>=0.50; extra == "fast"
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest>=6.0; extra == "test"
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
39
|
+
Requires-Dist: build; extra == "dev"
|
|
40
|
+
Requires-Dist: twine; extra == "dev"
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# BLIT Python Bindings
|
|
44
|
+
|
|
45
|
+
Python interface for the BLIT (Block Iterative) sparse linear solver library.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### Prerequisites
|
|
50
|
+
|
|
51
|
+
- Python >= 3.8
|
|
52
|
+
- NumPy
|
|
53
|
+
- Fortran compiler (gfortran, ifort)
|
|
54
|
+
- UMFPACK/SuiteSparse library
|
|
55
|
+
- BLAS/LAPACK
|
|
56
|
+
|
|
57
|
+
On Ubuntu/Debian:
|
|
58
|
+
```bash
|
|
59
|
+
sudo apt install gfortran libsuitesparse-dev libblas-dev liblapack-dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On macOS (Homebrew):
|
|
63
|
+
```bash
|
|
64
|
+
brew install gcc suite-sparse openblas
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd python
|
|
71
|
+
pip install .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For development:
|
|
75
|
+
```bash
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
### Basic Usage
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
from blocksolver import blqmr_solve
|
|
86
|
+
|
|
87
|
+
# Define sparse matrix in CSC format (0-based indexing)
|
|
88
|
+
Ap = np.array([0, 2, 5, 9, 10, 12], dtype=np.int32)
|
|
89
|
+
Ai = np.array([0, 1, 0, 2, 4, 1, 2, 3, 4, 2, 1, 4], dtype=np.int32)
|
|
90
|
+
Ax = np.array([2., 3., 3., -1., 4., 4., -3., 1., 2., 2., 6., 1.])
|
|
91
|
+
b = np.array([8.0, 45.0, -3.0, 3.0, 19.0])
|
|
92
|
+
|
|
93
|
+
# Solve
|
|
94
|
+
result = blqmr_solve(Ap, Ai, Ax, b, tol=1e-8)
|
|
95
|
+
|
|
96
|
+
print(f"Solution: {result.x}")
|
|
97
|
+
print(f"Converged: {result.converged}")
|
|
98
|
+
print(f"Iterations: {result.iter}")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### With SciPy Sparse Matrices
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from scipy.sparse import csc_matrix
|
|
105
|
+
from blocksolver import blqmr_scipy
|
|
106
|
+
|
|
107
|
+
A = csc_matrix([[4, 1, 0], [1, 3, 1], [0, 1, 2]])
|
|
108
|
+
b = np.array([1., 2., 3.])
|
|
109
|
+
|
|
110
|
+
x, flag = blqmr_scipy(A, b, tol=1e-10)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Multiple Right-Hand Sides
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from blocksolver import blqmr_solve_multi
|
|
117
|
+
|
|
118
|
+
B = np.column_stack([b1, b2, b3]) # n x nrhs
|
|
119
|
+
result = blqmr_solve_multi(Ap, Ai, Ax, B)
|
|
120
|
+
# result.x is n x nrhs
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### `blqmr_solve(Ap, Ai, Ax, b, **kwargs) -> BLQMRResult`
|
|
126
|
+
|
|
127
|
+
Solve sparse system Ax = b.
|
|
128
|
+
|
|
129
|
+
**Parameters:**
|
|
130
|
+
- `Ap`: Column pointers (int32, length n+1)
|
|
131
|
+
- `Ai`: Row indices (int32, length nnz)
|
|
132
|
+
- `Ax`: Non-zero values (float64, length nnz)
|
|
133
|
+
- `b`: Right-hand side (float64, length n)
|
|
134
|
+
- `tol`: Convergence tolerance (default: 1e-6)
|
|
135
|
+
- `maxiter`: Maximum iterations (default: n)
|
|
136
|
+
- `droptol`: ILU drop tolerance (default: 0.001)
|
|
137
|
+
- `use_precond`: Use ILU preconditioner (default: True)
|
|
138
|
+
- `zero_based`: Input uses 0-based indexing (default: True)
|
|
139
|
+
|
|
140
|
+
**Returns:** `BLQMRResult` with attributes:
|
|
141
|
+
- `x`: Solution vector
|
|
142
|
+
- `flag`: 0=converged, 1=maxiter, 2=precond fail, 3=stagnation
|
|
143
|
+
- `iter`: Iterations performed
|
|
144
|
+
- `relres`: Relative residual
|
|
145
|
+
- `converged`: Boolean property
|
|
146
|
+
|
|
147
|
+
## Testing
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
make test
|
|
151
|
+
# or
|
|
152
|
+
pytest tests/ -v
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
BSD / LGPL / GPL - see LICENSE files in parent directory.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
blocksolver-0.8.0.dist-info/METADATA,sha256=cVuOEGnvH6q9AkdgI6atXNUUTfgRTWgGS3gvp_HRBHU,4286
|
|
2
|
+
blocksolver-0.8.0.dist-info/WHEEL,sha256=vIXzP6jLUy4sdmrQppnovVBqmdfNCkEM0I7EHxeJ-zs,83
|
|
3
|
+
blocksolver/_blqmr.cp38-win_amd64.pyd,sha256=7QPbnZJGTKgdZxdetdchn-VSOcxaRctyindGAb-jBjY,432815
|
|
4
|
+
blocksolver/_blqmr.cp38-win_amd64.dll.a,sha256=PJ8X80yp-iR2yXbjoqT0cy2dtxG8TfzwdNaRvMggI90,1696
|
|
5
|
+
blocksolver/__init__.py,sha256=N_xYxL3DWfy9uKBL2pVCwRwuNyDsu2faJsYNaW26_yk,1982
|
|
6
|
+
blocksolver/blqmr.py,sha256=NT0R7Rydvlj5DadbXn0IhVYHGKI3qiPHgD0f6q7CHHY,32874
|
|
7
|
+
blocksolver-0.8.0.dist-info/RECORD,,
|