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:
objectInfer 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:
- y_min, y_max
Range of dependent variable
- Type:
- x_range, y_range
Span of data
- Type:
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.
- 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:
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:
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_boundsStandard bound inference with smaller safety_factor
merge_boundsMerge 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:
- Returns:
lower_bounds (ndarray) – Merged lower bounds
upper_bounds (ndarray) – Merged upper bounds
- Return type:
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¶
|
Infer reasonable parameter bounds from data characteristics. |
Functions¶
|
Infer reasonable parameter bounds from data and initial guess. |
|
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¶
Configuration Reference : Configuration reference
nlsq.minpack module : Main curve fitting API
nlsq.validators module : Input validation