Numerical Stability Guide¶
This guide explains NLSQ’s numerical stability features and how to use them effectively for challenging optimization problems.
Overview¶
NLSQ provides automatic numerical stability monitoring and correction to prevent optimization divergence. The stability system handles:
NaN/Inf detection in Jacobian matrices
Condition number monitoring for ill-conditioned problems
Data rescaling to improve numerical conditioning
SVD skip for large Jacobians to avoid performance degradation
Stability Modes¶
The stability parameter in curve_fit() controls behavior:
Mode |
Behavior |
Use Case |
|---|---|---|
|
No stability checks (default) |
Simple problems, maximum speed |
|
Warn about issues, don’t fix |
Debugging, identify problems |
|
Auto-detect and fix issues |
Production use, challenging problems |
Basic Usage¶
Enable stability mode with a single parameter:
from nlsq import curve_fit
import jax.numpy as jnp
import numpy as np
def exponential(x, a, b, c):
return a * jnp.exp(-b * x) + c
# Data with challenging characteristics
x = np.linspace(0, 1e6, 1000) # Large x-range
y = 2.5 * np.exp(-0.5 * x) + 1.0
# Enable automatic stability fixes
popt, pcov = curve_fit(exponential, x, y, p0=[2.5, 0.5, 1.0], stability="auto")
Physics Applications¶
For physics applications (XPCS, scattering, spectroscopy) where data must
maintain physical units, use rescale_data=False:
from nlsq import curve_fit
import jax.numpy as jnp
def g2_model(tau, baseline, contrast, gamma):
"""XPCS intensity autocorrelation function."""
return baseline + contrast * jnp.exp(-2 * gamma * tau) ** 2
# Time delays in physical units (seconds)
tau = np.logspace(-6, 1, 200) # 1µs to 10s
y = 1.0 + 0.3 * np.exp(-2 * 100 * tau) ** 2
# Preserve physical units
popt, pcov = curve_fit(
g2_model,
tau,
y,
p0=[1.0, 0.3, 100.0],
stability="auto",
rescale_data=False, # Don't normalize data
)
Why use rescale_data=False?
Time delays in seconds have physical meaning
Scattering vectors (q) in nm^-1 should not be normalized
Decay rates (gamma) are in physical units (s^-1)
Normalizing would change the interpretation of fitted parameters
Large Jacobian Optimization¶
For large datasets (>10M Jacobian elements), SVD computation becomes expensive. NLSQ automatically skips SVD for large Jacobians:
from nlsq import curve_fit
# Large dataset: 10M points × 3 params = 30M Jacobian elements
x_large = np.linspace(0, 100, 10_000_000)
y_large = model(x_large, *true_params) + noise
# SVD automatically skipped (>10M elements)
popt, pcov = curve_fit(model, x_large, y_large, p0=p0, stability="auto")
# Custom threshold
popt, pcov = curve_fit(
model,
x_large,
y_large,
p0=p0,
stability="auto",
max_jacobian_elements_for_svd=5_000_000, # Skip above 5M
)
What happens when SVD is skipped?
NaN/Inf checking is still performed (O(n) complexity)
Condition number monitoring is disabled
No regularization applied
Optimization proceeds without stability overhead
Performance Impact¶
Setting |
Overhead |
Notes |
|---|---|---|
|
0 |
No stability checks |
|
~1ms for 1M points |
Only monitoring, no fixes |
|
~1-5ms |
Full detection and fixes |
Per-iteration vs initialization-only:
Prior to v0.3.0, stability checks ran per-iteration, causing optimization divergence due to accumulated perturbations. Now stability checks run only at initialization, reducing overhead and preventing divergence.
Configuration Options¶
All stability-related parameters:
from nlsq import curve_fit
popt, pcov = curve_fit(
model,
x,
y,
p0=p0,
# Stability mode
stability="auto", # 'auto', 'check', or False
# Data rescaling
rescale_data=True, # Rescale data to [0,1] (default)
# SVD threshold
max_jacobian_elements_for_svd=10_000_000, # Skip SVD above this
)
Environment Variables¶
Configure stability defaults via environment:
# Disable persistent JAX cache
export NLSQ_DISABLE_PERSISTENT_CACHE=1
# Custom JAX cache directory
export NLSQ_JAX_CACHE_DIR=/tmp/nlsq_cache
# Minimum compilation time to cache
export NLSQ_CACHE_MIN_COMPILE_TIME_SECS=2
Troubleshooting¶
Optimization Diverges¶
Symptoms: Cost increases, parameters explode, NaN in results
Solutions:
Enable stability mode:
popt, pcov = curve_fit(model, x, y, p0=p0, stability="auto")
Check initial parameters:
from nlsq.stability import check_problem_stability report = check_problem_stability(model, x, y, p0=p0) print(f"Condition number: {report['condition_number']:.2e}")
Use bounds to constrain parameters:
popt, pcov = curve_fit(model, x, y, p0=p0, bounds=([0, 0, 0], [10, 10, 10]))
Slow Optimization¶
Symptoms: Optimization takes much longer than expected
Solutions:
Disable stability checks:
popt, pcov = curve_fit(model, x, y, p0=p0, stability=False)
Lower SVD threshold for large datasets:
popt, pcov = curve_fit( model, x, y, p0=p0, stability="auto", max_jacobian_elements_for_svd=1_000_000 )
Use check mode instead of auto:
popt, pcov = curve_fit(model, x, y, p0=p0, stability="check")
Ill-Conditioned Problems¶
Symptoms: Large uncertainties, unstable parameter estimates
Solutions:
Rescale data:
x_scaled = (x - x.mean()) / x.std() y_scaled = (y - y.mean()) / y.std() popt, pcov = curve_fit(model, x_scaled, y_scaled, p0=p0)
Use automatic rescaling:
popt, pcov = curve_fit(model, x, y, p0=p0, stability="auto", rescale_data=True)
Add regularization via bounds:
# Soft bounds prevent extreme parameters popt, pcov = curve_fit( model, x, y, p0=p0, bounds=([-1e10] * n_params, [1e10] * n_params) )
See Also¶
nlsq.stability module - Stability module API reference
nlsq.validators module - Input validation
Troubleshooting Guide - General troubleshooting guide
Performance Optimization Guide - Performance optimization