homodyne.optimization.nlsq — Wrapper

NLSQWrapper is the stable fallback adapter for advanced NLSQ optimisation. It is preferred over NLSQAdapter when:

  • Datasets exceed 100 M points and require custom streaming/chunking strategies

  • laminar_flow mode uses many phi angles (> 6) where full anti-degeneracy control is required

  • Custom parameter transforms or advanced 3-attempt recovery mechanisms are needed

  • Production stability is the primary concern

Note

For most new code, use fit_nlsq_jax() with use_adapter=True (default). NLSQWrapper is invoked automatically as a fallback if NLSQAdapter fails.


Key Differences from NLSQAdapter

Capability

NLSQWrapper

NLSQAdapter

Model caching

None (rebuild each fit)

WeakValueDictionary cache

JIT compilation

Manual

Automatic

Anti-degeneracy control

Full (all layers)

Via fit() delegation

Streaming (>100 M pts)

Full custom chunking

Via NLSQ package

Recovery strategy

3-attempt retry

NLSQ native

Per-angle stratification

Angle-stratified chunking

Automatic


Out-of-Core Routing

When a dataset exceeds the memory threshold (default 75% of RAM), NLSQWrapper routes to an out-of-core accumulation solver that processes data in chunks. This path has a simplified Levenberg-Marquardt implementation that lacks anti-degeneracy layers, hierarchical optimization, shear weighting, and adaptive regularization.

Important

Effective parameter count pre-check: Before memory routing, the wrapper queries the per_angle_mode and computes the effective parameter count. For auto mode with n_phi ≥ 3, this is 9 (7 physical + 2 averaged scaling) rather than the 53 expanded parameters. This prevents auto_averaged datasets from being unnecessarily routed to the out-of-core path.

Without this pre-check, memory is overestimated by ~6× (53 vs 9 params), triggering out-of-core for datasets that fit comfortably in RAM.

Convergence criteria: The out-of-core solver uses multi-criteria convergence:

  • xtol = 1e-6: per-component maximum parameter change

  • ftol = 1e-6: relative cost function change

Both must be satisfied simultaneously. This replaces the previous norm-based criterion (||step||/||params|| < 1e-4) which was scale-sensitive and produced false single-iteration convergence when large-magnitude parameters (e.g., D₀ ~ 19000) dominated the norm.


NLSQWrapper

class homodyne.optimization.nlsq.wrapper.NLSQWrapper[source]

Bases: NLSQAdapterBase

Adapter class for NLSQ package integration with homodyne optimization.

This class translates between homodyne’s optimization API and the NLSQ package’s curve_fit interface, handling: - Data format transformations - Parameter validation and bounds checking - Automatic strategy selection for large datasets - Hybrid error handling and recovery

Usage:

wrapper = NLSQWrapper(enable_large_dataset=True) result = wrapper.fit(data, config, initial_params, bounds, analysis_mode)

__init__(enable_large_dataset=True, enable_recovery=True, enable_numerical_validation=True, max_retries=2, fast_mode=False)[source]

Initialize NLSQWrapper.

Parameters:
  • enable_large_dataset (bool) – Use curve_fit_large for datasets >1M points

  • enable_recovery (bool) – Enable automatic error recovery strategies

  • enable_numerical_validation (bool) – Enable NaN/Inf validation at 3 critical points

  • max_retries (int) – Maximum retry attempts per batch (default: 2)

  • fast_mode (bool) – Disable non-essential checks for < 1% overhead (Task 5.5)

fit(data, config, initial_params=None, bounds=None, analysis_mode='static_isotropic', per_angle_scaling=True, diagnostics_enabled=False, shear_transforms=None, per_angle_scaling_initial=None)[source]

Execute NLSQ optimization with automatic strategy selection and per-angle scaling.

Parameters:
  • data (Any) – XPCS experimental data

  • config (Any) – Configuration manager with optimization settings

  • initial_params (ndarray | None) – Initial parameter guess (auto-loaded if None)

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds as (lower, upper) tuple

  • analysis_mode (str) – ‘static_isotropic’ or ‘laminar_flow’

  • per_angle_scaling (bool) – MUST be True. Per-angle contrast/offset parameters are physically correct as each scattering angle has different optical properties and detector responses. Legacy scalar mode (False) is no longer supported.

Return type:

OptimizationResult

Returns:

OptimizationResult with converged parameters and diagnostics

Raises:

ValueError – If bounds are invalid (lower > upper) or if per_angle_scaling=False


NLSQConfig

The NLSQConfig dataclass controls all NLSQ optimisation settings. It is populated from the YAML configuration file via NLSQConfig.from_yaml().

class homodyne.optimization.nlsq.config.NLSQConfig[source]

Bases: object

Configuration for NLSQ (Nonlinear Least Squares) optimization.

This dataclass consolidates NLSQ settings that were previously scattered across wrapper.py, improving maintainability and testability.

loss

Loss function for robust fitting. Options: “linear”, “soft_l1”, “huber”, “cauchy”, “arctan”. Default: “soft_l1”.

Type:

str

trust_region_scale

Scale factor for trust region. Default: 1.0.

Type:

float

max_iterations

Maximum number of optimization iterations. Default: 1000.

Type:

int

ftol

Function tolerance for convergence. Default: 1e-8.

Type:

float

xtol

Parameter tolerance for convergence. Default: 1e-8.

Type:

float

gtol

Gradient tolerance for convergence. Default: 1e-8.

Type:

float

x_scale

Parameter scaling. “jac” for Jacobian-based, list for manual. Default: “jac”.

Type:

str | list[float] | None

x_scale_map

Per-parameter scaling overrides. Default: None.

