homodyne.optimization.nlsq — Adapter

NLSQAdapter is the recommended entry point for NLSQ optimisation in Homodyne. It wraps the NLSQ package’s CurveFit class with:

  • Built-in JIT compilation (2–3× speedup for single fits)

  • Model instance caching via WeakValueDictionary (3–5× speedup for multi-start)

  • Automatic dataset-size and memory-aware workflow selection

  • Native NLSQ stability and recovery systems

  • Automatic fallback to NLSQWrapper on failure

Note

For the simplest usage, call fit_nlsq_jax() with use_adapter=True (default is False). NLSQAdapter is used internally when enabled. Drop to NLSQAdapter directly only when you need fine-grained control over adapter configuration.


When to Use NLSQAdapter vs NLSQWrapper

Feature

NLSQAdapter

NLSQWrapper

Model caching

Built-in

None

JIT compilation

Automatic

Manual

Multi-start speedup

3–5×

Anti-degeneracy layers

Via fit()

Full control

Streaming (>100 M pts)

Via NLSQ

Full custom

Recovery strategy

NLSQ native

3-attempt retry

Recommended for

Standard / multi-start

Complex / large datasets


fit_nlsq_jax

The primary entry point. Automatically selects NLSQAdapter (default) or NLSQWrapper based on configuration.

homodyne.optimization.nlsq.core.fit_nlsq_jax(data, config, initial_params=None, per_angle_scaling=True, use_adapter=False, _skip_global_selection=False)[source]

NLSQ trust-region nonlinear least squares optimization with per-angle scaling.

Uses NLSQ package (github.com/imewei/NLSQ) for trust-region optimization.

v2.11.0+: Experimental NLSQAdapter with CurveFit class available for improved JIT caching and automatic workflow selection. Set use_adapter=True to enable (default is False, uses NLSQWrapper).

Primary optimization method implementing the scaled optimization process: c₂(φ,t₁,t₂) = 1 + contrast × [c₁(φ,t₁,t₂)]²

Parameters:
  • data (dict[str, Any]) –

    XPCS experimental data. Accepts two formats:

    Format 1 (CLI/loader format): - ‘phi_angles_list’: phi angle array (mapped to ‘phi’) - ‘c2_exp’: experimental correlation data (n_phi, n_t1, n_t2) (mapped to ‘g2’) - ‘t1’: first delay time array - ‘t2’: second delay time array - ‘wavevector_q_list’: q-vector array (first element extracted as scalar ‘q’) - ‘sigma’: (optional) uncertainty array, defaults to 0.01 * ones_like(g2) - ‘L’: (optional) stator-rotor gap (rheology) or sample-detector distance (standard XPCS), defaults to config value or 2000000 Å (200 µm, typical rheology-XPCS gap) - ‘dt’: (optional) time step, defaults to config value or None

    Format 2 (Direct format): - ‘phi’: phi angle array - ‘g2’: experimental correlation data (n_phi, n_t1, n_t2) - ‘t1’: first delay time array - ‘t2’: second delay time array - ‘q’: wavevector magnitude (scalar) - ‘sigma’: (optional) uncertainty array - ‘L’: (optional) stator-rotor gap or sample-detector distance [Å] - ‘dt’: (optional) time step [s]

  • config (ConfigManager) – Configuration manager with optimization settings

  • initial_params (dict[str, float] | None) – Initial parameter guesses. If None, uses defaults from config.

  • 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 (removed Nov 2025).

  • use_adapter (bool) – EXPERIMENTAL (v2.11.0+): If True, use NLSQAdapter with NLSQ’s CurveFit class for improved JIT caching and automatic workflow selection. If False (default), use the stable NLSQWrapper implementation.

Notes

Global Optimization Selection (v2.15.0+): This function serves as the unified entry point for NLSQ optimization. When called, it first checks for global optimization methods:

  1. If cmaes.enable: true → delegates to fit_nlsq_cmaes()

  2. If multi_start.enable: true → delegates to fit_nlsq_multistart()

  3. Otherwise → runs local trust-region optimization

The CMA-ES function will fall back to multi-start (if enabled) when the scale ratio is below the threshold, implementing the full fallback chain.

Returns:

Optimization result with parameters, uncertainties, and diagnostics

Return type:

