nlsq.bound_inference module

Smart Parameter Bounds Inference

This module provides automatic inference of reasonable parameter bounds based on data characteristics and model structure.

Key Features: - Data-driven bound estimation from x/y ranges - Model-aware heuristics for common function types - Conservative bounds that avoid over-constraining - Support for user-provided partial bounds

Example

>>> from nlsq.precision.bound_inference import infer_bounds
>>>
>>> bounds = infer_bounds(xdata, ydata, p0=[1, 0.5, 0.1])
>>> print(f"Lower bounds: {bounds[0]}")
>>> print(f"Upper bounds: {bounds[1]}")
class nlsq.precision.bound_inference.BoundsInference(xdata, ydata, p0, safety_factor=10.0, enforce_positivity=True)[source]

Bases: object

Infer reasonable parameter bounds from data characteristics.

This class implements heuristics to estimate parameter bounds that help constrain optimization without being overly restrictive. The bounds are based on data ranges, parameter scales, and common patterns.

Parameters:
  • xdata (array_like) – Independent variable data

  • ydata (array_like) – Dependent variable data

  • p0 (array_like) – Initial parameter guess

  • safety_factor (float, optional) – Multiplier for bound ranges (larger = more conservative). Default: 10.0

  • enforce_positivity (bool, optional) – Force all bounds to be non-negative if p0 is positive. Default: True

x_min, x_max

Range of independent variable

Type:

float

y_min, y_max

Range of dependent variable

Type:

float

x_range, y_range

Span of data

Type:

float

Examples

>>> x = np.linspace(0, 10, 100)
>>> y = 2.5 * np.exp(-0.5 * x) + 1.0
>>> p0 = [2.0, 0.5, 1.0]
>>>
>>> inference = BoundsInference(x, y, p0)
>>> bounds = inference.infer()
>>> print(bounds)
([0.0, 0.0, 0.0], [25.0, 5.0, 10.0])
__init__(xdata, ydata, p0, safety_factor=10.0, enforce_positivity=True)[source]

Initialize bounds inference.

infer()[source]

Infer bounds for all parameters.

Returns:

  • lower_bounds (ndarray) – Lower bounds for each parameter

  • upper_bounds (ndarray) – Upper bounds for each parameter

Return type:

tuple[ndarray, ndarray]

nlsq.precision.bound_inference.infer_bounds(xdata, ydata, p0, safety_factor=10.0, enforce_positivity=True)[source]

Infer reasonable parameter bounds from data and initial guess.

This is a convenience function that creates a BoundsInference instance and returns the inferred bounds.

Parameters:
  • xdata (array_like) – Independent variable data

  • ydata (array_like) – Dependent variable data

  • p0 (array_like) – Initial parameter guess

  • safety_factor (float, optional) – Multiplier for bound ranges (larger = more conservative). Default: 10.0

  • enforce_positivity (bool, optional) – Force all bounds to be non-negative if p0 is positive. Default: True

Returns:

  • lower_bounds (ndarray) – Lower bounds for each parameter

  • upper_bounds (ndarray) – Upper bounds for each parameter

Return type:

tuple[ndarray, ndarray]

Examples

Basic usage:

>>> import numpy as np
>>> x = np.linspace(0, 10, 100)
>>> y = 2.5 * np.exp(-0.5 * x) + 1.0
>>> p0 = [2.0, 0.5, 1.0]
>>> bounds = infer_bounds(x, y, p0)
>>> print(f"Lower: {bounds[0]}")
>>> print(f"Upper: {bounds[1]}")

With custom safety factor:

>>> bounds = infer_bounds(x, y, p0, safety_factor=20.0)  # More conservative

Allow negative parameters:

>>> bounds = infer_bounds(x, y, p0, enforce_positivity=False)
nlsq.precision.bound_inference.infer_bounds_for_multistart(xdata, ydata, p0, user_bounds=None, safety_factor=20.0, enforce_positivity=True)[source]

Infer bounds suitable for multi-start LHS sampling.

This is a wrapper around infer_bounds() with defaults appropriate for global exploration in multi-start optimization. Uses a larger safety_factor to allow broader exploration of the parameter space.

