
Numerical Integration
- 16 installs
- 869 repo stars
- Updated June 8, 2026
- beita6969/scienceclaw
numerical-integration is a skill that selects and configures time-integration methods and adaptive stepping for ODE and PDE simulations.
About
This skill guides selecting and configuring time integration methods for ODE and PDE simulations. A developer classifies stiffness, chooses an integrator like RK45, BDF, or Rosenbrock, sets tolerances, and controls adaptive time steps. It also plans IMEX splitting for mixed stiff and non-stiff systems. It matters for keeping time-dependent simulations accurate and efficient.
- Integrator selection flowchart for stiff vs non-stiff ODE/PDE problems
- Five scripts: error norm, adaptive step controller, integrator selector, IMEX planner, splitting error
- Guidance on tolerances, PI/PID step control, and IMEX splitting
Numerical Integration 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-integration capabilities & compatibility
Free; core functionality needs only Python with NumPy.
- Capabilities
- data analysis · debugging
- Use cases
- data analysis · debugging
- Pricing
- Free
What numerical-integration says it does
Select and configure time integration methods for ODE/PDE simulations.
Provide a reliable workflow to select integrators, set tolerances, and manage adaptive time stepping for time-dependent simulations.
Recommend: Use IMEX-BDF2 with diffusion term implicit, double-well reaction explicit.
npx skills add https://github.com/beita6969/scienceclaw --skill numerical-integrationAdd 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
Select time-integration methods and control adaptive stepping for ODE and PDE simulations.
Who is it for?
Choosing integrators (RK45, BDF, Rosenbrock), setting tolerances, and managing adaptive time steps and IMEX splitting.
Skip if: Automatic stiffness detection, which it defers to the numerical-stability skill.
When should I use this skill?
You are choosing explicit or implicit schemes or tuning time-stepping for a simulation.
What you get
The developer gets a recommended integrator, tolerance settings, and an adaptive step-control plan.
By the numbers
- 5 scripts
- 5-item pre-integration checklist
Files
Numerical Integration
Goal
Provide a reliable workflow to select integrators, set tolerances, and manage adaptive time stepping for time-dependent simulations.
Requirements
- Python 3.8+
- NumPy (for some scripts)
- No heavy dependencies for core functionality
Inputs to Gather
| Input | Description | Example |
|---|---|---|
| Problem type | ODE/PDE, stiff/non-stiff | stiff PDE |
| Jacobian available | Can compute ∂f/∂u? | yes |
| Target accuracy | Desired error level | 1e-6 |
| Constraints | Memory, implicit allowed? | implicit OK |
| Time scale | Characteristic time | 1e-3 s |
Decision Guidance
Choosing an Integrator
Is the problem stiff?
├── YES → Is Jacobian available?
│ ├── YES → Use Rosenbrock or BDF
│ └── NO → Use BDF with numerical Jacobian
└── NO → Is high accuracy needed?
├── YES → Use RK45 or DOP853
└── NO → Use RK4 or Adams-BashforthStiff vs Non-Stiff Detection
| Symptom | Likely Stiff | Action |
|---|---|---|
| dt shrinks to tiny values | Yes | Switch to implicit |
| Eigenvalues span many decades | Yes | Use BDF/Radau |
| Smooth solution, reasonable dt | No | Stay explicit |
Script Outputs (JSON Fields)
| Script | Key Outputs |
|---|---|
scripts/error_norm.py | error_norm, scale_min, scale_max |
scripts/adaptive_step_controller.py | accept, dt_next, factor |
scripts/integrator_selector.py | recommended, alternatives, notes |
scripts/imex_split_planner.py | implicit_terms, explicit_terms, splitting_strategy |
scripts/splitting_error_estimator.py | error_estimate, substeps |
Workflow
1. Classify stiffness - Check eigenvalue spread or use stiffness_detector 2. Choose tolerances - See references/tolerance_guidelines.md 3. Select integrator - Run scripts/integrator_selector.py 4. Compute error norms - Use scripts/error_norm.py for step acceptance 5. Adapt step size - Use scripts/adaptive_step_controller.py 6. Plan IMEX/splitting - If mixed stiff/nonstiff, use scripts/imex_split_planner.py 7. Validate convergence - Repeat with tighter tolerances
Conversational Workflow Example
User: I'm solving the Allen-Cahn equation with a stiff double-well potential. What integrator should I use?
Agent workflow: 1. Check integrator options:
python3 scripts/integrator_selector.py --stiff --jacobian-available --accuracy high --json2. Plan the IMEX splitting (diffusion implicit, reaction explicit):
python3 scripts/imex_split_planner.py --stiff-terms diffusion --nonstiff-terms reaction --coupling weak --json3. Recommend: Use IMEX-BDF2 with diffusion term implicit, double-well reaction explicit.
Pre-Integration Checklist
- [ ] Identify stiffness and dominant time scales
- [ ] Set
rtol/atolconsistent with physics and units - [ ] Confirm integrator compatibility with stiffness
- [ ] Use error norm to accept/reject steps
- [ ] Verify convergence with tighter tolerance run
CLI Examples
# Select integrator for stiff problem with Jacobian
python3 scripts/integrator_selector.py --stiff --jacobian-available --accuracy high --json
# Compute scaled error norm
python3 scripts/error_norm.py --error 0.01,0.02 --solution 1.0,2.0 --rtol 1e-3 --atol 1e-6 --json
# Adaptive step control with PI controller
python3 scripts/adaptive_step_controller.py --dt 1e-2 --error-norm 0.8 --order 4 --controller pi --json
# Plan IMEX splitting
python3 scripts/imex_split_planner.py --stiff-terms diffusion,elastic --nonstiff-terms reaction --coupling strong --json
# Estimate splitting error
python3 scripts/splitting_error_estimator.py --dt 1e-4 --scheme strang --commutator-norm 50 --target-error 1e-6 --jsonError Handling
| Error | Cause | Resolution |
|---|---|---|
rtol and atol must be positive | Invalid tolerances | Use positive values |
error-norm must be positive | Negative error norm | Check error computation |
Unknown controller | Invalid controller type | Use i, pi, or pid |
Splitting requires at least one term | Empty term list | Specify stiff or nonstiff terms |
Interpretation Guidance
Error Norm Values
| Error Norm | Meaning | Action |
|---|---|---|
| < 1.0 | Step acceptable | Accept, maybe increase dt |
| ≈ 1.0 | At tolerance boundary | Accept with current dt |
| > 1.0 | Step rejected | Reject, reduce dt |
Controller Selection
| Controller | Properties | Best For |
|---|---|---|
| I (integral) | Simple, some overshoot | Non-stiff, moderate accuracy |
| PI (proportional-integral) | Smooth, robust | General use |
| PID | Aggressive adaptation | Rapidly varying dynamics |
IMEX Strategy
| Coupling | Strategy |
|---|---|
| Weak | Simple operator splitting |
| Moderate | Strang splitting |
| Strong | Fully coupled IMEX-RK |
Limitations
- No automatic stiffness detection: Use stiffness_detector from numerical-stability
- Splitting assumes separability: Terms must be cleanly separable
- Jacobian requirement: Some methods need analytical or numerical Jacobian
References
references/method_catalog.md- Integrator options and propertiesreferences/tolerance_guidelines.md- Choosing rtol/atolreferences/error_control.md- Error norm and adaptation formulasreferences/imex_guidelines.md- Stiff/non-stiff splittingreferences/splitting_catalog.md- Operator splitting patternsreferences/multiphase_field_patterns.md- Phase-field specific splits
Version History
- v1.1.0 (2024-12-24): Enhanced documentation, decision guidance, examples
- v1.0.0: Initial release with 5 integration scripts
Error Control
Comprehensive guide for adaptive step size control in time integration.
Error Norm Computation
Scaled Error Norm
The standard approach scales the error by tolerance:
scale_i = atol_i + rtol × max(|y_n,i|, |y_{n+1},i|)
err_scaled_i = error_i / scale_iNorm Types
| Norm | Formula | Use Case |
|---|---|---|
| RMS (L2) | sqrt(Σ err_i² / n) | General purpose, smooth |
| Max (L∞) | max(\ | err_i\ |
| Weighted RMS | sqrt(Σ w_i × err_i² / Σ w_i) | Variable importance |
RMS Norm (Recommended Default):
error_norm = sqrt( (1/n) × Σᵢ (error_i / scale_i)² )Max Norm (Conservative):
error_norm = max_i |error_i / scale_i|When to Use Each Norm
| Scenario | Recommended | Reasoning |
|---|---|---|
| Most problems | RMS | Balanced, smooth control |
| Safety-critical | Max | No component exceeds tolerance |
| Many components | RMS | Max overly conservative |
| Localized dynamics | Max | Catch local errors |
| Smooth problems | RMS | Reduces rejected steps |
Step Acceptance Criterion
IF error_norm ≤ 1.0:
ACCEPT step
Possibly increase dt
ELSE:
REJECT step
Decrease dt
Retry with smaller stepNear-Acceptance Zone
| error_norm | Action |
|---|---|
| < 0.5 | Accept, increase dt aggressively |
| 0.5 - 1.0 | Accept, mild dt increase |
| 1.0 - 2.0 | Reject, mild dt decrease |
| > 2.0 | Reject, aggressive dt decrease |
Step Size Controllers
I-Controller (Integral/Proportional)
Simplest controller based on current error only:
dt_new = dt × safety × error_norm^(-1/(p+1))Where:
- safety = 0.8 - 0.9 (prevents oscillation)
- p = method order
Characteristics:
- Simple implementation
- May oscillate if error varies rapidly
- Suitable for smooth problems
PI-Controller (Proportional-Integral)
Uses current and previous error for smoother adaptation:
dt_new = dt × safety × error_norm^(-α) × error_prev^(β)Standard coefficients:
α = 0.7 / (p + 1)
β = 0.4 / (p + 1)Alternative (Gustafsson):
α = 1 / (p + 1)
β = 1 / (p + 1) × (dt_prev / dt)Characteristics:
- Smoother step size evolution
- Reduces oscillations
- Industry standard choice
PID-Controller
Adds derivative term for aggressive adaptation:
dt_new = dt × safety × error_norm^(-α) × error_prev^(β) × error_prev2^(γ)Typical coefficients:
α = 0.49 / (p + 1)
β = 0.34 / (p + 1)
γ = 0.10 / (p + 1)Characteristics:
- Most aggressive adaptation
- Best for rapidly changing dynamics
- Risk of oscillation if poorly tuned
Controller Comparison
| Controller | Smoothness | Responsiveness | Complexity |
|---|---|---|---|
| I | Low | Immediate | Simple |
| PI | Medium | Balanced | Moderate |
| PID | High | Fast | Complex |
Safety Factors and Limits
Safety Factor
safety = 0.8 - 0.9 (typical)Purpose:
- Prevents accepting steps at exact tolerance boundary
- Accounts for error estimate uncertainty
- Reduces rejected steps due to fluctuations
Tuning:
- Conservative (stiff/sensitive): safety = 0.8
- Aggressive (smooth): safety = 0.95
Step Size Limits
dt_min = ε_machine × |t| × 100 (prevent stall)
dt_max = T_end / 10 (or physics-based limit)Max step increase per step:
dt_new ≤ factor_max × dt (typically factor_max = 2-5)Max step decrease per step:
dt_new ≥ factor_min × dt (typically factor_min = 0.1-0.2)Limiting Formulas
# Apply all limits
dt_new = dt × safety × error_norm^(-1/(p+1))
dt_new = max(dt_new, dt × factor_min)
dt_new = min(dt_new, dt × factor_max)
dt_new = max(dt_new, dt_min)
dt_new = min(dt_new, dt_max)Error Estimation Methods
Embedded Methods
Use two methods of different orders in one step:
y_high = O(h^{p+1}) method result
y_low = O(h^p) embedded result
error = y_high - y_low| Method Pair | Orders | Evaluations |
|---|---|---|
| RK45 (Dormand-Prince) | 5(4) | 6 |
| RK78 (Fehlberg) | 7(8) | 13 |
| RK23 (Bogacki-Shampine) | 3(2) | 4 |
Advantage: Error estimate is "free" from the integration.
Richardson Extrapolation
Compare full step with two half-steps:
y_full = one step of size h
y_half = two steps of size h/2
error = (y_half - y_full) / (2^p - 1)Advantage: Works with any method. Disadvantage: Doubles function evaluations.
Milne Device (Multi-step)
Compare predictor and corrector:
y_predict = Adams-Bashforth (explicit)
y_correct = Adams-Moulton (implicit)
error = (y_correct - y_predict) × CWhere C depends on method orders.
Handling Rejected Steps
Standard Approach
1. Compute y_new and error estimate
2. If error_norm > 1:
a. Reduce dt using controller
b. Discard y_new
c. Retry from y_current with smaller dt
3. If many consecutive rejections:
a. Check for stiffness
b. Consider method changeRejection Statistics
| Rejection Rate | Interpretation | Action |
|---|---|---|
| < 5% | Normal | None |
| 5-20% | Borderline | Consider loosening tol |
| 20-50% | Problem | Check for stiffness |
| > 50% | Severe | Wrong method or extreme stiffness |
After Rejection Strategy
Conservative:
dt_new = dt × 0.5 (halve step)Controller-based:
dt_new = dt × safety × error_norm^(-1/(p+1))Minimum reduction:
dt_new = max(dt × 0.1, dt_min) (never reduce by more than 10×)Special Situations
Output Points
When output is required at specific times:
1. Integrate to t_out with adaptive stepping
2. Last step may need adjustment to hit t_out exactly
3. Options:
a. Interpolate from nearby steps (dense output)
b. Force exact step to t_out
c. Accept small overstep and interpolate backDiscontinuities
When a discontinuity is detected (event, switch):
1. Locate discontinuity time t_disc (bisection/Newton)
2. Integrate exactly to t_disc
3. Apply discontinuity (jump conditions)
4. Restart integrator with small initial step
5. Rebuild multi-step history if neededStiffness Detection
Monitor for implicit stiffness transition:
IF step rejections increase rapidly:
Estimate stiffness ratio
IF ratio > threshold (e.g., 1000):
Switch to implicit methodImplementation Example
def adaptive_step(y, t, dt, f, tol, order, controller='pi'):
"""One adaptive step with PI control."""
safety = 0.9
factor_min, factor_max = 0.2, 5.0
# Error history for PI controller
global error_prev
while True:
# Take step and estimate error
y_new, error = step_with_error(y, t, dt, f)
# Compute scaled error norm
scale = tol['atol'] + tol['rtol'] * np.maximum(np.abs(y), np.abs(y_new))
error_norm = np.sqrt(np.mean((error / scale)**2))
if error_norm <= 1.0:
# Accept step
if controller == 'pi':
factor = safety * error_norm**(-0.7/(order+1)) * error_prev**(0.4/(order+1))
else: # 'i' controller
factor = safety * error_norm**(-1/(order+1))
factor = np.clip(factor, factor_min, factor_max)
dt_new = dt * factor
error_prev = error_norm
return y_new, t + dt, dt_new, True
else:
# Reject step
factor = safety * error_norm**(-1/(order+1))
factor = max(factor, factor_min)
dt = dt * factor
# Loop continues with smaller dtTuning Guidelines
Start Conservative
1. Begin with safety = 0.8, PI controller 2. Use default α, β coefficients 3. Set factor_max = 2.0
Tune for Efficiency
1. If few rejections, increase safety to 0.9 2. If smooth evolution, increase factor_max to 5.0 3. If oscillating dt, reduce α or switch to PI
Monitor Health
Track and report:
- Total step count
- Rejected step count
- Minimum dt reached
- Maximum error norm encountered
IMEX Guidelines
Comprehensive guide for Implicit-Explicit time integration methods.
When to Use IMEX
Problem Structure
IMEX methods are designed for problems of the form:
du/dt = f_stiff(u) + f_nonstiff(u)Where:
- f_stiff: Requires implicit treatment for stability
- f_nonstiff: Can be handled explicitly (cheaper)
Canonical Examples
| Problem | Stiff Term | Non-stiff Term |
|---|---|---|
| Advection-diffusion | Diffusion (∇²u) | Advection (v·∇u) |
| Reaction-diffusion | Diffusion | Mild reactions |
| Phase-field | Laplacian | Bulk free energy |
| Combustion | Fast chemistry | Convection |
| Incompressible flow | Pressure (implicit) | Advection |
Decision Criteria
Use IMEX when: 1. Clear separation exists between stiff and non-stiff terms 2. Stiff term is linear or easy to solve implicitly 3. Non-stiff term dominates cost if treated implicitly 4. Explicit dt limit would be too restrictive for stiff part
Avoid IMEX when: 1. All terms have similar stiffness 2. Terms are strongly coupled (nonlinear interaction) 3. Stiff term is highly nonlinear 4. Splitting error is unacceptable
IMEX Scheme Classes
IMEX-Runge-Kutta
Combines explicit and implicit RK methods:
Stage i:
U_i = u_n + dt × Σⱼ aᵢⱼᴱ f_explicit(Uⱼ) + dt × Σⱼ aᵢⱼᴵ f_implicit(Uⱼ)
Update:
u_{n+1} = u_n + dt × Σᵢ bᵢᴱ f_explicit(Uᵢ) + dt × Σᵢ bᵢᴵ f_implicit(Uᵢ)Common Schemes:
| Name | Order | ERK Stages | DIRK Stages | Properties |
|---|---|---|---|---|
| IMEX-Euler | 1 | 1 | 1 | Simple, first-order |
| ARS(2,2,2) | 2 | 2 | 2 | L-stable implicit part |
| ARK3(2)4L | 3 | 4 | 4 | L-stable, stiff accurate |
| ARK4(3)6L | 4 | 6 | 6 | High order |
IMEX-Multistep (SBDF)
Semi-implicit BDF combines BDF for stiff with Adams-Bashforth for non-stiff:
SBDF1:
(u_{n+1} - u_n) / dt = f_implicit(u_{n+1}) + f_explicit(u_n)SBDF2:
(3u_{n+1} - 4u_n + u_{n-1}) / (2dt) = f_implicit(u_{n+1}) + 2f_explicit(u_n) - f_explicit(u_{n-1})| Order | Stability | Startup | Memory |
|---|---|---|---|
| SBDF1 | A-stable | None | 1 level |
| SBDF2 | A-stable | RK2 | 2 levels |
| SBDF3 | A(86°)-stable | RK3 | 3 levels |
| SBDF4 | A(73°)-stable | RK4 | 4 levels |
IMEX-Peer Methods
Multi-stage multi-step methods with good parallel properties:
- Each stage can be computed in parallel
- Combine strengths of RK and multistep
- Emerging class with ongoing research
Splitting Strategies
Term Assignment
| Physics | Treatment | Reasoning |
|---|---|---|
| Diffusion (∇²u) | Implicit | Stiff, linear |
| Fast linear reactions | Implicit | Stiff |
| Advection (v·∇u) | Explicit | CFL-limited anyway |
| Slow reactions | Explicit | Not stiff |
| Nonlinear bulk terms | Explicit | Difficult implicit solve |
| External forcing | Explicit | Usually smooth |
Semi-implicit Linearization
When nonlinear terms must be implicit, linearize:
Original: du/dt = -∇·(D(u)∇u)
Linearized: du/dt = -∇·(D(u_n)∇u_{n+1})
↑ ↑ ↑
implicit lagged new valueLagging strategies:
- Lag coefficient by one step (first-order in time for coefficient)
- Extrapolate coefficient (higher order)
- Iterate until converged (fully implicit)
Stability Analysis
Combined Stability Region
The effective stability region is the intersection of: 1. Implicit method's region for stiff term 2. Explicit method's region for non-stiff term
For stability: both λ_stiff × dt and λ_nonstiff × dt must be in respective regionsStability Plots
For IMEX methods, plot stability in (λ_E × dt, λ_I × dt) plane:
- Horizontal axis: explicit eigenvalue × dt
- Vertical axis: implicit eigenvalue × dt
- Shaded region: stable combinations
CFL for Explicit Part
Even with IMEX, the explicit part imposes a CFL constraint:
dt ≤ C × dx / |v_max| (for advection)The implicit treatment of diffusion removes:
dt ≤ dx² / (2D) (this constraint is gone!)Accuracy Considerations
Order Conditions
For IMEX-RK, both tableaux must satisfy order conditions:
- Individual ERK and DIRK must be consistent
- Coupling conditions between tableaux
- Stiff accuracy (optional but helpful)
Splitting Error
IMEX introduces splitting error beyond method truncation error:
Total error = O(dt^p) + splitting errorSplitting error depends on:
- Commutator [f_stiff, f_nonstiff]
- Method coupling (how stages interact)
For additive IMEX-RK: splitting error = O(dt^p) when properly designed.
Order Reduction
Near stability boundaries:
- Classical order reduction for stiff ODEs applies
- Some IMEX schemes are "stiff accurate" to mitigate this
Implementation Guidelines
Linear Solver Requirements
The implicit part requires solving:
(I - dt × γ × J_implicit) × δu = RHSWhere J_implicit is the Jacobian of f_implicit.
Requirements: 1. Jacobian or Jacobian-vector product 2. Linear solver (direct or iterative) 3. Preconditioner for large systems
Jacobian Reuse
For IMEX-RK with SDIRK (same diagonal):
- Same Jacobian matrix at all implicit stages
- Factor once per step, reuse for all stages
- Significant cost savings
For varying Jacobian:
- Recompute each stage OR
- Lag Jacobian across stages (lose some order)
Iteration Strategy
Fixed number of iterations:
- Predictable cost
- May not converge fully
- OK if splitting dominates error
Converge to tolerance:
- Fully implicit behavior
- Variable cost per step
- Use when high accuracy needed
Memory Management
| Scheme | Storage Required |
|---|---|
| IMEX-RK (s stages) | 2s arrays |
| SBDF2 | 2 time levels |
| SBDF3 | 3 time levels |
Practical Examples
Advection-Diffusion
∂u/∂t + v · ∇u = D ∇²u
f_explicit = -v · ∇u (advection)
f_implicit = D ∇²u (diffusion)
dt constraint: CFL for advection only
dt ≤ dx / |v_max| (instead of min with dx²/D)Allen-Cahn Equation
∂φ/∂t = M(ε²∇²φ - φ(φ²-1))
Option 1 (linear implicit):
f_implicit = M ε² ∇²φ
f_explicit = -M φ(φ²-1)
Option 2 (stabilized explicit):
f_implicit = M ε² ∇²φ - M s φ (add stabilizing term)
f_explicit = -M φ(φ²-1) + M s φ (subtract same term)
Choose s to make explicit part bounded.Cahn-Hilliard Equation
∂φ/∂t = M ∇²(f'(φ) - ε² ∇²φ)
Convex-concave splitting:
f_implicit = M ∇²(c₁ φ - ε² ∇²φ) (convex part)
f_explicit = M ∇²(f'(φ) - c₁ φ) (concave part)
c₁ chosen so that f''(φ) - c₁ ≤ 0Coupling Strength
Weak Coupling
Terms interact slowly:
Example: Heat conduction + slow reaction
Solution: Simple splitting works wellModerate Coupling
Terms have some interaction:
Example: Fast diffusion + moderate reaction
Solution: IMEX-RK with good coupling coefficientsStrong Coupling
Terms are tightly coupled:
Example: Phase-field with strong anisotropy
Solution: Fully coupled IMEX-RK or iterationVerification
Order Verification
1. Run with dt, dt/2, dt/4 2. Compute error vs exact (if known) or Richardson 3. Confirm error ∝ dt^p
Splitting Error Check
1. Run pure implicit (full problem) 2. Run IMEX with same dt 3. Difference reveals splitting error
Stability Boundary
1. Gradually increase dt 2. Find maximum stable dt 3. Compare with theoretical prediction
Method Catalog
Comprehensive reference for time integration methods in ODE/PDE simulations.
Explicit Methods (Non-Stiff)
Runge-Kutta Family
| Method | Order | Stages | Error Est. | Best For |
|---|---|---|---|---|
| Euler | 1 | 1 | No | Prototyping only |
| RK2 (Heun) | 2 | 2 | No | Simple problems |
| RK4 (Classical) | 4 | 4 | No | Fixed-step, smooth |
| RK45 (Dormand-Prince) | 5(4) | 6 | Yes | General adaptive |
| DOP853 | 8(5,3) | 12 | Yes | High accuracy |
RK45 (Dormand-Prince)
- Default choice for non-stiff problems
- Embedded 4th-order method for error estimation
- FSAL (First Same As Last) optimization
- Recommended tolerances: rtol=1e-3, atol=1e-6
DOP853
- 8th-order with 5th and 3rd-order error estimates
- Excellent for high-precision requirements
- More expensive per step but fewer steps needed
- Use when rtol < 1e-6 is required
Adams-Bashforth (Multi-step)
| Order | Points | Formula |
|---|---|---|
| 1 | 1 | y_{n+1} = y_n + h*f_n |
| 2 | 2 | y_{n+1} = y_n + h*(3f_n - f_{n-1})/2 |
| 3 | 3 | y_{n+1} = y_n + h*(23f_n - 16f_{n-1} + 5f_{n-2})/12 |
| 4 | 4 | y_{n+1} = y_n + h*(55f_n - 59f_{n-1} + 37f_{n-2} - 9f_{n-3})/24 |
Advantages:
- Only one function evaluation per step
- Efficient for smooth, non-stiff problems
Disadvantages:
- Requires startup procedure
- Less robust for discontinuous forcing
- Needs variable-step modification for adaptivity
Implicit Methods (Stiff)
BDF (Backward Differentiation Formulas)
| Order | Stability | Formula |
|---|---|---|
| BDF1 | A-stable | y_{n+1} = y_n + h*f_{n+1} |
| BDF2 | A-stable | (3y_{n+1} - 4y_n + y_{n-1})/2 = h*f_{n+1} |
| BDF3 | A(α)-stable | (11y_{n+1} - 18y_n + 9y_{n-1} - 2y_{n-2})/6 = h*f_{n+1} |
| BDF4 | A(α)-stable | Higher order, smaller stability region |
| BDF5 | A(α)-stable | Use only for mildly stiff |
| BDF6 | Not A-stable | Avoid - stability issues |
When to use:
- Large eigenvalue spread (> 100)
- Chemistry with fast/slow reactions
- Diffusion-dominated problems
Considerations:
- Requires nonlinear solver (Newton)
- Jacobian needed (analytical or numerical)
- Order reduction near stability boundary
Radau IIA
| Order | Stages | Properties |
|---|---|---|
| Radau IIA-3 | 2 | L-stable, 3rd order |
| Radau IIA-5 | 3 | L-stable, 5th order |
Properties:
- L-stable (strong damping of stiff modes)
- Excellent for very stiff problems
- Superconvergent at endpoints
Use when:
- BDF order reduction is problematic
- DAE (differential-algebraic) systems
- Very stiff chemistry
Rosenbrock Methods
Characteristics:
- Linearly implicit (one Jacobian factorization per step)
- No nonlinear iteration needed
- Excellent for moderate stiffness
| Method | Order | Stages |
|---|---|---|
| ROS2 | 2 | 2 |
| ROS3P | 3 | 3 |
| ROS4 | 4 | 4 |
| RODAS | 4 | 6 |
Advantages over BDF:
- No iteration convergence issues
- Fixed number of Jacobian evaluations
- Better for time-varying Jacobians
Structure-Preserving Methods
Symplectic Integrators
For Hamiltonian systems: dp/dt = -∂H/∂q, dq/dt = ∂H/∂p
| Method | Order | Type |
|---|---|---|
| Symplectic Euler | 1 | Explicit |
| Störmer-Verlet | 2 | Explicit |
| Ruth's 3rd order | 3 | Explicit |
| Forest-Ruth | 4 | Explicit |
Use for:
- Long-time molecular dynamics
- Orbital mechanics
- Oscillatory systems with energy conservation
Properties:
- Exactly conserve symplectic structure
- Near-conservation of Hamiltonian for exponentially long times
- Time-reversible (symmetric methods)
Geometric Integrators
| Property | Methods |
|---|---|
| Volume-preserving | Implicit midpoint, Gauss-Legendre |
| Energy-preserving | Discrete gradient methods |
| Momentum-preserving | Variational integrators |
IMEX Methods
For problems with mixed stiff/non-stiff terms: du/dt = f_stiff(u) + f_nonstiff(u)
Common IMEX-RK Schemes
| Scheme | Implicit | Explicit | Order |
|---|---|---|---|
| IMEX-Euler | BE | FE | 1 |
| IMEX-SSP2 | Trapezoid | SSP-RK2 | 2 |
| IMEX-ARK2 | SDIRK | ERK | 2 |
| IMEX-ARK4 | SDIRK | ERK | 4 |
IMEX-BDF
| Order | Properties |
|---|---|
| SBDF1 | BE + AB1 extrapolation |
| SBDF2 | BDF2 + AB2 extrapolation |
| SBDF3 | BDF3 + AB3 extrapolation |
Selection Guide
Quick Decision Table
| Stiffness | Accuracy | Smoothness | Recommended |
|---|---|---|---|
| Non-stiff | Moderate | Smooth | RK45 |
| Non-stiff | High | Smooth | DOP853 |
| Non-stiff | Low | Smooth | Adams-Bashforth |
| Stiff | Moderate | Any | BDF |
| Very stiff | Any | Any | Radau IIA |
| Moderate stiff | Any | Jacobian available | Rosenbrock |
| Mixed | Any | Split possible | IMEX |
| Hamiltonian | Long-time | Oscillatory | Symplectic |
Cost Comparison
| Method | f evals/step | Jacobian evals | Linear solves |
|---|---|---|---|
| RK45 | 6 | 0 | 0 |
| BDF2 | 1 + Newton | 1 per few steps | 1 per Newton |
| Radau5 | 3 + Newton | 1 per step | 3 per Newton |
| Rosenbrock4 | 4 | 1 | 4 |
Stability Regions
Explicit Methods
- RK4: Extends to Re(λh) ≈ -2.8 on real axis
- RK45: Similar to RK4
- Adams-Bashforth: Smaller regions, decreasing with order
Implicit Methods
- BDF1-2: A-stable (entire left half-plane)
- BDF3-5: A(α)-stable (wedge-shaped regions)
- Radau: L-stable (A-stable + stiff decay)
Implementation Notes
Jacobian Handling
1. Analytical: Most accurate, requires code derivation 2. Automatic differentiation: Accurate, some overhead 3. Numerical (finite difference): Simple, may be inaccurate for stiff 4. Jacobian-free (GMRES): For very large systems
Step Size Limits
- Minimum dt: Floating-point precision, typically 1e-15 * t_current
- Maximum dt: Physical time scale or output frequency
- Safety factor: Typically 0.8-0.9 for adaptive methods
Multiphase-Field Splitting Patterns
Specialized integration patterns for phase-field and multi-order-parameter models.
Phase-Field Model Classes
Allen-Cahn (Non-Conserved)
Model A kinetics for non-conserved order parameters:
∂φ/∂t = -M × δF/δφ = -M × (f'(φ) - ε²∇²φ)Where:
- φ: order parameter (e.g., phase indicator)
- M: mobility
- f(φ): bulk free energy density
- ε: interface width parameter
Cahn-Hilliard (Conserved)
Model B kinetics for conserved order parameters:
∂c/∂t = ∇·(M∇μ)
μ = δF/δc = f'(c) - ε²∇²cWhere:
- c: conserved field (e.g., concentration)
- μ: chemical potential
- M: mobility (may be M(c))
Multi-Order-Parameter
Multiple coupled order parameters:
∂φᵢ/∂t = -Mᵢ × δF/δφᵢ + Σⱼ coupling_ij(φⱼ)Examples:
- Grain growth: φᵢ for each grain
- Eutectic: φ_α, φ_β, c
- Polycrystal: φᵢ + θᵢ (orientation)
Allen-Cahn Splitting
Standard Double-Well
f(φ) = (1/4)(φ² - 1)² = (1/4)φ⁴ - (1/2)φ² + 1/4IMEX Split (Linear Implicit):
Implicit: ε²∇²φ (stiffest term)
Explicit: -φ(φ² - 1) (bulk driving force)Stability: Explicit part bounded for |φ| ≤ 1 + O(ε)
Stabilized Splitting
Add/subtract linear stabilizing term:
∂φ/∂t = M[ε²∇²φ - sφ] + M[-φ³ + φ + sφ]
└─implicit─┘ └───explicit───┘Choose s > max|f''(φ)| = 3 for double-well.
Effect: Explicit term becomes contractive, unconditionally stable.
Convex-Concave Splitting (Eyre)
Split f(φ) into convex and concave parts:
f(φ) = f_convex(φ) + f_concave(φ)
f_convex = (c/2)φ² where c ≥ max f''
f_concave = f(φ) - (c/2)φ²Scheme:
(φ_{n+1} - φ_n)/dt = M[ε²∇²φ_{n+1} - f'_convex(φ_{n+1}) - f'_concave(φ_n)]Properties:
- Unconditionally energy stable
- Unique solvability
- First-order in time
Second-Order Convex Splitting
Using Crank-Nicolson for spatial:
(φ_{n+1} - φ_n)/dt = M[ε²∇²(φ_{n+1}+φ_n)/2
- f'_convex(φ_{n+1})
- f'_concave(φ_n)]Or BDF2:
(3φ_{n+1} - 4φ_n + φ_{n-1})/(2dt) = M[ε²∇²φ_{n+1}
- f'_convex(φ_{n+1})
- f'_concave(2φ_n - φ_{n-1})]Cahn-Hilliard Splitting
Fourth-Order Challenge
Cahn-Hilliard involves ∇⁴:
∂c/∂t = M∇²μ = M∇²(f'(c) - ε²∇²c)Direct discretization:
- Explicit: dt ~ dx⁴ (very restrictive!)
- Implicit: fourth-order operator
Mixed Formulation
Introduce chemical potential as separate variable:
∂c/∂t = M∇²μ
μ = f'(c) - ε²∇²cTwo second-order equations instead of one fourth-order.
IMEX for Mixed Form
Implicit: -ε²∇²c (in μ equation)
∇²μ (in c equation)
Explicit: f'(c)Linear system per step:
[I ε²L ] [c_{n+1}] [c_n + dt×M×L×μ_{n+1}]
[-1 I ] [μ_{n+1}] = [f'(c_n) ]Where L = discrete Laplacian.
Convex Splitting for Cahn-Hilliard
f(c) = f_convex(c) + f_concave(c)
(c_{n+1} - c_n)/dt = M∇²[f'_convex(c_{n+1}) + f'_concave(c_n) - ε²∇²c_{n+1}]Properties:
- Energy decreasing: F[c_{n+1}] ≤ F[c_n]
- Mass conserving: ∫c dx = constant
- Unconditionally stable
Scalar Auxiliary Variable (SAV)
Introduce r(t) = √(F[c] + C) where C ensures positivity:
dc/dt = M∇²μ
μ = r(t)/√(F+C) × δF/δc - ε²∇²c
dr/dt = (1/2√(F+C)) ∫ (δF/δc) × (dc/dt) dxAdvantages:
- Linear implicit solve
- Energy stable
- Second-order possible
Coupled Multi-Phase Systems
Two-Phase with Concentration
φ: phase field, c: concentration
∂φ/∂t = -M_φ × δF/δφ
∂c/∂t = ∇·(M_c ∇(δF/δc))Splitting strategy:
Step 1: Evolve φ with c fixed (Allen-Cahn)
Step 2: Evolve c with φ fixed (diffusion with phase-dependent mobility)Coupling terms:
- Free energy: f(φ,c) couples fields
- Mobility: M_c(φ) may depend on phase
Multi-Grain Systems
N order parameters φ₁, ..., φ_N with constraint:
Σᵢ φᵢ² = 1 (at interfaces)Sequential update:
For i = 1 to N:
Update φᵢ with φⱼ (j≠i) fixed
Enforce constraintProjection method:
Update all φᵢ without constraint
Project: φᵢ → φᵢ / √(Σⱼ φⱼ²)Phase-Field Crystal
∂ψ/∂t = ∇²[(r + (1+∇²)²)ψ + ψ³]Splitting:
Implicit: (1+∇²)²ψ (sixth-order!)
Explicit: rψ + ψ³Requires careful treatment due to high-order derivatives.
Anisotropy Handling
Weakly Anisotropic
Interface energy: γ(n) = γ₀(1 + ε₄cos(4θ))
Gradient energy:
W(∇φ) = (1/2)|∇φ|² × a(n)²IMEX approach:
Implicit: isotropic part (1/2)|∇φ|²
Explicit: anisotropic correctionStrongly Anisotropic
When a(n)² can become negative (missing orientations):
Regularization:
W_reg = (1/2)|∇φ|² × a(n)² + (β/2)|∇∇φ|²Corner regularization term requires implicit treatment.
Faceted Interfaces
For crystalline anisotropy (Wulff shapes with corners):
Use Willmore regularization or
Level-set with crystalline curvatureSpecialized schemes needed (not standard IMEX).
Adaptive Mesh Refinement (AMR)
Where to Refine
Refine near interfaces where gradients are large:
Criterion: |∇φ| > threshold
or |φ - 0.5| < δTime Stepping with AMR
Subcycling:
Fine levels: dt_fine = dt_coarse / ratio
Synchronize at coarse stepSplitting with AMR:
1. Coarse step for bulk
2. Fine steps for interface region
3. Synchronize solutions at boundariesPractical Guidelines
Time Step Selection
| Model | Limiting Factor | Typical dt |
|---|---|---|
| Allen-Cahn | Interface motion | dx²/(M×ε²) |
| Cahn-Hilliard | Fourth derivative | dx⁴/(M×ε²) |
| Coupled | Slowest physics | min of above |
With IMEX: can exceed explicit limits by 10-100×.
Convergence Checking
1. Energy decay: dF/dt ≤ 0 (thermodynamically consistent) 2. Mass conservation: ∫c dx = const (for Cahn-Hilliard) 3. Interface width: Check ε² matches expected profile 4. Steady state: φ converges to equilibrium
Common Pitfalls
| Issue | Symptom | Fix |
|---|---|---|
| Energy increase | F[n+1] > F[n] | Use convex splitting |
| Interface sharpening | φ goes outside [0,1] | Reduce dt, add stabilization |
| Mass loss | ∫c drifts | Use conservative discretization |
| Wrong interface width | Profile too sharp/wide | Check ε, dx relationship |
| Slow convergence | Many iterations | Better preconditioner |
Interface Resolution
Rule of thumb:
dx ≤ ε / 3 (at least 3 points across interface)
Better: dx ≤ ε / 5 (5-6 points)For IMEX stability:
dt ≤ C × dx² / (M × ε²) (explicit part)With stabilization, C can be O(1) instead of O(0.1).
Verification Benchmarks
Allen-Cahn
1. Shrinking circle: R(t) = √(R₀² - 2Mt) 2. Traveling wave: Compare to analytical profile 3. Equilibrium: Flat interface, f'(φ) = 0
Cahn-Hilliard
1. Spinodal decomposition: Compare coarsening rate 2. Ostwald ripening: R ~ t^{1/3} for droplets 3. Equilibrium: μ = constant, phase fractions correct
Multi-Order-Parameter
1. Grain growth: Normal grain growth law 2. Triple junction: 120° angles for isotropic 3. Wetting: Contact angle vs surface energies
Operator Splitting Catalog
Comprehensive reference for operator splitting methods in PDEs and multiphysics.
Fundamental Concepts
Problem Setup
Given:
du/dt = A(u) + B(u)Where A and B are operators (may be differential, algebraic, or mixed).
Goal: Solve A and B separately, combine solutions.
Why Split?
| Reason | Example |
|---|---|
| Different physics | Advection + reaction |
| Different solvers | Spectral + finite difference |
| Different time scales | Fast chemistry + slow transport |
| Code modularity | Reuse existing solvers |
| Parallelization | Different operators on different processors |
First-Order Methods
Lie-Trotter Splitting
Step 1: Solve du/dt = A(u) for time dt → u*
Step 2: Solve du/dt = B(u) for time dt starting from u* → u_{n+1}Properties:
- Order: 1 (error = O(dt))
- Simplest implementation
- Non-symmetric (order matters)
Error:
Error ≈ (dt/2) × [A, B] × uWhere [A, B] = AB - BA is the commutator.
Choosing Order (A then B vs B then A)
| Order | Preferred When |
|---|---|
| A → B | B's result needed for output |
| B → A | A is diagnostic, B is physics |
| Symmetric | Use Strang instead |
Second-Order Methods
Strang Splitting
Step 1: Solve du/dt = A(u) for time dt/2 → u*
Step 2: Solve du/dt = B(u) for time dt → u**
Step 3: Solve du/dt = A(u) for time dt/2 → u_{n+1}Properties:
- Order: 2 (error = O(dt²))
- Symmetric (time-reversible)
- Standard choice for moderate accuracy
Error:
Error ≈ (dt²/24) × [[A, B], A + B] × uInvolves nested commutators.
Marchuk-Strang Alternating
Step n (even): A/2 → B → A/2
Step n (odd): B/2 → A → B/2Alternating between Strang orderings:
- Averages directional bias
- Can improve symmetry for anisotropic problems
Higher-Order Methods
Triple-Jump (Order 4)
γ = 1 / (2 - 2^{1/3}) ≈ 1.3512
Sub-step 1: Strang with dt × γ
Sub-step 2: Strang with dt × (1 - 2γ) [negative!]
Sub-step 3: Strang with dt × γNote: Requires a negative time step!
- Problematic for irreversible operators (diffusion)
- Works for Hamiltonian systems
Yoshida (Order 4, Symmetric)
For symmetric operators only:
w₀ = -2^{1/3} / (2 - 2^{1/3})
w₁ = 1 / (2 - 2^{1/3})
Sequence: S(w₁ dt) → S(w₀ dt) → S(w₁ dt)Where S is Strang splitting.
Forest-Ruth (Order 4)
For Hamiltonian splitting H = T(p) + V(q):
θ = 1 / (2 - 2^{1/3})
Sequence: V(θ/2) → T(θ) → V((1-θ)/2) → T(1-2θ) → V((1-θ)/2) → T(θ) → V(θ/2)Directional Splitting (ADI)
2D Diffusion
∂u/∂t = D(∂²u/∂x² + ∂²u/∂y²)Split by direction:
A: ∂u/∂t = D ∂²u/∂x² (x-diffusion)
B: ∂u/∂t = D ∂²u/∂y² (y-diffusion)Douglas-Gunn (Unconditionally Stable)
Step 1: (I - dt/2 × D_xx) u* = (I + dt/2 × D_xx + dt × D_yy) u_n
Step 2: (I - dt/2 × D_yy) u_{n+1} = u* - dt/2 × D_yy u_nProperties:
- Second-order accurate
- Unconditionally stable for diffusion
- Tridiagonal solves only
Peaceman-Rachford
Step 1: (I - dt/2 × D_xx) u* = (I + dt/2 × D_yy) u_n
Step 2: (I - dt/2 × D_yy) u_{n+1} = (I + dt/2 × D_xx) u*Properties:
- Second-order
- Symmetric form
- Classic ADI method
3D Extension
For 3D: ∂u/∂t = D(∂²u/∂x² + ∂²u/∂y² + ∂²u/∂z²)
Douglas-Rachford-Gunn:
(I - dt/2 × D_xx) u* = (I + dt/2 × D_xx + dt × D_yy + dt × D_zz) u_n
(I - dt/2 × D_yy) u** = u* - dt/2 × D_yy u_n
(I - dt/2 × D_zz) u_{n+1} = u** - dt/2 × D_zz u_nPhysics-Based Splitting
Advection-Reaction
∂u/∂t + v·∇u = R(u)
A: ∂u/∂t + v·∇u = 0 (pure advection)
B: du/dt = R(u) (pure reaction, ODE at each point)Advantages:
- Use specialized advection scheme (upwind, WENO)
- Use stiff ODE solver for reaction
- Each solver optimized for its physics
Diffusion-Reaction
∂u/∂t = D∇²u + R(u)
A: ∂u/∂t = D∇²u (diffusion)
B: du/dt = R(u) (reaction)Common Approach:
- Implicit for diffusion (larger dt)
- CVODE/LSODA for stiff reaction
Full Advection-Diffusion-Reaction
∂u/∂t + v·∇u = D∇²u + R(u)
Three-way split:
A: Advection
B: Diffusion
C: Reaction
Strang-like: A/2 → B/2 → C → B/2 → A/2Splitting Error Analysis
Commutator Size
Splitting error depends on how much operators "fail to commute":
[A, B] = AB - BA| Commutator | Splitting Error | Example |
|---|---|---|
| [A,B] = 0 | Zero | Linear independent operators |
| [A,B] small | O(dt²) for Strang | Diffusion + mild reaction |
| [A,B] large | Significant | Coupled nonlinear terms |
Error Estimation
A posteriori estimate:
Compare: Strang (dt) vs 2 × Strang (dt/2)
Error ≈ (result_dt - result_dt/2) / 3Adaptive splitting:
- If error small: increase dt
- If error large: decrease dt or refine splitting
Error Accumulation
Over long times:
Total error ≈ (T_final / dt) × local_splitting_error
= O(dt^{p-1}) for order-p methodSpecial Techniques
Balanced Splitting
For problems with conservation laws:
Ensure: ∫ u dx conserved by each sub-stepModify operators to preserve conservation:
- Conservative discretization for each piece
- Flux-form splitting
Stabilized Splitting
Add and subtract stabilizing terms:
Original: du/dt = A(u)
Modified: du/dt = [A(u) + Su] - Su
Split: implicit explicitChoose S to improve stability of explicit part.
Iterative Splitting
Iterate between operators until converged:
While not converged:
Solve A with current B(u)
Solve B with updated A(u)Converges to coupled solution (removes splitting error).
Implementation Patterns
Modular Code Structure
def splitting_step(u, dt, A_solver, B_solver, method='strang'):
if method == 'lie':
u = A_solver(u, dt)
u = B_solver(u, dt)
elif method == 'strang':
u = A_solver(u, dt/2)
u = B_solver(u, dt)
u = A_solver(u, dt/2)
return uState Management
For multi-step splittings:
class SplitSolver:
def __init__(self):
self.u_history = [] # For multi-step methods
def step(self, u, dt):
# Store for multi-step
self.u_history.append(u.copy())
if len(self.u_history) > 3:
self.u_history.pop(0)
# Splitting step...Parallel Splitting
When operators act on independent domains:
A: operates on spatial points 0:N/2
B: operates on spatial points N/2:N
→ Solve A and B in parallel!Choosing a Method
Decision Table
| Accuracy Need | Coupling | Recommended |
|---|---|---|
| Low (1st order) | Any | Lie |
| Moderate (2nd) | Weak | Strang |
| Moderate (2nd) | Directional | ADI |
| High (4th) | Weak, reversible | Yoshida |
| High | Strong | Iterative or monolithic |
Red Flags
| Warning Sign | Problem | Solution |
|---|---|---|
| Solution blows up | Splitting unstable | Reduce dt, change order |
| Conservation violated | Unbalanced split | Use conservative form |
| Oscillations at interface | Commutator large | Reduce dt or iterate |
| Wrong steady state | Splitting error | Tighter tolerance |
Verification Tests
Manufactured Solutions
1. Choose exact u(x,t) 2. Compute source S = ∂u/∂t - A(u) - B(u) 3. Solve with splitting + source S 4. Compare to exact
Order Verification
1. Run with dt, dt/2, dt/4 2. Measure error vs reference 3. Compute order = log(e₁/e₂) / log(2) 4. Should match theoretical order
Conservation Test
1. Compute ∫ u dx at t=0 2. Run splitting simulation 3. Check ∫ u dx drift over time 4. Should be O(dt^p) or machine precision
Tolerance Guidelines
Comprehensive guide for setting integration tolerances in time-dependent simulations.
Fundamental Concepts
Relative vs Absolute Tolerance
Local error estimate ≤ atol + rtol × |y|| Tolerance | Controls | When Dominates |
|---|---|---|
| rtol | Relative accuracy (significant digits) | Large values |
| atol | Absolute accuracy (noise floor) | Small values |
Error Interpretation
- rtol = 1e-3: ~3 significant digits accuracy
- rtol = 1e-6: ~6 significant digits accuracy
- atol = 1e-10: Values below 1e-10 treated as "zero"
Default Starting Points
By Application Type
| Application | rtol | atol | Notes |
|---|---|---|---|
| Exploratory/debugging | 1e-2 | 1e-4 | Fast but coarse |
| Engineering design | 1e-3 | 1e-6 | Good balance |
| Validation studies | 1e-4 | 1e-8 | Publication quality |
| Benchmark/reference | 1e-6 | 1e-10 | Near machine precision |
| Coupled multiphysics | 1e-4 | 1e-7 | Conservative choice |
By Problem Type
| Problem | rtol | atol | Reasoning |
|---|---|---|---|
| Diffusion (smooth) | 1e-3 | 1e-6 | Errors smooth out |
| Wave propagation | 1e-4 | 1e-8 | Phase errors accumulate |
| Stiff chemistry | 1e-5 | 1e-10 | Fast modes need accuracy |
| Phase-field | 1e-4 | 1e-8 | Interface needs precision |
| Turbulence (DNS) | 1e-4 | 1e-8 | Energy cascade sensitive |
Multicomponent Systems
The Scaling Problem
When variables have vastly different magnitudes:
Temperature: O(1000 K)
Concentration: O(1e-6 mol/L)
Velocity: O(10 m/s)A single atol cannot work for all!
Solution: Component-wise atol
# Python/SciPy example
atol = [1e-3, 1e-12, 1e-5] # T, C, V
rtol = 1e-4 # Same for allDetermining Component atol
| Variable Type | atol Rule |
|---|---|
| Temperature | 0.001 × typical_T |
| Concentration | 1e-3 × smallest_meaningful_C |
| Phase fraction | 1e-6 (order parameter) |
| Velocity | 1e-3 × typical_U |
| Pressure | 1e-3 × typical_P |
Nondimensionalization Alternative
Scale all variables to O(1):
T* = T / T_ref
C* = C / C_ref
Then use atol = 1e-6 for allAdvantages:
- Simpler tolerance specification
- Better numerical conditioning
- Easier debugging
Dimensional Analysis Approach
Physical Scale Identification
1. Identify characteristic scales
- Length: L (domain size or feature size)
- Time: τ (diffusion time, reaction time)
- Velocity: U (imposed or derived)
- Temperature: ΔT (driving temperature difference)
2. Compute derived scales
- Concentration change: ΔC = C_initial - C_eq
- Energy: ρ × c_p × ΔT × L³
3. Set atol
- atol_T = 1e-3 × ΔT
- atol_C = 1e-3 × ΔC
- atol_U = 1e-3 × U
Diagnosing Tolerance Issues
Symptoms and Solutions
| Symptom | Likely Cause | Solution |
|---|---|---|
| Many rejected steps | rtol too tight | Loosen rtol by 10× |
| Noisy solution | rtol too loose | Tighten rtol by 10× |
| Negative concentrations | atol too large | Reduce atol for that component |
| Conservation violation | tolerances too loose | Tighten both |
| Slow convergence | atol >> solution values | Reduce atol |
| dt shrinks to minimum | Stiffness, not tolerance | Switch to implicit method |
Diagnostic Workflow
1. Run with default (rtol=1e-3, atol=1e-6)
2. Check:
- Step rejection rate < 10%? If not, loosen
- Conservation errors acceptable? If not, tighten
- Physical variables positive? If not, reduce atol
3. Tighten by 10× and re-run
4. If results change significantly, tolerances were too looseConservation and Tolerance
Mass Conservation
For conserved quantities (mass, energy):
Relative conservation error ≈ rtol × (integration time / characteristic time)Example:
- rtol = 1e-4
- Integration: 1000 time units
- Characteristic time: 1 unit
- Expected drift: ~1e-4 × 1000 = 0.1 (10% error!)
Solution: Use tighter rtol for long integrations or specialized conservative schemes.
Energy Drift in Hamiltonian Systems
| Method | Energy Error |
|---|---|
| RK4 | Drifts linearly with t |
| Symplectic | Bounded, oscillates |
| Energy-preserving | Exact to tolerance |
For long-time simulations, prefer structure-preserving methods.
Stiff Problems
Special Considerations
1. Stiff components need tighter atol
- Fast modes decay to small values quickly
- Large atol masks stiff dynamics
2. Watch for order reduction
- Near stability boundary, effective order drops
- May need tighter rtol to compensate
3. Jacobian accuracy matters
- Numerical Jacobian errors ∝ sqrt(eps)
- May need tighter tolerances with numerical Jacobian
Recommended Settings for Stiff
| Stiffness | rtol | atol |
|---|---|---|
| Mildly stiff (ratio ~100) | 1e-4 | 1e-8 |
| Moderately stiff (~10⁴) | 1e-5 | 1e-10 |
| Very stiff (~10⁶+) | 1e-6 | 1e-12 |
Practical Examples
Phase-Field Simulation
# Two-phase Allen-Cahn
# phi: order parameter [-1, 1]
# T: temperature [300-2000 K]
rtol = 1e-4
atol_phi = 1e-6 # Order parameter precision
atol_T = 1e-1 # 0.1 K absolute accuracy
atol = [atol_phi, atol_T]Reactive Flow
# Combustion with trace species
# Major species: O(1)
# Minor species: O(1e-6)
# Temperature: O(1000)
rtol = 1e-5 # Tighter for stiff chemistry
atol_major = 1e-8
atol_minor = 1e-12 # Detect small species
atol_T = 1e-2
atol = [..., atol_minor, ..., atol_T]Solidification
# Phase-field solidification
# phi: solid fraction [0, 1]
# C: concentration [0.01 - 0.05]
# T: temperature [1700-1800 K]
rtol = 1e-4
atol = [1e-6, # phi (interface precision)
1e-6, # C (microsegregation)
0.01] # T (0.01 K accuracy)Tolerance Tightening Protocol
Convergence Study
1. Run with baseline tolerances (tol₀) 2. Run with tol₀/10 3. Compare key metrics (peak values, integrals, times) 4. If difference < acceptable threshold, tol₀ is adequate 5. If not, repeat with tighter tolerances
What to Compare
| Metric | Acceptable Difference |
|---|---|
| Peak temperature | < 1% |
| Total energy | < 0.1% |
| Interface position | < dx/2 |
| Reaction completion time | < 1% |
| Species concentrations | < 1% |
Common Mistakes
1. Same atol for all components
Problem: Variables with small physical values are ignored. Solution: Use component-wise atol scaled to each variable.
2. atol = 0
Problem: Division by zero when solution passes through zero. Solution: Always use small positive atol.
3. rtol too tight for noisy data
Problem: Excessive step rejection, slow progress. Solution: Match rtol to data precision.
4. Ignoring units
Problem: atol = 1e-6 means nothing without units. Solution: Express atol in simulation units: "1e-6 mol/L" or "0.1 K".
#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import Dict, Optional
def clamp(value: float, min_value: float, max_value: float) -> float:
return max(min_value, min(max_value, value))
def compute_step(
dt: float,
error_norm: float,
order: int,
accept_threshold: float,
safety: float,
min_factor: float,
max_factor: float,
controller: str,
prev_error: Optional[float],
) -> Dict[str, object]:
if dt <= 0:
raise ValueError("dt must be positive")
if order < 1:
raise ValueError("order must be >= 1")
if accept_threshold <= 0:
raise ValueError("accept_threshold must be positive")
if safety <= 0:
raise ValueError("safety must be positive")
if min_factor <= 0 or max_factor <= 0:
raise ValueError("min_factor and max_factor must be positive")
if min_factor > max_factor:
raise ValueError("min_factor must be <= max_factor")
if error_norm < 0 or not math.isfinite(error_norm):
raise ValueError("error_norm must be finite and non-negative")
if prev_error is not None and (prev_error <= 0 or not math.isfinite(prev_error)):
raise ValueError("prev_error must be positive and finite when provided")
accept = error_norm <= accept_threshold
if error_norm == 0:
factor = max_factor
controller_used = "zero-error"
else:
exp = 1.0 / (order + 1.0)
if controller == "pi" and prev_error is not None:
k1 = 0.7 * exp
k2 = 0.3 * exp
factor = safety * (accept_threshold / error_norm) ** k1
factor *= (accept_threshold / prev_error) ** k2
controller_used = "pi"
else:
factor = safety * (accept_threshold / error_norm) ** exp
controller_used = "p"
factor = clamp(factor, min_factor, max_factor)
dt_next = dt * factor
note = None
if not accept:
note = "Step rejected; consider reducing dt or using a stiffer method."
return {
"accept": accept,
"dt_next": dt_next,
"factor": factor,
"controller_used": controller_used,
"note": note,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Adaptive step size controller for time integration.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--dt", type=float, required=True, help="Current time step")
parser.add_argument("--error-norm", type=float, required=True, help="Scaled error norm")
parser.add_argument("--order", type=int, required=True, help="Method order")
parser.add_argument(
"--accept-threshold",
type=float,
default=1.0,
help="Acceptance threshold for error norm",
)
parser.add_argument("--safety", type=float, default=0.9, help="Safety factor")
parser.add_argument("--min-factor", type=float, default=0.2, help="Min dt factor")
parser.add_argument("--max-factor", type=float, default=5.0, help="Max dt factor")
parser.add_argument(
"--controller",
choices=["p", "pi"],
default="p",
help="Controller type",
)
parser.add_argument(
"--prev-error",
type=float,
default=None,
help="Previous error norm (for PI controller)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
payload = compute_step(
dt=args.dt,
error_norm=args.error_norm,
order=args.order,
accept_threshold=args.accept_threshold,
safety=args.safety,
min_factor=args.min_factor,
max_factor=args.max_factor,
controller=args.controller,
prev_error=args.prev_error,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
result = {
"inputs": {
"dt": args.dt,
"error_norm": args.error_norm,
"order": args.order,
"accept_threshold": args.accept_threshold,
"safety": args.safety,
"min_factor": args.min_factor,
"max_factor": args.max_factor,
"controller": args.controller,
"prev_error": args.prev_error,
},
"results": payload,
}
if args.json:
print(json.dumps(result, indent=2, sort_keys=True))
return
print("Adaptive step control")
print(f" accept: {payload['accept']}")
print(f" factor: {payload['factor']:.6g}")
print(f" dt_next: {payload['dt_next']:.6g}")
print(f" controller: {payload['controller_used']}")
if payload["note"]:
print(f" note: {payload['note']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import math
import sys
from typing import 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_error_norm(
error: List[float],
solution: Optional[List[float]],
scale: Optional[List[float]],
rtol: float,
atol: float,
norm: str,
min_scale: float,
) -> Tuple[float, float, float, float]:
if not error:
raise ValueError("error list must be non-empty")
if rtol < 0 or atol < 0:
raise ValueError("rtol and atol must be non-negative")
if min_scale < 0:
raise ValueError("min_scale must be non-negative")
if norm not in {"rms", "inf"}:
raise ValueError("norm must be 'rms' or 'inf'")
if scale is None:
if solution is None:
raise ValueError("solution or scale must be provided")
if len(solution) != len(error):
raise ValueError("solution length must match error length")
scale = [max(min_scale, atol + rtol * abs(y)) for y in solution]
else:
if len(scale) != len(error):
raise ValueError("scale length must match error length")
if any(s <= 0 for s in scale):
raise ValueError("scale values must be positive")
scaled = [e / s for e, s in zip(error, scale)]
abs_scaled = [abs(v) for v in scaled]
if norm == "inf":
error_norm = max(abs_scaled)
else:
error_norm = math.sqrt(sum(v * v for v in scaled) / len(scaled))
return (
error_norm,
max(abs_scaled),
min(scale),
max(scale),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compute scaled error norm for adaptive time stepping.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--error", required=True, help="Comma-separated error values")
parser.add_argument(
"--solution",
default=None,
help="Comma-separated solution values (for scaling)",
)
parser.add_argument(
"--scale",
default=None,
help="Comma-separated scale values (overrides solution-based scaling)",
)
parser.add_argument("--rtol", type=float, default=1e-3, help="Relative tolerance")
parser.add_argument("--atol", type=float, default=1e-6, help="Absolute tolerance")
parser.add_argument(
"--norm",
choices=["rms", "inf"],
default="rms",
help="Error norm type",
)
parser.add_argument(
"--min-scale",
type=float,
default=0.0,
help="Lower bound for scale values",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
error = parse_list(args.error)
solution = parse_list(args.solution) if args.solution is not None else None
scale = parse_list(args.scale) if args.scale is not None else None
error_norm, max_component, scale_min, scale_max = compute_error_norm(
error=error,
solution=solution,
scale=scale,
rtol=args.rtol,
atol=args.atol,
norm=args.norm,
min_scale=args.min_scale,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"error": error,
"solution": solution,
"scale": scale,
"rtol": args.rtol,
"atol": args.atol,
"norm": args.norm,
"min_scale": args.min_scale,
},
"results": {
"error_norm": error_norm,
"max_component": max_component,
"scale_min": scale_min,
"scale_max": scale_max,
},
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Error norm")
print(f" norm: {args.norm}")
print(f" error_norm: {error_norm:.6g}")
print(f" max_component: {max_component:.6g}")
print(f" scale_min: {scale_min:.6g}")
print(f" scale_max: {scale_max:.6g}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
import argparse
import json
import sys
from typing import Dict, List
def parse_terms(raw: str) -> List[str]:
if raw is None:
return []
parts = [p.strip() for p in raw.split(",") if p.strip()]
return parts
def plan_imex(
stiff_terms: List[str],
nonstiff_terms: List[str],
coupling: str,
accuracy: str,
stiffness_ratio: float,
conservative: bool,
) -> Dict[str, object]:
if coupling not in {"weak", "moderate", "strong"}:
raise ValueError("coupling must be weak, moderate, or strong")
if accuracy not in {"low", "medium", "high"}:
raise ValueError("accuracy must be low, medium, or high")
if stiffness_ratio <= 0:
raise ValueError("stiffness_ratio must be positive")
if not stiff_terms and not nonstiff_terms:
raise ValueError("Provide at least one stiff or non-stiff term")
implicit_terms = stiff_terms
explicit_terms = nonstiff_terms
notes: List[str] = []
recommended: List[str] = []
if stiff_terms and nonstiff_terms:
recommended.append("IMEX-ARK")
recommended.append("SBDF (semi-implicit BDF)")
elif stiff_terms:
recommended.append("BDF")
recommended.append("Radau IIA")
else:
recommended.append("RK45")
recommended.append("DOP853")
if conservative:
notes.append("Preserve conserved quantities; avoid overly aggressive splitting.")
if coupling == "strong" or stiffness_ratio >= 1e4:
splitting = "imex-coupled"
notes.append("Strong coupling: avoid loose operator splitting.")
elif accuracy == "high":
splitting = "strang"
notes.append("Strang splitting for higher accuracy.")
else:
splitting = "lie"
notes.append("Lie splitting for efficiency.")
return {
"implicit_terms": implicit_terms,
"explicit_terms": explicit_terms,
"recommended_integrator": recommended,
"splitting_strategy": splitting,
"notes": notes,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Plan IMEX split for stiff/non-stiff coupling.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--stiff-terms", default=None, help="Comma-separated stiff terms")
parser.add_argument("--nonstiff-terms", default=None, help="Comma-separated non-stiff terms")
parser.add_argument(
"--coupling",
choices=["weak", "moderate", "strong"],
default="moderate",
help="Coupling strength between operators",
)
parser.add_argument(
"--accuracy",
choices=["low", "medium", "high"],
default="medium",
help="Desired accuracy level",
)
parser.add_argument(
"--stiffness-ratio",
type=float,
default=1e3,
help="Estimated stiffness ratio",
)
parser.add_argument(
"--conservative",
action="store_true",
help="Preserve conserved quantities",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
result = plan_imex(
stiff_terms=parse_terms(args.stiff_terms),
nonstiff_terms=parse_terms(args.nonstiff_terms),
coupling=args.coupling,
accuracy=args.accuracy,
stiffness_ratio=args.stiffness_ratio,
conservative=args.conservative,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"stiff_terms": parse_terms(args.stiff_terms),
"nonstiff_terms": parse_terms(args.nonstiff_terms),
"coupling": args.coupling,
"accuracy": args.accuracy,
"stiffness_ratio": args.stiffness_ratio,
"conservative": args.conservative,
},
"results": result,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("IMEX split plan")
print(f" implicit_terms: {', '.join(result['implicit_terms']) or 'none'}")
print(f" explicit_terms: {', '.join(result['explicit_terms']) or 'none'}")
print(f" splitting_strategy: {result['splitting_strategy']}")
print(f" recommended_integrator: {', '.join(result['recommended_integrator'])}")
for note in result["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_integrator(
stiff: bool,
oscillatory: bool,
event_detection: bool,
jacobian_available: bool,
implicit_allowed: bool,
accuracy: str,
dimension: int,
low_memory: bool,
) -> Dict[str, List[str] | str]:
if dimension <= 0:
raise ValueError("dimension must be positive")
if accuracy not in {"low", "medium", "high"}:
raise ValueError("accuracy must be low, medium, or high")
recommended: List[str] = []
alternatives: List[str] = []
notes: List[str] = []
if stiff:
if implicit_allowed or jacobian_available:
recommended.extend(["BDF", "Radau IIA"])
if jacobian_available:
recommended.append("Rosenbrock")
else:
alternatives.append("Rosenbrock (needs Jacobian)")
else:
recommended.extend(["IMEX", "RK-Chebyshev"])
notes.append("Stiff problem without implicit solves; expect smaller dt.")
else:
if oscillatory:
recommended.extend(["Symplectic Verlet", "Stormer-Verlet"])
alternatives.append("RK45 (if invariants are not critical)")
else:
recommended.append("RK45")
if accuracy == "high":
alternatives.append("DOP853")
elif accuracy == "low":
alternatives.append("RK23")
if event_detection:
notes.append("Prefer methods with dense output for event detection.")
if "RK45" in recommended:
alternatives.append("RK45 (dense output)")
else:
alternatives.append("DOP853 (dense output)")
if low_memory or dimension > 1_000_000:
notes.append("Large state: consider low-storage RK or linearly implicit methods.")
alternatives.append("Low-storage RK")
if stiff and not jacobian_available:
notes.append("Provide Jacobian or Jacobian-vector products for efficiency.")
if accuracy == "high":
notes.append("Tight tolerances required; expect smaller dt and higher cost.")
return {
"recommended": recommended,
"alternatives": alternatives,
"notes": notes,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Select a suitable time integrator based on problem characteristics.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--stiff", action="store_true", help="Treat the problem as stiff")
parser.add_argument(
"--oscillatory",
action="store_true",
help="System exhibits oscillatory dynamics",
)
parser.add_argument(
"--event-detection",
action="store_true",
help="Events or root finding required",
)
parser.add_argument(
"--jacobian-available",
action="store_true",
help="Jacobian or Jv product is available",
)
parser.add_argument(
"--implicit-allowed",
action="store_true",
help="Implicit solves are feasible",
)
parser.add_argument(
"--accuracy",
choices=["low", "medium", "high"],
default="medium",
help="Desired accuracy level",
)
parser.add_argument(
"--dimension",
type=int,
default=1,
help="State dimension (for memory considerations)",
)
parser.add_argument(
"--low-memory",
action="store_true",
help="Prefer low-memory methods",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
result = select_integrator(
stiff=args.stiff,
oscillatory=args.oscillatory,
event_detection=args.event_detection,
jacobian_available=args.jacobian_available,
implicit_allowed=args.implicit_allowed,
accuracy=args.accuracy,
dimension=args.dimension,
low_memory=args.low_memory,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"stiff": args.stiff,
"oscillatory": args.oscillatory,
"event_detection": args.event_detection,
"jacobian_available": args.jacobian_available,
"implicit_allowed": args.implicit_allowed,
"accuracy": args.accuracy,
"dimension": args.dimension,
"low_memory": args.low_memory,
},
"results": result,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Integrator 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 math
import sys
from typing import Dict
def estimate_error(
dt: float,
scheme: str,
commutator_norm: float,
target_error: float,
) -> Dict[str, object]:
if dt <= 0:
raise ValueError("dt must be positive")
if commutator_norm < 0:
raise ValueError("commutator_norm must be non-negative")
if scheme not in {"lie", "strang"}:
raise ValueError("scheme must be lie or strang")
if target_error < 0:
raise ValueError("target_error must be non-negative")
order = 1 if scheme == "lie" else 2
error_est = commutator_norm * (dt ** (order + 1))
substeps = 1
dt_effective = dt
if target_error > 0 and error_est > target_error:
ratio = error_est / target_error
substeps = int(math.ceil(ratio ** (1.0 / (order + 1))))
substeps = max(substeps, 1)
dt_effective = dt / substeps
error_est = commutator_norm * (dt_effective ** (order + 1))
return {
"scheme": scheme,
"order": order,
"error_estimate": error_est,
"dt_effective": dt_effective,
"substeps": substeps,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Estimate operator splitting error and suggest substeps.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--dt", type=float, required=True, help="Base time step")
parser.add_argument(
"--scheme",
choices=["lie", "strang"],
default="strang",
help="Splitting scheme",
)
parser.add_argument(
"--commutator-norm",
type=float,
required=True,
help="Estimated commutator norm",
)
parser.add_argument(
"--target-error",
type=float,
default=0.0,
help="Target splitting error (optional)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON output")
return parser.parse_args()
def main() -> None:
args = parse_args()
try:
result = estimate_error(
dt=args.dt,
scheme=args.scheme,
commutator_norm=args.commutator_norm,
target_error=args.target_error,
)
except ValueError as exc:
print(str(exc), file=sys.stderr)
sys.exit(2)
payload = {
"inputs": {
"dt": args.dt,
"scheme": args.scheme,
"commutator_norm": args.commutator_norm,
"target_error": args.target_error,
},
"results": result,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
return
print("Splitting error estimate")
print(f" scheme: {result['scheme']}")
print(f" order: {result['order']}")
print(f" error_estimate: {result['error_estimate']:.6g}")
print(f" dt_effective: {result['dt_effective']:.6g}")
print(f" substeps: {result['substeps']}")
if __name__ == "__main__":
main()
Related skills
FAQ
How do I pick an integrator for a stiff problem?
With a Jacobian, use Rosenbrock or BDF; without one, use BDF with a numerical Jacobian.
Which step controllers are available?
Integral (I), proportional-integral (PI), and PID controllers, with PI recommended for general use.