OptimizationResult

Raises:

NLSQAdapter

class homodyne.optimization.nlsq.adapter.NLSQAdapter[source]

Bases: NLSQAdapterBase

Adapter for NLSQ package using CurveFit class.

Uses NLSQ’s CurveFit for JIT caching and WorkflowSelector for automatic strategy selection. This is the modern integration path for NLSQ v0.4+ with improved performance and reliability.

Usage:

adapter = NLSQAdapter() result = adapter.fit(data, config, initial_params, bounds, analysis_mode)

Compared to NLSQWrapper:
  • Uses CurveFit class for JIT compilation caching

  • Leverages WorkflowSelector for auto strategy selection

  • Delegates recovery to NLSQ’s built-in systems

  • Simpler codebase with less custom logic

Note

Anti-degeneracy layers (hierarchical, shear_weighting, etc.) remain in homodyne as they are physics-specific to XPCS analysis.

__init__(config=None)[source]

Initialize NLSQAdapter.

Parameters:

config (AdapterConfig | None) – Adapter configuration. If None, uses defaults.

Raises:

ImportError – If NLSQ CurveFit class is not available.

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, anti_degeneracy_controller=None)[source]

Execute NLSQ optimization using CurveFit class.

This method provides the same interface as NLSQWrapper.fit() for backward compatibility while using NLSQ’s modern CurveFit class.

Parameters:
  • data (Any) – XPCS experimental data

  • config (Any) – Configuration manager with optimization settings

  • initial_params (ndarray | None) – Initial parameter guess (required)

  • 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 is physically correct)

  • diagnostics_enabled (bool) – Enable extended diagnostics

  • shear_transforms (dict[str, Any] | None) – Shear parameter transformations

  • per_angle_scaling_initial (dict[str, list[float]] | None) – Initial per-angle contrast/offset

  • anti_degeneracy_controller (Any | None) – Anti-degeneracy controller (physics-specific)

Return type:

OptimizationResult

Returns:

OptimizationResult with converged parameters and diagnostics

Raises:
  • ValueError – If bounds are invalid or per_angle_scaling=False

  • ImportError – If NLSQ CurveFit is not available

is_available()[source]

Check if NLSQ CurveFit is available.

Return type:

bool

property workflow_available: bool

Check if NLSQ WorkflowSelector is available.

AdapterConfig

class homodyne.optimization.nlsq.adapter.AdapterConfig[source]

Bases: object

Configuration for NLSQAdapter.

enable_cache

Enable model instance caching (new in v2.11.0)

enable_jit

Enable JIT compilation of model functions (new in v2.11.0)

enable_recovery

Enable NLSQ’s built-in recovery system

enable_stability

Enable NLSQ’s numerical stability guard

goal

Optimization goal (fast, robust, quality, memory_efficient)

workflow

Workflow tier override (auto, standard, streaming)

enable_cache: bool = True
enable_jit: bool = True
enable_recovery: bool = True
enable_stability: bool = True
goal: str = 'quality'
workflow: str = 'auto'
__init__(enable_cache=True, enable_jit=True, enable_recovery=True, enable_stability=True, goal='quality', workflow='auto')

Usage Examples

Basic static mode

from homodyne.optimization.nlsq import fit_nlsq_jax

# Minimal usage — NLSQAdapter is used by default
result = fit_nlsq_jax(data, config, use_adapter=True)

print(f"D0      = {result.parameters[0]:.4g}")
print(f"alpha   = {result.parameters[1]:.4f}")
print(f"D_offset= {result.parameters[2]:.4g}")
print(f"chi^2   = {result.chi_squared:.4f}")

Laminar flow with per-angle scaling

from homodyne.optimization.nlsq import fit_nlsq_jax
from homodyne.config.manager import ConfigManager

config = ConfigManager("flow_config.yaml").config

# Ensure anti-degeneracy layer is active
config["optimization"]["nlsq"]["anti_degeneracy"]["per_angle_mode"] = "auto"

result = fit_nlsq_jax(data, config, use_adapter=True)
# 9 parameters: 7 physical + 2 averaged scaling (auto mode, n_phi >= 3)

Direct NLSQAdapter usage

from homodyne.optimization.nlsq.adapter import NLSQAdapter, AdapterConfig

