NLSQ Optimization¶
Non-linear least squares fitting with JAX JIT-compiled residuals, multiple solver strategies, Fourier reparameterization for multi-angle fits, CMA-ES global optimization, and multi-start support.
Core Fitting¶
Core NLSQ fitting for heterodyne analysis.
Unified entry point for NLSQ optimization with: - Global optimization selection (CMA-ES → multi-start → local) - Adapter/wrapper fallback with automatic recovery - Memory-aware strategy selection - Per-angle and multi-angle fitting
- heterodyne.optimization.nlsq.core.fit_nlsq_jax(model, c2_data, phi_angle=0.0, config=None, weights=None, use_nlsq_library=True, *, _skip_global_selection=False)[source]
Fit heterodyne model to correlation data using NLSQ.
This is the unified entry point for all NLSQ optimization. When called it first checks for global optimization methods:
If
cmaes.enable: true→ delegates to CMA-ESIf
multi_start.enable: true→ delegates to multi-startOtherwise → runs local trust-region optimization
The adapter is tried first; on failure the wrapper provides automatic retry with progressive recovery (HybridRecoveryConfig).
- Parameters:
model (
HeterodyneModel) – HeterodyneModel instance with parameters configured.c2_data (
ndarray|Array) – Experimental correlation data, shape (N, N).phi_angle (
float) – Detector phi angle (degrees).config (
NLSQConfig|None) – NLSQ configuration (default if None).weights (
ndarray|Array|None) – Optional weights (1/sigma²) for weighted least squares.use_nlsq_library (
bool) – Whether to prefer nlsq library over scipy._skip_global_selection (
bool) – Internal flag — skip CMA-ES / multi-start check.
- Return type:
NLSQResult- Returns:
NLSQResult with fitted parameters and diagnostics.
- heterodyne.optimization.nlsq.core.fit_nlsq_multi_phi(model, c2_data, phi_angles, config=None, weights=None)[source]
Fit model to correlation data at multiple phi angles.
Two modes of operation controlled by
config.per_angle_mode:Joint fit (
"fourier","independent", or"auto"with multiple angles) – All angles are fit simultaneously in a single optimization. In"fourier"mode, the optimizer vector is[physics_varying | fourier_contrast_coeffs | fourier_offset_coeffs], where the Fourier basis constrains smooth angular variation. In"independent"mode, each angle has its own contrast/offset (2*n_phiscaling parameters), all optimized jointly.Sequential mode (single angle or fallback) – Angles are fit one at a time with warm-starting.
- Parameters:
model (
HeterodyneModel) – HeterodyneModel instance.c2_data (
ndarray) – Correlation data, shape(n_phi, N, N)or(N, N).phi_angles (
list[float] |ndarray) – Array of phi angles (degrees).config (
NLSQConfig|None) – NLSQ configuration.weights (
ndarray|None) – Optional weights, shape(n_phi, N, N)or(N, N).
- Return type:
list[NLSQResult]- Returns:
List of
NLSQResult, one per angle.
- heterodyne.optimization.nlsq.core.fit_nlsq_cmaes(model, c2_data, phi_angle=0.0, config=None, weights=None)[source]
CMA-ES global optimization for multi-scale parameter problems.
Public entry point that delegates to the internal
_fit_cmaesimplementation. RaisesImportErrorif CMA-ES is not available andValueErrorif CMA-ES is not enabled in config.- Parameters:
model (
HeterodyneModel) – HeterodyneModel instance with parameters configured.c2_data (
ndarray|Array) – Experimental correlation data, shape (N, N).phi_angle (
float) – Detector phi angle (degrees).config (
NLSQConfig|None) – NLSQ configuration. Defaults toNLSQConfig().weights (
ndarray|Array|None) – Optional weights (1/σ²) for weighted least squares.
- Return type:
NLSQResult- Returns:
NLSQResult with fitted parameters and diagnostics.
- Raises:
ImportError – If CMA-ES (
cmapackage) is not available.ValueError – If CMA-ES is not enabled in config.
Examples
>>> config = NLSQConfig(enable_cmaes=True) >>> result = fit_nlsq_cmaes(model, c2_data, config=config) >>> print(f"Chi2: {result.reduced_chi_squared:.4f}")
- heterodyne.optimization.nlsq.core.fit_nlsq_multistart(model, c2_data, phi_angle=0.0, config=None, weights=None, use_nlsq_library=True)[source]
Multi-start NLSQ optimization with Latin Hypercube Sampling.
Public entry point that delegates to the internal
_fit_multistartimplementation. Explores the parameter space to avoid local minima. FULL strategy is always used — no subsampling.- Parameters:
model (
HeterodyneModel) – HeterodyneModel instance with parameters configured.c2_data (
ndarray|Array) – Experimental correlation data, shape (N, N).phi_angle (
float) – Detector phi angle (degrees).config (
NLSQConfig|None) – NLSQ configuration. Defaults toNLSQConfig().weights (
ndarray|Array|None) – Optional weights (1/σ²) for weighted least squares.use_nlsq_library (
bool) – Whether to prefer nlsq library over scipy.
- Return type:
NLSQResult- Returns:
NLSQResult with best parameters across all starts.
- Raises:
ImportError – If the multi-start module is not available.
ValueError – If multi-start is not enabled in config.
Examples
>>> config = NLSQConfig(multistart=True, multistart_n=20) >>> result = fit_nlsq_multistart(model, c2_data, config=config) >>> print(f"Best chi2: {result.reduced_chi_squared:.4f}")
Configuration¶
Configuration for NLSQ optimization in the heterodyne analysis pipeline.
This module defines the full configuration hierarchy for non-linear least squares fitting of heterodyne XPCS correlation functions:
HybridRecoveryConfig— progressive retry / fallback parametersNLSQValidationConfig— post-fit validation thresholdsNLSQConfig— master configuration dataclass
- heterodyne.optimization.nlsq.config.safe_float(value, default)[source]
Convert value to float, returning default on failure.
- heterodyne.optimization.nlsq.config.safe_int(value, default)[source]
Convert value to int, returning default on failure.
- class heterodyne.optimization.nlsq.config.HybridRecoveryConfig[source]
Bases:
objectProgressive retry / fallback parameters for NLSQ recovery.
When a fit fails to converge, the optimizer retries with progressively more aggressive regularisation and a smaller trust region. Each attempt k (0-based) applies the following scaling to the baseline settings:
learning rate :
lr_decay ** kregularisation :
lambda_growth ** ktrust radius :
trust_decay ** k
- max_retries
Maximum number of recovery attempts before giving up.
- lr_decay
Multiplicative factor applied to the learning rate per retry (< 1 shrinks the effective step size).
- lambda_growth
Multiplicative factor applied to the regularisation strength per retry (> 1 increases damping).
- trust_decay
Multiplicative factor applied to the trust-region radius per retry (< 1 tightens the constraint).
- perturb_scale
Standard deviation of the Gaussian perturbation added to the starting parameters before each retry, expressed as a fraction of the parameter range.
- max_retries: int = 3
- lr_decay: float = 0.5
- lambda_growth: float = 10.0
- trust_decay: float = 0.5
- perturb_scale: float = 0.1
- log_retries: bool = True
- get_retry_settings(attempt)[source]
Return scaled optimiser settings for a given retry attempt.
- Parameters:
attempt (
int) – 0-based retry index.attempt=0returns the unscaled baseline (scale factor = 1).- Return type:
- Returns:
Dictionary with keys
"lr_scale","lambda_scale", and"trust_radius_scale", each a multiplicative factor to apply to the corresponding optimiser hyperparameter.
- __init__(max_retries=3, lr_decay=0.5, lambda_growth=10.0, trust_decay=0.5, perturb_scale=0.1, log_retries=True)
- class heterodyne.optimization.nlsq.config.NLSQValidationConfig[source]
Bases:
objectThresholds used when validating post-fit quality metrics.
- chi2_warn_low
Reduced chi-squared below this value triggers a warning (possible over-fitting or under-estimated errors).
- chi2_warn_high
Reduced chi-squared above this value triggers a warning (possible under-fitting or under-estimated model).
- chi2_fail_high
Reduced chi-squared above this value is treated as a hard failure.
- max_relative_uncertainty
Maximum acceptable relative uncertainty (sigma /
|param|) for any fitted parameter. Value of 1.0 means 100 %.
- correlation_warn
Off-diagonal correlation coefficient magnitude above this threshold triggers a collinearity warning.
- chi2_warn_low: float = 0.5
- chi2_warn_high: float = 2.0
- chi2_fail_high: float = 10.0
- max_relative_uncertainty: float = 1.0
- correlation_warn: float = 0.95
- __init__(chi2_warn_low=0.5, chi2_warn_high=2.0, chi2_fail_high=10.0, max_relative_uncertainty=1.0, correlation_warn=0.95)
- class heterodyne.optimization.nlsq.config.NLSQConfig[source]
Bases:
objectMaster configuration for NLSQ fitting of heterodyne XPCS data.
The heterodyne model has 14 parameters organised into two-component (signal + background) correlation functions. This configuration covers the full pipeline: solver hyperparameters, multi-start, streaming / chunking, recovery on failure, and post-fit diagnostics.
- max_iterations
Maximum number of optimiser iterations per fit.
- tolerance
Convergence tolerance for the cost function.
- method
Trust-region algorithm variant passed to the
nlsq CurveFit optimizer. Note thatdogboxis coerced totrfby the strategy layer.
- multistart
Whether to run multi-start optimisation to avoid local minima.
- multistart_n
Number of random starting points when multistart is enabled.
- verbose
Verbosity level forwarded to the solver (0 = silent, 1 = summary, 2 = detailed).
- use_jac
Whether to supply an analytic Jacobian to the solver.
- x_scale
Parameter scaling strategy.
"jac"uses the Jacobian diagonal; a list of floats provides explicit per-parameter scales.
- ftol
Relative tolerance on the cost function change.
- xtol
Relative tolerance on the parameter step norm.
- gtol
Absolute tolerance on the projected gradient norm.
- loss
Robust loss function kernel.
- diff_step
Finite-difference step size.
Noneselects the solver default.
- max_nfev
Hard cap on function evaluations.
Noneis unlimited.
- chunk_size
Number of q-points per processing chunk.
Nonemeans auto-select based on available memory.
- workflow
High-level workflow preset. One of
"auto","auto_global","hpc".
- goal
Optimisation goal preset controlling the balance between speed, robustness, and solution quality. One of
"fast","robust","quality","memory_efficient".
- enable_streaming
Process data in a streaming fashion (chunk-by-chunk) rather than loading all q-points at once.
- streaming_chunk_size
Number of q-points per streaming chunk when enable_streaming is
True.
- enable_stratified
Use stratified sampling across q-point subsets.
- target_chunk_size
Target number of data points per stratified chunk.
- enable_recovery
Automatically retry failed fits with more aggressive regularisation (see recovery_config).
- max_recovery_attempts
Maximum retries before a fit is declared failed.
- recovery_config
Per-retry scaling parameters.
- enable_diagnostics
Emit structured convergence / quality diagnostics after each fit.
- enable_anti_degeneracy
Apply anti-degeneracy constraints to prevent parameter collapse (e.g. two identical relaxation modes).
- x_scale_map
Per-parameter scale overrides keyed by parameter name. Entries here are merged into (and override) the default Jacobian-based scaling.
- loss_weights
Per-data-point loss weights.
Noneuses uniform weighting.
- loss_scale
Global scale factor applied to the loss function value before passing to the solver.
- tr_solver
Trust-region sub-problem solver override (
"exact","lsmr", orNonefor solver default).
- step_bound
Upper bound on the step norm relative to the trust radius.
0.0defers to the solver default.
- use_nlsq_library
Prefer the
nlsqlibrary over the scipy fallback.
- n_params
Number of model parameters. Fixed at 14 for heterodyne.
- analysis_mode
Which physical model variant to use. One of
"static_ref"(reference beam treated as static background),"static_both"(both beams treated as static),"two_component"(full two-component model, default).
- validation
Post-fit validation thresholds.
- max_iterations: int = 1000
- tolerance: float = 1e-08
- method: Literal['trf', 'lm', 'dogbox'] = 'trf'
- multistart: bool = False
- multistart_n: int = 10
- verbose: int = 1
- use_jac: bool = True
- ftol: float = 1e-08
- xtol: float = 1e-08
- gtol: float = 1e-08
- loss: Literal['linear', 'soft_l1', 'huber', 'cauchy', 'arctan'] = 'soft_l1'
- trust_region_scale: float = 1.0
- workflow: str = 'auto'
- goal: str = 'robust'
- enable_streaming: bool = False
- streaming_chunk_size: int = 50000
- enable_stratified: bool = False
- target_chunk_size: int = 10000
- enable_recovery: bool = True
- max_recovery_attempts: int = 3
- recovery_config: HybridRecoveryConfig
- enable_diagnostics: bool = True
- enable_anti_degeneracy: bool = True
- loss_scale: float = 1.0
- step_bound: float = 0.0
- per_angle_mode: Literal['individual', 'constant', 'fourier', 'auto', 'independent'] = 'auto'
- fourier_order: int = 2
- fourier_auto_threshold: int = 6
- enable_hierarchical: bool = False
- hierarchical_max_outer_iterations: int = 20
- hierarchical_inner_tolerance: float = 1e-06
- hierarchical_outer_tolerance: float = 0.0001
- hierarchical_physical_max_iterations: int = 100
- hierarchical_per_angle_max_iterations: int = 50
- regularization_mode: Literal['none', 'tikhonov', 'adaptive'] = 'none'
- group_variance_lambda: float = 0.01
- regularization_target_cv: float = 0.5
- regularization_target_contribution: float = 0.1
- regularization_max_cv: float = 0.2
- regularization_auto_tune_lambda: bool = True
- enable_gradient_monitoring: bool = False
- gradient_ratio_threshold: float = 100.0
- gradient_consecutive_triggers: int = 3
- gradient_collapse_response: str = 'hierarchical'
- enable_cmaes: bool = False
- cmaes_sigma0: float = 0.3
- cmaes_max_iterations: int = 1000
- cmaes_tolx: float = 1e-06
- cmaes_tolfun: float = 1e-08
- cmaes_diagonal_filtering: str = 'remove'
- cmaes_anti_degeneracy: bool = False
- cmaes_warmstart_auto_skip: bool = True
- cmaes_warmstart_skip_threshold: float = 5.0
- cmaes_restart_strategy: str = 'bipop'
- cmaes_max_restarts: int = 9
- cmaes_preset: str = 'cmaes'
- cmaes_sigma: float = 0.5
- cmaes_sigma_warmstart: float = 0.05
- cmaes_tol_fun: float = 1e-08
- cmaes_tol_x: float = 1e-08
- 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
- enable_progress_bar: bool = True
- log_iteration_interval: int = 10
- hybrid_enable: bool = False
- hybrid_warmup_fraction: float = 0.1
- hybrid_normalization: bool = True
- hybrid_method: Literal['lbfgs', 'gauss_newton'] = 'gauss_newton'
- hybrid_lbfgs_memory: int = 10
- hybrid_convergence_window: int = 5
- hybrid_convergence_threshold: float = 1e-06
- hybrid_max_phases: int = 4
- 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
- sampling_strategy: Literal['lhs', 'sobol', 'random'] = 'lhs'
- screen_keep_fraction: float = 0.5
- refine_top_k: int = 3
- 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
- constant_scaling_threshold: int = 3
- use_nlsq_library: bool = True
- n_params: int = 14
- analysis_mode: str = 'two_component'
- nlsq_stability: str = 'auto'
- nlsq_rescale_data: bool = False
- nlsq_memory_fraction: float = 0.75
- nlsq_memory_fallback_gb: float = 16.0
- validation: NLSQValidationConfig
- __post_init__()[source]
Validate invariants that must hold immediately after construction.
- Return type:
- validate()[source]
Return a list of configuration error strings.
An empty list means the configuration is consistent. Callers should treat a non-empty list as a hard error before launching a fit.
- classmethod from_dict(config)[source]
Construct an
NLSQConfigfrom a plain dictionary.Nested sub-dictionaries under
"recovery"and"validation"are automatically parsed into their respective dataclasses. Unrecognised top-level keys are logged as warnings and ignored.
- to_dict()[source]
Serialise the configuration to a plain dictionary.
Nested dataclasses are serialised as nested dicts, making the output suitable for round-tripping through YAML / JSON.
- classmethod from_yaml(yaml_path)[source]
Create NLSQConfig from YAML configuration file.
This is the recommended single entry point for loading NLSQ configuration. It reads the YAML file, extracts the
optimization.nlsqsection, and creates a validatedNLSQConfigobject.- Parameters:
yaml_path (
str) – Path to YAML configuration file.- Return type:
NLSQConfig- Returns:
Validated
NLSQConfiginstance.- Raises:
FileNotFoundError – If the YAML file does not exist.
ValueError – If the YAML file is invalid or missing required sections.
Examples
>>> config = NLSQConfig.from_yaml("heterodyne_config.yaml") >>> print(config.loss) soft_l1
- is_valid()[source]
Check if the configuration is valid.
- Return type:
- Returns:
Trueifvalidate()returns an empty list,Falseotherwise.
- to_workflow_kwargs()[source]
Convert settings to kwargs for NLSQ’s
curve_fit().Maps
NLSQConfigsettings to NLSQ 0.6.10+curve_fit()parameters. Heterodyne usescurve_fit()directly rather than the unifiedfit()API.Notes
NLSQ 0.6.3+ workflows:
"auto","auto_global","hpc". Old presets ("streaming","standard") were removed. Heterodyne uses its own strategy selection for memory-aware dispatch, soworkflowis not forwarded.
- __init__(max_iterations=1000, tolerance=1e-08, method='trf', multistart=False, multistart_n=10, verbose=1, use_jac=True, x_scale='jac', ftol=1e-08, xtol=1e-08, gtol=1e-08, loss='soft_l1', trust_region_scale=1.0, diff_step=None, max_nfev=None, chunk_size=None, workflow='auto', goal='robust', enable_streaming=False, streaming_chunk_size=50000, enable_stratified=False, target_chunk_size=10000, enable_recovery=True, max_recovery_attempts=3, recovery_config=<factory>, enable_diagnostics=True, enable_anti_degeneracy=True, x_scale_map=<factory>, loss_weights=None, loss_scale=1.0, tr_solver=None, step_bound=0.0, per_angle_mode='auto', fourier_order=2, fourier_auto_threshold=6, enable_hierarchical=False, hierarchical_max_outer_iterations=20, hierarchical_inner_tolerance=1e-06, hierarchical_outer_tolerance=0.0001, hierarchical_physical_max_iterations=100, hierarchical_per_angle_max_iterations=50, regularization_mode='none', group_variance_lambda=0.01, regularization_target_cv=0.5, regularization_target_contribution=0.1, regularization_max_cv=0.2, regularization_auto_tune_lambda=True, enable_gradient_monitoring=False, gradient_ratio_threshold=100.0, gradient_consecutive_triggers=3, gradient_collapse_response='hierarchical', enable_cmaes=False, cmaes_sigma0=0.3, cmaes_max_iterations=1000, cmaes_population_size=None, cmaes_tolx=1e-06, cmaes_tolfun=1e-08, cmaes_diagonal_filtering='remove', cmaes_anti_degeneracy=False, cmaes_warmstart_auto_skip=True, cmaes_warmstart_skip_threshold=5.0, cmaes_restart_strategy='bipop', cmaes_max_restarts=9, cmaes_preset='cmaes', cmaes_max_generations=None, cmaes_popsize=None, cmaes_sigma=0.5, cmaes_sigma_warmstart=0.05, cmaes_tol_fun=1e-08, cmaes_tol_x=1e-08, 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, enable_progress_bar=True, log_iteration_interval=10, hybrid_enable=False, hybrid_warmup_fraction=0.1, hybrid_normalization=True, hybrid_method='gauss_newton', hybrid_lbfgs_memory=10, hybrid_convergence_window=5, hybrid_convergence_threshold=1e-06, hybrid_max_phases=4, 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, sampling_strategy='lhs', screen_keep_fraction=0.5, refine_top_k=3, 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, constant_scaling_threshold=3, use_nlsq_library=True, n_params=14, analysis_mode='two_component', nlsq_stability='auto', nlsq_rescale_data=False, nlsq_x_scale='jac', nlsq_memory_fraction=0.75, nlsq_memory_fallback_gb=16.0, validation=<factory>)
Results¶
Result container for NLSQ optimization.
Provides:
- NLSQResult — primary heterodyne result dataclass
- OptimizationResult — homodyne-parity result (dict-builder output)
- FallbackInfo — adapter-to-wrapper fallback tracking
- FunctionEvaluationCounter — callable invocation counter
- UseSequentialOptimization — marker for sequential fallback strategy
- class heterodyne.optimization.nlsq.results.NLSQResult[source]
Bases:
objectResult of NLSQ optimization.
Contains fitted parameters, uncertainties, and fit quality metrics.
- parameters: ndarray
- success: bool
- message: str
- n_iterations: int = 0
- n_function_evals: int = 0
- convergence_reason: str = ''
- property n_params: int
Number of fitted parameters.
- get_param(name)[source]
Get parameter value by name.
- get_uncertainty(name)[source]
Get uncertainty for parameter by name.
- get_correlation_matrix()[source]
Compute correlation matrix from covariance.
- validate()[source]
Validate result quality.
- __init__(parameters, parameter_names, success, message, uncertainties=None, covariance=None, final_cost=None, reduced_chi_squared=None, n_iterations=0, n_function_evals=0, convergence_reason='', residuals=None, jacobian=None, fitted_correlation=None, wall_time_seconds=None, metadata=<factory>)
- class heterodyne.optimization.nlsq.results.FunctionEvaluationCounter[source]
Bases:
objectWraps a callable and counts invocations.
Useful for tracking the number of function evaluations during optimization.
- fn
The wrapped callable
- count
Number of times the callable has been invoked
- count: int = 0
- __init__(fn, count=0)
- class heterodyne.optimization.nlsq.results.OptimizationResult[source]
Bases:
objectComplete optimization result with fit quality metrics and diagnostics.
Homodyne-parity class. This mirrors homodyne’s
OptimizationResultand is returned byResultBuilder.build(). Heterodyne’s primary result type isNLSQResult(richer API);OptimizationResultis the dict-builder-compatible surface used by wrapper code.- 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:
- reduced_chi_squared
chi_squared / (n_data - n_params).
- Type:
- convergence_status
‘converged’, ‘max_iter’, or ‘failed’.
- Type:
- iterations
Number of optimization iterations.
- Type:
- execution_time
Wall-clock execution time in seconds.
- Type:
- quality_flag
‘good’, ‘marginal’, or ‘poor’.
- Type:
- stratification_diagnostics
Diagnostics for angle-stratified chunking.
- Type:
Any | None
- sigma_is_default
True if sigma weights were defaulted (not user-supplied).
- Type:
- parameters: ndarray
- uncertainties: ndarray
- covariance: ndarray
- chi_squared: float
- reduced_chi_squared: float
- convergence_status: str
- iterations: int
- execution_time: float
- quality_flag: str = 'good'
- 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)
- class heterodyne.optimization.nlsq.results.FallbackInfo[source]
Bases:
objectTracks fallback from NLSQAdapter to NLSQWrapper.
Included in OptimizationResult.device_info when fallback occurs.
- fallback_occurred
True if fallback was triggered
- adapter_used
“NLSQAdapter” or “NLSQWrapper”
- adapter_error
Error message if adapter failed (None if succeeded)
- wrapper_error
Error message if wrapper also failed (None otherwise)
States:
NLSQAdapter + fallback_occurred=False + adapter_error=None: Adapter succeeded
NLSQWrapper + fallback_occurred=True + adapter_error=”…”: Fallback succeeded
NLSQWrapper + fallback_occurred=True + adapter_error=”…” + wrapper_error=”…”: Both failed
- fallback_occurred: bool
- adapter_used: str
- __init__(fallback_occurred, adapter_used, adapter_error=None, wrapper_error=None)
- class heterodyne.optimization.nlsq.results.UseSequentialOptimization[source]
Bases:
objectMarker indicating sequential per-angle optimization should be used.
This is returned by _apply_stratification_if_needed when conditions require sequential per-angle optimization as a fallback strategy.
- data
Original XPCS data object.
- Type:
Any
- reason
Why sequential optimization is needed.
- Type:
- data: Any
- reason: str
- __init__(data, reason)
Adapter¶
NLSQ adapters: NLSQAdapter (JAX-traced) and NLSQWrapper (memory-aware fallback).
Import order: nlsq imports appear before JAX so that nlsq can configure JAX x64 mode before JAX is initialised.
- class heterodyne.optimization.nlsq.adapter.ModelCacheKey[source]
Bases:
objectCache key for CurveFit instances.
Includes phi_angles and scaling_mode so that different multi-angle or scaling configurations do not share the same compiled fitter.
Homodyne-parity fields (analysis_mode, q, per_angle_scaling) are present but default to heterodyne-appropriate values so existing callers that use the fitter-centric path (n_data, n_params, scaling_mode) continue to work.
- n_data: int
- n_params: int
- scaling_mode: str
- analysis_mode: str = 'full'
- q: float = 0.0
- per_angle_scaling: bool = True
- __init__(n_data, n_params, phi_angles, scaling_mode, callable_scope=None, analysis_mode='full', q=0.0, per_angle_scaling=True)
- class heterodyne.optimization.nlsq.adapter.CachedModel[source]
Bases:
objectA cached CurveFit instance with usage stats.
The
modelandmodel_funcfields mirror homodyne’s CachedModel for API parity; in the heterodyne fitter-centric path they remain None andfittercarries the nlsq.CurveFit instance.- fitter: object
- created_at: float
- last_accessed: float
- n_hits: int = 0
- model: Any = None
- __init__(fitter, created_at=<factory>, last_accessed=<factory>, n_hits=0, model=None, model_func=None)
- heterodyne.optimization.nlsq.adapter.get_or_create_fitter(n_data, n_params, phi_angles=None, scaling_mode='auto', callable_scope=None)[source]
Get a CurveFit instance from cache or create a new one.
- Parameters:
n_data (
int) – Number of data points (flength).n_params (
int) – Number of parameters.phi_angles (
tuple[float,...] |None) – Tuple of azimuthal angles (distinguishes multi-angle configs).scaling_mode (
str) – Contrast/offset scaling mode (e.g. “auto”, “individual”).callable_scope (
object|None) – Optional residual/model callable that must not share a stateful fitter with different residual closures.
- Returns:
bool).
- Return type:
- heterodyne.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 a new placeholder for heterodyne.
This function provides model instance caching for API parity with homodyne’s
get_or_create_model(). Heterodyne’s NLSQ path uses a residual-function interface rather than a high-level model object, somodelandmodel_funcare returned asNonehere — callers that need a concrete fitter should useget_or_create_fitter().- Parameters:
analysis_mode (
str) – Physics mode string (heterodyne always uses'full').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 (unused; kept for API parity).enable_jit (
bool) – Whether JIT compilation is requested (advisory).
- Return type:
- Returns:
Tuple of (model, model_func, cache_hit) where model and model_func are always
Nonein heterodyne (residual path), and cache_hit reflects whether the key was already registered.
- heterodyne.optimization.nlsq.adapter.clear_model_cache()[source]
Clear the CurveFit model cache and reset hit/miss counters.
- Return type:
- Returns:
Number of models removed from the cache.
- heterodyne.optimization.nlsq.adapter.get_cache_stats()[source]
Return cache hit/miss/size statistics.
- class heterodyne.optimization.nlsq.adapter.AdapterConfig[source]
Bases:
objectConfiguration for NLSQAdapter.
- enable_cache
Enable model instance caching.
- enable_jit
Enable JIT compilation of model functions.
- 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')
- class heterodyne.optimization.nlsq.adapter.NLSQAdapter[source]
Bases:
NLSQAdapterBaseAdapter for the nlsq library’s CurveFit optimizer.
Uses JAX-accelerated nonlinear least squares from the nlsq package. The
fit()method callsnlsq.CurveFitdirectly — no scipy delegation. For pure-JAX residual functions, preferfit_jax()which passes a JAX-traceable function toCurveFit.curve_fit().- __init__(parameter_names=None, config=None)[source]
Initialise the adapter.
- Parameters:
parameter_names (
list[str] |None) – Names of parameters being optimised, in order. Kept as the primary heterodyne argument. Defaults to an empty list so thatNLSQAdapter()(homodyne-style, no names) works.config (
AdapterConfig|None) – Optional AdapterConfig for feature flags (parity with homodyne). When provided,enable_recoveryandenable_stabilityare forwarded to the underlying CurveFit constructor if the installed nlsq version supports them.
- property name: str
Name of the optimization backend.
- property workflow_available: bool
Check if NLSQ WorkflowSelector is available.
WorkflowSelector was removed in NLSQ v0.6.0; heterodyne uses its own
select_nlsq_strategy()frommemory.pyinstead. Always returns False for parity with homodyne post-v0.6.0.
- fit(residual_fn, initial_params, bounds, config, jacobian_fn=None, *, analysis_mode='full', per_angle_scaling=True, diagnostics_enabled=False, per_angle_scaling_initial=None, anti_degeneracy_controller=None)[source]
Run NLSQ optimisation using nlsq.CurveFit.
Wraps the residual function into the
(xdata, *params)signature expected byCurveFit.curve_fitand normalises the result viabuild_result_from_nlsq.- Parameters:
residual_fn (
Callable[[ndarray],ndarray]) – Callable(params: ndarray) -> residuals: ndarray.initial_params (
ndarray) – Starting parameter values.bounds (
tuple[ndarray,ndarray]) –(lower, upper)bound arrays.config (
NLSQConfig) – Optimisation configuration.jacobian_fn (
Callable[[ndarray],ndarray] |None) – Optional analytic Jacobian (unused by CurveFit; kept for API compatibility).analysis_mode (
str) – Physics mode string (heterodyne always uses'full'). Present for homodyne API parity; not used internally because heterodyne residuals are pre-computed.per_angle_scaling (
bool) – Whether per-angle contrast/offset is used. Present for homodyne API parity; heterodyne encodes scaling insideresidual_fn.diagnostics_enabled (
bool) – Enable extended diagnostics logging.per_angle_scaling_initial (
dict[str,list[float]] |None) – Initial per-angle contrast/offset. Present for homodyne API parity; not used in residual path.anti_degeneracy_controller (
Any|None) – Anti-degeneracy controller. When provided and it exposescreate_nlsq_callbacks(), the returned callbacks are injected into the optimizer call.
- Return type:
NLSQResult- Returns:
NLSQResult with fit results.
- fit_jax(jax_residual_fn, initial_params, bounds, config, n_data)[source]
Run NLSQ optimisation using a pure JAX-traceable residual function.
This method accepts a function with the signature
(xdata, *params) -> residualsthat nlsq can trace through JAX.- Parameters:
jax_residual_fn (
Callable[...,Any]) – JAX-compatible callable(x, *params) -> residuals.initial_params (
ndarray) – Starting parameter values.bounds (
tuple[ndarray,ndarray]) –(lower, upper)bound arrays.config (
NLSQConfig) – Optimisation configuration.n_data (
int) – Number of data points (used as CurveFitflength).
- Return type:
NLSQResult- Returns:
NLSQResult with fit results.
- class heterodyne.optimization.nlsq.adapter.NLSQWrapper[source]
Bases:
NLSQAdapterBaseStable fallback adapter with memory-aware strategy routing.
Selects between STANDARD, LARGE, and STREAMING optimization tiers based on the estimated peak memory usage of the Jacobian matrix. Falls back down the tier list if a higher tier fails.
- Fallback order (descending resource intensity):
STREAMING → LARGE → STANDARD
Each tier is retried up to
max_retriestimes before falling back.- __init__(parameter_names, enable_large_dataset=True, enable_recovery=True, max_retries=3)[source]
Initialise the wrapper.
- property name: str
Name of the optimization backend.
- fit(residual_fn, initial_params, bounds, config, jacobian_fn=None)[source]
Run NLSQ optimisation with automatic memory-based strategy routing.
- Parameters:
residual_fn (
Callable[[ndarray],ndarray]) – Callable(params: ndarray) -> residuals: ndarray.initial_params (
ndarray) – Starting parameter values.bounds (
tuple[ndarray,ndarray]) –(lower, upper)bound arrays.config (
NLSQConfig) – Optimisation configuration.jacobian_fn (
Callable[[ndarray],ndarray] |None) – Optional analytic Jacobian (for API compatibility).
- Return type:
NLSQResult- Returns:
NLSQResult with fit results.
- heterodyne.optimization.nlsq.adapter.get_adapter(config=None)[source]
Factory function to get an NLSQAdapter instance.
- Parameters:
config (
AdapterConfig|None) – Adapter configuration. IfNone, uses defaults.- Return type:
NLSQAdapter- Returns:
NLSQAdapter instance.
- heterodyne.optimization.nlsq.adapter.is_adapter_available()[source]
Check if NLSQAdapter can be used.
- Return type:
- Returns:
True if the nlsq CurveFit class is importable.
- heterodyne.optimization.nlsq.adapter.LowLevelNLSQWrapper
alias of
NLSQWrapper
JIT Strategies¶
NLSQ fitting strategies for heterodyne model optimization.
Strategies determine how the residual function is evaluated: - ResidualStrategy: Direct residual evaluation (default) - JITStrategy: JAX JIT-compiled residual + Jacobian - ChunkedStrategy: Memory-efficient chunked evaluation for large datasets - SequentialStrategy: Per-angle sequential fitting with warm-start - HybridStreamingStrategy: 4-phase hybrid optimizer (L-BFGS + Gauss-Newton) - OutOfCoreStrategy: Memory-mapped evaluation for very large datasets - StratifiedLSStrategy: Stratified sampling across q-point subsets - ResidualJITStrategy: JIT-compiled residual with FD Jacobian
Fourier Reparameterization¶
Joint multi-angle fitting via Fourier coefficient reparameterization of contrast and offset parameters.
Fourier Reparameterization for Per-Angle Scaling Parameters.
This module replaces n_phi independent per-angle contrast/offset values with truncated Fourier series, dramatically reducing structural degeneracy in joint multi-angle fits.
Adapted from homodyne Anti-Degeneracy Defense System. The Fourier basis is model-agnostic (captures smooth phi-variation); here it regularises the angle-dependent velocity phase term cos(q·cos(φ)·∫v dt) rather than the homodyne shear sinc term.
Mathematical Formulation¶
contrast(phi) = c0 + sum_k[ck*cos(k*phi) + sk*sin(k*phi)] for k=1..order offset(phi) = o0 + sum_k[ok*cos(k*phi) + tk*sin(k*phi)] for k=1..order
For order=2: - Contrast: 5 coefficients [c0, c1, s1, c2, s2] - Offset: 5 coefficients [o0, o1, t1, o2, t2] - Total: 10 Fourier coefficients vs 2*n_phi independent params
Parameter Count Comparison:
n_phi | Independent | Fourier (order=2) | Reduction
------|-------------|-------------------|----------
2 | 4 | 4 | 0%
3 | 6 | 6 | 0%
10 | 20 | 10 | 50%
23 | 46 | 10 | 78%
100 | 200 | 10 | 95%
Note: For n_phi <= 2*(order+1), independent mode is used.
- class heterodyne.optimization.nlsq.fourier_reparam.FourierReparamConfig[source]
Bases:
objectConfiguration for Fourier reparameterization.
- mode
Per-angle parameter mode (mirrors NLSQConfig.per_angle_mode):
“individual”: Use n_phi independent contrast/offset values
“independent”: Legacy alias for “individual” (normalised in __post_init__)
“fourier”: Use truncated Fourier series
“auto”: Use Fourier when n_phi > auto_threshold
“constant”: Not supported here; handled upstream by _fit_joint_fixed_constant_multi_phi. Raises ValueError if reached.
- fourier_order
Number of Fourier harmonics. Default 2. order=2 gives 5 coefficients per parameter (c0, c1, s1, c2, s2).
- auto_threshold
Use Fourier when n_phi > this threshold in auto mode.
- c0_bounds
Bounds for mean contrast coefficient.
- ck_bounds
Bounds for harmonic contrast amplitudes.
- o0_bounds
Bounds for mean offset coefficient.
- ok_bounds
Bounds for harmonic offset amplitudes.
- mode: Literal['individual', 'constant', 'fourier', 'auto', 'independent'] = 'auto'
- fourier_order: int = 2
- auto_threshold: int = 6
- classmethod from_dict(config_dict)[source]
Create config from dictionary.
- Return type:
FourierReparamConfig
- __init__(mode='auto', fourier_order=2, auto_threshold=6, c0_bounds=(0.01, 1.0), ck_bounds=(-0.2, 0.2), o0_bounds=(0.5, 1.5), ok_bounds=(-0.3, 0.3))
- class heterodyne.optimization.nlsq.fourier_reparam.FourierReparameterizer[source]
Bases:
objectHandles conversion between Fourier coefficients and per-angle values.
Core functionality: 1. Convert per-angle values to Fourier coefficients (initialization) 2. Convert Fourier coefficients to per-angle values (model evaluation) 3. Compute Jacobian for covariance transformation
The Fourier basis ensures smooth variation of contrast/offset with angle, preventing the optimizer from using per-angle parameters to absorb angle-dependent physical signals.
- Parameters:
phi_angles (
ndarray) – Unique phi angles in radians, shape (n_phi,).config (
FourierReparamConfig) – Fourier configuration.
- n_phi
Number of unique phi angles.
- Type:
- n_coeffs
Total number of Fourier coefficients (contrast + offset).
- Type:
- n_coeffs_per_param
Coefficients per parameter type (contrast or offset).
- Type:
- use_fourier
Whether Fourier mode is active.
- Type:
Examples
>>> phi_angles = np.linspace(-np.pi, np.pi, 23) >>> config = FourierReparamConfig(mode="fourier", fourier_order=2) >>> fourier = FourierReparameterizer(phi_angles, config) >>> contrast = np.full(23, 0.3) >>> offset = np.full(23, 1.0) >>> fourier_coeffs = fourier.per_angle_to_fourier(contrast, offset) >>> contrast_out, offset_out = fourier.fourier_to_per_angle(fourier_coeffs)
- __init__(phi_angles, config)[source]
- get_basis_matrix()[source]
Get the Fourier basis matrix.
- property order: int
Fourier order (number of harmonics).
- fourier_to_per_angle(fourier_coeffs)[source]
Convert Fourier coefficients to per-angle contrast/offset.
- Parameters:
fourier_coeffs (
ndarray) – Shape (n_coeffs,) = [c0,c1,s1,c2,s2,…,o0,o1,t1,o2,t2,…].- Return type:
- Returns:
(contrast, offset) each of shape (n_phi,).
- Raises:
ValueError – If fourier_coeffs has wrong shape.
- per_angle_to_fourier(contrast, offset)[source]
Convert per-angle values to Fourier coefficients.
Uses least squares fitting when n_phi > n_coeffs_per_param.
- get_jacobian_transform()[source]
Get Jacobian of transformation: d(per_angle)/d(fourier).
- Used for covariance transformation back to per-angle space:
Cov_per_angle = J @ Cov_fourier @ J.T
- Return type:
- Returns:
Jacobian matrix of shape (2*n_phi, n_coeffs).
- get_bounds()[source]
Get bounds for Fourier coefficients.
- get_initial_coefficients(contrast_init, offset_init)[source]
Get initial Fourier coefficients from initial values.
- get_coefficient_labels()[source]
Get parameter labels for Fourier coefficients.
- to_fourier(per_angle_values)[source]
Convert a single per-angle array to Fourier coefficients.
Convenience method for one parameter group at a time.
- from_fourier(fourier_coeffs)[source]
Convert Fourier coefficients to per-angle values for one group.
CMA-ES Wrapper¶
Covariance Matrix Adaptation Evolution Strategy for global optimization.
CMA-ES optimization wrapper for heterodyne parameter fitting.
Provides a derivative-free global optimizer as an alternative to gradient-based NLSQ methods. Useful when the cost landscape is multi-modal or the Jacobian is unreliable.
The cma package is an optional dependency. If it is not installed,
importing this module succeeds but CMAESWrapper.fit() raises
ImportError at call time.
Parity note¶
This module matches the homodyne cmaes_wrapper public API:
CMAESWrapperConfig(v2.16.0 normalization + refinement settings)CMAESResult(parameters, covariance, chi_squared, …)CMAESWrapper(compute_scale_ratio, is_available, should_use_cmaes, fit)
Heterodyne-specific additions (not in homodyne):
CMAESConfig— low-levelcmalibrary settingsfit_with_cmaes()— objective-based convenience (returns NLSQResult)build_anti_degeneracy_objective()normalize_to_unit_cube()/denormalize_from_unit_cube()compute_adaptive_cmaes_params()adjust_covariance_for_bounds()
D3 divergence: heterodyne uses the cma library backend instead of
NLSQ’s evosax backend (CPU-only; NLSQ evosax not required).
- heterodyne.optimization.nlsq.cmaes_wrapper.CMAES_AVAILABLE: bool = False
Public availability flag consumed by
core.py.
- class heterodyne.optimization.nlsq.cmaes_wrapper.CMAESConfig[source]
Bases:
objectConfiguration for CMA-ES optimization via the
cmalibrary.- sigma0
Initial step-size (standard deviation) for the search distribution. A good default is ~1/4 of the expected parameter range.
- popsize
Population size.
Nonelets cma choose automatically.
- maxiter
Maximum number of CMA-ES generations.
- tolx
Termination tolerance on parameter changes.
- tolfun
Termination tolerance on cost function changes.
- seed
Random seed for reproducibility.
- restart_strategy
Restart strategy for CMA-ES.
"bipop"alternates large/small population restarts for robust global search."none"runs a single CMA-ES run with no restarts. Callers should override to"none"when a warmstart sigma is already small (warmstart mode), because BIPOP large-population restarts are incoherent with a tight initial distribution.
- max_restarts
Maximum number of BIPOP restarts. Ignored when
restart_strategy="none".
- sigma0: float = 0.5
- maxiter: int = 1000
- tolx: float = 1e-11
- tolfun: float = 1e-11
- seed: int = 42
- diagonal_filtering: str = 'none'
- restart_strategy: str = 'bipop'
- max_restarts: int = 9
- __init__(sigma0=0.5, popsize=None, maxiter=1000, tolx=1e-11, tolfun=1e-11, seed=42, diagonal_filtering='none', restart_strategy='bipop', max_restarts=9)
- class heterodyne.optimization.nlsq.cmaes_wrapper.CMAESWrapperConfig[source]
Bases:
objectConfiguration for
CMAESWrapper.Mirrors homodyne’s
CMAESWrapperConfigfor structural parity.- preset
CMA-ES preset: “cmaes-fast” (50 gen), “cmaes” (100 gen), “cmaes-global” (200 gen).
- Type:
- max_generations
Maximum CMA-ES generations. None = use preset default.
- Type:
int | None
- popsize
Population size (None = auto from 4+3*ln(n)).
- Type:
int | None
- sigma
Initial step size as fraction of search range (0, 1].
- Type:
- sigma_warmstart
Reduced sigma for warm-start (local refinement).
- Type:
- tol_fun
Function value tolerance for convergence.
- Type:
- tol_x
Parameter tolerance for convergence.
- Type:
- restart_strategy
Restart strategy: “none” or “bipop”.
- Type:
- max_restarts
Maximum number of BIPOP restarts.
- Type:
- refine_with_nlsq
Whether to refine CMA-ES solution with NLSQ TRF.
- Type:
- normalize
Enable bounds-based parameter normalization.
- Type:
- normalization_epsilon
Prevent division by zero in normalization.
- Type:
- preset: str = 'cmaes'
- sigma: float = 0.5
- sigma_warmstart: float = 0.05
- tol_fun: float = 1e-08
- tol_x: float = 1e-08
- restart_strategy: str = 'bipop'
- max_restarts: int = 9
- refine_with_nlsq: bool = True
- refinement_ftol: float = 1e-10
- refinement_xtol: float = 1e-10
- refinement_gtol: float = 1e-10
- refinement_max_nfev: int = 500
- refinement_loss: str = 'linear'
- normalize: bool = True
- normalization_epsilon: float = 1e-12
- classmethod from_nlsq_config(config)[source]
Create
CMAESWrapperConfigfromNLSQConfig.- Parameters:
config (
NLSQConfig) – NLSQ configuration object.- Returns:
CMA-ES wrapper configuration.
- Return type:
CMAESWrapperConfig
- __init__(preset='cmaes', max_generations=None, popsize=None, sigma=0.5, sigma_warmstart=0.05, tol_fun=1e-08, tol_x=1e-08, restart_strategy='bipop', max_restarts=9, refine_with_nlsq=True, refinement_ftol=1e-10, refinement_xtol=1e-10, refinement_gtol=1e-10, refinement_max_nfev=500, refinement_loss='linear', normalize=True, normalization_epsilon=1e-12)
- class heterodyne.optimization.nlsq.cmaes_wrapper.CMAESResult[source]
Bases:
objectResult from CMA-ES optimization.
Matches homodyne’s
CMAESResultfor structural parity.- parameters
Optimized parameter values.
- Type:
np.ndarray
- covariance
Parameter covariance matrix (if computed).
- Type:
np.ndarray | None
- chi_squared
Final chi-squared value.
- Type:
- success
Whether optimization converged successfully.
- Type:
- diagnostics
CMA-ES diagnostics (generations, evaluations, etc.).
- Type:
- method_used
Method used: “cmaes” or “multi-start”.
- Type:
- nlsq_refined
Whether result was refined with NLSQ L-M.
- Type:
- message
Convergence message.
- Type:
- parameters: ndarray
- chi_squared: float
- success: bool
- diagnostics: dict
- method_used: str = 'cmaes'
- nlsq_refined: bool = False
- message: str = ''
- __init__(parameters, covariance, chi_squared, success, diagnostics=<factory>, method_used='cmaes', nlsq_refined=False, message='')
- class heterodyne.optimization.nlsq.cmaes_wrapper.CMAESWrapper[source]
Bases:
objectWrapper around
cma.fmin2for bounded parameter optimization.Public API matches homodyne’s
CMAESWrapper:is_available()— check ifcmalibrary is installedcompute_scale_ratio()— max/min bound range ratioshould_use_cmaes()— scale-ratio based selectionfit()— run CMA-ES, returnCMAESResult
Heterodyne-specific internal method:
fit_with_objective()— objective-fn based fit, returns NLSQResult
- Parameters:
- __init__(config=None, parameter_names=None)[source]
- property is_available: bool
Check if CMA-ES is available (
cmalibrary installed).
- compute_scale_ratio(bounds)[source]
Compute scale ratio from parameter bounds.
The scale ratio is the ratio of the largest to smallest parameter range. High scale ratios (> 1000) indicate multi-scale problems where CMA-ES excels.
- should_use_cmaes(bounds, scale_threshold=1000.0)[source]
Determine if CMA-ES should be used based on scale ratio.
- fit(model_func, xdata, ydata, p0, bounds, sigma=None, warmstart_chi2=None)[source]
Run CMA-ES global optimization.
Matches homodyne’s
CMAESWrapper.fitsignature exactly. Uses thecmalibrary backend (heterodyne CPU-only path).- Parameters:
model_func (
Callable) – Model function:y = f(x, *params).xdata (
ndarray) – Independent variable data.ydata (
ndarray) – Dependent variable data to fit.p0 (
ndarray) – Initial parameter guess.bounds (
tuple[ndarray,ndarray]) – Parameter bounds as (lower, upper).warmstart_chi2 (
float|None) – Chi-squared from NLSQ warm-start. If provided and CMA-ES chi2 exceeds_REFINEMENT_SKIP_CHI2_RATIO× this value, refinement is skipped. Also triggerssigma_warmstartinstead ofsigmafor the CMA-ES search.
- Returns:
Optimization result with parameters, covariance, diagnostics.
- Return type:
CMAESResult- Raises:
ImportError – If the
cmapackage is not installed.
- fit_with_objective(objective_fn, x0, bounds, *, residual_fn=None, n_data=None, parameter_names=None, metadata=None)[source]
Run CMA-ES optimization from a scalar objective function.
Heterodyne-specific; not in homodyne. Used by
fit_with_cmaes().- Parameters:
objective_fn (
Callable[[ndarray],float]) – Scalar objectivef(x) -> floatto minimize.x0 (
ndarray) – Initial guess, shape(n_params,).bounds (
tuple[ndarray,ndarray]) –(lower, upper)arrays, each shape(n_params,).residual_fn (
Callable[[ndarray],ndarray] |None) – Optional residual function for building a fullNLSQResultwith covariance information.n_data (
int|None) – Number of data points (for reduced chi-squared).parameter_names (
list[str] |None) – Override parameter names for this call.metadata (
dict[str,Any] |None) – Extra metadata attached to the result.
- Return type:
NLSQResult- Returns:
An
NLSQResultpopulated from the CMA-ES solution.- Raises:
ImportError – If the
cmapackage is not installed.
- heterodyne.optimization.nlsq.cmaes_wrapper.normalize_to_unit_cube(x, lower, upper)[source]
Map parameter vector from physical bounds to the unit hypercube [0, 1].
- Parameters:
- Return type:
- Returns:
Transformed array of shape
(n_params,)with values in [0, 1]. Dimensions wherelower == upperare mapped to 0.0.- Raises:
ValueError – If arrays have mismatched shapes.
- heterodyne.optimization.nlsq.cmaes_wrapper.denormalize_from_unit_cube(x_norm, lower, upper)[source]
Map parameter vector from the unit hypercube [0, 1] to physical bounds.
This is the inverse of
normalize_to_unit_cube().- Parameters:
- Return type:
- Returns:
De-normalized array of shape
(n_params,)in physical units.- Raises:
ValueError – If arrays have mismatched shapes.
- heterodyne.optimization.nlsq.cmaes_wrapper.compute_adaptive_cmaes_params(bounds)[source]
Compute adaptive population size and max generations from bound ranges.
Scales popsize from 50 to 200 and max_generations from 200 to 500 based on the ratio between the largest and smallest parameter ranges.
- heterodyne.optimization.nlsq.cmaes_wrapper.build_anti_degeneracy_objective(base_objective, bounds, parameter_names, penalty_weight=0.01)[source]
Wrap an objective with penalty terms for known degenerate parameter pairs.
For each known degenerate pair (e.g. v0/v_offset, D0_ref/D0_sample), a soft penalty encourages separation of their normalized values.
- Parameters:
- Return type:
- Returns:
Wrapped objective function with degeneracy penalties.
- heterodyne.optimization.nlsq.cmaes_wrapper.fit_with_cmaes(objective_fn, initial_params, bounds, parameter_names=None, *, config=None, residual_fn=None, n_data=None, anti_degeneracy=False, metadata=None)[source]
Convenience function to run CMA-ES from a scalar objective.
Applies adaptive population sizing and max_generations based on the parameter scale ratio when the user hasn’t explicitly set these values.
- Parameters:
objective_fn (
Callable[[ndarray],float]) – Scalar objectivef(x) -> floatto minimize.initial_params (
ndarray) – Initial guess, shape(n_params,).parameter_names (
list[str] |None) – Ordered parameter names.config (
CMAESConfig|None) – Low-levelCMAESConfig;Nonefor defaults.residual_fn (
Callable[[ndarray],ndarray] |None) – Optional residual function for covariance estimation.n_data (
int|None) – Number of data points (for reduced chi-squared).anti_degeneracy (
bool) – Apply degeneracy-penalty weighting to objective.metadata (
dict[str,Any] |None) – Extra metadata attached to the result.
- Return type:
NLSQResult- Returns:
An
NLSQResultfrom the CMA-ES solution.
- heterodyne.optimization.nlsq.cmaes_wrapper.adjust_covariance_for_bounds(cov, lower, upper)[source]
Scale a covariance matrix to account for parameter bounds ranges.
When parameters have very different dynamic ranges, a unit-cube covariance should be scaled to physical space. The adjustment is:
- cov_adjusted[i, j] = cov[i, j] * (upper[i] - lower[i])
(upper[j] - lower[j])
- Parameters:
- Return type:
- Returns:
Adjusted covariance matrix of shape
(n_params, n_params).- Raises:
ValueError – If array shapes are inconsistent.
Multi-Start Optimizer¶
Multi-start optimization with Latin Hypercube Sampling.
Supports parallel execution via ProcessPoolExecutor with automatic fallback to sequential when JAX functions cannot be serialized for inter-process communication.
NOTE: Subsampling is explicitly NOT supported per project requirements. Numerical precision and reproducibility take priority over computational speed.
- class heterodyne.optimization.nlsq.multistart.MultiStartConfig[source]
Bases:
objectConfiguration for multi-start optimization.
- enable
Whether to use multi-start optimization. Default: False.
- n_starts
Number of starting points (including the user-provided one).
- seed
Random seed for Latin Hypercube Sampling reproducibility.
- sampling_strategy
Method for generating starting points.
"latin_hypercube"or"random".
- custom_starts
User-provided custom starting points to include alongside generated starts.
- n_workers
Number of parallel workers. 0 = auto (cpu_count). Default: 0.
- use_screening
Whether to pre-screen starting points by cost.
- screen_keep_fraction
Fraction of starts to keep after screening.
- refine_top_k
Refine only the top-k starts after screening.
- refinement_ftol
Function tolerance for refinement phase.
- degeneracy_threshold
Relative chi-squared threshold for degeneracy detection across basins.
- parallel
Whether to attempt parallel execution via ProcessPoolExecutor. Defaults to False because JAX closures cannot be sent across process boundaries.
- max_workers
Maximum number of worker processes. Defaults to
min(n_starts, os.cpu_count() or 4).
- worker_timeout
Per-worker timeout in seconds.
- max_data_points_for_parallel
Auto-disable parallel when
n_data * n_startsexceeds this threshold.
- enable: bool = False
- n_starts: int = 10
- sampling_strategy: str = 'latin_hypercube'
- n_workers: int = 0
- use_screening: bool = True
- screen_keep_fraction: float = 0.5
- refine_top_k: int = 3
- refinement_ftol: float = 1e-12
- degeneracy_threshold: float = 0.1
- parallel: bool = False
- worker_timeout: float = 1800.0
- max_data_points_for_parallel: int = 500000
- classmethod from_dict(d)[source]
Create from dictionary, ignoring unknown keys.
- classmethod from_nlsq_config(nlsq_config)[source]
Create MultiStartConfig from NLSQConfig.
- Parameters:
nlsq_config (
Any) – NLSQ configuration object.- Returns:
Multi-start configuration.
- Return type:
MultiStartConfig
- to_nlsq_global_config()[source]
Convert to NLSQ’s GlobalOptimizationConfig.
- Returns:
NLSQ global optimization configuration.
- Return type:
- Raises:
ImportError – If NLSQ GlobalOptimizationConfig is not available.
Notes
Maps heterodyne’s multi-start configuration to NLSQ’s GlobalOptimizationConfig:
sampling_strategy -> sampler (lhs, sobol, halton)
use_screening -> elimination_rounds (0 if disabled)
screen_keep_fraction -> elimination_fraction (inverted)
- __init__(enable=False, n_starts=10, seed=None, sampling_strategy='latin_hypercube', custom_starts=None, n_workers=0, use_screening=True, screen_keep_fraction=0.5, refine_top_k=3, refinement_ftol=1e-12, degeneracy_threshold=0.1, parallel=False, max_workers=None, worker_timeout=1800.0, max_data_points_for_parallel=500000)
- class heterodyne.optimization.nlsq.multistart.SingleStartResult[source]
Bases:
objectResult from a single optimization start.
- result
The
NLSQResultreturned by the adapter.
- start_index
Zero-based index of this start within the run.
- initial_params
Starting parameter vector used for this run.
- wall_time
Wall-clock time in seconds for this single start.
- result: NLSQResult
- start_index: int
- initial_params: ndarray
- wall_time: float
- __init__(result, start_index, initial_params, wall_time)
- class heterodyne.optimization.nlsq.multistart.MultiStartResult[source]
Bases:
objectAggregated result from a multi-start optimization run.
- best_result
The
NLSQResultwith the lowest final cost.
- all_starts
Ordered list of
SingleStartResultfor every start.
- n_successful
Number of starts that reported
success=True.
- n_total
Total number of starts attempted.
- config
The
MultiStartConfigused for this run.
- strategy_used
Strategy that was used (always
"full").
- n_unique_basins
Number of distinct local minima found.
- degeneracy_detected
Whether parameter degeneracy was detected.
- total_wall_time
Total execution time in seconds (homodyne-parity alias).
- wall_time_total
Total elapsed wall-clock time in seconds.
- screening_costs
Initial costs from screening phase.
- basin_labels
Cluster labels for each result.
- best_result: NLSQResult
- all_starts: list[SingleStartResult]
- n_successful: int
- n_total: int
- config: MultiStartConfig
- strategy_used: str = 'full'
- n_unique_basins: int = 1
- degeneracy_detected: bool = False
- wall_time_total: float = 0.0
- property best: NLSQResult
best result by cost.
- Type:
Homodyne-parity alias
- property total_wall_time: float
Homodyne-parity alias for
wall_time_total.Was a duplicate dataclass field; now a derived property so callers using either name always see the same value (previously
total_wall_timewas always 0.0 becausefit()only setwall_time_total).
- property all_results: list[NLSQResult]
Backward-compatible accessor returning all
NLSQResultobjects.
- to_nlsq_result()[source]
Convert the best result to an
NLSQResultwith multistart metadata.Attaches a
"multistart"key toresult.metadatacontaining summary statistics for the full multi-start run.- Return type:
NLSQResult- Returns:
The best
NLSQResultwith metadata populated.
- to_optimization_result()[source]
Convert MultiStartResult to a result dict for CLI compatibility.
Returns a dictionary containing the best solution with multi-start metadata, compatible with downstream reporting.
- Returns:
Result dict with best parameters and multi-start diagnostics.
- Return type:
- __init__(best_result, all_starts, n_successful, n_total, config, strategy_used='full', n_unique_basins=1, degeneracy_detected=False, wall_time_total=0.0, screening_costs=None, basin_labels=None)
- class heterodyne.optimization.nlsq.multistart.MultiStartOptimizer[source]
Bases:
objectMulti-start optimizer using Latin Hypercube Sampling.
Runs optimization from multiple starting points to improve the chances of finding the global minimum. The first starting point is always the user-supplied
initial_params; the remainingn_starts - 1points are drawn via Latin Hypercube Sampling within the supplied bounds.Parallel execution is available via
MultiStartConfig.parallel=Truebut is disabled by default because JAX residual closures cannot be transmitted across process boundaries. When parallel is requested, the optimizer logs a warning and falls back to sequential execution.- __init__(adapter, config=None, *, n_starts=None, seed=None)[source]
Initialize the multi-start optimizer.
- generate_starting_points(initial_params, bounds)[source]
Generate starting points using Latin Hypercube Sampling.
The first point is always
initial_params. The remainingn_starts - 1points are drawn via LHS withinbounds.
- fit(residual_fn, initial_params, bounds, config, jacobian_fn=None)[source]
Run multi-start optimization.
Generates starting points via LHS, then runs the adapter from each point sequentially. If
MultiStartConfig.parallel=True, a warning is emitted and execution falls back to sequential because JAX residual closures cannot be transmitted across process boundaries.- Parameters:
residual_fn (
Callable[[ndarray],ndarray]) – Residual function mapping parameters to residual vector.initial_params (
ndarray) – Initial guess; always used as the first start.bounds (
tuple[ndarray,ndarray]) –(lower, upper)bound arrays.config (
NLSQConfig) – Per-run NLSQ solver configuration.jacobian_fn (
Callable[[ndarray],ndarray] |None) – Optional analytic Jacobian.
- Return type:
MultiStartResult- Returns:
MultiStartResultwith the best result and per-start details.
- heterodyne.optimization.nlsq.multistart.check_zero_volume_bounds(bounds_lower, bounds_upper)[source]
Identify parameter dimensions with zero sampling volume.
A dimension has zero volume when its lower bound equals its upper bound, meaning the parameter is effectively fixed. LHS sampling should skip these dimensions and keep all starts at the fixed value.
- Parameters:
- Return type:
- Returns:
Sorted list of dimension indices where
bounds_lower[i] == bounds_upper[i].- Raises:
ValueError – If arrays have different lengths.
- heterodyne.optimization.nlsq.multistart.generate_lhs_starts(n_starts, bounds_lower, bounds_upper, seed=42)[source]
Generate Latin Hypercube starting points, excluding fixed dimensions.
Fixed dimensions (where
bounds_lower[i] == bounds_upper[i]) are detected viacheck_zero_volume_bounds()and excluded from LHS sampling. All starts receive the fixed value for those dimensions.- Parameters:
- Return type:
- Returns:
Array of shape
(n_starts, n_params)containing starting points.- Raises:
ValueError – If
n_starts < 1or bound arrays have mismatched shapes.
- heterodyne.optimization.nlsq.multistart.validate_n_starts_for_lhs(n_starts, n_params, warn=True)[source]
Validate n_starts for Latin Hypercube Sampling coverage.
For LHS to provide meaningful coverage, n_starts should be at least n_params. Very large n_starts relative to parameter space may produce redundant samples.
- heterodyne.optimization.nlsq.multistart.generate_random_starts(bounds, n_starts, seed=42)[source]
Generate starting points via random uniform sampling.
- Parameters:
- Returns:
Starting points as (n_starts, n_params) array.
- Return type:
- heterodyne.optimization.nlsq.multistart.include_custom_starts(generated_starts, custom_starts, bounds)[source]
Include user-provided custom starting points alongside generated starts.
Custom starting points are prepended to the generated starts so they are always included (not filtered by screening).
- Parameters:
generated_starts (
ndarray[tuple[Any,...],dtype[double]]) – Starting points generated by LHS or random sampling.custom_starts (
list[list[float]] |ndarray[tuple[Any,...],dtype[double]] |None) – User-provided custom starting points.bounds (
ndarray[tuple[Any,...],dtype[double]]) – Parameter bounds as (n_params, 2) array, used for validation.
- Returns:
Combined starting points with custom starts first.
- Return type:
- heterodyne.optimization.nlsq.multistart.screen_starts(cost_func, starts, keep_fraction=0.5, min_keep=3, n_workers=0)[source]
Pre-filter starting points by initial cost.
- Parameters:
cost_func (
Callable[[ndarray[tuple[Any,...],dtype[double]]],float]) – Function that computes cost (chi-squared) for a parameter vector.starts (
ndarray[tuple[Any,...],dtype[double]]) – Starting points as (n_starts, n_params) array.keep_fraction (
float) – Fraction of starting points to keep (0, 1].min_keep (
int) – Minimum number of starting points to keep.n_workers (
int) – Number of parallel workers for cost evaluation. 0 = auto (cpu_count - 1).
- Returns:
Filtered starting points and their initial costs (all costs, not just kept ones).
- Return type:
tuple[ndarray[tuple[Any,...],dtype[double]],ndarray[tuple[Any,...],dtype[double]]]
- heterodyne.optimization.nlsq.multistart.get_n_workers(config, n_starts)[source]
Determine number of parallel workers.
- heterodyne.optimization.nlsq.multistart.detect_degeneracy(results, chi_sq_threshold=0.1, param_threshold=0.2)[source]
Detect parameter degeneracy from multiple optimization results.
Input Validation & Fit Quality¶
Pre-fit and post-fit validation pipeline:
InputValidator— shape/dtype checks, finite-value guards, and initial-parameter bounds.BoundsValidator— physical parameter bound enforcement.ConvergenceValidator— convergence quality assessment.FitQualityConfig/FitQualityReport/validate_fit_quality()— homodyne-parity post-fit quality validator with configurable thresholds for chi-squared, residuals, and parameter bounds.ResultValidator— structuralNLSQResultquality checks.
NLSQ validation for heterodyne model optimization.
Provides pre-fit input validation and post-fit quality checks:
InputValidator: pre-fit checks for data, bounds, and initial parametersvalidate_array_dimensions,validate_no_nan_inf,validate_bounds_consistency,validate_initial_params: functional input validators (homodyne parity)ResultValidator: homodyne-parity result validator (bool API)validate_optimized_params,validate_covariance,validate_result_consistency: functional result validators (homodyne parity)BoundsValidator: checks parameters against physical boundsConvergenceValidator: assesses convergence qualityFitQualityConfig/FitQualityReport/validate_fit_quality: homodyne-parity post-fit quality validator with configurable thresholdsclassify_fit_quality: legacy 3-bin fit-quality classifierValidationIssue/ValidationReport/ValidationSeverity: structured report types (heterodyne-native)