Type:

dict[str, float] | None

enable_diagnostics

Whether to compute diagnostics (Jacobian stats, etc.). Default: True.

Type:

bool

enable_streaming

Whether to enable streaming optimizer for large datasets. Default: True.

Type:

bool

streaming_chunk_size

Points per chunk for streaming optimizer. Default: 50000.

Type:

int

enable_stratified

Whether to enable stratified least squares. Default: True.

Type:

bool

target_chunk_size

Target points per chunk for stratified optimization. Default: 100000.

Type:

int

enable_recovery

Whether to enable automatic error recovery. Default: True.

Type:

bool

max_recovery_attempts

Maximum recovery attempts per strategy. Default: 3.

Type:

int

workflow: str = 'auto'
goal: str = 'quality'
loss: str = 'soft_l1'
trust_region_scale: float = 1.0
max_iterations: int = 1000
ftol: float = 1e-08
xtol: float = 1e-08
gtol: float = 1e-08
x_scale: str | list[float] | None = 'jac'
x_scale_map: dict[str, float] | None = None
enable_diagnostics: bool = True
enable_streaming: bool = True
streaming_chunk_size: int = 50000
enable_stratified: bool = True
target_chunk_size: int = 100000
enable_recovery: bool = True
max_recovery_attempts: int = 3
enable_progress_bar: bool = True
verbose: int = 1
log_iteration_interval: int = 10
enable_hybrid_streaming: bool = True
hybrid_normalize: bool = True
hybrid_normalization_strategy: str = 'auto'
hybrid_warmup_iterations: int = 200
hybrid_max_warmup_iterations: int = 500
hybrid_warmup_learning_rate: float = 0.001
hybrid_gauss_newton_max_iterations: int = 100
hybrid_gauss_newton_tol: float = 1e-08
hybrid_chunk_size: int = 10000
hybrid_trust_region_initial: float = 1.0
hybrid_regularization_factor: float = 1e-10
hybrid_enable_checkpoints: bool = True
hybrid_checkpoint_frequency: int = 100
hybrid_validate_numerics: bool = True
hybrid_enable_warm_start_detection: bool = True
hybrid_warm_start_threshold: float = 0.01
hybrid_enable_adaptive_warmup_lr: bool = True
hybrid_warmup_lr_refinement: float = 1e-06
hybrid_warmup_lr_careful: float = 1e-05
hybrid_enable_cost_guard: bool = True
hybrid_cost_increase_tolerance: float = 0.05
hybrid_enable_step_clipping: bool = True
hybrid_max_warmup_step_size: float = 0.1
enable_multi_start: bool = False
multi_start_n_starts: int = 10
multi_start_seed: int = 42
multi_start_sampling_strategy: str = 'latin_hypercube'
multi_start_n_workers: int = 0
multi_start_use_screening: bool = True
multi_start_screen_keep_fraction: float = 0.5
multi_start_refine_top_k: int = 3
multi_start_refinement_ftol: float = 1e-12
multi_start_degeneracy_threshold: float = 0.1
per_angle_mode: str = 'auto'
fourier_order: int = 2
fourier_auto_threshold: int = 6
constant_scaling_threshold: int = 3
enable_hierarchical: bool = True
hierarchical_max_outer_iterations: int = 5
hierarchical_outer_tolerance: float = 1e-06
hierarchical_physical_max_iterations: int = 100
hierarchical_per_angle_max_iterations: int = 50
regularization_mode: str = 'relative'
group_variance_lambda: float = 1.0
regularization_target_cv: float = 0.1
regularization_target_contribution: float = 0.1
regularization_max_cv: float = 0.2
regularization_auto_tune_lambda: bool = True
enable_gradient_monitoring: bool = True
gradient_ratio_threshold: float = 0.01
gradient_consecutive_triggers: int = 5
gradient_collapse_response: str = 'hierarchical'
enable_cmaes: bool = False
cmaes_preset: str = 'cmaes'
cmaes_max_generations: int | None = None
cmaes_popsize: int | None = None
cmaes_sigma: float = 0.5
cmaes_sigma_warmstart: float = 0.05
cmaes_warmstart_auto_skip: bool = True
cmaes_warmstart_skip_threshold: float = 5.0
cmaes_tol_fun: float = 1e-08
cmaes_tol_x: float = 1e-08
cmaes_restart_strategy: str = 'bipop'
cmaes_max_restarts: int = 9
cmaes_population_batch_size: int | None = None
cmaes_data_chunk_size: int | None = None
cmaes_refine_with_nlsq: bool = True
cmaes_auto_select: bool = True
cmaes_scale_threshold: float = 1000.0
cmaes_memory_limit_gb: float = 8.0
cmaes_refinement_workflow: str = 'auto'
cmaes_refinement_ftol: float = 1e-10
cmaes_refinement_xtol: float = 1e-10
cmaes_refinement_gtol: float = 1e-10
cmaes_refinement_max_nfev: int = 500
cmaes_refinement_loss: str = 'linear'
cmaes_normalize: bool = True
cmaes_normalization_epsilon: float = 1e-12
enable_quality_validation: bool = True
quality_reduced_chi_squared_threshold: float = 10.0
quality_warn_on_max_restarts: bool = True
quality_warn_on_bounds_hit: bool = True
quality_warn_on_convergence_failure: bool = True
quality_bounds_tolerance: float = 1e-09
classmethod from_dict(config_dict)[source]

Create NLSQConfig from configuration dictionary.

Parameters:

config_dict (dict[str, Any]) – NLSQ configuration dictionary from ConfigManager.

Returns:

Validated configuration object.

Return type:

NLSQConfig

classmethod from_yaml(yaml_path)[source]