Parameters:
  • xdata (array_like) – Independent variable data

  • ydata (array_like) – Dependent variable data

  • p0 (array_like) – Initial parameter guess (used for centering and scale inference)

  • user_bounds (tuple, optional) – User-provided (lower, upper) bounds. If provided and finite, these take precedence over inferred bounds.

  • safety_factor (float, optional) – Multiplier for bound ranges. Default: 20.0 (larger than standard infer_bounds to allow broader exploration)

  • enforce_positivity (bool, optional) – Force all bounds to be non-negative if p0 is positive. Default: True

Returns:

  • lower_bounds (ndarray) – Lower bounds suitable for LHS sampling

  • upper_bounds (ndarray) – Upper bounds suitable for LHS sampling

Return type:

tuple[ndarray, ndarray]

Notes

This function ensures that returned bounds are always finite, which is required for Latin Hypercube Sampling. If user provides infinite bounds, they are replaced with inferred finite bounds.

The default safety_factor of 20.0 is larger than the standard 10.0 used in infer_bounds() to allow for broader exploration during multi-start optimization.

Examples

Basic usage for multi-start:

>>> import numpy as np
>>> x = np.linspace(0, 10, 100)
>>> y = 2.5 * np.exp(-0.5 * x) + 1.0
>>> p0 = [2.0, 0.5, 1.0]
>>> bounds = infer_bounds_for_multistart(x, y, p0)
>>> # bounds will be wider than standard infer_bounds

With user-provided bounds (finite bounds are preserved):

>>> user_bounds = ([0, 0, 0], [10, np.inf, 5])
>>> bounds = infer_bounds_for_multistart(x, y, p0, user_bounds=user_bounds)
>>> # First and third params use user bounds, second is inferred

See also

infer_bounds

Standard bound inference with smaller safety_factor

merge_bounds

Merge user-provided and inferred bounds

nlsq.precision.bound_inference.merge_bounds(inferred_bounds, user_bounds=None)[source]

Merge user-provided bounds with inferred bounds.

User-provided bounds take precedence. If user provides partial bounds (e.g., only lower or only for some parameters), the remaining bounds are filled from inferred bounds.

Parameters:
  • inferred_bounds (tuple of ndarray) – Inferred (lower, upper) bounds

  • user_bounds (tuple of array_like or None, optional) – User-provided (lower, upper) bounds. Can contain -np.inf or np.inf for unbounded parameters.

Returns:

  • lower_bounds (ndarray) – Merged lower bounds

  • upper_bounds (ndarray) – Merged upper bounds

Return type:

tuple[ndarray, ndarray]

Examples

>>> inferred = (np.array([0, 0, 0]), np.array([10, 5, 10]))
>>> user = (np.array([1, -np.inf, 0]), np.array([5, np.inf, 10]))
>>> merged = merge_bounds(inferred, user)
>>> print(merged)
(array([1., 0., 0.]), array([5., 5., 10.]))

Notes

  • If user provides -np.inf for lower bound, use inferred lower bound

  • If user provides np.inf for upper bound, use inferred upper bound

  • Scalar user bounds are broadcast to all parameters

Overview

The nlsq.bound_inference module provides intelligent automatic inference of parameter bounds from data. Instead of manually specifying bounds, the module analyzes your data and model to determine reasonable constraints automatically.

New in version 0.1.1: Complete bounds inference system with smart defaults.

Key Features

  • Automatic bounds detection from data characteristics

  • Model-aware inference for common function types

  • Physical constraints enforcement (positivity, ranges)

  • Data-driven estimation using statistics

  • Merge with user bounds for hybrid constraints

  • Validation of inferred bounds

Classes

BoundsInference(xdata, ydata, p0[, ...])

Infer reasonable parameter bounds from data characteristics.

Functions

infer_bounds(xdata, ydata, p0[, ...])

Infer reasonable parameter bounds from data and initial guess.

merge_bounds(inferred_bounds[, user_bounds])

Merge user-provided bounds with inferred bounds.

Usage Examples

Basic Automatic Bounds

Let NLSQ infer parameter bounds automatically:

from nlsq import curve_fit
import jax.numpy as jnp


def exponential(x, a, b):
    return a * jnp.exp(-b * x)


# Enable automatic bounds inference
result = curve_fit(
    exponential, x, y, p0=[1.0, 0.5], auto_bounds=True  # Infer bounds automatically
)

# Access inferred bounds
print(f"Inferred bounds: {result.bounds_used}")

Explicit Bounds Inference

