
Linear Solvers
- 16 installs
- 869 repo stars
- Updated June 8, 2026
- beita6969/scienceclaw
Linear-solvers is a Claude skill that selects and configures direct or iterative solvers for linear systems Ax=b and diagnoses convergence in methods like GMRES, CG, and BiCGSTAB.
About
Linear-solvers helps select and configure a solver for linear systems Ax=b in dense and sparse problems. It walks through characterizing the matrix, choosing direct or iterative methods, picking preconditioners, and diagnosing convergence or stagnation in GMRES, CG, and BiCGSTAB. It ships Python scripts that emit JSON outputs for each analysis step.
- Selects direct vs iterative solvers via a decision flowchart
- Diagnoses convergence and stagnation in GMRES/CG/BiCGSTAB
- Ships 6 Python scripts for sparsity, solver, preconditioner, and residual analysis
Linear Solvers by the numbers
- 16 all-time installs (skills.sh)
- Ranked #1,318 of 2,065 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
linear-solvers capabilities & compatibility
Free; needs Python with NumPy and SciPy
- Capabilities
- lean4 prover
- Use cases
- data analysis · debugging
- Pricing
- Free
What linear-solvers says it does
Select and configure linear solvers for systems Ax=b in dense and sparse problems.
diagnosing convergence issues, estimating conditioning, selecting preconditioners, or debugging stagnation in GMRES/CG/BiCGSTAB.
npx skills add https://github.com/beita6969/scienceclaw --skill linear-solversAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 869 |
| Last updated | June 8, 2026 |
| Repository | beita6969/scienceclaw ↗ |
What it does
Choose and tune a linear solver and preconditioner for an Ax=b system and diagnose convergence issues.
Who is it for?
Selecting direct vs iterative solvers, choosing preconditioners, and diagnosing GMRES/CG/BiCGSTAB stagnation.
Skip if: Very large dense matrices, which may exhaust memory in direct solvers.
When should I use this skill?
You need to pick or debug a linear solver and preconditioner for an Ax=b system.
What you get
A recommended solver and preconditioner plus convergence and stagnation diagnostics for a given matrix.
- Solver and preconditioner recommendations as JSON
- Convergence diagnostics
By the numbers
- Ships 6 solver analysis scripts
- 6-step solve workflow
Files
Linear Solvers
Goal
Provide a universal workflow to select a solver, assess conditioning, and diagnose convergence for linear systems arising in numerical simulations.
Requirements
- Python 3.8+
- NumPy, SciPy (for matrix operations)
- See individual scripts for dependencies
Inputs to Gather
| Input | Description | Example |
|---|---|---|
| Matrix size | Dimension of system | n = 1000000 |
| Sparsity | Fraction of nonzeros | 0.01% |
| Symmetry | Is A = Aᵀ? | yes |
| Definiteness | Is A positive definite? | yes (SPD) |
| Conditioning | Estimated condition number | 10⁶ |
Decision Guidance
Solver Selection Flowchart
Is matrix small (n < 5000) and dense?
├── YES → Use direct solver (LU, Cholesky)
└── NO → Is matrix symmetric?
├── YES → Is it positive definite?
│ ├── YES → Use CG with AMG/IC preconditioner
│ └── NO → Use MINRES
└── NO → Is it nearly symmetric?
├── YES → Use BiCGSTAB
└── NO → Use GMRES with ILU/AMGQuick Reference
| Matrix Type | Solver | Preconditioner |
|---|---|---|
| SPD, sparse | CG | AMG, IC |
| Symmetric indefinite | MINRES | ILU |
| Nonsymmetric | GMRES, BiCGSTAB | ILU, AMG |
| Dense | LU, Cholesky | None |
| Saddle point | Schur complement, Uzawa | Block preconditioner |
Script Outputs (JSON Fields)
| Script | Key Outputs |
|---|---|
scripts/solver_selector.py | recommended, alternatives, notes |
scripts/convergence_diagnostics.py | rate, stagnation, recommended_action |
scripts/sparsity_stats.py | nnz, density, bandwidth, symmetry |
scripts/preconditioner_advisor.py | suggested, notes |
scripts/scaling_equilibration.py | row_scale, col_scale, notes |
scripts/residual_norms.py | residual_norms, relative_norms, converged |
Workflow
1. Characterize matrix - symmetry, definiteness, sparsity 2. Analyze sparsity - Run scripts/sparsity_stats.py 3. Select solver - Run scripts/solver_selector.py 4. Choose preconditioner - Run scripts/preconditioner_advisor.py 5. Apply scaling - If ill-conditioned, use scripts/scaling_equilibration.py 6. Monitor convergence - Use scripts/convergence_diagnostics.py 7. Diagnose issues - Check residual history with scripts/residual_norms.py
Conversational Workflow Example
User: My GMRES solver is stagnating after 50 iterations. The residual drops to 1e-3 then stops improving.
Agent workflow: 1. Diagnose convergence:
python3 scripts/convergence_diagnostics.py --residuals 1,0.1,0.01,0.005,0.003,0.002,0.002,0.002 --json2. Check for preconditioning advice:
python3 scripts/preconditioner_advisor.py --matrix-type nonsymmetric --sparse --stagnation --json3. Recommend: Increase restart parameter, try ILU(k) with higher k, or switch to AMG.
Pre-Solve Checklist
- [ ] Confirm matrix symmetry/definiteness
- [ ] Decide direct vs iterative based on size and sparsity
- [ ] Set residual tolerance relative to physics scale
- [ ] Choose preconditioner appropriate to matrix structure
- [ ] Apply scaling/equilibration if needed
- [ ] Track convergence and adjust if stagnation occurs
CLI Examples
# Analyze sparsity pattern
python3 scripts/sparsity_stats.py --matrix A.npy --json
# Select solver for SPD sparse system
python3 scripts/solver_selector.py --symmetric --positive-definite --sparse --size 1000000 --json
# Get preconditioner recommendation
python3 scripts/preconditioner_advisor.py --matrix-type spd --sparse --json
# Diagnose convergence from residual history
python3 scripts/convergence_diagnostics.py --residuals 1,0.2,0.05,0.01 --json
# Apply scaling
python3 scripts/scaling_equilibration.py --matrix A.npy --symmetric --json
# Compute residual norms
python3 scripts/residual_norms.py --residual 1,0.1,0.01 --rhs 1,0,0 --jsonError Handling
| Error | Cause | Resolution |
|---|---|---|
Matrix file not found | Invalid path | Check file exists |
Matrix must be square | Non-square input | Verify matrix dimensions |
Residuals must be positive | Invalid residual data | Check input format |
Interpretation Guidance
Convergence Rate
| Rate | Meaning | Action |
|---|---|---|
| < 0.1 | Excellent | Current setup optimal |
| 0.1 - 0.5 | Good | Acceptable for most problems |
| 0.5 - 0.9 | Slow | Consider better preconditioner |
| > 0.9 | Stagnation | Change solver or preconditioner |
Stagnation Diagnosis
| Pattern | Likely Cause | Fix |
|---|---|---|
| Flat residual | Poor preconditioner | Improve preconditioner |
| Oscillating | Near-singular or indefinite | Check matrix, try different solver |
| Very slow decay | Ill-conditioned | Apply scaling, use AMG |
Limitations
- Large dense matrices: Direct solvers may run out of memory
- Highly indefinite: Standard preconditioners may fail
- Saddle-point: Requires specialized block preconditioners
References
references/solver_decision_tree.md- Selection logicreferences/preconditioner_catalog.md- Preconditioner optionsreferences/convergence_patterns.md- Diagnosing failuresreferences/scaling_guidelines.md- Equilibration guidance
Version History
- v1.1.0 (2024-12-24): Enhanced documentation, decision guidance, examples
- v1.0.0: Initial release with 6 solver analysis scripts
Convergence Patterns
Comprehensive guide for diagnosing and improving iterative solver convergence.
Convergence Monitoring
Key Metrics
| Metric | Formula | Interpretation |
|---|---|---|
| Residual norm | \ | \ |
| Relative residual | \ | \ |
| Reduction factor | \ | \ |
| True error | \ | \ |
Convergence Rate
Asymptotic convergence rate ρ = lim_{k→∞} ||r_k|| / ||r_{k-1}||
Linear convergence: ||r_k|| ≤ ρ^k ||r_0||, ρ < 1
Superlinear: ρ_k → 0 as k → ∞| Rate | Value | Iterations to 10⁻⁶ |
|---|---|---|
| Excellent | ρ < 0.1 | < 10 |
| Good | ρ = 0.1-0.5 | 10-30 |
| Slow | ρ = 0.5-0.9 | 30-100 |
| Stagnation | ρ ≈ 1 | Does not converge |
Typical Convergence Patterns
Pattern 1: Fast Monotonic Decay
Residual:
1e0 ****
1e-2 ****
1e-4 ****
1e-6 ****
|----|----|----|----|
0 10 20 30 40 iterationsCharacteristics:
- Smooth, consistent reduction
- Constant or decreasing rate
- Reaches tolerance quickly
Indicates:
- Good preconditioner match
- Well-conditioned problem
- Appropriate solver choice
Action: None needed, this is ideal.
Pattern 2: Initial Stall Then Decay
Residual:
1e0 ****----
1e-2 \____
1e-4 \____
1e-6 \****
|----|----|----|----|
0 10 20 30 40 iterationsCharacteristics:
- Slow start, then acceleration
- Superlinear convergence later
- Common with GMRES
Indicates:
- Krylov subspace building useful information
- Eventually finds good direction
Action:
- Be patient
- Increase max iterations
- Don't restart GMRES too early
Pattern 3: Stagnation
Residual:
1e0 ****
1e-2 ****--------------------
1e-4
1e-6
|----|----|----|----|----|----|
0 20 40 60 80 100 iterationsCharacteristics:
- Residual stops decreasing
- Convergence rate ≈ 1
- May plateau at various levels
Indicates:
- Preconditioner inadequate
- Matrix too ill-conditioned
- Reached limits of floating-point precision
Actions:
| Plateau Level | Likely Cause | Fix |
|---|---|---|
| 1e-2 to 1e-4 | Weak preconditioner | Strengthen preconditioner |
| 1e-6 to 1e-8 | Condition number limit | Scale matrix, regularize |
| 1e-12 to 1e-14 | Machine precision | Accept, use extended precision |
Pattern 4: Oscillation
Residual:
1e0 * * *
1e-2 * * * * *
1e-4 * * *
1e-6
|----|----|----|----|
0 10 20 30 40 iterationsCharacteristics:
- Residual bounces up and down
- Net progress may be slow
- Common with BiCGSTAB
Indicates:
- Near-singular or indefinite matrix
- Eigenvalue close to origin
- Loss of orthogonality (GMRES)
Actions:
- Try different solver (GMRES if using BiCGSTAB)
- Increase GMRES restart parameter
- Better preconditioner
- Check matrix for issues
Pattern 5: Divergence
Residual:
1e0 ****
1e2 ****
1e4 ****
NaN ****
|----|----|----|----|
0 10 20 30 40 iterationsCharacteristics:
- Residual increases each iteration
- Eventually overflow/NaN
Indicates:
- Matrix singular or nearly so
- Instability in method
- Severe ill-conditioning
Actions:
- Check matrix (is it singular?)
- Apply scaling
- Try direct solver
- Regularize if appropriate
Pattern 6: Plateau with Breakthrough
Residual:
1e0 ****
1e-2 ****--------****
1e-4 \____
1e-6 ****
|----|----|----|----|----|----|
0 20 40 60 80 100 iterationsCharacteristics:
- Long plateau, then sudden progress
- Multiple plateaus possible
Indicates:
- Multiple scales in problem
- Different eigenvalue clusters
Action:
- Increase max iterations
- Consider multigrid or multilevel preconditioner
Solver-Specific Patterns
CG (Conjugate Gradient)
Expected behavior:
- Monotonic residual decrease
- At most n iterations for exact arithmetic
- Affected by eigenvalue distribution
Warning signs:
| Observation | Likely Problem |
|---|---|
| Non-monotonic | Matrix not SPD |
| Very slow | High condition number |
| Breakdown (0 division) | Indefinite matrix |
GMRES
Expected behavior:
- Monotonically decreasing residual (in exact arithmetic)
- May stall then accelerate
- Memory grows with iterations
Restart effects:
| Restart m | Effect |
|---|---|
| m small (10-20) | May stagnate |
| m medium (30-50) | Good balance |
| m large (100+) | Memory intensive |
| m = n | Optimal but expensive |
BiCGSTAB
Expected behavior:
- May oscillate
- Two matrix-vector products per iteration
- Residual can temporarily increase
Warning signs:
| Observation | Likely Problem |
|---|---|
| Wild oscillation | Matrix strongly nonsymmetric |
| Breakdown | Lucky/unlucky breakdowns |
| Slow overall | Try GMRES instead |
Diagnostic Procedures
Step 1: Basic Health Check
1. Verify matrix properties:
- Is it supposed to be SPD? Check.
- Approximate condition number?
- Check for zero rows/columns.
2. Check RHS:
- Is ||b|| reasonable?
- No NaN or Inf?
3. Verify preconditioner:
- Did it build successfully?
- Is it appropriate for this matrix type?Step 2: Convergence Analysis
1. Plot log(||r_k||) vs k
2. Compute average convergence rate
3. Identify pattern (decay, stagnation, oscillation)
4. Compare to expected behavior for solver typeStep 3: Condition Number Estimation
1. Run CG/GMRES without preconditioner
2. Estimate κ from convergence rate:
ρ ≈ (√κ - 1)/(√κ + 1) for CG
3. If κ > 10⁶, scaling/preconditioning essentialStep 4: Preconditioner Quality Check
1. Compare iterations with/without preconditioner
2. If minimal improvement:
- Preconditioner too weak
- Wrong type for this matrix
- Implementation bug
3. Target: 10-50× fewer iterations with preconditionerConvergence Criteria
Relative Residual
Stop when: ||r_k|| / ||b|| < tol
Typical tol: 1e-6 to 1e-10Caution: Can be fooled by ill-conditioned systems.
Absolute Residual
Stop when: ||r_k|| < tol
Use when: ||b|| is very small or known scaleTrue Error (if available)
Stop when: ||x - x_k|| / ||x|| < tol
Best criterion but rarely computablePractical Multi-Criteria
def converged(r_k, r_0, b, x_k, tol_rel=1e-6, tol_abs=1e-10):
rel_resid = np.linalg.norm(r_k) / np.linalg.norm(b)
abs_resid = np.linalg.norm(r_k)
reduction = np.linalg.norm(r_k) / np.linalg.norm(r_0)
# Any criterion met
return (rel_resid < tol_rel or
abs_resid < tol_abs or
reduction < tol_rel)Improving Convergence
General Strategies
| Strategy | When to Apply | Expected Improvement |
|---|---|---|
| Better preconditioner | Slow convergence | 2-10× fewer iterations |
| Matrix scaling | High condition number | 2-100× fewer iterations |
| Different solver | Wrong solver type | May converge vs diverge |
| Increase restart (GMRES) | Stagnation | Variable |
| Add regularization | Near-singular | Enables convergence |
Preconditioner Strengthening Ladder
Level 0: No preconditioner
↓ slow →
Level 1: Jacobi (diagonal)
↓ slow →
Level 2: ILU(0) or IC(0)
↓ slow →
Level 3: ILU(1) or IC(1)
↓ slow →
Level 4: ILUT with moderate fill
↓ slow →
Level 5: AMG or high-fill ILU
↓ slow →
Level 6: Direct solverWhen to Switch Solvers
| From | To | When |
|---|---|---|
| CG | GMRES | Matrix not SPD |
| BiCGSTAB | GMRES | Oscillation, breakdown |
| GMRES | BiCGSTAB | Memory limited |
| Iterative | Direct | n < 10000, multiple RHS |
Implementation: Convergence Logger
class ConvergenceMonitor:
"""Track and analyze iterative solver convergence."""
def __init__(self, b_norm):
self.residuals = []
self.b_norm = b_norm
def log(self, r_norm):
self.residuals.append(r_norm)
def relative_residual(self):
return [r / self.b_norm for r in self.residuals]
def convergence_rate(self, window=5):
"""Average convergence rate over last window iterations."""
if len(self.residuals) < window + 1:
return None
recent = self.residuals[-window-1:]
rates = [recent[i+1]/recent[i] for i in range(window)]
return np.mean(rates)
def diagnose(self):
if len(self.residuals) < 2:
return "Insufficient data"
rate = self.convergence_rate()
if rate is None:
return "Need more iterations"
rel_final = self.residuals[-1] / self.b_norm
if rel_final < 1e-10:
return "Converged well"
elif rate > 0.99:
return "Stagnated - strengthen preconditioner"
elif rate > 0.9:
return "Slow - consider better preconditioner"
elif rate < 0:
return "Oscillating - try different solver"
elif rate > 1:
return "Diverging - check matrix properties"
else:
return "Good progress - continue"
def plot(self):
"""Plot convergence history."""
import matplotlib.pyplot as plt
plt.semilogy(self.residuals, 'b-o')
plt.xlabel('Iteration')
plt.ylabel('Residual norm')
plt.title(f'Convergence: final rate = {self.convergence_rate():.3f}')
plt.grid(True)
plt.show()Quick Troubleshooting Guide
| Symptom | First Check | Quick Fix |
|---|---|---|
| No convergence after 1000 iter | Matrix singular? | Add regularization |
| Stagnation at 1e-4 | Preconditioner strength | Increase ILU fill |
| Oscillating residual | Solver appropriate? | Switch to GMRES |
| Very slow decay | Condition number | Apply scaling |
| Breakdown/NaN | Matrix properties | Check for issues, scale |
| Works but slow | Preconditioner type | Try AMG for elliptic |
Preconditioner Catalog
Comprehensive reference for preconditioners in iterative linear solvers.
Preconditioner Fundamentals
Purpose
Transform Ax = b into M⁻¹Ax = M⁻¹b where M⁻¹A has better conditioning.
Goals:
- Cluster eigenvalues away from zero
- Reduce condition number κ(M⁻¹A)
- Make M⁻¹ cheap to apply
Application Modes
| Mode | System Solved | When to Use |
|---|---|---|
| Left | M⁻¹Ax = M⁻¹b | Most common |
| Right | AM⁻¹y = b, x = M⁻¹y | Preserves residual meaning |
| Split | L⁻¹AR⁻¹y = L⁻¹b | Symmetric preconditioning |
Key Trade-offs
| Factor | Cheap Precond. | Expensive Precond. |
|---|---|---|
| Setup cost | Low | High |
| Apply cost | Low | High |
| Iterations | Many | Few |
| Total time | May be optimal | May be optimal |
Incomplete Factorization Family
Incomplete Cholesky (IC)
For SPD matrices: A ≈ LLᵀ where L is sparse.
IC(0) - Zero fill-in:
- Same sparsity pattern as lower triangle of A
- Cheap, often effective
- May fail for indefinite or poorly scaled
IC(k) - Level-k fill:
- Allow fill-in up to k levels from original pattern
- More robust, higher cost
- k = 1 or 2 often sufficient
Modified IC (MIC):
- Add dropped entries to diagonal
- Better for M-matrices (e.g., Laplacian)
- Preserves row sums
Incomplete LU (ILU)
For general matrices: A ≈ LU where L, U are sparse.
ILU(0) - Zero fill-in:
Pattern(L + U) = Pattern(A)
Cheap, first try for nonsymmetricILU(k) - Level-k fill:
Allow fill paths up to k edges
k = 1: moderate fill
k = 2: substantial fillILUT - Threshold-based:
Parameters: τ (drop tolerance), p (max fill per row)
Drop if |entry| < τ × ||row||
Keep at most p entries per row| Parameter | Effect |
|---|---|
| τ small | More fill, better approximation |
| τ large | Less fill, weaker preconditioner |
| p small | Limit memory, may reduce quality |
| p large | Better quality, more memory |
Typical values:
τ = 1e-4 to 1e-2
p = 10 to 50 (or 2× to 5× original nnz/row)Choosing IC vs ILU Parameters
| Symptom | Adjustment |
|---|---|
| Convergence too slow | Increase k or decrease τ |
| Too much memory | Increase τ or decrease p |
| Factorization fails | Add diagonal shift, try different ordering |
| Negative pivot (IC) | Matrix not SPD, use ILU |
Algebraic Multigrid (AMG)
When to Use
| Good for | Poor for |
|---|---|
| Elliptic PDEs | Highly nonsymmetric |
| Diffusion-dominated | Pure advection |
| Smooth error | Oscillatory error |
| Large systems | Small systems (overhead) |
AMG Components
Coarsening:
- Classical (Ruge-Stüben): Strength-based C/F splitting
- Aggregation: Group nodes into aggregates
- Smoothed aggregation: SA-AMG, good for elasticity
Interpolation:
- Direct: Use strong connections
- Standard: Include weak connections
- Extended+i: For harder problems
Smoothing:
- Jacobi: Simple, parallelizable
- Gauss-Seidel: Better smoothing, less parallel
- Polynomial: Good for GPU
AMG Tuning
| Parameter | Effect |
|---|---|
| Strong threshold | Lower = more connections = slower coarsening |
| Coarsening ratio | 2:1 to 4:1 typical |
| Max levels | 10-20 typical |
| Smoother | Jacobi (parallel) vs GS (sequential) |
| Cycles | V-cycle (cheap) vs W-cycle (robust) |
AMG for Nonsymmetric
Some AMG can handle mildly nonsymmetric:
- Use symmetric part for coarsening
- May need more smoothing
- Verify convergence experimentally
Specialized Preconditioners
Jacobi and Block Jacobi
Point Jacobi: M = diag(A)
Simple, parallel, weak
Use as smoother in multigridBlock Jacobi: M = block_diag(A)
Blocks from natural structure (elements, nodes)
Stronger than point Jacobi
Embarrassingly parallelGauss-Seidel
Forward/Backward GS:
M = L + D (forward) or D + U (backward)
Stronger than Jacobi
Sequential, hard to parallelizeSymmetric GS (SSOR):
Apply forward then backward
Good smoother for symmetric problems
Parameter ω (relaxation): typically 1.0SSOR (Symmetric SOR)
For SPD systems:
M = (D/ω + L) × (D/ω)⁻¹ × (D/ω + U)
ω: overrelaxation parameter (0 < ω < 2)
ω = 1: Symmetric Gauss-Seidel
ω optimal ≈ 2/(1 + sin(πh)) for model problemPolynomial Preconditioners
Approximate M⁻¹ ≈ p(A) for some polynomial p.
Neumann series:
M⁻¹ ≈ I + (I - A) + (I - A)² + ...
Requires ρ(I - A) < 1Chebyshev:
Optimal polynomial for given eigenvalue bounds
Requires [λ_min, λ_max] estimates
Good for GPU (only matrix-vector products)Block Preconditioners
For Saddle-Point Systems
K = [A B ] x = [u] b = [f]
[Bᵀ -C] [p] [g]Block Diagonal:
P = [Â 0 ]
[0 Ŝ ]
 ≈ A (e.g., AMG for A)