Create NLSQConfig from YAML configuration file (T099).

This is the recommended single entry point for loading NLSQ configuration. It reads the YAML file, extracts the optimization.nlsq section, and creates a validated NLSQConfig object.

Parameters:

yaml_path (str) – Path to YAML configuration file.

Returns:

Validated configuration object.

Return type:

NLSQConfig

Raises:

Examples

>>> config = NLSQConfig.from_yaml("homodyne_config.yaml")
>>> print(config.loss)
soft_l1
validate()[source]

Validate configuration values.

Returns:

List of validation error messages (empty if valid).

Return type:

list[str]

is_valid()[source]

Check if configuration is valid.

Returns:

True if configuration has no validation errors.

Return type:

bool

to_dict()[source]

Convert configuration to dictionary.

Returns:

Configuration as dictionary.

Return type:

dict[str, Any]

to_workflow_kwargs()[source]

Convert settings to kwargs for NLSQ’s curve_fit().

Maps NLSQConfig settings to NLSQ 0.6.4+ curve_fit() parameters. Note: Homodyne uses curve_fit() directly, not the fit() unified API.

Returns:

Kwargs for curve_fit() (ftol, gtol, xtol, max_nfev, loss).

Return type:

dict[str, Any]

Notes

NLSQ 0.6.3+ Changes: - Simplified to 3 workflows: “auto”, “auto_global”, “hpc” - Old presets (“streaming”, “standard”) were removed - WorkflowSelector was removed; use MemoryBudgetSelector instead - Homodyne uses its own select_nlsq_strategy() for memory selection

The ‘goal’ parameter can be passed to NLSQ’s fit() API but homodyne uses curve_fit() directly, so goal is handled internally.

Example

>>> config = NLSQConfig.from_dict(yaml_config)
>>> kwargs = config.to_workflow_kwargs()
>>> result = fitter.curve_fit(f, xdata, ydata, **kwargs)
__init__(workflow='auto', goal='quality', loss='soft_l1', trust_region_scale=1.0, max_iterations=1000, ftol=1e-08, xtol=1e-08, gtol=1e-08, x_scale='jac', x_scale_map=None, enable_diagnostics=True, enable_streaming=True, streaming_chunk_size=50000, enable_stratified=True, target_chunk_size=100000, enable_recovery=True, max_recovery_attempts=3, enable_progress_bar=True, verbose=1, log_iteration_interval=10, enable_hybrid_streaming=True, hybrid_normalize=True, hybrid_normalization_strategy='auto', hybrid_warmup_iterations=200, hybrid_max_warmup_iterations=500, hybrid_warmup_learning_rate=0.001, hybrid_gauss_newton_max_iterations=100, hybrid_gauss_newton_tol=1e-08, hybrid_chunk_size=10000, hybrid_trust_region_initial=1.0, hybrid_regularization_factor=1e-10, hybrid_enable_checkpoints=True, hybrid_checkpoint_frequency=100, hybrid_validate_numerics=True, hybrid_enable_warm_start_detection=True, hybrid_warm_start_threshold=0.01, hybrid_enable_adaptive_warmup_lr=True, hybrid_warmup_lr_refinement=1e-06, hybrid_warmup_lr_careful=1e-05, hybrid_enable_cost_guard=True, hybrid_cost_increase_tolerance=0.05, hybrid_enable_step_clipping=True, hybrid_max_warmup_step_size=0.1, enable_multi_start=False, multi_start_n_starts=10, multi_start_seed=42, multi_start_sampling_strategy='latin_hypercube', multi_start_n_workers=0, multi_start_use_screening=True, multi_start_screen_keep_fraction=0.5, multi_start_refine_top_k=3, multi_start_refinement_ftol=1e-12, multi_start_degeneracy_threshold=0.1, per_angle_mode='auto', fourier_order=2, fourier_auto_threshold=6, constant_scaling_threshold=3, enable_hierarchical=True, hierarchical_max_outer_iterations=5, hierarchical_outer_tolerance=1e-06, hierarchical_physical_max_iterations=100, hierarchical_per_angle_max_iterations=50, regularization_mode='relative', group_variance_lambda=1.0, regularization_target_cv=0.1, regularization_target_contribution=0.1, regularization_max_cv=0.2, regularization_auto_tune_lambda=True, enable_gradient_monitoring=True, gradient_ratio_threshold=0.01, gradient_consecutive_triggers=5, gradient_collapse_response='hierarchical', enable_cmaes=False, cmaes_preset='cmaes', cmaes_max_generations=None, cmaes_popsize=None, cmaes_sigma=0.5, cmaes_sigma_warmstart=0.05, cmaes_warmstart_auto_skip=True, cmaes_warmstart_skip_threshold=5.0, cmaes_tol_fun=1e-08, cmaes_tol_x=1e-08, cmaes_restart_strategy='bipop', cmaes_max_restarts=9, cmaes_population_batch_size=None, cmaes_data_chunk_size=None, cmaes_refine_with_nlsq=True, cmaes_auto_select=True, cmaes_scale_threshold=1000.0, cmaes_memory_limit_gb=8.0, cmaes_refinement_workflow='auto', cmaes_refinement_ftol=1e-10, cmaes_refinement_xtol=1e-10, cmaes_refinement_gtol=1e-10, cmaes_refinement_max_nfev=500, cmaes_refinement_loss='linear', cmaes_normalize=True, cmaes_normalization_epsilon=1e-12, enable_quality_validation=True, quality_reduced_chi_squared_threshold=10.0, quality_warn_on_max_restarts=True, quality_warn_on_bounds_hit=True, quality_warn_on_convergence_failure=True, quality_bounds_tolerance=1e-09, _validation_errors=<factory>)

