
Numerical Stability
- 16 installs
- 869 repo stars
- Updated June 8, 2026
- beita6969/scienceclaw
numerical-stability is a skill that analyzes and enforces numerical stability for time-dependent PDE simulations using CFL, von Neumann, and conditioning checks.
About
This skill analyzes and enforces numerical stability for time-dependent PDE simulations. A developer checks CFL, Fourier, and reaction criteria, runs von Neumann analysis on custom schemes, checks matrix conditioning, and detects stiffness. It diagnoses numerical blow-up and recommends reducing dt or switching schemes. It matters for keeping explicit and implicit simulations stable and defensible.
- CFL, Fourier, and reaction stability limits with quick-reference formulas
- Four scripts: CFL checker, von Neumann analyzer, matrix condition, stiffness detector
- Diagnoses blow-up and recommends dt or scheme changes
Numerical Stability 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)
numerical-stability capabilities & compatibility
Free; needs only Python with NumPy for the analysis scripts.
- Capabilities
- data analysis · debugging
- Use cases
- data analysis · debugging
- Pricing
- Free
What numerical-stability says it does
Analyze and enforce numerical stability for time-dependent PDE simulations.
Provide a repeatable checklist and script-driven checks to keep time-dependent simulations stable and defensible.
Violation**: Fo = 1.0 > 0.25, unstable!
npx skills add https://github.com/beita6969/scienceclaw --skill numerical-stabilityAdd 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
Check CFL, Fourier, and von Neumann stability and diagnose numerical blow-up in PDE simulations.
Who is it for?
Checking CFL and Fourier limits, running von Neumann analysis, and diagnosing simulation blow-up.
Skip if: Implicit-scheme CFL checks, since implicit schemes are unconditionally stable per the docs.
When should I use this skill?
You are selecting a time step, seeing numerical blow-up, or verifying a scheme's stability.
What you get
The developer gets a stability verdict, a recommended time step, and a diagnosis of any instability.
By the numbers
- 4 stability analysis scripts
- 6-item pre-simulation checklist
- Fourier limit Fo <= 0.5 in 1D
Files
Numerical Stability
Goal
Provide a repeatable checklist and script-driven checks to keep time-dependent simulations stable and defensible.
Requirements
- Python 3.8+
- NumPy (for matrix_condition.py and von_neumann_analyzer.py)
- See
scripts/requirements.txtfor dependencies
Inputs to Gather
| Input | Description | Example |
|---|---|---|
Grid spacing dx | Spatial discretization | 0.01 m |
Time step dt | Temporal discretization | 1e-4 s |
Velocity v | Advection speed | 1.0 m/s |
Diffusivity D | Thermal/mass diffusivity | 1e-5 m²/s |
Reaction rate k | First-order rate constant | 100 s⁻¹ |
| Dimensions | 1D, 2D, or 3D | 2 |
| Scheme type | Explicit or implicit | explicit |
Decision Guidance
Choosing Explicit vs Implicit
Is the problem stiff (fast + slow dynamics)?
├── YES → Use implicit or IMEX scheme
│ └── Check conditioning with matrix_condition.py
└── NO → Is CFL/Fourier satisfied with reasonable dt?
├── YES → Use explicit scheme (cheaper per step)
└── NO → Consider implicit or reduce dxStability Limit Quick Reference
| Physics | Number | Explicit Limit (1D) | Formula |
|---|---|---|---|
| Advection | CFL | C ≤ 1 | C = v·dt/dx |
| Diffusion | Fourier | Fo ≤ 0.5 | Fo = D·dt/dx² |
| Reaction | Reaction | R ≤ 1 | R = k·dt |
Multi-dimensional correction: For d dimensions, diffusion limit is Fo ≤ 1/(2d).
Script Outputs (JSON Fields)
| Script | Key Outputs |
|---|---|
scripts/cfl_checker.py | metrics.cfl, metrics.fourier, recommended_dt, stable |
scripts/von_neumann_analyzer.py | results.max_amplification, results.stable |
scripts/matrix_condition.py | results.condition_number, results.is_symmetric |
scripts/stiffness_detector.py | results.stiffness_ratio, results.stiff, results.recommendation |
Workflow
1. Identify dominant physics (advection vs diffusion vs reaction) 2. Run CFL checker with scripts/cfl_checker.py 3. Compare to limits and adjust dt if needed 4. Check stiffness with scripts/stiffness_detector.py if multiple scales 5. Analyze custom schemes with scripts/von_neumann_analyzer.py 6. Check conditioning with scripts/matrix_condition.py for implicit solves 7. Document the stability verdict and recommended time step
Conversational Workflow Example
User: My phase-field simulation is blowing up after 100 steps. I'm using explicit Euler with dx=0.01, dt=1e-4, and diffusivity D=1e-3.
Agent workflow: 1. Check stability criteria:
python3 scripts/cfl_checker.py --dx 0.01 --dt 1e-4 --diffusivity 1e-3 --dimensions 2 --json2. Interpret results:
- Fourier number:
Fo = 1e-3 × 1e-4 / (0.01)² = 1.0 - 2D limit:
Fo ≤ 0.25 - Violation: Fo = 1.0 > 0.25, unstable!
3. Recommend fix:
- Reduce dt to
2.5e-5(to get Fo = 0.25) - Or increase dx, or switch to implicit
Pre-Simulation Stability Checklist
- [ ] Identify dominant physics and nondimensional groups
- [ ] Compute CFL/Fourier/Reaction numbers with
cfl_checker.py - [ ] If explicit and limit violated, reduce
dtor change scheme - [ ] If stiffness ratio > 1000, select implicit/stiff integrator
- [ ] For custom schemes, verify amplification factor ≤ 1
- [ ] Document stability reasoning with inputs and outputs
CLI Examples
# Check CFL/Fourier for 2D diffusion-advection
python3 scripts/cfl_checker.py --dx 0.1 --dt 0.01 --velocity 1.0 --diffusivity 0.1 --dimensions 2 --json
# Von Neumann analysis for custom 3-point stencil
python3 scripts/von_neumann_analyzer.py --coeffs 0.2,0.6,0.2 --dx 1.0 --nk 128 --json
# Detect stiffness from eigenvalue estimates
python3 scripts/stiffness_detector.py --eigs=-1,-1000 --json
# Check matrix conditioning for implicit system
python3 scripts/matrix_condition.py --matrix A.npy --norm 2 --jsonError Handling
| Error | Cause | Resolution |
|---|---|---|
dx and dt must be positive | Zero or negative values | Provide valid positive numbers |
No stability criteria applied | Missing velocity/diffusivity | Provide at least one physics parameter |
Matrix file not found | Invalid path | Check matrix file exists |
Could not compute eigenvalues | Singular or ill-formed matrix | Check matrix validity |
Interpretation Guidance
| Scenario | Meaning | Action |
|---|---|---|
stable: true | All checked criteria satisfied | Proceed with simulation |
stable: false | At least one limit violated | Reduce dt or change scheme |
stable: null | No criteria could be applied | Provide more physics inputs |
| Stiffness ratio > 1000 | Problem is stiff | Use implicit integrator |
| Condition number > 10⁶ | Ill-conditioned | Use scaling/preconditioning |
Limitations
- Explicit schemes only for CFL/Fourier checks (implicit is unconditionally stable)
- Von Neumann analysis assumes linear, constant-coefficient, periodic BCs
- Stiffness detection requires eigenvalue estimates from user
References
references/stability_criteria.md- Decision thresholds and formulasreferences/common_pitfalls.md- Frequent failure modes and fixesreferences/scheme_catalog.md- Stability properties of common schemes
Version History
- v1.1.0 (2024-12-24): Enhanced documentation, decision guidance, examples
- v1.0.0: Initial release with 4 stability analysis scripts
Common Stability Pitfalls
Overview
This document catalogs frequent stability failures and their fixes. Use this as a diagnostic guide when simulations blow up.
---
Pitfall Categories
| Category | Frequency | Severity |
|---|---|---|
| CFL Violations | Very Common | High |
| Stiffness Issues | Common | High |
| Boundary Conditions | Common | Medium |
| Numerical Precision | Occasional | Medium |
| Initialization | Occasional | High |
---
CFL/Fourier Violations
Symptom: Exponential Growth
Pattern: Solution grows exponentially, values reach 10^30+ within a few steps.
Cause: Time step exceeds stability limit.
Diagnosis:
python3 scripts/cfl_checker.py --dx YOUR_DX --dt YOUR_DT --diffusivity YOUR_D --velocity YOUR_V --dimensions YOUR_DFix: 1. Reduce dt to meet all limits 2. Or switch to implicit scheme 3. Or increase dx (reduces resolution)
Symptom: Oscillations Then Blow-up
Pattern: Solution develops 2dx oscillations that grow unbounded.
Cause: Central differences for advection-dominated problems.
Fix: 1. Use upwind scheme for advection terms 2. Or add artificial diffusion 3. Or use flux limiters (TVD schemes)
---
Stiffness Issues
Symptom: Extremely Small Time Steps
Pattern: Adaptive stepping reduces dt to 10^-15 or smaller.
Cause: Stiff system with widely separated time scales.
Diagnosis:
python3 scripts/stiffness_detector.py --eigs=FAST_EIGENVALUE,SLOW_EIGENVALUEFix: 1. Use implicit or IMEX scheme 2. Identify and treat stiff terms implicitly 3. Use BDF, Radau, or Rosenbrock methods
Symptom: Correct Results But Very Slow
Pattern: Simulation runs but takes millions of small steps.
Cause: Explicit scheme on stiff problem.
Fix: Same as above - switch to stiff solver.
---
Boundary Condition Issues
Symptom: Instability at Boundaries
Pattern: Blow-up starts at domain edges.
Causes: 1. Incompatible BC with interior scheme 2. Reflection of outgoing waves 3. Wrong-sided stencil at boundaries
Fixes:
| Problem | Solution |
|---|---|
| Wave reflection | Use absorbing/sponge layers |
| Stencil mismatch | Use one-sided differences at boundaries |
| Dirichlet oscillations | Check that BC is applied correctly |
Symptom: Spurious Waves from Boundaries
Pattern: Waves emanate from boundaries into domain.
Cause: Non-physical boundary treatment.
Fix: 1. Use characteristic BCs for hyperbolic problems 2. Implement proper Neumann/Robin conditions 3. Add buffer zones
---
Initialization Issues
Symptom: Immediate Blow-up
Pattern: Solution diverges within first 1-10 steps.
Causes: 1. Initial condition violates conservation 2. Discontinuous IC with central schemes 3. IC incompatible with BCs
Fixes:
| Problem | Solution |
|---|---|
| Sharp discontinuities | Smooth IC or use shock-capturing |
| IC/BC mismatch | Ensure IC satisfies BCs |
| Non-physical IC | Check conservation laws |
Symptom: Transient Spike Then Settling
Pattern: Large values early, then stabilizes.
Cause: IC not in equilibrium with dynamics.
Fix: 1. Ramp parameters gradually (see time-stepping skill) 2. Use smaller dt during startup 3. Pre-condition IC to equilibrium
---
Numerical Precision Issues
Symptom: Late-Time Instability
Pattern: Stable for many steps, then suddenly diverges.
Causes: 1. Accumulating round-off error 2. Loss of conservation 3. Condition number degradation
Fixes:
| Problem | Solution |
|---|---|
| Round-off accumulation | Use double precision, Kahan summation |
| Conservation loss | Use conservative discretization |
| Conditioning | Re-scale/equilibrate matrices |
Symptom: Checkerboard Pattern
Pattern: Alternating high/low values in 2dx wavelength.
Cause: Odd-even decoupling (central schemes without stabilization).
Fix: 1. Add small diffusion 2. Use staggered grids 3. Apply filtering
---
Multi-Physics Coupling Issues
Symptom: Instability When Coupling
Pattern: Each physics stable alone, unstable when coupled.
Causes: 1. Operator splitting error 2. Incompatible time scales 3. Conservation violation at interface
Fixes:
| Problem | Solution |
|---|---|
| Splitting error | Use smaller dt or higher-order splitting |
| Time scale mismatch | Use IMEX or subcycling |
| Interface issues | Ensure conservation across coupling |
---
Setup Mistakes
Unit Mixing
Pattern: Inconsistent results or immediate blow-up.
Cause: Mixing units (e.g., dx in microns, v in m/s).
Fix: Convert all quantities to consistent units before computing.
Mesh Refinement
Pattern: Instability after mesh refinement.
Cause: Reusing old dt after refining (dx changed).
Fix: Recompute dt after any mesh change.
Anisotropic Grids
Pattern: Instability in one direction.
Cause: Using dx but ignoring smaller dy.
Fix: Use smallest grid spacing for stability calculations.
---
Diagnostic Workflow
When simulation blows up:
1. WHERE does it start?
├── Boundaries → Check BC implementation
├── Interior → Check CFL/Fourier
└── Everywhere → Check IC or stiffness
2. WHEN does it start?
├── Immediately → IC or gross CFL violation
├── After some time → Gradual instability or round-off
└── At specific event → Check event handling
3. HOW does it manifest?
├── Exponential growth → Linear instability
├── Oscillations → Dispersion or central diff issue
└── Checkerboard → Odd-even decoupling---
Prevention Checklist
Before running:
- [ ] Verify CFL/Fourier with
cfl_checker.py - [ ] Check stiffness ratio if multiple scales
- [ ] Validate IC satisfies BCs
- [ ] Test with smaller dt first
- [ ] Enable conservation monitoring
During run:
- [ ] Monitor max/min values
- [ ] Track residuals or energy
- [ ] Watch for early warning signs
- [ ] Log dt changes (if adaptive)
---
Quick Fixes by Symptom
| Symptom | Quick Fix |
|---|---|
| Exponential blow-up | Reduce dt by 2x |
| 2dx oscillations | Add upwinding or diffusion |
| Boundary instability | Check BC implementation |
| Very slow convergence | Switch to implicit |
| Checkerboard | Add stabilization or filter |
| Conservation drift | Use conservative form |
Numerical Scheme Stability Catalog
Overview
This catalog summarizes stability properties of common time integration and spatial discretization schemes used in PDE simulations.
---
Time Integration Schemes
Explicit Methods
| Scheme | Order | Stability | CFL Limit | Memory | Notes |
|---|---|---|---|---|---|
| Forward Euler | 1 | Conditional | Region-dependent | Low | Simple but diffusive |
| RK2 (Heun) | 2 | Conditional | C ≤ ~1 | Low | Better accuracy |
| RK4 (Classic) | 4 | Conditional | C ≤ ~1.4 | Low | Most common explicit |
| RK45 (Dormand-Prince) | 4/5 | Conditional | Adaptive | Medium | Error estimation |
| Adams-Bashforth 2 | 2 | Conditional | C ≤ ~0.5 | Low | Multi-step, cheaper |
| Adams-Bashforth 4 | 4 | Conditional | C ≤ ~0.3 | Low | More restrictive |
| Leapfrog | 2 | Neutral | C ≤ 1 | Low | No damping, needs filter |
Implicit Methods
| Scheme | Order | Stability | Properties | Memory | Notes |
|---|---|---|---|---|---|
| Backward Euler | 1 | A-stable | Unconditional | Medium | Very diffusive |
| Crank-Nicolson | 2 | A-stable | Unconditional | Medium | May oscillate |
| BDF2 | 2 | A-stable | Unconditional | Medium | Good for stiff |
| BDF3-6 | 3-6 | A(α)-stable | Nearly unconditional | Medium | Higher order |
| Radau IIA | 3,5 | L-stable | Unconditional | High | Very stiff problems |
| SDIRK | 2-4 | L-stable | Unconditional | Medium | Good balance |
IMEX Methods
| Scheme | Order | Implicit Part | Explicit Part | Use Case |
|---|---|---|---|---|
| IMEX-Euler | 1 | Backward Euler | Forward Euler | Simple stiff/nonstiff |
| IMEX-RK2 | 2 | SDIRK | RK2 | Moderate stiffness |
| ARK4(3)6L | 4 | L-stable | RK4 | High-order IMEX |
---
Spatial Discretization Schemes
First Derivatives (Advection)
| Scheme | Order | Stability | Dispersion | Diffusion |
|---|---|---|---|---|
| Upwind (1st order) | 1 | Stable for C ≤ 1 | High | Adds artificial |
| Central (2nd order) | 2 | Unstable | Moderate | None |
| Upwind (3rd order) | 3 | Stable for C ≤ 1 | Low | Small artificial |
| QUICK | 3 | Conditional | Low | Small |
| WENO5 | 5 | Conditional | Very low | Adaptive |
Second Derivatives (Diffusion)
| Scheme | Order | Stability (1D) | Notes |
|---|---|---|---|
| Central (3-point) | 2 | Fo ≤ 0.5 | Standard |
| Central (5-point) | 4 | Fo ≤ 0.5 | Higher accuracy |
| Compact (Padé) | 4-6 | Similar | Implicit system |
---
Stability Regions
A-Stability
A method is A-stable if stable for all z = λ·dt with Re(λ) < 0.
A-stable methods:
✓ Backward Euler
✓ Crank-Nicolson
✓ BDF1-2
✓ Radau IIA
✓ All fully implicit methods
Not A-stable:
✗ All explicit methods
✗ BDF3-6 (A(α)-stable only)
✗ Adams methodsL-Stability
A method is L-stable if A-stable and |R(z)| → 0 as Re(z) → -∞.
L-stable methods:
✓ Backward Euler
✓ Radau IIA
✓ SDIRK methods
A-stable but not L-stable:
✗ Crank-Nicolson (oscillates for very stiff)
✗ Trapezoidal rule---
Scheme Selection Guide
By Problem Type
| Problem Type | Recommended Schemes |
|---|---|
| Smooth advection | RK4 + Upwind/Central |
| Advection with shocks | TVD, WENO, DG |
| Diffusion-dominated | Implicit or Crank-Nicolson |
| Stiff reactions | BDF, Radau, Rosenbrock |
| Wave propagation | Leapfrog + filter, DG |
| Navier-Stokes | IMEX, projection methods |
By Accuracy Requirement
| Accuracy | Time Integration | Spatial |
|---|---|---|
| Low (1st order) | Forward/Backward Euler | Upwind |
| Medium (2nd order) | RK2, Crank-Nicolson | Central, MUSCL |
| High (4th order) | RK4, BDF4 | Compact, DG |
| Very high (6th+) | DOP853, Spectral | Spectral, high-order DG |
---
Stability Tables
Forward Euler + Central Diffusion
| Dimensions | Fourier Limit | Max dt Formula |
|---|---|---|
| 1D | Fo ≤ 0.5 | dt ≤ dx²/(2D) |
| 2D | Fo ≤ 0.25 | dt ≤ dx²/(4D) |
| 3D | Fo ≤ 0.167 | dt ≤ dx²/(6D) |
RK4 + Central Advection
| Property | Value |
|---|---|
| CFL Limit | C ≤ 2√2 ≈ 2.83 (linear) |
| Practical Limit | C ≤ 1.5 (nonlinear) |
BDF Methods
| Order | Stability | Error Constant |
|---|---|---|
| BDF1 | A-stable, L-stable | 1 |
| BDF2 | A-stable | 2/3 |
| BDF3 | A(86°)-stable | 6/11 |
| BDF4 | A(73°)-stable | 12/25 |
| BDF5 | A(51°)-stable | 60/137 |
| BDF6 | A(17°)-stable | 60/147 |
---
Comparison Matrix
| Feature | FE | BE | CN | RK4 | BDF2 | Radau |
|---|---|---|---|---|---|---|
| Order | 1 | 1 | 2 | 4 | 2 | 3-5 |
| A-stable | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ |
| L-stable | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ |
| Linear solve | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Memory | Low | Med | Med | Low | Med | High |
| Stiff OK | ✗ | ✓ | ⚠️ | ✗ | ✓ | ✓ |
Legend: FE=Forward Euler, BE=Backward Euler, CN=Crank-Nicolson
---
Quick Reference
NON-STIFF: RK4, RK45, Adams-Bashforth
STIFF: BDF2, Radau, SDIRK, Rosenbrock
MIXED: IMEX schemes
WAVE PROPAGATION: Leapfrog, symplectic
SHOCK CAPTURING: TVD, WENO, DGStability Criteria Reference
Overview
This document provides comprehensive stability criteria for explicit time integration of PDEs. Use these formulas to determine maximum stable time steps.
---
Core Nondimensional Numbers
Courant-Friedrichs-Lewy (CFL) Number
For advection-dominated problems:
C = v × dt / dxWhere:
v= advection velocity (m/s)dt= time step (s)dx= grid spacing (m)
Physical meaning: Ratio of distance traveled per time step to grid spacing.
Fourier Number
For diffusion-dominated problems:
Fo = D × dt / dx²Where:
D= diffusivity (m²/s)dt= time step (s)dx= grid spacing (m)
Physical meaning: Ratio of diffusion distance per time step to grid spacing squared.
Reaction Number
For reaction-dominated problems:
R = k × dtWhere:
k= reaction rate constant (1/s)dt= time step (s)
Physical meaning: Number of reaction time constants per time step.
---
Explicit Stability Limits
Advection (1D)
| Scheme | CFL Limit | Notes |
|---|---|---|
| Upwind (first-order) | C ≤ 1 | Dissipative, stable |
| Lax-Friedrichs | C ≤ 1 | More dissipative |
| Lax-Wendroff | C ≤ 1 | Second-order, dispersive |
| FTCS (central) | Unstable | Never use for advection |
| Leapfrog | C ≤ 1 | Neutral stability, needs filter |
Advection (Multi-dimensional)
For d dimensions with velocity components v_i:
dt ≤ dx / (sum of |v_i|) [L1 norm]
dt ≤ dx / sqrt(sum of v_i²) [L2 norm, less restrictive]Diffusion (1D)
| Scheme | Fourier Limit | Notes |
|---|---|---|
| FTCS | Fo ≤ 0.5 | Standard explicit |
| DuFort-Frankel | Unconditional | But conditionally consistent |
Diffusion (Multi-dimensional)
Fo ≤ 1 / (2 × d)| Dimensions | Fourier Limit |
|---|---|
| 1D | Fo ≤ 0.50 |
| 2D | Fo ≤ 0.25 |
| 3D | Fo ≤ 0.167 |
Reaction
R ≤ 1 (or R ≤ 0.2 for safety margin)For stiff reactions (k >> 1), explicit methods are inefficient.
---
Combined Criteria
Advection + Diffusion
When both are present, apply both limits:
dt ≤ min(dt_advection, dt_diffusion)
dt_advection = C_limit × dx / v
dt_diffusion = Fo_limit × dx² / DGrid Péclet Number
Ratio of advection to diffusion:
Pe = v × dx / D| Pe | Dominant Physics |
|---|---|
| Pe << 1 | Diffusion-dominated |
| Pe ≈ 1 | Both important |
| Pe >> 1 | Advection-dominated |
Note: High Péclet numbers (Pe > 2) cause spurious oscillations with central schemes.
Advection + Diffusion + Reaction
dt ≤ min(dt_adv, dt_diff, dt_react)Use the most restrictive limit.
---
Safety Factors
Recommended Safety Margins
| Scenario | Safety Factor | Resulting dt |
|---|---|---|
| Standard | 0.9 | 90% of limit |
| Strong coupling | 0.5-0.7 | 50-70% of limit |
| Stiff systems | 0.3-0.5 | 30-50% of limit |
| Near stability boundary | 0.8 | 80% of limit |
When to Use Smaller Safety Factors
- Multiple coupled physics
- Nonlinear problems (limits may vary in time)
- Near phase transitions or sharp gradients
- Adaptive mesh refinement (dx changes)
---
Special Cases
Anisotropic Meshes
When dx ≠ dy ≠ dz:
dt_diff ≤ 1/(2D) × 1/(1/dx² + 1/dy² + 1/dz²)Use the smallest spacing for the most conservative estimate.
Variable Coefficients
When v(x) or D(x) vary in space:
Use maximum values: v_max, D_maxTime-Dependent Coefficients
Re-evaluate stability at each time step or periodically.
Nonlinear Problems
Estimate local velocity/diffusivity from solution and re-check limits.
---
Decision Algorithm
def compute_stable_dt(dx, v, D, k, dimensions, safety=0.9):
dt_candidates = []
# Advection limit
if v > 0:
dt_adv = dx / v # CFL = 1
dt_candidates.append(dt_adv)
# Diffusion limit
if D > 0:
fo_limit = 1.0 / (2.0 * dimensions)
dt_diff = fo_limit * dx**2 / D
dt_candidates.append(dt_diff)
# Reaction limit
if k > 0:
dt_react = 1.0 / k
dt_candidates.append(dt_react)
if dt_candidates:
return min(dt_candidates) * safety
else:
return None # No limits apply---
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Ignoring multi-D factor | Instability in 2D/3D | Use Fo ≤ 1/(2d) |
| Using central diff for advection | Oscillations | Use upwind or limiters |
| No safety factor | Borderline stability | Apply 0.8-0.9 factor |
| Forgetting anisotropy | Instability | Use smallest dx |
| Ignoring variable coefficients | Instability in some regions | Use max values |
---
Quick Reference Card
ADVECTION: dt ≤ dx / v (CFL ≤ 1)
DIFFUSION: dt ≤ dx² / (2·d·D) (Fo ≤ 1/(2d))
REACTION: dt ≤ 1 / k (R ≤ 1)
COMBINED: dt = min(all limits) × safety#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import Dict, List, Optional
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute CFL/Fourier numbers and suggest stable dt limits.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--dx", type=float, required=True, help="Grid spacing")
parser.add_argument("--dt", type=float, required=True, help="Time step")
parser.add_argument("--velocity", type=float, default=None, help="Advection velocity")
parser.add_argument("--diffusivity", type=float, default=None, help="Diffusivity")
parser.add_argument("--reaction-rate", type=float, default=None, help="Reaction rate")
parser.add_argument("--dimensions", type=int, default=1, help="Spatial dimensions")
parser.add_argument(
"--scheme",
choices=["explicit", "implicit"],
default="explicit",
help="Time integration scheme",
)
parser.add_argument("--advection-limit", type=float, default=None, help="CFL limit")
parser.add_argument("--diffusion-limit", type=float, default=None, help="Fourier limit")
parser.add_argument("--reaction-limit", type=float, default=None, help="Reaction limit")
parser.add_argument("--safety", type=float, default=1.0, help="Safety factor for dt")
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_cfl(
dx: float,
dt: float,
velocity: Optional[float],
diffusivity: Optional[float],
reaction_rate: Optional[float],
dimensions: int,
scheme: str,
advection_limit: Optional[float],
diffusion_limit: Optional[float],
reaction_limit: Optional[float],
safety: float,
) -> Dict[str, object]:
notes: List[str] = []
if dx <= 0 or dt <= 0:
raise ValueError("dx and dt must be positive")
if dimensions <= 0:
raise ValueError("dimensions must be positive")
if safety <= 0:
raise ValueError("safety must be positive")
v = None
if velocity is not None:
if velocity < 0:
notes.append("Negative velocity detected; using absolute value for stability analysis")
v = abs(velocity)
d = None
if diffusivity is not None:
if diffusivity < 0:
notes.append("Negative diffusivity detected; may indicate spinodal decomposition; using absolute value")
d = abs(diffusivity)
k = None
if reaction_rate is not None:
if reaction_rate < 0:
notes.append("Negative reaction rate detected; using absolute value for stability analysis")
k = abs(reaction_rate)
if advection_limit is None:
advection_limit = 1.0 if scheme == "explicit" else math.inf
if diffusion_limit is None:
diffusion_limit = 1.0 / (2.0 * dimensions) if scheme == "explicit" else math.inf
if reaction_limit is None:
reaction_limit = 1.0 if scheme == "explicit" else math.inf
cfl = None
dt_max_adv = None
if v is not None and v > 0:
cfl = v * dt / dx
if math.isfinite(advection_limit):
dt_max_adv = advection_limit * dx / v
elif v == 0:
cfl = 0.0
fo = None
dt_max_diff = None
if d is not None and d > 0:
fo = d * dt / (dx ** 2)
if math.isfinite(diffusion_limit):
dt_max_diff = diffusion_limit * (dx ** 2) / d
elif d == 0:
fo = 0.0
react = None
dt_max_react = None
if k is not None and k > 0:
react = k * dt
if math.isfinite(reaction_limit):
dt_max_react = reaction_limit / k
elif k == 0:
react = 0.0
criteria_applied: List[str] = []
stable: Optional[bool] = True
if scheme == "explicit":
if cfl is not None and math.isfinite(advection_limit):
criteria_applied.append("advection")
stable = stable and (cfl <= advection_limit + 1e-12)
if fo is not None and math.isfinite(diffusion_limit):
criteria_applied.append("diffusion")
stable = stable and (fo <= diffusion_limit + 1e-12)
if react is not None and math.isfinite(reaction_limit):
criteria_applied.append("reaction")
stable = stable and (react <= reaction_limit + 1e-12)
else:
notes.append("Implicit scheme: stability limits are relaxed; check accuracy.")
if not criteria_applied:
stable = None
notes.append("No stability criteria applied; provide velocity/diffusivity/reaction.")
dt_candidates = [x for x in [dt_max_adv, dt_max_diff, dt_max_react] if x is not None]
recommended_dt = min(dt_candidates) * safety if dt_candidates else None
return {
"inputs": {
"dx": dx,
"dt": dt,
"velocity": velocity,
"diffusivity": diffusivity,
"reaction_rate": reaction_rate,
"dimensions": dimensions,
"scheme": scheme,
"safety": safety,
},
"metrics": {
"cfl": cfl,
"fourier": fo,
"reaction": react,
},
"limits": {
"advection_limit": advection_limit,
"diffusion_limit": diffusion_limit,
"reaction_limit": reaction_limit,
},
"criteria_applied": criteria_applied,
"recommended_dt": recommended_dt,
"stable": stable,
"notes": notes,
}
def main() -> None:
args = parse_args()
try:
payload = compute_cfl(
dx=args.dx,
dt=args.dt,
velocity=args.velocity,
diffusivity=args.diffusivity,
reaction_rate=args.reaction_rate,
dimensions=args.dimensions,
scheme=args.scheme,
advection_limit=args.advection_limit,
diffusion_limit=args.diffusion_limit,
reaction_limit=args.reaction_limit,
safety=args.safety,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("CFL check")
print(f" scheme: {args.scheme}")
cfl = payload["metrics"]["cfl"]
if cfl is not None:
print(f" CFL: {cfl:.6g} (limit {payload['limits']['advection_limit']:.6g})")
else:
print(" CFL: n/a")
fo = payload["metrics"]["fourier"]
if fo is not None:
print(f" Fourier: {fo:.6g} (limit {payload['limits']['diffusion_limit']:.6g})")
else:
print(" Fourier: n/a")
react = payload["metrics"]["reaction"]
if react is not None:
print(f" Reaction: {react:.6g} (limit {payload['limits']['reaction_limit']:.6g})")
else:
print(" Reaction: n/a")
stable = payload["stable"]
stable_label = "unknown" if stable is None else str(stable)
print(f" stable: {stable_label}")
recommended_dt = payload["recommended_dt"]
if recommended_dt is not None:
print(f" recommended_dt: {recommended_dt:.6g}")
for note in payload["notes"]:
print(f" note: {note}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from typing import Dict, Optional, Union
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 parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute condition number and eigenvalue spread.",
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(
"--norm",
default="2",
help="Condition number norm: 2, fro, inf, -inf, 1, -1",
)
parser.add_argument(
"--symmetry-tol",
type=float,
default=1e-8,
help="Tolerance for symmetry check",
)
parser.add_argument(
"--skip-eigs",
action="store_true",
help="Skip eigenvalue spread for large matrices",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def parse_norm(value: str) -> Union[float, str]:
value = value.strip().lower()
if value in {"2", "1", "-1"}:
return float(value)
if value in {"inf", "-inf", "fro"}:
return value
raise ValueError("norm must be one of: 2, 1, -1, inf, -inf, fro")
def compute_condition(
matrix: np.ndarray,
norm: Union[float, str],
symmetry_tol: float,
skip_eigs: bool,
) -> Dict[str, object]:
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
raise ValueError("matrix must be square")
if not np.all(np.isfinite(matrix)):
raise ValueError("matrix contains non-finite values")
cond = float(np.linalg.cond(matrix, p=norm))
is_symmetric = bool(np.allclose(matrix, matrix.T, atol=symmetry_tol, rtol=0.0))
spread = None
eig_min = None
eig_max = None
if not skip_eigs:
eigvals = np.linalg.eigvals(matrix)
abs_eigs = np.abs(eigvals)
nonzero = abs_eigs[abs_eigs > 0]
if nonzero.size:
eig_min = float(np.min(nonzero))
eig_max = float(np.max(nonzero))
spread = float(eig_max / eig_min)
else:
spread = float("inf")
status = "ok"
note = None
if cond > 1e10:
status = "ill-conditioned"
note = "Consider preconditioning or scaling."
elif cond > 1e8:
status = "poorly-conditioned"
note = "Preconditioning likely needed."
return {
"condition_number": cond,
"eigenvalue_spread": spread,
"eigenvalue_min_abs": eig_min,
"eigenvalue_max_abs": eig_max,
"is_symmetric": is_symmetric,
"status": status,
"note": note,
}
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:
norm = parse_norm(args.norm)
matrix = load_matrix(args.matrix, args.delimiter)
results = compute_condition(matrix, norm, args.symmetry_tol, args.skip_eigs)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"matrix": args.matrix,
"shape": list(matrix.shape),
"norm": args.norm,
"symmetry_tol": args.symmetry_tol,
"skip_eigs": args.skip_eigs,
},
"results": results,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Matrix conditioning")
print(f" shape: {matrix.shape}")
print(f" condition number: {results['condition_number']:.6g}")
if results["eigenvalue_spread"] is not None:
print(f" eigenvalue spread: {results['eigenvalue_spread']:.6g}")
else:
print(" eigenvalue spread: skipped")
print(f" symmetric: {results['is_symmetric']}")
print(f" status: {results['status']}")
if results["note"]:
print(f" note: {results['note']}")
if __name__ == "__main__":
main()
numpy>=1.21
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from typing import Dict, Optional
import numpy as np
def parse_eigs(raw: str) -> np.ndarray:
parts = [p.strip() for p in raw.split(",") if p.strip()]
if not parts:
raise ValueError("eigs must be a comma-separated list")
return np.array([complex(p) for p in parts], dtype=complex)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Detect stiffness from eigenvalues or a Jacobian matrix.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--eigs", help="Comma-separated eigenvalues")
group.add_argument("--jacobian", help="Path to Jacobian matrix (.npy or text)")
parser.add_argument(
"--delimiter",
default=None,
help="Delimiter for text Jacobians (default: any whitespace)",
)
parser.add_argument(
"--threshold",
type=float,
default=1e3,
help="Stiffness ratio threshold",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
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_stiffness(eigs: np.ndarray, threshold: float) -> Dict[str, object]:
if threshold <= 0:
raise ValueError("threshold must be positive")
if eigs.size == 0:
raise ValueError("eigs must be non-empty")
if not np.all(np.isfinite(eigs)):
raise ValueError("eigs contain non-finite values")
abs_eigs = np.abs(eigs)
nonzero = abs_eigs[abs_eigs > 0]
ratio = float(np.max(nonzero) / np.min(nonzero)) if nonzero.size else float("inf")
stiff = ratio >= threshold
recommendation = "implicit (BDF/Radau)" if stiff else "explicit (RK/Adams)"
return {
"stiffness_ratio": ratio,
"stiff": stiff,
"recommendation": recommendation,
"nonzero_count": int(nonzero.size),
"total_count": int(eigs.size),
}
def main() -> None:
args = parse_args()
try:
if args.eigs is not None:
eigs = parse_eigs(args.eigs)
source = "eigs"
else:
if not os.path.exists(args.jacobian):
print(f"Jacobian not found: {args.jacobian}", file=sys.stderr)
sys.exit(2)
jacobian = load_matrix(args.jacobian, args.delimiter)
if jacobian.ndim != 2 or jacobian.shape[0] != jacobian.shape[1]:
print("Jacobian must be square.", file=sys.stderr)
sys.exit(2)
eigs = np.linalg.eigvals(jacobian)
source = "jacobian"
results = compute_stiffness(eigs, args.threshold)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"source": source,
"threshold": args.threshold,
},
"results": results,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Stiffness detection")
print(f" stiffness ratio: {results['stiffness_ratio']:.6g}")
print(f" stiff: {results['stiff']}")
print(f" recommendation: {results['recommendation']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import Dict, Optional
import numpy as np
def parse_coeffs(raw: str) -> np.ndarray:
parts = [p.strip() for p in raw.split(",") if p.strip()]
if not parts:
raise ValueError("coeffs must be a comma-separated list")
return np.array([float(p) for p in parts], dtype=float)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute amplification factor for a linear update stencil.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--coeffs",
required=True,
help="Comma-separated stencil coefficients from negative to positive index",
)
parser.add_argument("--dx", type=float, default=1.0, help="Grid spacing")
parser.add_argument(
"--offset",
type=int,
default=None,
help="Index of coefficient corresponding to j=0 (default: center index)",
)
parser.add_argument("--kmin", type=float, default=None, help="Minimum wavenumber")
parser.add_argument("--kmax", type=float, default=None, help="Maximum wavenumber")
parser.add_argument("--nk", type=int, default=256, help="Number of k samples")
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def compute_amplification(
coeffs: np.ndarray,
dx: float,
nk: int,
offset: Optional[int],
kmin: Optional[float],
kmax: Optional[float],
) -> Dict[str, object]:
if dx <= 0:
raise ValueError("dx must be positive")
if nk <= 1:
raise ValueError("nk must be > 1")
n = len(coeffs)
if n == 0:
raise ValueError("coeffs must be non-empty")
if not np.all(np.isfinite(coeffs)):
raise ValueError("coeffs must be finite (no NaN or Inf values)")
if offset is None:
offset = n // 2
if offset < 0 or offset >= n:
raise ValueError("offset must be within coefficient indices")
if kmin is None:
kmin = -math.pi / dx
if kmax is None:
kmax = math.pi / dx
if kmin >= kmax:
raise ValueError("kmin must be < kmax")
j = np.arange(-offset, n - offset)
ks = np.linspace(kmin, kmax, nk)
phase = np.exp(1j * np.outer(ks, j) * dx)
amplification = phase @ coeffs
amp_mag = np.abs(amplification)
max_idx = int(np.argmax(amp_mag))
max_amp = float(amp_mag[max_idx])
k_at_max = float(ks[max_idx])
stable = max_amp <= 1.0 + 1e-12
warning = None
if n % 2 == 0 and offset == n // 2:
warning = "Even-length stencil: confirm offset aligns with j=0."
return {
"inputs": {
"coeffs": coeffs.tolist(),
"dx": dx,
"kmin": kmin,
"kmax": kmax,
"nk": nk,
"offset": offset,
},
"results": {
"max_amplification": max_amp,
"k_at_max": k_at_max,
"stable": stable,
"warning": warning,
},
}
def main() -> None:
args = parse_args()
try:
coeffs = parse_coeffs(args.coeffs)
payload = compute_amplification(
coeffs=coeffs,
dx=args.dx,
nk=args.nk,
offset=args.offset,
kmin=args.kmin,
kmax=args.kmax,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Von Neumann analysis")
print(f" coeffs: {payload['inputs']['coeffs']}")
print(
" k range: [{:.6g}, {:.6g}] with {} samples".format(
payload["inputs"]["kmin"],
payload["inputs"]["kmax"],
payload["inputs"]["nk"],
)
)
print(
" max amplification: {:.6g} at k={:.6g}".format(
payload["results"]["max_amplification"],
payload["results"]["k_at_max"],
)
)
print(f" stable: {payload['results']['stable']}")
if payload["results"]["warning"]:
print(f" warning: {payload['results']['warning']}")
if __name__ == "__main__":
main()
Related skills
FAQ
What are the explicit stability limits?
CFL C <= 1 for advection, Fourier Fo <= 0.5 for diffusion (1/(2d) in d dimensions), and reaction R <= 1.
When should I switch to an implicit scheme?
When the problem is stiff, for example a stiffness ratio above 1000, or when the CFL/Fourier limit needs an impractically small dt.