Manually infer and inspect bounds before fitting:

from nlsq.precision.bound_inference import infer_bounds

# Infer bounds from data
bounds = infer_bounds(exponential, x, y, p0=[1.0, 0.5])

print(f"Lower bounds: {bounds.lower}")
print(f"Upper bounds: {bounds.upper}")
print(f"Confidence: {bounds.confidence}")

# Use inferred bounds
result = curve_fit(
    exponential, x, y, p0=[1.0, 0.5], bounds=(bounds.lower, bounds.upper)
)

Merging with User Bounds

Combine automatic inference with user-specified constraints:

from nlsq.precision.bound_inference import infer_bounds, merge_bounds

# Infer bounds
auto_bounds = infer_bounds(exponential, x, y, p0=[1.0, 0.5])

# User knows that parameter 'a' must be between 0 and 10
user_bounds = ([0, -np.inf], [10, np.inf])

# Merge: use user bounds where specified, auto-inferred elsewhere
final_bounds = merge_bounds(auto_bounds, user_bounds)

result = curve_fit(exponential, x, y, p0=[1.0, 0.5], bounds=final_bounds)

Model-Specific Inference

For known model types, inference uses domain knowledge:

from nlsq.core.functions import gaussian
from nlsq.precision.bound_inference import infer_bounds

# For Gaussian, inference knows:
# - Amplitude should be ~ max(y)
# - Mean should be in range of x
# - Std should be positive and ~ range(x)
bounds = infer_bounds(gaussian, x, y, p0=None, model_type="gaussian")

print(f"Gaussian-specific bounds: {bounds}")

Physical Constraints

Enforce physical constraints (positivity, ranges):

from nlsq.precision.bound_inference import BoundsInference

inference = BoundsInference(
    enforce_positivity=["amplitude", "rate"],  # These must be > 0
    enforce_ranges={"temperature": (0, 1000)},  # Physical limits
)

bounds = inference.infer(
    model, x, y, p0, param_names=["amplitude", "rate", "temperature"]
)

Data-Driven Estimation

Use data statistics for bounds estimation:

from nlsq.precision.bound_inference import infer_bounds

# Uses data statistics (min, max, mean, std)
bounds = infer_bounds(
    model,
    x,
    y,
    p0,
    use_data_range=True,  # Use min/max of data
    margin_factor=1.5,  # Add 50% margin
)

print(f"Data-driven bounds: {bounds}")

Bounds Validation

Validate inferred or user-provided bounds:

from nlsq.precision.bound_inference import validate_bounds

# Check if bounds are reasonable
validation_result = validate_bounds(
    bounds,
    p0,
    check_coverage=True,  # Ensure p0 is within bounds
    check_feasibility=True,  # Ensure bounds make sense
)

if not validation_result.valid:
    print(f"Warning: {validation_result.warnings}")

Inference Strategies

The module uses multiple strategies for bounds inference:

Data Range Strategy

Estimates bounds from data characteristics:

  • Lower: min(y) - margin * range(y)

  • Upper: max(y) + margin * range(y)

  • Margin: Typically 10-20% of data range

Model Type Strategy

Uses domain knowledge for common models:

  • Gaussian: amplitude ~ max(y), mean ~ median(x), std ~ range(x)/6

  • Exponential: amplitude ~ max(y), rate > 0, offset ~ min(y)

  • Sigmoid: L ~ max(y), x0 ~ median(x), k > 0

Parameter Name Strategy

Infers from parameter names:

  • “amplitude”, “height”: 0 to 2*max(y)

  • “rate”, “decay”: Positive only

  • “temperature”: Physical limits (0 to reasonable max)

  • “concentration”: Non-negative

Initialization Strategy

Based on initial parameter guess:

  • Lower: p0 / factor (default factor = 10)

  • Upper: p0 * factor

  • Sign preservation: If p0 > 0, lower = 0

Configuration

Configure bounds inference behavior:

from nlsq.precision.bound_inference import BoundsInference

inference = BoundsInference(
    default_margin=0.2,  # 20% margin around data range
    enforce_positivity=[],  # Parameter indices for positivity
    use_model_heuristics=True,  # Use model-specific knowledge
    confidence_threshold=0.8,  # Minimum confidence for inference
    fallback_to_unbounded=True,  # Use inf bounds if uncertain
)

See Also