HybridRecoveryConfig

class homodyne.optimization.nlsq.config.HybridRecoveryConfig[source]

Bases: object

Configuration for hybrid streaming optimizer recovery strategy.

T029: Implements 3-attempt recovery with progressively conservative settings.

When the hybrid streaming optimizer fails, it retries with: - Reduced learning rate (0.5× per retry) - Increased regularization (2× per retry) - Smaller trust region (0.5× per retry)

max_retries

Maximum retry attempts. Default: 3.

Type:

int

lr_decay

Learning rate multiplier per retry. Default: 0.5.

Type:

float

lambda_growth

Regularization multiplier per retry. Default: 2.0.

Type:

float

trust_decay

Trust region multiplier per retry. Default: 0.5.

Type:

float

log_retries

Whether to log retry attempts. Default: True.

Type:

bool

max_retries: int = 3
lr_decay: float = 0.5
lambda_growth: float = 2.0
trust_decay: float = 0.5
log_retries: bool = True
get_retry_settings(attempt)[source]

Get settings for a specific retry attempt.

Parameters:

attempt (int) – Retry attempt number (1-based).

Returns:

Settings for this retry attempt.

Return type:

dict

__init__(max_retries=3, lr_decay=0.5, lambda_growth=2.0, trust_decay=0.5, log_retries=True)

Safe type conversion utilities

homodyne.optimization.nlsq.config.safe_float(value, default)[source]

Convert value to float safely, returning default on failure.

Parameters:
  • value (Any) – Value to convert to float.

  • default (float) – Default value to return if conversion fails.

Returns:

Converted float value or default.

Return type:

float

Examples

>>> safe_float("3.14", 0.0)
3.14
>>> safe_float(None, 1.0)
1.0
>>> safe_float("invalid", 2.5)
2.5
homodyne.optimization.nlsq.config.safe_int(value, default)[source]

Convert value to int safely, returning default on failure.

Parameters:
  • value (Any) – Value to convert to int.

  • default (int) – Default value to return if conversion fails.

Returns:

Converted int value or default.

Return type:

int

Examples

>>> safe_int("42", 0)
42
>>> safe_int(None, 10)
10
>>> safe_int("invalid", 5)
5

Usage Examples

Direct usage of NLSQWrapper

from homodyne.optimization.nlsq.wrapper import NLSQWrapper
from homodyne.core.fitting import ParameterSpace
from homodyne.config.manager import ConfigManager

config = ConfigManager("config.yaml").config
param_space = ParameterSpace.from_config(config)

wrapper = NLSQWrapper(config=config)
result = wrapper.fit(data=data, parameter_space=param_space)

print(result.parameters)
print(result.residuals)

Large dataset (>100 M points) with streaming

optimization:
  nlsq:
    memory_fraction: 0.75
    # Or set explicit memory threshold:
    # memory_threshold_gb: 48
from homodyne.optimization.nlsq import fit_nlsq_jax

# NLSQWrapper handles streaming automatically when memory threshold is exceeded
result = fit_nlsq_jax(data, config, use_adapter=False)

laminar_flow with many phi angles and full anti-degeneracy

from homodyne.optimization.nlsq.wrapper import NLSQWrapper

# NLSQWrapper applies all anti-degeneracy layers when n_phi > 6
wrapper = NLSQWrapper(config=config)
result = wrapper.fit(
    data=data,
    parameter_space=param_space,
    analysis_mode="laminar_flow",
    per_angle_mode="individual",   # Full per-angle scaling
)

3-attempt error recovery

NLSQWrapper retries automatically on solver failure, applying progressively conservative settings per retry:

  • Attempt 1: Original settings

  • Attempt 2: 0.5× learning rate, 2× regularisation, 0.5× trust region

  • Attempt 3: 0.25× learning rate, 4× regularisation, 0.25× trust region

from homodyne.optimization.nlsq.config import HybridRecoveryConfig

recovery = HybridRecoveryConfig(
    max_retries=3,
    lr_decay=0.5,
    lambda_growth=2.0,
    trust_decay=0.5,
)
print(recovery.get_retry_settings(attempt=2))
# {"lr_multiplier": 0.25, "lambda_multiplier": 4.0, "trust_multiplier": 0.25}

NLSQ Wrapper for Homodyne Optimization.

Role and When to Use (v2.11.0+)

NLSQWrapper (this module) is the stable fallback adapter for: - Complex optimizations requiring full anti-degeneracy integration - laminar_flow mode with many phi angles (> 6) - Large datasets (> 100M points) requiring streaming/chunking strategies - Custom transforms or advanced recovery mechanisms - Production stability where reliability is critical

Use NLSQAdapter instead for: - Standard optimizations (static_isotropic mode) - Small to medium datasets (< 10M points) - Multi-start optimization (model caching provides 3-5× speedup) - Performance-critical workflows requiring JIT compilation

Key Differences:

  • Model caching: NLSQWrapper=None, NLSQAdapter=Built-in

  • JIT compilation: NLSQWrapper=Manual, NLSQAdapter=Auto

  • Workflow auto-select: NLSQWrapper=Custom, NLSQAdapter=Via NLSQ

  • Anti-degeneracy layers: NLSQWrapper=Full, NLSQAdapter=Via fit()

  • Recovery system: NLSQWrapper=3-attempt, NLSQAdapter=NLSQ native

  • Streaming support: NLSQWrapper=Full custom, NLSQAdapter=Via NLSQ

Decision Guide:

  1. If you need robust streaming for 100M+ points: Use NLSQWrapper

  2. If you need full anti-degeneracy control: Use NLSQWrapper

  3. If you need maximum speed for multi-start optimization: Use NLSQAdapter

  4. Default recommendation: NLSQAdapter with automatic fallback to NLSQWrapper