Ŝ ≈ S = C + BᵀA⁻¹B (Schur complement)Block Triangular:
P = [Â 0 ] or P = [Â B]
[Bᵀ -Ŝ] [0 -Ŝ]Schur Complement Approximations
| Approximation | Formula | Use Case |
|---|---|---|
| Mass matrix | Ŝ = M_p (pressure mass) | Stokes |
| BFBt | Ŝ = B diag(A)⁻¹ Bᵀ | General |
| LSC | Ŝ = (B Bᵀ)(B A Bᵀ)⁻¹(B Bᵀ) | Navier-Stokes |
| PCD | Convection-diffusion-reaction | Navier-Stokes |
Field-Split Preconditioners
For multi-physics with fields u₁, u₂, ...:
Multiplicative: Solve u₁, then u₂ using updated u₁
Additive: Solve each independently, sum correctionsPreconditioner Selection Guide
By Matrix Type
| Matrix | First Choice | Alternative |
|---|---|---|
| SPD, diffusion | AMG | IC(k) |
| SPD, elasticity | SA-AMG | IC(k) |
| SPD, general | IC(0) | AMG |
| Nonsymmetric, mild | ILU(0) | ILUT |
| Nonsymmetric, advection | ILUT (strong) | Stream-wise ILU |
| Saddle-point | Block diagonal/triangular | Uzawa |
| Dense | - (direct solver) | - |
By Problem Size
| Size | Recommendation |
|---|---|
| n < 1000 | Direct solver |
| n = 1000-10000 | ILU/IC or AMG |
| n = 10000-1M | AMG (if applicable) |
| n > 1M | AMG, domain decomposition |
By Available Resources
| Resource | Recommendation |
|---|---|
| Single core | Sequential GS, ILU |
| Multi-core | Block Jacobi, AMG |
| GPU | Polynomial, Block Jacobi |
| Distributed | Domain decomposition + local precond. |
Troubleshooting
Preconditioner Fails to Build
| Error | Cause | Fix |
|---|---|---|
| Zero pivot | Singular or structurally singular | Reorder, add diagonal shift |
| Negative pivot (IC) | Not SPD | Use ILU, check matrix |
| Out of memory | Too much fill | Increase τ, reduce p or k |
Poor Convergence Despite Preconditioner
| Symptom | Cause | Fix |
|---|---|---|
| Slow decay | Weak preconditioner | Strengthen (lower τ, higher k) |
| Stagnation | Unfavorable eigenvalue distribution | Try different preconditioner |
| Oscillation | Near-singular modes | Check matrix, regularize |
Parameter Tuning Strategy
1. Start simple: ILU(0) or IC(0) 2. Monitor: iteration count, residual curve 3. Adjust: If slow, strengthen preconditioner 4. Balance: Setup time vs iteration time 5. Validate: Check solution accuracy
Implementation Notes
Setup vs Apply Cost
| Preconditioner | Setup | Apply | When Setup Dominates |
|---|---|---|---|
| Jacobi | O(n) | O(n) | Never |
| ILU(0) | O(nnz) | O(nnz) | Many solves |
| AMG | O(n log n) | O(n) | Few solves |
Reusing Preconditioners
When matrix changes slightly:
Same pattern: May reuse structure, update values
Small changes: Lag preconditioner (update every k solves)
Large changes: Rebuild preconditionerQuality Metrics
| Metric | Good Value |
|---|---|
| Fill ratio (nnz(LU)/nnz(A)) | 2-10 for ILU |
| Operator complexity (AMG) | 1.2-2.0 |
| Convergence factor | < 0.3 |
| Iterations | < 50 typically |
Scaling and Equilibration
Comprehensive guide for matrix scaling to improve conditioning and solver performance.
When Scaling is Needed
Indicators of Poor Scaling
| Symptom | Likely Cause | Solution |
|---|---|---|
| Residual stagnates | Large condition number | Row/column scaling |
| Small parameter changes → big solution changes | Ill-conditioning | Equilibration |
| Preconditioner fails | Extreme value ranges | Scale before factorization |
| Overflow/underflow | Very large/small entries | Scaling to O(1) |
| Slow convergence | Unbalanced row/column norms | Equilibration |
Matrix Value Ranges
| Range (max/min) | Status | Action |
|---|---|---|
| < 10³ | Good | Usually no scaling needed |
| 10³ - 10⁶ | Moderate | Consider scaling |
| 10⁶ - 10¹² | Poor | Scaling recommended |
| > 10¹² | Severe | Scaling essential |
Scaling Methods
Row Scaling
Multiply each row by a scalar: D_r × A
For each row i:
scale_i = 1 / max_j |A_ij| (max scaling)
or
scale_i = 1 / ||row_i||₂ (norm scaling)
or
scale_i = 1 / ||row_i||₁ (sum scaling)Effect: All row maxima (or norms) become 1.
Preserves: Column space structure.
Column Scaling
Multiply each column by a scalar: A × D_c
For each column j:
scale_j = 1 / max_i |A_ij|Effect: All column maxima become 1.
Preserves: Row space structure.
Row and Column Scaling
Apply both: D_r × A × D_c
Balanced approach:
Scaled system: (D_r × A × D_c) × (D_c⁻¹ × x) = D_r × b
Solve for: y = D_c⁻¹ × x
Recover: x = D_c × ySymmetric Scaling
For symmetric matrices, use same scaling for rows and columns: D × A × D
Preserves symmetry: (DAD)ᵀ = DAD
Essential for CG, MINRESComputing symmetric scaling:
d_i = 1 / sqrt(|A_ii|) (diagonal scaling)
or
d_i = 1 / sqrt(max_j |A_ij|) (max scaling)Equilibration Algorithms
Ruiz Equilibration
Iterative algorithm to make row and column norms approximately equal:
Repeat until converged:
For each row i: r_i = ||row_i||_∞
For each col j: c_j = ||col_j||_∞
D_r = diag(1/sqrt(r_i))
D_c = diag(1/sqrt(c_j))
A = D_r × A × D_cProperties:
- Converges to doubly stochastic-like scaling
- Usually 5-10 iterations sufficient
- Works for nonsymmetric matrices
Sinkhorn-Knopp (Doubly Stochastic)
For nonnegative matrices, scale to doubly stochastic:
All row sums = 1
All column sums = 1Algorithm:
Repeat:
Normalize rows to sum to 1
Normalize columns to sum to 1
Until convergedGeometric Mean Scaling
Scale by geometric mean of max and min absolute values:
For row i:
r_max = max_j |A_ij|
r_min = min_{j: A_ij≠0} |A_ij|
scale_i = 1 / sqrt(r_max × r_min)Good for: Matrices with entries spanning many orders of magnitude.
Special Considerations
Preserving Symmetry
CRITICAL: For symmetric matrices, use symmetric scaling only!
Wrong: D_r × A × D_c with D_r ≠ D_c (destroys symmetry)
Right: D × A × D with same D for rows and columnsCheck after scaling:
||A_scaled - A_scaled^T||_F / ||A_scaled||_F < εPreserving Positive Definiteness
Symmetric scaling preserves definiteness:
If A is SPD and D is nonsingular diagonal:
Then D × A × D is also SPDCheck after scaling (if needed):
Try Cholesky factorization
Or check smallest eigenvalue > 0Scaling the RHS
When scaling Ax = b → (D_r A D_c) y = D_r b:
Solve: (D_r A D_c) y = D_r b
Recover: x = D_c y
Residual: r = b - Ax = b - A(D_c y)Important: Scale b with the same D_r used for rows!
Practical Scaling Strategies
Safe Default Strategy
def scale_matrix(A, symmetric=False):
# Row scaling
row_norms = np.max(np.abs(A), axis=1)
D_r = 1.0 / np.maximum(row_norms, 1e-15)
if symmetric:
# Use same scaling for rows and columns
D_c = D_r
else:
# Scale by rows first, then columns
A_scaled = np.diag(D_r) @ A
col_norms = np.max(np.abs(A_scaled), axis=0)
D_c = 1.0 / np.maximum(col_norms, 1e-15)
return D_r, D_cWhen to Apply Scaling
| Stage | Apply Scaling? |
|---|---|
| Before analysis | Yes (check properties) |
| Before preconditioner | Often yes |
| Before solve | Yes |
| After solve | Unscale solution |
Scaling Order
1. Analyze original matrix (symmetry, definiteness) 2. Choose appropriate scaling method 3. Scale matrix and RHS 4. Build preconditioner on scaled system 5. Solve scaled system 6. Unscale solution
Diagnosing Scaling Issues
Before Scaling
Check: ratio of max to min nonzero absolute values
||A||_∞ / min_{ij: A_ij≠0} |A_ij|
If ratio > 10⁶: scaling strongly recommendedAfter Scaling
Verify:
- Row norms approximately equal
- Column norms approximately equal
- No overflow/underflow
- Symmetry preserved (if was symmetric)Monitoring Convergence
| Metric | Before Scaling | After Scaling | Status |
|---|---|---|---|
| Iterations | 500+ | 50 | Good |
| Residual decrease | 0.999/iter | 0.9/iter | Good |
| Final residual | Stagnated | Converged | Good |
Common Mistakes
Mistake 1: Destroying Symmetry
Problem: Used different row and column scaling on symmetric matrix
Result: CG fails (requires symmetric)
Fix: Use symmetric scaling D × A × DMistake 2: Forgetting to Unscale
Problem: Returned y instead of x = D_c × y
Result: Wrong solution
Fix: Always unscale: x = D_c × yMistake 3: Not Scaling RHS
Problem: Scaled A but not b
Result: Solving wrong system
Fix: b_scaled = D_r × bMistake 4: Scaling Zero Rows
Problem: Row of zeros gets 1/0 = inf scaling
Result: NaN in solution
Fix: Handle zero rows specially (or they indicate singular matrix)Advanced Topics
Iterative Refinement with Scaling
For very ill-conditioned systems:
1. Scale: Ã = D_r A D_c, b̃ = D_r b
2. Solve: Ã ỹ = b̃ (in lower precision if available)
3. Compute residual: r = b - A(D_c ỹ) (in high precision)
4. Solve: Ã δỹ = D_r r
5. Update: ỹ = ỹ + δỹ
6. Repeat until converged
7. Unscale: x = D_c ỹScaling for Eigenvalue Problems
When solving Ax = λx after scaling D_r A D_c:
Eigenvalues: Same (scaling is similarity transform)
Eigenvectors: v_original = D_c × v_scaledBlock Scaling
For block-structured matrices:
Scale each block independently, or
Scale entire rows/columns of blocksUseful for: Multiphysics with different variable scales.
Implementation Example
def equilibrate_matrix(A, tol=1e-6, max_iter=20):
"""Ruiz equilibration for general matrices."""
n, m = A.shape
D_r = np.ones(n)
D_c = np.ones(m)
for _ in range(max_iter):
# Row scaling
row_norms = np.max(np.abs(A), axis=1)
row_norms = np.maximum(row_norms, 1e-15)
d_r = 1.0 / np.sqrt(row_norms)
A = np.diag(d_r) @ A
D_r *= d_r
# Column scaling
col_norms = np.max(np.abs(A), axis=0)
col_norms = np.maximum(col_norms, 1e-15)
d_c = 1.0 / np.sqrt(col_norms)
A = A @ np.diag(d_c)
D_c *= d_c
# Check convergence
if np.max(np.abs(row_norms - 1)) < tol:
if np.max(np.abs(col_norms - 1)) < tol:
break
return A, D_r, D_c
def solve_scaled(A, b, solver, symmetric=False):
"""Solve with scaling and unscaling."""
A_scaled, D_r, D_c = equilibrate_matrix(A)
b_scaled = D_r * b
if symmetric:
# Ensure symmetric scaling
D = np.sqrt(D_r * D_c)
A_scaled = np.diag(D) @ A @ np.diag(D)
b_scaled = D * b
y = solver(A_scaled, b_scaled)
x = D * y
else:
y = solver(A_scaled, b_scaled)
x = D_c * y
return xQuick Reference
| Matrix Type | Scaling Method | Notes |
|---|---|---|
| General | Row + column equilibration | Ruiz algorithm |
| Symmetric | Symmetric diagonal scaling | D × A × D |
| SPD | Symmetric diagonal scaling | Preserve definiteness |
| Block-structured | Block-aware scaling | Match physics scales |
| Very ill-conditioned | Equilibration + iterative refinement | May need extended precision |
Solver Decision Tree
Comprehensive decision guide for selecting linear solvers for Ax = b.
Matrix Classification
Key Properties to Determine
| Property | How to Check | Impact |
|---|---|---|
| Symmetric | A = Aᵀ | Enables CG, MINRES |
| Positive definite | All eigenvalues > 0 | Enables CG, Cholesky |
| Sparse | nnz/n² < 0.01 | Iterative preferred |
| Well-conditioned | κ(A) < 10⁶ | Standard methods work |
| Banded | Nonzeros near diagonal | Band solvers efficient |
Quick Classification
Check symmetry: ||A - Aᵀ||_F / ||A||_F < ε
Check SPD: try Cholesky, if succeeds → SPD
Check sparsity: nnz / n² gives density
Check conditioning: estimate κ using power iteration or LanczosPrimary Decision Tree
START: Need to solve Ax = b
│
├─ Is n < 5000 and matrix dense?
│ └── YES → Use DIRECT solver
│ ├── Symmetric PD → Cholesky (LLᵀ)
│ ├── Symmetric indefinite → LDLᵀ (Bunch-Kaufman)
│ ├── Nonsymmetric → LU with pivoting
│ └── Least squares → QR or SVD
│
└── NO → Use ITERATIVE solver
│
├─ Is matrix symmetric?
│ │
│ ├── YES → Is it positive definite?
│ │ │
│ │ ├── YES (SPD) → CG (Conjugate Gradient)
│ │ │ ├── Elliptic PDE → AMG preconditioner
│ │ │ ├── Banded structure → IC(0) or IC(k)
│ │ │ └── General sparse → AMG or IC
│ │ │
│ │ └── NO/Unknown (symmetric indefinite)
│ │ ├── Eigenvalues both signs → MINRES
│ │ ├── Near-singular → SYMMLQ
│ │ └── Preconditioner: ILU or block diagonal
│ │
│ └── NO (nonsymmetric) → Is it nearly symmetric?
│ │
│ ├── YES (A ≈ Aᵀ) → BiCGSTAB
│ │ ├── Smooth convergence needed → BiCGSTAB(ℓ)
│ │ └── Preconditioner: ILUT or AMG
│ │
│ └── NO (strongly nonsymmetric) → GMRES
│ ├── Memory limited → GMRES(m) restarted
│ ├── Very nonsymmetric → Full GMRES if affordable
│ └── Preconditioner: ILU(k), ILUT, or AMGDirect Solver Selection
When to Use Direct
| Condition | Direct Recommended |
|---|---|
| n < 5000 | Usually |
| Dense matrix | Yes |
| Multiple RHS | Yes (factor once) |
| Need exact solution | Yes |
| High accuracy required | Yes |
| Robustness critical | Yes |
Direct Solver Types
| Matrix Type | Method | Complexity |
|---|---|---|
| General dense | LU with pivoting | O(n³) |
| Symmetric dense | LDLᵀ | O(n³/2) |
| SPD dense | Cholesky | O(n³/3) |
| Sparse | Sparse LU (SuperLU, UMFPACK) | O(nnz^α), α ≈ 1.5 |
| Banded | Band LU/Cholesky | O(n × b²) |
Memory Considerations
| Method | Memory | Fill-in |
|---|---|---|
| Dense LU | O(n²) | N/A |
| Sparse LU | O(n) to O(n²) | Depends on ordering |
| Cholesky | Half of LU | Less fill-in |
| Iterative | O(n) × k | No fill-in |
Iterative Solver Details
Conjugate Gradient (CG)
Requirements: A must be SPD
Convergence:
||e_k||_A ≤ 2 × ((√κ - 1)/(√κ + 1))^k × ||e_0||_A| Condition Number | Iterations (to 10⁻⁶) |
|---|---|
| κ = 10 | ~6 |
| κ = 100 | ~20 |
| κ = 1000 | ~60 |
| κ = 10⁶ | ~2000 |
Breakdown: CG is breakdown-free for SPD matrices.
GMRES
Requirements: None (works for any nonsingular matrix)
Properties:
- Minimizes residual over Krylov subspace
- Optimal for nonsymmetric systems
- Memory: O(m × n) for m iterations
Restart Strategy:
GMRES(m): Restart after m iterations
├── m = 20-30: Typical for memory-limited
├── m = 50-100: Better convergence
└── m = n: Full GMRES (no restart)When GMRES stalls:
- Increase restart parameter m
- Improve preconditioner
- Check for near-singularity
BiCGSTAB
Requirements: Works for nonsymmetric, best if A ≈ Aᵀ
Properties:
- Lower memory than GMRES
- May have irregular convergence
- Two matrix-vector products per iteration
Variants:
| Variant | Properties |
|---|---|
| BiCGSTAB | Standard |
| BiCGSTAB(ℓ) | Smoother convergence, higher cost |
| IDR(s) | Alternative, can be faster |
MINRES
Requirements: A symmetric (not necessarily PD)
Properties:
- Works for indefinite symmetric systems
- Minimizes residual (like GMRES for symmetric)
- Short recurrences (memory efficient)
Use for:
- Saddle-point systems
- Systems from variational problems
- Eigenvalue problems (shift-invert)
Special Matrix Structures
Saddle-Point Systems
[A B ] [x] [f]
[Bᵀ -C] [y] = [g]Solvers:
- Schur complement method
- Uzawa iteration
- Block preconditioned GMRES/MINRES
Preconditioners:
Block diagonal: [A⁻¹ 0 ]
[0 S⁻¹]
Block triangular: [A⁻¹ 0 ]
[Bᵀ S⁻¹]Where S = C + BᵀA⁻¹B is the Schur complement.
Banded Systems
For bandwidth b << n:
| Method | Cost | When to Use |
|---|---|---|
| Band LU | O(nb²) | Direct, small b |
| Thomas (tridiag) | O(n) | b = 1 |
| Cyclic reduction | O(n log n) | Parallel |
Block Systems
When A has natural block structure:
Block Jacobi: Solve diagonal blocks independently
Block Gauss-Seidel: Sequential block updates
Block ILU: Factor with block structure preservedSolver Selection by Application
Elliptic PDEs (Laplacian-like)
| Grid | Recommended |
|---|---|
| Structured | CG + FFT or Multigrid |
| Unstructured | CG + AMG |
| Anisotropic | CG + line/plane smoothing |
Parabolic PDEs (Heat equation)
| Implicit Method | Recommended |
|---|---|
| Backward Euler | CG + IC/AMG (SPD) |
| Crank-Nicolson | CG + IC/AMG (SPD) |
| BDF | Same as above |
Hyperbolic PDEs (Advection-dominated)
| Character | Recommended |
|---|---|
| Pure advection | GMRES + ILU (nonsymmetric) |
| Advection-diffusion | GMRES/BiCGSTAB + ILUT |
| High Péclet | Upwind + GMRES + strong ILU |
Navier-Stokes
| Formulation | Recommended |
|---|---|
| Coupled velocity-pressure | Block preconditioned GMRES |
| Projection methods | Poisson: CG + AMG |
| SIMPLE-like | Momentum: BiCGSTAB, Pressure: CG |
Phase-Field
| Equation | Recommended |
|---|---|
| Allen-Cahn | CG + AMG (if implicit) |
| Cahn-Hilliard | GMRES + Block preconditioner (4th order) |
| Mixed form | CG + AMG for each block |
Failure Modes and Remedies
Common Problems
| Symptom | Cause | Solution |
|---|---|---|
| No convergence | Poor preconditioner | Stronger preconditioner |
| Slow convergence | High condition number | Scale matrix, better preconditioner |
| Breakdown | Singular or near-singular | Check matrix, use pseudo-inverse |
| Oscillation | Loss of orthogonality | Increase GMRES restart |
| NaN/Inf | Overflow | Scale matrix |
When All Else Fails
1. Check the matrix: Is it actually nonsingular? 2. Scale rows/columns: Equilibrate to unit row norms 3. Try direct solver: Even for large sparse, may work 4. Regularize: Add small diagonal for near-singular 5. Reformulate problem: Different discretization
Quick Reference Table
| Matrix Type | First Choice | Preconditioner | Backup |
|---|---|---|---|
| SPD, elliptic | CG | AMG | CG + IC |
| SPD, general | CG | IC(k) | Cholesky |
| Sym. indef. | MINRES | Block diag | GMRES |
| Nonsym., mild | BiCGSTAB | ILUT | GMRES |
| Nonsym., strong | GMRES(50) | ILU(k) | GMRES(100) |
| Saddle-point | Block GMRES | Schur approx | Uzawa |
| Dense | LU/Cholesky | - | - |
#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import List, Tuple
def parse_list(raw: str) -> List[float]:
parts = [p.strip() for p in raw.split(",") if p.strip()]
if not parts:
raise ValueError("residual list must be a comma-separated list")
return [float(p) for p in parts]
def compute_diagnostics(residuals: List[float]) -> Tuple[float, bool, str]:
if len(residuals) < 2:
raise ValueError("residual list must have at least 2 entries")
if any(r <= 0 or not math.isfinite(r) for r in residuals):
raise ValueError("residuals must be positive and finite")
ratios = [residuals[i + 1] / residuals[i] for i in range(len(residuals) - 1)]
avg_ratio = sum(ratios) / len(ratios)
stagnation = avg_ratio > 0.95
if avg_ratio < 0.2:
action = "Convergence is fast; consider tightening tolerance."
elif stagnation:
action = "Stagnation detected; strengthen preconditioner or change method."
else:
action = "Convergence is acceptable; continue monitoring."
return avg_ratio, stagnation, action
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Analyze residual convergence behavior.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--residuals", required=True, help="Comma-separated residuals")
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
residuals = parse_list(args.residuals)
rate, stagnation, action = compute_diagnostics(residuals)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {"residuals": residuals},
"results": {
"rate": rate,
"stagnation": stagnation,
"recommended_action": action,
},
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Convergence diagnostics")
print(f" rate: {rate:.6g}")
print(f" stagnation: {stagnation}")
print(f" action: {action}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import sys
from typing import Dict, List
def advise_preconditioner(
matrix_type: str,
sparse: bool,
ill_conditioned: bool,
saddle_point: bool,
symmetric: bool,
) -> Dict[str, List[str] | str]:
if matrix_type not in {"spd", "symmetric-indefinite", "nonsymmetric"}:
raise ValueError("matrix_type must be spd, symmetric-indefinite, or nonsymmetric")
suggested: List[str] = []
notes: List[str] = []
if saddle_point:
suggested.append("Block preconditioner (Schur complement)")
notes.append("Use physics-informed block structure when available.")
return {"suggested": suggested, "notes": notes}
if matrix_type == "spd":
suggested.extend(["Incomplete Cholesky (IC)", "AMG"])
elif matrix_type == "symmetric-indefinite":
suggested.extend(["Incomplete LDL^T", "AMG"])
else:
suggested.extend(["ILU(0)/ILUT", "AMG"])
if not sparse:
notes.append("Dense systems: direct solver or dense preconditioner may be better.")
if ill_conditioned:
notes.append("Increase fill-in or use multigrid for robustness.")
if symmetric and matrix_type == "nonsymmetric":
notes.append("Check symmetry assumption; matrix_type may be incorrect.")
return {"suggested": suggested, "notes": notes}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Suggest preconditioners for a linear system.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--matrix-type",
required=True,
choices=["spd", "symmetric-indefinite", "nonsymmetric"],
help="Matrix type",
)
parser.add_argument("--sparse", action="store_true", help="Matrix is sparse")
parser.add_argument(
"--ill-conditioned",
action="store_true",
help="Matrix is ill-conditioned",
)
parser.add_argument(
"--saddle-point",
action="store_true",
help="Saddle-point system",
)
parser.add_argument(
"--symmetric",
action="store_true",
help="Matrix is symmetric (sanity check)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
result = advise_preconditioner(
matrix_type=args.matrix_type,
sparse=args.sparse,
ill_conditioned=args.ill_conditioned,
saddle_point=args.saddle_point,
symmetric=args.symmetric,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"matrix_type": args.matrix_type,
"sparse": args.sparse,
"ill_conditioned": args.ill_conditioned,
"saddle_point": args.saddle_point,
"symmetric": args.symmetric,
},
"results": result,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Preconditioner advice")
print(f" suggested: {', '.join(result['suggested'])}")
for note in result["notes"]:
print(f" note: {note}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import Dict, List, Optional, Tuple
def parse_list(raw: str) -> List[float]:
parts = [p.strip() for p in raw.split(",") if p.strip()]
if not parts:
raise ValueError("value list must be a comma-separated list")
return [float(p) for p in parts]
def compute_norms(vec: List[float]) -> Dict[str, float]:
if not vec:
raise ValueError("vector must be non-empty")
if any(not math.isfinite(v) for v in vec):
raise ValueError("vector contains non-finite values")
l1 = sum(abs(v) for v in vec)
l2 = math.sqrt(sum(v * v for v in vec))
linf = max(abs(v) for v in vec)
return {"l1": l1, "l2": l2, "linf": linf}
def select_norm_value(norms: Dict[str, float], norm: str) -> float:
if norm == "l1":
return norms["l1"]
if norm == "l2":
return norms["l2"]
if norm == "inf":
return norms["linf"]
raise ValueError("norm must be l1, l2, or inf")
def compute_residual_metrics(
residual: List[float],
rhs: Optional[List[float]],
initial: Optional[List[float]],
abs_tol: float,
rel_tol: float,
norm: str,
require_both: bool,
) -> Tuple[Dict[str, float], Optional[Dict[str, float]], Optional[Dict[str, float]], Dict[str, object]]:
if abs_tol < 0 or rel_tol < 0:
raise ValueError("abs_tol and rel_tol must be non-negative")
residual_norms = compute_norms(residual)
reference = None
note = None
if rhs is not None:
reference = rhs
if initial is not None:
note = "Using rhs as reference; initial ignored."
elif initial is not None:
reference = initial
reference_norms = compute_norms(reference) if reference is not None else None
relative_norms = None
if reference_norms is not None:
relative_norms = {
key: residual_norms[key] / reference_norms[key]
if reference_norms[key] != 0
else float("inf")
for key in residual_norms
}
norm_value = select_norm_value(residual_norms, norm)
ref_value = select_norm_value(reference_norms, norm) if reference_norms else None
rel_value = norm_value / ref_value if ref_value not in (None, 0) else None
converged_abs = norm_value <= abs_tol
converged_rel = None if rel_value is None else rel_value <= rel_tol
if converged_rel is None:
converged = converged_abs
else:
converged = converged_abs and converged_rel if require_both else converged_abs or converged_rel
meta = {
"norm_used": norm,
"norm_value": norm_value,
"reference_value": ref_value,
"relative_value": rel_value,
"converged_abs": converged_abs,
"converged_rel": converged_rel,
"converged": converged,
"note": note,
}
return residual_norms, reference_norms, relative_norms, meta
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute residual norms and evaluate stopping criteria.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--residual", required=True, help="Comma-separated residual values")
parser.add_argument("--rhs", default=None, help="Comma-separated RHS values")
parser.add_argument("--initial", default=None, help="Comma-separated initial residual values")
parser.add_argument("--abs-tol", type=float, default=1e-8, help="Absolute tolerance")
parser.add_argument("--rel-tol", type=float, default=1e-6, help="Relative tolerance")
parser.add_argument(
"--norm",
choices=["l1", "l2", "inf"],
default="l2",
help="Norm for convergence check",
)
parser.add_argument(
"--require-both",
action="store_true",
help="Require both abs and rel criteria for convergence",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
residual = parse_list(args.residual)
rhs = parse_list(args.rhs) if args.rhs is not None else None
initial = parse_list(args.initial) if args.initial is not None else None
residual_norms, reference_norms, relative_norms, meta = compute_residual_metrics(
residual=residual,
rhs=rhs,
initial=initial,
abs_tol=args.abs_tol,
rel_tol=args.rel_tol,
norm=args.norm,
require_both=args.require_both,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"residual": residual,
"rhs": rhs,
"initial": initial,
"abs_tol": args.abs_tol,
"rel_tol": args.rel_tol,
"norm": args.norm,
"require_both": args.require_both,
},
"results": {
"residual_norms": residual_norms,
"reference_norms": reference_norms,
"relative_norms": relative_norms,
"meta": meta,
},
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Residual norms")
print(f" norm: {meta['norm_used']}")
print(f" norm_value: {meta['norm_value']:.6g}")
if meta["relative_value"] is not None:
print(f" relative_value: {meta['relative_value']:.6g}")
print(f" converged: {meta['converged']}")
if meta["note"]:
print(f" note: {meta['note']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from typing import Dict, List, Optional
import numpy as np
def load_matrix(path: str, delimiter: Optional[str]) -> np.ndarray:
_, ext = os.path.splitext(path)
if ext == ".npy":
return np.load(path)
return np.loadtxt(path, delimiter=delimiter)
def compute_scaling(
matrix: np.ndarray,
symmetry_tol: float,
symmetric: bool,
) -> Dict[str, object]:
if matrix.ndim != 2:
raise ValueError("matrix must be 2D")
if not np.all(np.isfinite(matrix)):
raise ValueError("matrix contains non-finite values")
m, n = matrix.shape
if symmetric and m != n:
raise ValueError("symmetric scaling requires a square matrix")
abs_matrix = np.abs(matrix)
row_max = np.max(abs_matrix, axis=1)
col_max = np.max(abs_matrix, axis=0)
zero_rows = [int(i) for i, v in enumerate(row_max) if v == 0]
zero_cols = [int(i) for i, v in enumerate(col_max) if v == 0]
row_scale = [1.0 / v if v > 0 else 1.0 for v in row_max]
col_scale = [1.0 / v if v > 0 else 1.0 for v in col_max]
symmetric_scale = None
is_symmetric = bool(np.allclose(matrix, matrix.T, atol=symmetry_tol, rtol=0.0))
if symmetric:
symmetric_scale = [1.0 / np.sqrt(v) if v > 0 else 1.0 for v in row_max]
notes: List[str] = []
if zero_rows:
notes.append("Zero rows detected; scaling set to 1 for those rows.")
if zero_cols:
notes.append("Zero cols detected; scaling set to 1 for those cols.")
if symmetric and not is_symmetric:
notes.append("Matrix is not symmetric within tolerance; check inputs.")
return {
"shape": [m, n],
"row_scale": row_scale,
"col_scale": col_scale,
"row_scale_min": float(min(row_scale)) if row_scale else 0.0,
"row_scale_max": float(max(row_scale)) if row_scale else 0.0,
"col_scale_min": float(min(col_scale)) if col_scale else 0.0,
"col_scale_max": float(max(col_scale)) if col_scale else 0.0,
"zero_rows": zero_rows,
"zero_cols": zero_cols,
"symmetric_scale": symmetric_scale,
"symmetric": is_symmetric,
"notes": notes,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Suggest row/column scaling for matrix equilibration.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--matrix", required=True, help="Path to matrix file (.npy or text)")
parser.add_argument(
"--delimiter",
default=None,
help="Delimiter for text matrices (default: any whitespace)",
)
parser.add_argument(
"--symmetry-tol",
type=float,
default=1e-8,
help="Tolerance for symmetry check",
)
parser.add_argument(
"--symmetric",
action="store_true",
help="Request symmetric scaling (uses row max norms)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
if not os.path.exists(args.matrix):
print(f"Matrix not found: {args.matrix}", file=sys.stderr)
sys.exit(2)
try:
matrix = load_matrix(args.matrix, args.delimiter)
results = compute_scaling(matrix, args.symmetry_tol, args.symmetric)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"matrix": args.matrix,
"symmetry_tol": args.symmetry_tol,
"symmetric": args.symmetric,
},
"results": results,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Scaling equilibration")
print(f" shape: {results['shape']}")
print(f" row_scale_min: {results['row_scale_min']:.6g}")
print(f" row_scale_max: {results['row_scale_max']:.6g}")
print(f" col_scale_min: {results['col_scale_min']:.6g}")
print(f" col_scale_max: {results['col_scale_max']:.6g}")
if results["symmetric_scale"] is not None:
print(" symmetric_scale: provided")
for note in results["notes"]:
print(f" note: {note}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import sys
from typing import Dict, List
def select_solver(
symmetric: bool,
positive_definite: bool,
sparse: bool,
size: int,
nearly_symmetric: bool,
ill_conditioned: bool,
complex_valued: bool,
memory_limited: bool,
) -> Dict[str, List[str] | str]:
if size <= 0:
raise ValueError("size must be positive")
recommended: List[str] = []
alternatives: List[str] = []
notes: List[str] = []
large = size >= 200_000
if symmetric:
if positive_definite:
if sparse or large or memory_limited:
recommended.append("CG")
alternatives.append("MINRES")
notes.append("Use IC/AMG preconditioning for SPD systems.")
else:
recommended.append("Cholesky")
alternatives.append("CG")
else:
recommended.append("MINRES")
alternatives.append("SYMMLQ")
notes.append("Indefinite symmetric system; avoid CG.")
else:
if nearly_symmetric:
recommended.append("BiCGSTAB")
alternatives.append("GMRES")
else:
recommended.append("GMRES (restarted)")
alternatives.append("BiCGSTAB")
notes.append("Use ILU/AMG preconditioning for nonsymmetric systems.")
if complex_valued:
notes.append("Ensure solver supports complex arithmetic.")
if ill_conditioned:
notes.append("Consider scaling/equilibration and stronger preconditioning.")
if memory_limited and "GMRES (restarted)" in recommended:
notes.append("Restarted GMRES reduces memory at the cost of robustness.")
return {
"recommended": recommended,
"alternatives": alternatives,
"notes": notes,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Select a linear solver based on matrix properties.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--symmetric", action="store_true", help="Matrix is symmetric")
parser.add_argument(
"--positive-definite",
action="store_true",
help="Matrix is positive definite",
)
parser.add_argument("--sparse", action="store_true", help="Matrix is sparse")
parser.add_argument("--size", type=int, required=True, help="Matrix size (n)")
parser.add_argument(
"--nearly-symmetric",
action="store_true",
help="Matrix is nearly symmetric",
)
parser.add_argument(
"--ill-conditioned",
action="store_true",
help="Matrix is ill-conditioned",
)
parser.add_argument(
"--complex-valued",
action="store_true",
help="Matrix has complex entries",
)
parser.add_argument(
"--memory-limited",
action="store_true",
help="Memory is constrained",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
result = select_solver(
symmetric=args.symmetric,
positive_definite=args.positive_definite,
sparse=args.sparse,
size=args.size,
nearly_symmetric=args.nearly_symmetric,
ill_conditioned=args.ill_conditioned,
complex_valued=args.complex_valued,
memory_limited=args.memory_limited,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"symmetric": args.symmetric,
"positive_definite": args.positive_definite,
"sparse": args.sparse,
"size": args.size,
"nearly_symmetric": args.nearly_symmetric,
"ill_conditioned": args.ill_conditioned,
"complex_valued": args.complex_valued,
"memory_limited": args.memory_limited,
},
"results": result,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Solver selection")
print(f" recommended: {', '.join(result['recommended'])}")
if result["alternatives"]:
print(f" alternatives: {', '.join(result['alternatives'])}")
for note in result["notes"]:
print(f" note: {note}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from typing import Optional
import numpy as np
def load_matrix(path: str, delimiter: Optional[str]) -> np.ndarray:
_, ext = os.path.splitext(path)
if ext == ".npy":
return np.load(path)
return np.loadtxt(path, delimiter=delimiter)
def compute_stats(matrix: np.ndarray, symmetry_tol: float) -> dict:
if matrix.ndim != 2:
raise ValueError("matrix must be 2D")
if not np.all(np.isfinite(matrix)):
raise ValueError("matrix contains non-finite values")
m, n = matrix.shape
nnz = int(np.count_nonzero(matrix))
density = float(nnz) / float(m * n) if m * n > 0 else 0.0
# bandwidth: max |i-j| where A_ij != 0
rows, cols = np.nonzero(matrix)
if rows.size:
bandwidth = int(np.max(np.abs(rows - cols)))
else:
bandwidth = 0
symmetric = bool(np.allclose(matrix, matrix.T, atol=symmetry_tol, rtol=0.0))
return {
"shape": [m, n],
"nnz": nnz,
"density": density,
"bandwidth": bandwidth,
"symmetry": symmetric,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute sparsity statistics for a matrix.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--matrix", required=True, help="Path to matrix file (.npy or text)")
parser.add_argument(
"--delimiter",
default=None,
help="Delimiter for text matrices (default: any whitespace)",
)
parser.add_argument(
"--symmetry-tol",
type=float,
default=1e-8,
help="Tolerance for symmetry check",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
if not os.path.exists(args.matrix):
print(f"Matrix not found: {args.matrix}", file=sys.stderr)
sys.exit(2)
try:
matrix = load_matrix(args.matrix, args.delimiter)
results = compute_stats(matrix, args.symmetry_tol)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"matrix": args.matrix,
"symmetry_tol": args.symmetry_tol,
},
"results": results,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Sparsity stats")
print(f" shape: {results['shape']}")
print(f" nnz: {results['nnz']}")
print(f" density: {results['density']:.6g}")
print(f" bandwidth: {results['bandwidth']}")
print(f" symmetric: {results['symmetry']}")
if __name__ == "__main__":
main()
Related skills
FAQ
How does it pick a solver?
It uses a decision flowchart based on matrix size, sparsity, symmetry, and definiteness, e.g. CG with AMG/IC for sparse SPD systems and GMRES for nonsymmetric ones.
Can it diagnose stagnation?
Yes. It analyzes residual history to classify convergence rate and recommend actions like a better preconditioner or scaling.