blocksolver 0.8.0__cp39-cp39-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.
@@ -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=8AdrFzOtKQ6LLJ-VyqCU3y1iN8N--fMXYqrdkeTKDn0,83
3
+ blocksolver/_blqmr.cp39-win_amd64.pyd,sha256=z6p0uyp2fHloOWv3a77NgNeBJdN74HuTzubMoza30w4,438963
4
+ blocksolver/_blqmr.cp39-win_amd64.dll.a,sha256=PG88J_rNgqkfMMrpzE74tEVP3nEj2TEhR_xvymYYHto,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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: meson
3
+ Root-Is-Purelib: false
4
+ Tag: cp39-cp39-win_amd64