This module provides an adapter layer between homodyne’s optimization API and the NLSQ package’s trust-region nonlinear least squares interface.

The NLSQWrapper class implements the Adapter pattern to translate: - Homodyne’s multi-dimensional XPCS data → NLSQ’s flattened array format - Homodyne’s parameter bounds tuple → NLSQ’s (lower, upper) format - NLSQ’s (popt, pcov) output → Homodyne’s OptimizationResult dataclass

Key Features: - Automatic dataset size detection and strategy selection - Angle-stratified chunking for per-angle parameter compatibility (v2.2+) - Intelligent error recovery with 3-attempt retry strategy (T022-T024) - Actionable error diagnostics with 5 error categories - CPU-optimized execution through JAX - Progress logging and convergence diagnostics - Scientifically validated (7/7 validation tests passed, T036-T041) - Serves as fallback when NLSQAdapter fails

Per-Angle Scaling Fix (v2.2): - Fixes silent optimization failures with per-angle parameters on large datasets - Applies angle-stratified chunking when: per_angle_scaling=True AND n_points>100k - Ensures every NLSQ chunk contains all phi angles → gradients always well-defined - <1% performance overhead (0.15s for 3M points) - Reference: ultra-think-20251106-012247

Production Status: - Production-ready with comprehensive error recovery - Scientifically validated (100% test pass rate) - Parameter recovery accuracy: 2-14% on core parameters - Sub-linear performance scaling with dataset size - Per-angle scaling compatible with large datasets (v2.2+)

References: - NLSQ Package: https://github.com/imewei/NLSQ - Validation: See tests/validation/test_scientific_validation.py (T036-T041) - Documentation: See CHANGELOG.md and CLAUDE.md for detailed status

homodyne.optimization.nlsq.wrapper.create_multistart_warmup_func(model_func, xdata, ydata, bounds=None, warmup_learning_rate=0.001, normalize=True, chunk_size=50_000)[source]

Create a warmup-only fit function for multi-start Phase 1 strategy.

This function creates a warmup_fit_func compatible with the multi-start optimization module Phase 1 strategy. It uses the L-BFGS warmup phase from the NLSQ AdaptiveHybridStreamingOptimizer to quickly explore the parameter space without full Gauss-Newton refinement.

Parameters:
  • model_func (Callable[..., ndarray]) – Model function with signature: func(x, *params) -> predictions

  • xdata (ndarray) – Independent variable data

  • ydata (ndarray) – Dependent variable data (observations)

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds as (lower, upper)

  • warmup_learning_rate (float) – L-BFGS line search scale for warmup phase

  • normalize (bool) – Whether to use parameter normalization (recommended for scale imbalance)

  • chunk_size (int) – Points per chunk for streaming computation

Returns:

warmup_fit_func – Function with signature: (data, initial_params, n_iterations) -> SingleStartResult Compatible with run_multistart_nlsq() warmup_fit_func parameter.

Return type:

Callable[[dict[str, Any], ndarray, int], Any]

Raises:

RuntimeError – If AdaptiveHybridStreamingOptimizer is not available (NLSQ < 0.3.2)

Examples

>>> from homodyne.optimization.nlsq.wrapper import create_multistart_warmup_func
>>> from homodyne.optimization.nlsq.multistart import run_multistart_nlsq
>>>
>>> # Create warmup function
>>> warmup_func = create_multistart_warmup_func(
...     model_func=my_model,
...     xdata=x_data,
...     ydata=y_data,
...     bounds=(lower, upper),
... )
>>>
>>> # Use with multi-start
>>> result = run_multistart_nlsq(
...     data=my_data,
...     bounds=bounds,
...     config=config,
...     single_fit_func=full_fit_func,
...     warmup_fit_func=warmup_func,  # For Phase 1 strategy
... )

Notes

This function integrates with the Phase 1 multi-start strategy which: 1. Runs parallel L-BFGS warmup from multiple starting points 2. Selects the best warmup result 3. Performs full Gauss-Newton refinement from the best starting point

This approach is memory-efficient for very large datasets (>100M points) and provides good exploration of the parameter space.

See also

homodyne.optimization.nlsq.multistart.run_multistart_nlsq

Main multi-start function

homodyne.optimization.nlsq.multistart._run_phase1_strategy

Phase 1 strategy implementation


Extracted Strategy Modules

The following modules were extracted from wrapper.py to improve maintainability. NLSQWrapper delegates to these internally; they are not part of the public API.

Fallback Chain

Strategy selection and fallback logic when the primary solver fails.

Fallback chain logic for NLSQ optimization strategy selection.

Extracted from wrapper.py to reduce file size and improve maintainability.

This module provides: - OptimizationStrategy enum for strategy selection - Strategy info retrieval for logging/diagnostics - NLSQ result normalization across different return formats - Fallback strategy chain (STREAMING -> CHUNKED -> LARGE -> STANDARD) - Optimization execution with automatic fallback

class homodyne.optimization.nlsq.fallback_chain.OptimizationStrategy[source]

Bases: Enum

Local optimization strategy enum for internal use.

Note: This replaces the deprecated selection.py OptimizationStrategy. For new code, use NLSQStrategy from memory.py instead.

STANDARD = 'standard'
LARGE = 'large'
CHUNKED = 'chunked'
STREAMING = 'streaming'
homodyne.optimization.nlsq.fallback_chain.get_fallback_strategy(current_strategy)[source]

Get fallback strategy when current strategy fails.