adapter_config = AdapterConfig(
    enable_recovery=True,
    enable_stability=True,
    fallback_to_wrapper=True,
)

adapter = NLSQAdapter(config=adapter_config)

result = adapter.fit(
    data=data,
    config=config,
    initial_params=initial_params,
    bounds=bounds,
    analysis_mode="laminar_flow",
)

Multi-start optimisation

from homodyne.optimization.nlsq import fit_nlsq_multistart
from homodyne.optimization.nlsq.multistart import MultiStartConfig

ms_config = MultiStartConfig(n_starts=8, strategy="latin_hypercube")

best_result = fit_nlsq_multistart(
    data=data,
    config=config,
    multistart_config=ms_config,
)

CMA-ES for multi-scale problems

Enable CMA-ES when \(D_0 \sim 10^4\) and \(\dot\gamma_0 \sim 10^{-3}\) (scale ratio > \(10^6\)):

optimization:
  nlsq:
    cmaes:
      enable: true
      preset: "cmaes-global"   # 200 generations
      refine_with_nlsq: true
result = fit_nlsq_jax(data, config)   # CMA-ES + NLSQ refinement

Return Type

class homodyne.optimization.nlsq.results.OptimizationResult[source]

Bases: object

Complete optimization result with fit quality metrics and diagnostics.

parameters

Converged parameter values.

Type:

np.ndarray

uncertainties

Standard deviations from covariance matrix diagonal.

Type:

np.ndarray

covariance

Full parameter covariance matrix.

Type:

np.ndarray

chi_squared

Sum of squared residuals.

Type:

float

reduced_chi_squared

chi_squared / (n_data - n_params).

Type:

float

convergence_status

‘converged’, ‘max_iter’, or ‘failed’.

Type:

str

iterations

Number of optimization iterations.

Type:

int

execution_time

Wall-clock execution time in seconds.

Type:

float

device_info

Device used for computation (CPU details).

Type:

dict[str, Any]

recovery_actions

List of error recovery actions taken.

Type:

list[str]

quality_flag

‘good’, ‘marginal’, or ‘poor’.

Type:

str

streaming_diagnostics

Enhanced diagnostics for streaming optimization.

Type:

dict[str, Any] | None

stratification_diagnostics

Diagnostics for angle-stratified chunking.

Type:

StratificationDiagnostics | None

nlsq_diagnostics

Additional NLSQ-specific diagnostics.

Type:

dict[str, Any] | None

parameters: ndarray
uncertainties: ndarray
covariance: ndarray
chi_squared: float
reduced_chi_squared: float
convergence_status: str
iterations: int
execution_time: float
device_info: dict[str, Any]
recovery_actions: list[str]
quality_flag: str = 'good'
streaming_diagnostics: dict[str, Any] | None = None
stratification_diagnostics: StratificationDiagnostics | None = None
nlsq_diagnostics: dict[str, Any] | None = None
sigma_is_default: bool = False
property success: bool

Return True if optimization converged (backward compatibility).

property message: str

Return descriptive message about optimization outcome.

__init__(parameters, uncertainties, covariance, chi_squared, reduced_chi_squared, convergence_status, iterations, execution_time, device_info, recovery_actions=<factory>, quality_flag='good', streaming_diagnostics=None, stratification_diagnostics=None, nlsq_diagnostics=None, sigma_is_default=False)

NLSQ Adapter using CurveFit class for homodyne optimization.

Role and When to Use (v2.11.0+)

NLSQAdapter (this module) is the recommended adapter 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

Use NLSQWrapper instead 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

Key Differences:

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

  • JIT compilation: NLSQAdapter=Auto, NLSQWrapper=Manual

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

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

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

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

Decision Guide:

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

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

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

  4. Default recommendation for new code: Use NLSQAdapter (via use_adapter=True)

This module provides a modern adapter layer between homodyne’s optimization API and the NLSQ package’s CurveFit class, leveraging: - CurveFit class for JIT compilation caching - Model instance caching (WeakValueDictionary) for multi-start speedup - WorkflowSelector for automatic strategy selection - Built-in stability and recovery systems - Runtime fallback to NLSQWrapper on failure

This is the recommended integration path for NLSQ v0.4+ (homodyne v2.11.0+).

