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:

.. list-table::
   :header-rows: 1
   :widths: 20 40 40

   * - Mode
     - Behavior
     - Use Case
   * - ``stability=False``
     - No stability checks (default)
     - Simple problems, maximum speed
   * - ``stability='check'``
     - Warn about issues, don't fix
     - Debugging, identify problems
   * - ``stability='auto'``
     - Auto-detect and fix issues
     - Production use, challenging problems

Basic Usage
-----------

Enable stability mode with a single parameter:

.. code-block:: python

   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``:

.. code-block:: python

   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:

.. code-block:: python

   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
------------------

.. list-table::
   :header-rows: 1
   :widths: 25 25 50

   * - Setting
     - Overhead
     - Notes
   * - ``stability=False``
     - 0
     - No stability checks
   * - ``stability='check'``
     - ~1ms for 1M points
     - Only monitoring, no fixes
   * - ``stability='auto'``
     - ~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:

.. code-block:: python

   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:

.. code-block:: bash

   # 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**:

1. Enable stability mode:

   .. code-block:: python

      popt, pcov = curve_fit(model, x, y, p0=p0, stability="auto")

2. Check initial parameters:

   .. code-block:: python

      from nlsq.stability import check_problem_stability

      report = check_problem_stability(model, x, y, p0=p0)
      print(f"Condition number: {report['condition_number']:.2e}")

3. Use bounds to constrain parameters:

   .. code-block:: python

      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**:

1. Disable stability checks:

   .. code-block:: python

      popt, pcov = curve_fit(model, x, y, p0=p0, stability=False)

2. Lower SVD threshold for large datasets:

   .. code-block:: python

      popt, pcov = curve_fit(
          model, x, y, p0=p0, stability="auto", max_jacobian_elements_for_svd=1_000_000
      )

3. Use check mode instead of auto:

   .. code-block:: python

      popt, pcov = curve_fit(model, x, y, p0=p0, stability="check")

Ill-Conditioned Problems
~~~~~~~~~~~~~~~~~~~~~~~~

**Symptoms**: Large uncertainties, unstable parameter estimates

**Solutions**:

1. Rescale data:

   .. code-block:: python

      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)

2. Use automatic rescaling:

   .. code-block:: python

      popt, pcov = curve_fit(model, x, y, p0=p0, stability="auto", rescale_data=True)

3. Add regularization via bounds:

   .. code-block:: python

      # Soft bounds prevent extreme parameters
      popt, pcov = curve_fit(
          model, x, y, p0=p0, bounds=([-1e10] * n_params, [1e10] * n_params)
      )

See Also
--------

- :doc:`../api/nlsq.stability` - Stability module API reference
- :doc:`../api/nlsq.validators` - Input validation
- :doc:`../howto/troubleshooting` - General troubleshooting guide
- :doc:`../howto/optimize_performance` - Performance optimization