Implements degradation chain: STREAMING -> CHUNKED -> LARGE -> STANDARD -> None

Parameters:

current_strategy (OptimizationStrategy) – Strategy that failed

Return type:

OptimizationStrategy | None

Returns:

Next strategy to try, or None if no fallback available

homodyne.optimization.nlsq.fallback_chain.handle_nlsq_result(result, strategy)[source]

Normalize NLSQ return values to consistent format.

NLSQ v0.1.5 has inconsistent return types across different functions: - curve_fit: Returns tuple (popt, pcov) OR CurveFitResult object - curve_fit_large: Returns tuple (popt, pcov) OR OptimizeResult object - StreamingOptimizer.fit: Returns dict with ‘x’, ‘pcov’, ‘streaming_diagnostics’

This function normalizes all these formats to a consistent tuple: (popt, pcov, info)

Parameters:
  • result (Any) – Return value from NLSQ optimization call

  • strategy (OptimizationStrategy) – Optimization strategy used (for logging/diagnostics)

Returns:

(popt, pcov, info) where:
  • popt: np.ndarray of optimized parameters

  • pcov: np.ndarray covariance matrix

  • info: dict with additional information (empty if not available)

Return type:

tuple[ndarray, ndarray, dict]

Raises:

TypeError – If result format is unrecognized

homodyne.optimization.nlsq.fallback_chain.execute_optimization_with_fallback(strategy, wrapped_residual_fn, xdata, ydata, validated_params, nlsq_bounds, loss_name, x_scale_value, config, start_time, log, enable_recovery, execute_with_recovery_fn, fit_with_hybrid_streaming_fn, streaming_available, curve_fit_fn, curve_fit_large_fn, fast_mode=False)[source]

Execute optimization with strategy fallback.

Tries selected strategy first, then falls back to simpler strategies if needed. Returns (popt, pcov, info, recovery_actions, convergence_status).

Return type:

tuple[ndarray, ndarray | None, dict[str, Any], list[str], str]

Error Recovery

3-attempt error recovery with progressive conservative settings and 5-category error diagnosis.

Error recovery and diagnostics for NLSQ optimization.

Extracted from wrapper.py to reduce file size and improve maintainability.

This module provides: - Safe uncertainty extraction from covariance matrices - Automatic error recovery with retry strategies (T022-T024) - Error diagnosis with actionable recovery guidance

homodyne.optimization.nlsq.recovery.safe_uncertainties_from_pcov(pcov, n_params)[source]

Extract uncertainties with diagonal regularization for singular pcov.

Return type:

ndarray

homodyne.optimization.nlsq.recovery.execute_with_recovery(residual_fn, xdata, ydata, initial_params, bounds, strategy, log, loss_name, x_scale_value, handle_nlsq_result_fn, curve_fit_fn, curve_fit_large_fn)[source]

Execute optimization with automatic error recovery (T022-T024).

Implements intelligent retry strategies: - Attempt 1: Original parameters with selected strategy - Attempt 2: Perturbed parameters (+/-10%) - Attempt 3: Relaxed convergence tolerance - Final failure: Comprehensive diagnostics

Parameters:
  • residual_fn (Callable[[ndarray], ndarray]) – Residual function

  • xdata (ndarray) – Data arrays

  • ydata (ndarray) – Data arrays

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds tuple

  • strategy (OptimizationStrategy) – Optimization strategy to use

  • log (Logger | LoggerAdapter[Logger]) – Logger instance

  • loss_name (str) – Loss function name

  • x_scale_value (float | str | ndarray) – Scaling value for parameters

  • handle_nlsq_result_fn (Callable) – Function to normalize NLSQ results

  • curve_fit_fn (Callable) – Standard curve_fit function

  • curve_fit_large_fn (Callable) – Large dataset curve_fit function

Return type:

tuple[ndarray, ndarray, dict, list[str], str]

Returns:

(popt, pcov, info, recovery_actions, convergence_status)

homodyne.optimization.nlsq.recovery.diagnose_error(error, params, bounds, attempt)[source]

Diagnose optimization error and provide actionable recovery strategy (T023).

Parameters:
  • error (Exception) – Exception raised during optimization

  • params (ndarray) – Current parameter values

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds

  • attempt (int) – Current attempt number (0-indexed)

Return type:

dict[str, Any]

Returns:

Diagnostic dictionary with error analysis and recovery strategy

Stratified Least Squares

Primary fitting strategy using stratified JTJ accumulation via NLSQ least_squares().

Stratified least-squares strategy for NLSQ optimization.

Extracted from wrapper.py to reduce file size and improve maintainability.

This module provides: - Stratified chunk creation from angle-stratified data - Stratified least-squares fitting with anti-degeneracy support - Supports fixed_constant, auto_averaged, individual, and fourier modes

homodyne.optimization.nlsq.strategies.stratified_ls.create_stratified_chunks(stratified_data, target_chunk_size=100_000)[source]

Convert stratified flat arrays into chunks for StratifiedResidualFunction.

Parameters:
  • stratified_data (Any) – StratifiedData object with flat stratified arrays

  • target_chunk_size (int) – Target size for each chunk

Return type:

Any

Returns:

Object with .chunks attribute containing list of chunk objects

homodyne.optimization.nlsq.strategies.stratified_ls.fit_with_stratified_least_squares(stratified_data, per_angle_scaling, physical_param_names, initial_params, bounds, log, target_chunk_size=100_000, anti_degeneracy_config=None, nlsq_config_dict=None)[source]

Fit using NLSQ’s least_squares() with stratified residual function.

This method solves the double-chunking problem by using NLSQ’s least_squares() function directly with a StratifiedResidualFunction. This gives us full control over chunking, ensuring angle completeness in each chunk for proper per-angle parameter gradients.