Key Features: - Model caching: 3-5× speedup for multi-start optimization - JIT compilation: 2-3× speedup for single fits - Automatic workflow selection based on dataset size and memory - Native NLSQ stability and recovery systems - Integration with homodyne’s anti-degeneracy defense system - Backward-compatible interface with NLSQWrapper.fit() - Automatic fallback to NLSQWrapper when adapter fails

Migration Guide: - Replace NLSQWrapper with NLSQAdapter - Set use_adapter=True in fit_nlsq_jax() (default in v2.11.0+) - Anti-degeneracy layers work unchanged

References: - NLSQ Package: https://github.com/imewei/NLSQ - Architecture: See CLAUDE.md for NLSQ integration details

class homodyne.optimization.nlsq.adapter.ModelCacheKey[source]

Bases: object

Immutable key for model cache lookup.

Hashable tuple of (analysis_mode, phi_angles_tuple, q, per_angle_scaling). NumPy arrays converted to tuples for hashability.

analysis_mode

“static_isotropic” or “laminar_flow”

phi_angles

Unique phi angles (sorted) as tuple

q

Scattering wavevector magnitude

per_angle_scaling

Whether per-angle contrast/offset is used

analysis_mode: str
phi_angles: tuple[float, ...]
q: float
per_angle_scaling: bool
__init__(analysis_mode, phi_angles, q, per_angle_scaling)
class homodyne.optimization.nlsq.adapter.CachedModel[source]

Bases: object

Cached model instance with JIT-compiled prediction function.

Stored in dict with LRU eviction - oldest entries removed when cache is full.

model

CombinedModel instance for computing g1/g2 values

model_func

Model prediction function (NumPy-compatible wrapper)

created_at

time.time() for diagnostics

n_hits

Cache hit counter for monitoring

model: Any
model_func: Callable[[ndarray, Any], ndarray]
created_at: float
n_hits: int = 0
__init__(model, model_func, created_at=<factory>, n_hits=0)
homodyne.optimization.nlsq.adapter.get_or_create_model(analysis_mode, phi_angles, q, per_angle_scaling=True, config=None, enable_jit=True)[source]

Get cached model or create new one.

This function provides model instance caching to avoid redundant model creation during multi-start optimization. Expected 3-5× speedup.

Uses CombinedModel (not HomodyneModel) for simpler initialization. The model function closure captures the model and experimental setup.

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

  • phi_angles (ndarray) – Unique phi angles in radians

  • q (float) – Scattering wavevector magnitude

  • per_angle_scaling (bool) – Whether per-angle contrast/offset is used

  • config (dict[str, Any] | None) – Optional config dict for model initialization

  • enable_jit (bool) – Whether to JIT-compile the model function

Returns:

  • model: CombinedModel instance (cached or newly created)

  • model_func: Prediction function (JIT-compiled if enable_jit=True)

  • cache_hit: True if model was retrieved from cache

Return type:

tuple[Any, Callable[[ndarray, Any], ndarray], bool]

Raises:

ValueError – If analysis_mode is invalid, phi_angles is empty, or q <= 0

Example

>>> model, model_func, hit = get_or_create_model(
...     "laminar_flow",
...     np.array([0.0, 0.5, 1.0]),
...     0.001,
... )
>>> if hit:
...     logger.debug("Model cache hit")
homodyne.optimization.nlsq.adapter.clear_model_cache()[source]

Clear all cached models.

Return type:

int

Returns:

Number of models removed from cache

Notes

Useful for testing or when configuration changes require fresh models.

homodyne.optimization.nlsq.adapter.get_cache_stats()[source]

Get cache statistics.

Returns:

  • “hits”: Cache hit count

  • ”misses”: Cache miss count

  • ”size”: Current cache size

Return type:

dict[str, int]

homodyne.optimization.nlsq.adapter.get_adapter(config=None)[source]

Factory function to get NLSQAdapter instance.

Parameters:

config (AdapterConfig | None) – Adapter configuration

Return type:

NLSQAdapter

Returns:

NLSQAdapter instance

Raises:

ImportError – If NLSQ CurveFit is not available

homodyne.optimization.nlsq.adapter.is_adapter_available()[source]

Check if NLSQAdapter can be used.

Return type:

bool

Returns:

True if NLSQ CurveFit class is available