blocksolver 0.8.0__py3-none-any.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.
@@ -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
+ }
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,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: blocksolver
3
+ Version: 0.8.0
4
+ Summary: Block Quasi-Minimal-Residual sparse linear solver
5
+ Home-page: https://blit.sourceforge.net
6
+ Author: Qianqian Fang
7
+ Author-email: Qianqian Fang <q.fang@neu.edu>
8
+ License: BSD-3-Clause OR LGPL-3.0-or-later OR GPL-3.0-or-later
9
+ Project-URL: Homepage, https://blit.sourceforge.net
10
+ Project-URL: Repository, https://github.com/fangq/blocksolver
11
+ Project-URL: Documentation, https://blit.sourceforge.net
12
+ Project-URL: Bug Tracker, https://github.com/fangq/blocksolver/issues
13
+ Keywords: sparse,linear-algebra,iterative-solver,qmr,fortran,umfpack
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
18
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Operating System :: MacOS
22
+ Classifier: Operating System :: Microsoft :: Windows
23
+ Classifier: Programming Language :: Fortran
24
+ Classifier: Programming Language :: Python :: 3
25
+ Classifier: Programming Language :: Python :: 3.8
26
+ Classifier: Programming Language :: Python :: 3.9
27
+ Classifier: Programming Language :: Python :: 3.10
28
+ Classifier: Programming Language :: Python :: 3.11
29
+ Classifier: Programming Language :: Python :: 3.12
30
+ Classifier: Programming Language :: Python :: 3.13
31
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
32
+ Requires-Python: >=3.8
33
+ Description-Content-Type: text/markdown
34
+ Requires-Dist: numpy>=1.20
35
+ Requires-Dist: scipy>=1.0
36
+ Provides-Extra: fast
37
+ Requires-Dist: numba>=0.50; extra == "fast"
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest>=6.0; extra == "test"
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=6.0; extra == "dev"
42
+ Requires-Dist: build; extra == "dev"
43
+ Requires-Dist: twine; extra == "dev"
44
+ Dynamic: author
45
+ Dynamic: home-page
46
+ Dynamic: requires-python
47
+
48
+ # BLIT Python Bindings
49
+
50
+ Python interface for the BLIT (Block Iterative) sparse linear solver library.
51
+
52
+ ## Installation
53
+
54
+ ### Prerequisites
55
+
56
+ - Python >= 3.8
57
+ - NumPy
58
+ - Fortran compiler (gfortran, ifort)
59
+ - UMFPACK/SuiteSparse library
60
+ - BLAS/LAPACK
61
+
62
+ On Ubuntu/Debian:
63
+ ```bash
64
+ sudo apt install gfortran libsuitesparse-dev libblas-dev liblapack-dev
65
+ ```
66
+
67
+ On macOS (Homebrew):
68
+ ```bash
69
+ brew install gcc suite-sparse openblas
70
+ ```
71
+
72
+ ### Install
73
+
74
+ ```bash
75
+ cd python
76
+ pip install .
77
+ ```
78
+
79
+ For development:
80
+ ```bash
81
+ pip install -e .
82
+ ```
83
+
84
+ ## Usage
85
+
86
+ ### Basic Usage
87
+
88
+ ```python
89
+ import numpy as np
90
+ from blocksolver import blqmr_solve
91
+
92
+ # Define sparse matrix in CSC format (0-based indexing)
93
+ Ap = np.array([0, 2, 5, 9, 10, 12], dtype=np.int32)
94
+ Ai = np.array([0, 1, 0, 2, 4, 1, 2, 3, 4, 2, 1, 4], dtype=np.int32)
95
+ Ax = np.array([2., 3., 3., -1., 4., 4., -3., 1., 2., 2., 6., 1.])
96
+ b = np.array([8.0, 45.0, -3.0, 3.0, 19.0])
97
+
98
+ # Solve
99
+ result = blqmr_solve(Ap, Ai, Ax, b, tol=1e-8)
100
+
101
+ print(f"Solution: {result.x}")
102
+ print(f"Converged: {result.converged}")
103
+ print(f"Iterations: {result.iter}")
104
+ ```
105
+
106
+ ### With SciPy Sparse Matrices
107
+
108
+ ```python
109
+ from scipy.sparse import csc_matrix
110
+ from blocksolver import blqmr_scipy
111
+
112
+ A = csc_matrix([[4, 1, 0], [1, 3, 1], [0, 1, 2]])
113
+ b = np.array([1., 2., 3.])
114
+
115
+ x, flag = blqmr_scipy(A, b, tol=1e-10)
116
+ ```
117
+
118
+ ### Multiple Right-Hand Sides
119
+
120
+ ```python
121
+ from blocksolver import blqmr_solve_multi
122
+
123
+ B = np.column_stack([b1, b2, b3]) # n x nrhs
124
+ result = blqmr_solve_multi(Ap, Ai, Ax, B)
125
+ # result.x is n x nrhs
126
+ ```
127
+
128
+ ## API Reference
129
+
130
+ ### `blqmr_solve(Ap, Ai, Ax, b, **kwargs) -> BLQMRResult`
131
+
132
+ Solve sparse system Ax = b.
133
+
134
+ **Parameters:**
135
+ - `Ap`: Column pointers (int32, length n+1)
136
+ - `Ai`: Row indices (int32, length nnz)
137
+ - `Ax`: Non-zero values (float64, length nnz)
138
+ - `b`: Right-hand side (float64, length n)
139
+ - `tol`: Convergence tolerance (default: 1e-6)
140
+ - `maxiter`: Maximum iterations (default: n)
141
+ - `droptol`: ILU drop tolerance (default: 0.001)
142
+ - `use_precond`: Use ILU preconditioner (default: True)
143
+ - `zero_based`: Input uses 0-based indexing (default: True)
144
+
145
+ **Returns:** `BLQMRResult` with attributes:
146
+ - `x`: Solution vector
147
+ - `flag`: 0=converged, 1=maxiter, 2=precond fail, 3=stagnation
148
+ - `iter`: Iterations performed
149
+ - `relres`: Relative residual
150
+ - `converged`: Boolean property
151
+
152
+ ## Testing
153
+
154
+ ```bash
155
+ make test
156
+ # or
157
+ pytest tests/ -v
158
+ ```
159
+
160
+ ## License
161
+
162
+ BSD / LGPL / GPL - see LICENSE files in parent directory.
@@ -0,0 +1,6 @@
1
+ blocksolver/__init__.py,sha256=AWJvMnXh15L1LtoUpQmrr94tf8xgZ2AWSehoxRoDPH0,1899
2
+ blocksolver/blqmr.py,sha256=n5QXKs4izQGj25uKFIzaXUA4yFfA6v18evte5R25Apk,31765
3
+ blocksolver-0.8.0.dist-info/METADATA,sha256=nSPEhi4BYIx8t6nknO1GCyJRnZDRKW8bBzalrG2PH6I,4408
4
+ blocksolver-0.8.0.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
5
+ blocksolver-0.8.0.dist-info/top_level.txt,sha256=R5OAf1b8tkdMHqg8VgcfGMg6vbYt25p9JNq2Pk_VlbA,12
6
+ blocksolver-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ blocksolver