Parameters:
  • stratified_data (Any) – StratifiedData object with flat stratified arrays

  • per_angle_scaling (bool) – Whether per-angle parameters are enabled

  • physical_param_names (list[str]) – List of physical parameter names

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds (lower, upper) tuple

  • log (Logger | LoggerAdapter[Logger]) – Logger instance

  • target_chunk_size (int) – Target size for each chunk (default: 100k points)

  • anti_degeneracy_config (dict | None) – Optional config dict for Anti-Degeneracy Defense System

  • nlsq_config_dict (dict | None) – Optional NLSQ convergence config dict

Return type:

tuple[ndarray, ndarray, dict]

Returns:

(popt, pcov, info) tuple

Hybrid Streaming

4-phase adaptive hybrid streaming optimizer for large datasets.

Hybrid streaming optimization strategy for NLSQ optimization.

Extracted from wrapper.py to reduce file size and improve maintainability.

This module provides: - Hybrid streaming optimizer (L-BFGS warmup + Gauss-Newton refinement) - Stratified hybrid streaming with anti-degeneracy defense - Memory estimation and streaming decision logic - Deprecated streaming optimizer stubs

homodyne.optimization.nlsq.strategies.hybrid_streaming.fit_with_streaming_optimizer_deprecated(residual_fn, xdata, ydata, initial_params, bounds, logger, checkpoint_config=None)[source]

Fit using streaming optimizer for large datasets.

Deprecated since version 2.9.1: The old non-stratified StreamingOptimizer was removed in NLSQ 0.4.0. Use the stratified optimization path with per-angle scaling instead, which automatically uses AdaptiveHybridStreamingOptimizer for large datasets.

Raises:

RuntimeError – Always - this code path is no longer supported

Return type:

tuple[ndarray, ndarray, dict]

homodyne.optimization.nlsq.strategies.hybrid_streaming.fit_with_hybrid_streaming_optimizer(residual_fn, xdata, ydata, initial_params, bounds, logger, nlsq_config=None, fast_mode=False)[source]

Fit using NLSQ AdaptiveHybridStreamingOptimizer for large datasets.

This method uses NLSQ’s four-phase hybrid optimizer to fix three key issues: 1. Shear-term weak gradients (scale imbalance) - via parameter normalization 2. Slow convergence - via L-BFGS warmup + Gauss-Newton refinement 3. Crude covariance - via exact J^T J accumulation + covariance transform

Four Phases: - Phase 0: Parameter normalization setup (bounds-based) - Phase 1: L-BFGS warmup with adaptive switching - Phase 2: Streaming Gauss-Newton with exact J^T J accumulation - Phase 3: Denormalization and covariance transform

Parameters:
  • residual_fn (Any) – Residual function (StratifiedResidualFunction or similar)

  • xdata (ndarray) – Independent variable data (flattened)

  • ydata (ndarray) – Dependent variable data (flattened)

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds (lower, upper)

  • logger (Any) – Logger instance

  • nlsq_config (Any) – NLSQ configuration with hybrid streaming settings

Return type:

tuple[ndarray, ndarray, dict]

Returns:

  • popt (np.ndarray) – Optimized parameters

  • pcov (np.ndarray) – Covariance matrix (properly transformed to original space)

  • info (dict) – Optimization information including phase diagnostics

Raises:
  • RuntimeError – If AdaptiveHybridStreamingOptimizer is not available

  • NLSQOptimizationError – If optimization fails

homodyne.optimization.nlsq.strategies.hybrid_streaming.fit_with_streaming_optimizer_stratified_deprecated(stratified_data, per_angle_scaling, physical_param_names, initial_params, bounds, logger, streaming_config=None)[source]

Fit using NLSQ streaming optimizer for memory-constrained large datasets.

Deprecated since version 2.9.1: The old StreamingOptimizer was removed in NLSQ 0.4.0. This method now delegates to _fit_with_stratified_hybrid_streaming which uses AdaptiveHybridStreamingOptimizer.

Parameters:
  • stratified_data (Any) – StratifiedData object with flat stratified arrays

  • per_angle_scaling (bool) – Whether per-angle parameters are enabled

  • physical_param_names (list[str]) – List of physical parameter names

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds (lower, upper) tuple

  • logger (Any) – Logger instance

  • streaming_config (dict | None) – Optional config dict (converted to hybrid_config)

Return type:

tuple[ndarray, ndarray, dict]

Returns:

(popt, pcov, info) tuple

Raises:

RuntimeError – If AdaptiveHybridStreamingOptimizer is not available

homodyne.optimization.nlsq.strategies.hybrid_streaming.fit_with_stratified_hybrid_streaming(stratified_data, per_angle_scaling, physical_param_names, initial_params, bounds, logger, hybrid_config=None, anti_degeneracy_config=None)[source]

Fit using NLSQ AdaptiveHybridStreamingOptimizer for large datasets.

This method implements the 4-phase hybrid optimization from NLSQ >=0.3.2: - Phase 0: Parameter normalization setup (bounds-based) - Phase 1: L-BFGS warmup with adaptive switching - Phase 2: Streaming Gauss-Newton with exact J^T J accumulation - Phase 3: Denormalization and covariance transform

With Anti-Degeneracy Defense System v2.9.0 integration: - Layer 1: Fourier Reparameterization (reduces per-angle DoF) - Layer 2: Hierarchical Optimization (alternating stage fitting) - Layer 3: Adaptive CV-based Regularization (scales properly) - Layer 4: Gradient Collapse Detection (runtime monitoring)

Key improvements over basic StreamingOptimizer: 1. Shear-term weak gradients: Fixed via parameter normalization 2. Slow convergence: Fixed via L-BFGS warmup + Gauss-Newton refinement 3. Crude covariance: Fixed via exact J^T J accumulation 4. Structural degeneracy: Fixed via anti-degeneracy defense layers

Parameters:
  • stratified_data (Any) – StratifiedData object with flat stratified arrays

  • per_angle_scaling (bool) – Whether per-angle parameters are enabled

  • physical_param_names (list[str]) – List of physical parameter names

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds (lower, upper) tuple

  • logger (Any) – Logger instance

  • hybrid_config (dict | None) – Optional config dict with keys: - normalize: Enable parameter normalization (default: True) - normalization_strategy: “bounds” or “scale” (default: “bounds”) - warmup_iterations: L-BFGS warmup iterations (default: 100) - max_warmup_iterations: Max L-BFGS iterations (default: 500) - warmup_learning_rate: L-BFGS line search scale (default: 0.001) - gauss_newton_max_iterations: GN iterations (default: 50) - gauss_newton_tol: Convergence tolerance (default: 1e-8) - chunk_size: Points per chunk for streaming (default: 50000)

  • anti_degeneracy_config (dict | None) – Optional config dict for Anti-Degeneracy Defense: - per_angle_mode: “independent”, “fourier”, or “auto” (default: “auto”) - fourier_order: Fourier harmonic order (default: 2) - fourier_auto_threshold: n_phi threshold for auto mode (default: 6) - hierarchical.enable: Enable hierarchical optimization (default: True) - regularization.mode: “absolute”, “relative”, or “auto” (default: “relative”) - regularization.lambda: Base regularization strength (default: 1.0) - gradient_monitoring.enable: Enable gradient collapse detection (default: True)

Return type:

tuple[ndarray, ndarray, dict]

Returns:

(popt, pcov, info) tuple

Raises:

RuntimeError – If AdaptiveHybridStreamingOptimizer is not available

homodyne.optimization.nlsq.strategies.hybrid_streaming.estimate_memory_for_stratified_ls(n_points, n_params, n_chunks)[source]

Estimate peak memory usage for stratified least-squares optimization.

The main memory consumers are: 1. Padded arrays: n_chunks × max_chunk_size × 5 arrays × 8 bytes 2. Dense Jacobian: n_points × n_params × 8 bytes 3. JAX autodiff intermediates: ~3× Jacobian size for backprop 4. JAX compilation cache: ~5-10 GB

Parameters:
  • n_points (int) – Total number of data points

  • n_params (int) – Number of parameters

  • n_chunks (int) – Number of stratified chunks

Return type:

float

Returns:

Estimated peak memory in bytes

homodyne.optimization.nlsq.strategies.hybrid_streaming.should_use_streaming(n_points, n_params, n_chunks, memory_threshold_gb=None, memory_fraction=None)[source]

Determine if streaming optimizer should be used based on memory estimate.

Uses adaptive memory thresholding (v2.7.0+) to automatically compute an appropriate threshold based on total system memory.

Parameters:
  • n_points (int) – Total number of data points

  • n_params (int) – Number of parameters

  • n_chunks (int) – Number of stratified chunks

  • memory_threshold_gb (float | None) – Memory threshold in GB above which to use streaming. If None (default), computes adaptive threshold as 75% of total memory.

  • memory_fraction (float | None) – Fraction of total memory for adaptive threshold (0.1-0.9). Only used if memory_threshold_gb is None.

Return type:

tuple[bool, float, str]

Returns:

(use_streaming, estimated_gb, reason) tuple

Out-of-Core Accumulation

Global JTJ accumulation solver for datasets exceeding the memory threshold.

Out-of-Core Global Accumulation strategy for NLSQ optimization.

Extracted from wrapper.py to reduce file size and improve maintainability.

This module provides: - Out-of-core J^T J / J^T r accumulation for massive datasets - Levenberg-Marquardt iteration with chunk-wise gradient accumulation - Parallel chunk computation with shared memory pools

homodyne.optimization.nlsq.strategies.out_of_core.fit_with_out_of_core_accumulation(stratified_data, data, per_angle_scaling, physical_param_names, initial_params, bounds, log, config, fast_chi2_mode=False, anti_degeneracy_config=None)[source]

Fit using Out-of-Core Global Accumulation for massive datasets.

This strategy virtually chunks the dataset using Index-Based Stratification, accumulates the full Hessian and Gradient (J^T J, J^T r) by iterating over chunks, and takes a global Levenberg-Marquardt step.

Guarantees identical convergence to standard NLSQ but with minimal memory.

Note (v2.14.1+):

This method now uses FULL homodyne physics via compute_g2_scaled(), identical to stratified least-squares. Anti-Degeneracy Defense System support is planned for a future release.

Parameters:
  • stratified_data (Any) – Stratified data object (unused, kept for API compat)

  • data (Any) – Original XPCS data object with .phi, .t1, .t2, .g2, .q, .L

  • per_angle_scaling (bool) – Whether per-angle scaling is enabled

  • physical_param_names (list[str]) – Names of physical parameters

  • initial_params (ndarray) – Initial parameter guess

  • bounds (tuple[ndarray, ndarray] | None) – Parameter bounds (lower, upper) or None

  • log (Logger | LoggerAdapter[Logger]) – Logger instance

  • config (Any) – Configuration object or dict

  • fast_chi2_mode (bool) – If True, subsample chunks for chi2 evaluation

  • anti_degeneracy_config (dict | None) – Anti-degeneracy configuration (reserved)

Return type:

tuple[ndarray, ndarray, dict]

Returns:

(popt, pcov, info) tuple