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:

  1. If cmaes.enable: true → delegates to CMA-ES

  2. If multi_start.enable: true → delegates to multi-start

  3. Otherwise → 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_phi scaling 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_cmaes implementation. Raises ImportError if CMA-ES is not available and ValueError if 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 to NLSQConfig().

  • 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 (cma package) 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_multistart implementation. 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 to NLSQConfig().

  • 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 parameters

  • NLSQValidationConfig — post-fit validation thresholds

  • NLSQConfig — master configuration dataclass

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

Convert value to float, returning default on failure.

Parameters:
  • value (Any) – Arbitrary input that should be numeric.

  • default (float) – Fallback value when conversion fails.

Return type:

float

Returns:

float(value) on success, default otherwise.

heterodyne.optimization.nlsq.config.safe_int(value, default)[source]

Convert value to int, returning default on failure.

Parameters:
  • value (Any) – Arbitrary input that should be integral.

  • default (int) – Fallback value when conversion fails.

Return type:

int

Returns:

int(value) on success, default otherwise.

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

Bases: object

Progressive 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  ** k

  • regularisation : lambda_growth ** k

  • trust 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=0 returns the unscaled baseline (scale factor = 1).

Return type:

dict[str, float]

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: object

Thresholds 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: object

Master 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 that dogbox is coerced to trf by 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. None selects the solver default.

max_nfev

Hard cap on function evaluations. None is unlimited.

chunk_size

Number of q-points per processing chunk. None means 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. None uses 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", or None for solver default).

step_bound

Upper bound on the step norm relative to the trust radius. 0.0 defers to the solver default.

use_nlsq_library

Prefer the nlsq library 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
x_scale: str | list[float] = 'jac'
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
diff_step: float | None = None
max_nfev: int | None = None
chunk_size: int | None = None
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
x_scale_map: dict[str, float]
loss_weights: list[float] | None = None
loss_scale: float = 1.0
tr_solver: str | None = None
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_population_size: int | None = None
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_max_generations: int | None = None
cmaes_popsize: int | None = None
cmaes_sigma: float = 0.5
cmaes_sigma_warmstart: float = 0.05
cmaes_tol_fun: float = 1e-08
cmaes_tol_x: float = 1e-08
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
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_x_scale: str | ndarray = 'jac'
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:

None

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.

Return type:

list[str]

Returns:

List of human-readable error strings, one per violation found.

classmethod from_dict(config)[source]

Construct an NLSQConfig from 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.

Parameters:

config (dict[str, Any]) – Flat or nested configuration dictionary, e.g. loaded from a YAML file.

Return type:

NLSQConfig

Returns:

Fully populated NLSQConfig instance.

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.

Return type:

dict[str, Any]

Returns:

Fully populated dictionary representation.

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.nlsq section, and creates a validated NLSQConfig object.

Parameters:

yaml_path (str) – Path to YAML configuration file.

Return type:

NLSQConfig

Returns:

Validated NLSQConfig instance.

Raises:

Examples

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

Check if the configuration is valid.

Return type:

bool

Returns:

True if validate() returns an empty list, False otherwise.

to_workflow_kwargs()[source]

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

Maps NLSQConfig settings to NLSQ 0.6.10+ curve_fit() parameters. Heterodyne uses curve_fit() directly rather than the unified fit() API.

Returns:

ftol, gtol, xtol, max_nfev, loss, and optionally goal.

Return type:

dict[str, Any]

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, so workflow is 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: object

Result of NLSQ optimization.

Contains fitted parameters, uncertainties, and fit quality metrics.

parameters: ndarray
parameter_names: list[str]
success: bool
message: str
uncertainties: ndarray | None = None
covariance: ndarray | None = None
final_cost: float | None = None
reduced_chi_squared: float | None = None
n_iterations: int = 0
n_function_evals: int = 0
convergence_reason: str = ''
residuals: ndarray | None = None
jacobian: ndarray | None = None
fitted_correlation: ndarray | None = None
wall_time_seconds: float | None = None
metadata: dict[str, Any]
property n_params: int

Number of fitted parameters.

property params_dict: dict[str, float]

Parameters as dictionary.

get_param(name)[source]

Get parameter value by name.

Parameters:

name (str) – Parameter name

Return type:

float

Returns:

Parameter value

Raises:

KeyError – If parameter not found

get_uncertainty(name)[source]

Get uncertainty for parameter by name.

Parameters:

name (str) – Parameter name

Return type:

float | None

Returns:

Uncertainty or None if not available

get_correlation_matrix()[source]

Compute correlation matrix from covariance.

Return type:

ndarray | None

Returns:

Correlation matrix or None if covariance not available

validate()[source]

Validate result quality.

Return type:

list[str]

Returns:

List of warning/error messages

summary()[source]

Generate summary string.

Return type:

str

Returns:

Multi-line summary

__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: object

Wraps 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

fn: Callable[[...], Any]
count: int = 0
__call__(*args, **kwargs)[source]

Call the wrapped function and increment count.

Return type:

Any

__init__(fn, count=0)
class heterodyne.optimization.nlsq.results.OptimizationResult[source]

Bases: object

Complete optimization result with fit quality metrics and diagnostics.

Homodyne-parity class. This mirrors homodyne’s OptimizationResult and is returned by ResultBuilder.build(). Heterodyne’s primary result type is NLSQResult (richer API); OptimizationResult is 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:

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:

Any | None

nlsq_diagnostics

Additional NLSQ-specific diagnostics.

Type:

dict[str, Any] | None

sigma_is_default

True if sigma weights were defaulted (not user-supplied).

Type:

bool

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: Any | 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)
class heterodyne.optimization.nlsq.results.FallbackInfo[source]

Bases: object

Tracks 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
adapter_error: str | None = None
wrapper_error: str | None = None
to_dict()[source]

Convert to dict for inclusion in device_info.

Return type:

dict[str, Any]

__init__(fallback_occurred, adapter_used, adapter_error=None, wrapper_error=None)
class heterodyne.optimization.nlsq.results.UseSequentialOptimization[source]

Bases: object

Marker 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:

str

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: object

Cache 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
phi_angles: tuple[float, ...] | None
scaling_mode: str
callable_scope: object | None = None
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: object

A cached CurveFit instance with usage stats.

The model and model_func fields mirror homodyne’s CachedModel for API parity; in the heterodyne fitter-centric path they remain None and fitter carries the nlsq.CurveFit instance.

fitter: object
created_at: float
last_accessed: float
n_hits: int = 0
model: Any = None
model_func: Callable[[...], Any] | None = 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:

tuple[object, bool]

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, so model and model_func are returned as None here — callers that need a concrete fitter should use get_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:

tuple[Any, Callable[..., Any] | None, bool]

Returns:

Tuple of (model, model_func, cache_hit) where model and model_func are always None in 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:

int

Returns:

Number of models removed from the cache.

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

Return cache hit/miss/size statistics.

Return type:

dict[str, int]

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

Bases: object

Configuration 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: NLSQAdapterBase

Adapter for the nlsq library’s CurveFit optimizer.

Uses JAX-accelerated nonlinear least squares from the nlsq package. The fit() method calls nlsq.CurveFit directly — no scipy delegation. For pure-JAX residual functions, prefer fit_jax() which passes a JAX-traceable function to CurveFit.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 that NLSQAdapter() (homodyne-style, no names) works.

  • config (AdapterConfig | None) – Optional AdapterConfig for feature flags (parity with homodyne). When provided, enable_recovery and enable_stability are forwarded to the underlying CurveFit constructor if the installed nlsq version supports them.

property name: str

Name of the optimization backend.

supports_bounds()[source]

Whether this adapter supports bounded optimization.

Return type:

bool

supports_jacobian()[source]

Whether this adapter supports analytic Jacobian.

Return type:

bool

is_available()[source]

Check if the NLSQ CurveFit backend is available.

Return type:

bool

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() from memory.py instead. 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 by CurveFit.curve_fit and normalises the result via build_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 inside residual_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 exposes create_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) -> residuals that 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 CurveFit flength).

Return type:

NLSQResult

Returns:

NLSQResult with fit results.

class heterodyne.optimization.nlsq.adapter.NLSQWrapper[source]

Bases: NLSQAdapterBase

Stable 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_retries times before falling back.

__init__(parameter_names, enable_large_dataset=True, enable_recovery=True, max_retries=3)[source]

Initialise the wrapper.

Parameters:
  • parameter_names (list[str]) – Names of parameters being optimised, in order.

  • enable_large_dataset (bool) – Allow the LARGE tier when memory warrants it.

  • enable_recovery (bool) – Enable cross-tier fallback on failure.

  • max_retries (int) – Maximum per-tier retries before falling back.

property name: str

Name of the optimization backend.

supports_bounds()[source]

Whether this adapter supports bounded optimization.

Return type:

bool

supports_jacobian()[source]

Whether this adapter supports analytic Jacobian.

Return type:

bool

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. If None, uses defaults.

Return type:

NLSQAdapter

Returns:

NLSQAdapter instance.

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

Check if NLSQAdapter can be used.

Return type:

bool

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: object

Configuration 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
c0_bounds: tuple[float, float] = (0.01, 1.0)
ck_bounds: tuple[float, float] = (-0.2, 0.2)
o0_bounds: tuple[float, float] = (0.5, 1.5)
ok_bounds: tuple[float, float] = (-0.3, 0.3)
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: object

Handles 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:

int

n_coeffs

Total number of Fourier coefficients (contrast + offset).

Type:

int

n_coeffs_per_param

Coefficients per parameter type (contrast or offset).

Type:

int

use_fourier

Whether Fourier mode is active.

Type:

bool

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.

Return type:

ndarray | None

Returns:

Basis matrix of shape (n_phi, n_coeffs_per_param) if Fourier mode, None if independent mode. Satisfies: per_angle_values = B @ coeffs.

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:

tuple[ndarray, ndarray]

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.

Parameters:
  • contrast (ndarray) – Per-angle contrast values, shape (n_phi,).

  • offset (ndarray) – Per-angle offset values, shape (n_phi,).

Return type:

ndarray

Returns:

Fourier coefficients, shape (n_coeffs,).

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:

ndarray

Returns:

Jacobian matrix of shape (2*n_phi, n_coeffs).

get_bounds()[source]

Get bounds for Fourier coefficients.

Return type:

tuple[ndarray, ndarray]

Returns:

(lower, upper) each of shape (n_coeffs,).

get_initial_coefficients(contrast_init, offset_init)[source]

Get initial Fourier coefficients from initial values.

Parameters:
  • contrast_init (float | ndarray) – Initial contrast (scalar for uniform, array for per-angle).

  • offset_init (float | ndarray) – Initial offset (scalar for uniform, array for per-angle).

Return type:

ndarray

Returns:

Initial Fourier coefficients, shape (n_coeffs,).

get_coefficient_labels()[source]

Get parameter labels for Fourier coefficients.

Return type:

list[str]

Returns:

List of parameter labels, length n_coeffs.

to_fourier(per_angle_values)[source]

Convert a single per-angle array to Fourier coefficients.

Convenience method for one parameter group at a time.

Parameters:

per_angle_values (ndarray) – Per-angle values, shape (n_phi,).

Return type:

ndarray

Returns:

Fourier coefficients, shape (n_coeffs_per_param,).

from_fourier(fourier_coeffs)[source]

Convert Fourier coefficients to per-angle values for one group.

Parameters:

fourier_coeffs (ndarray) – Fourier coefficients, shape (n_coeffs_per_param,).

Return type:

ndarray

Returns:

Per-angle values, shape (n_phi,).

get_diagnostics()[source]

Get Fourier reparameterization diagnostics.

Return type:

dict

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-level cma library settings

  • fit_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: object

Configuration for CMA-ES optimization via the cma library.

sigma0

Initial step-size (standard deviation) for the search distribution. A good default is ~1/4 of the expected parameter range.

popsize

Population size. None lets 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
popsize: int | None = None
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
__post_init__()[source]

Validate configuration values.

Return type:

None

__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: object

Configuration for CMAESWrapper.

Mirrors homodyne’s CMAESWrapperConfig for structural parity.

preset

CMA-ES preset: “cmaes-fast” (50 gen), “cmaes” (100 gen), “cmaes-global” (200 gen).

Type:

str

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:

float

sigma_warmstart

Reduced sigma for warm-start (local refinement).

Type:

float

tol_fun

Function value tolerance for convergence.

Type:

float

tol_x

Parameter tolerance for convergence.

Type:

float

restart_strategy

Restart strategy: “none” or “bipop”.

Type:

str

max_restarts

Maximum number of BIPOP restarts.

Type:

int

refine_with_nlsq

Whether to refine CMA-ES solution with NLSQ TRF.

Type:

bool

normalize

Enable bounds-based parameter normalization.

Type:

bool

normalization_epsilon

Prevent division by zero in normalization.

Type:

float

preset: str = 'cmaes'
max_generations: int | None = None
popsize: int | None = None
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 CMAESWrapperConfig from NLSQConfig.

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: object

Result from CMA-ES optimization.

Matches homodyne’s CMAESResult for 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:

float

success

Whether optimization converged successfully.

Type:

bool

diagnostics

CMA-ES diagnostics (generations, evaluations, etc.).

Type:

dict

method_used

Method used: “cmaes” or “multi-start”.

Type:

str

nlsq_refined

Whether result was refined with NLSQ L-M.

Type:

bool

message

Convergence message.

Type:

str

parameters: ndarray
covariance: ndarray | None
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: object

Wrapper around cma.fmin2 for bounded parameter optimization.

Public API matches homodyne’s CMAESWrapper:

  • is_available() — check if cma library is installed

  • compute_scale_ratio() — max/min bound range ratio

  • should_use_cmaes() — scale-ratio based selection

  • fit() — run CMA-ES, return CMAESResult

Heterodyne-specific internal method:

  • fit_with_objective() — objective-fn based fit, returns NLSQResult

Parameters:
  • config (CMAESWrapperConfig | None) – CMA-ES wrapper configuration. Uses defaults if None.

  • parameter_names (list[str] | None) – Ordered parameter names (used in returned NLSQResult from fit_with_objective()).

__init__(config=None, parameter_names=None)[source]
property is_available: bool

Check if CMA-ES is available (cma library 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.

Parameters:

bounds (tuple[ndarray, ndarray]) – Lower and upper bounds as (lower, upper) arrays.

Returns:

Scale ratio (max_range / min_range).

Return type:

float

should_use_cmaes(bounds, scale_threshold=1000.0)[source]

Determine if CMA-ES should be used based on scale ratio.

Parameters:
  • bounds (tuple[ndarray, ndarray]) – Parameter bounds as (lower, upper) arrays.

  • scale_threshold (float) – Scale ratio threshold for CMA-ES selection. Default: 1000.

Returns:

True if CMA-ES should be used.

Return type:

bool

fit(model_func, xdata, ydata, p0, bounds, sigma=None, warmstart_chi2=None)[source]

Run CMA-ES global optimization.

Matches homodyne’s CMAESWrapper.fit signature exactly. Uses the cma library 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).

  • sigma (ndarray | None) – Data uncertainties (optional).

  • 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 triggers sigma_warmstart instead of sigma for the CMA-ES search.

Returns:

Optimization result with parameters, covariance, diagnostics.

Return type:

CMAESResult

Raises:

ImportError – If the cma package 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 objective f(x) -> float to 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 full NLSQResult with 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 NLSQResult populated from the CMA-ES solution.

Raises:

ImportError – If the cma package 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:
  • x (ndarray) – Parameter vector of shape (n_params,).

  • lower (ndarray) – Lower bound array of shape (n_params,).

  • upper (ndarray) – Upper bound array of shape (n_params,).

Return type:

ndarray

Returns:

Transformed array of shape (n_params,) with values in [0, 1]. Dimensions where lower == upper are 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:
  • x_norm (ndarray) – Normalized parameter vector of shape (n_params,) with values in [0, 1].

  • lower (ndarray) – Lower bound array of shape (n_params,).

  • upper (ndarray) – Upper bound array of shape (n_params,).

Return type:

ndarray

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.

Parameters:

bounds (tuple[ndarray, ndarray]) – (lower, upper) arrays, each shape (n_params,).

Return type:

tuple[int, int]

Returns:

(popsize, max_generations) tuple.

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:
  • base_objective (Callable[[ndarray], float]) – Original scalar objective f(x) -> float.

  • bounds (tuple[ndarray, ndarray]) – (lower, upper) arrays.

  • parameter_names (list[str]) – Parameter names matching the indices in x.

  • penalty_weight (float) – Strength of the penalty terms relative to the base cost.

Return type:

Callable[[ndarray], float]

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 objective f(x) -> float to minimize.

  • initial_params (ndarray) – Initial guess, shape (n_params,).

  • bounds (tuple[ndarray, ndarray]) – (lower, upper) arrays.

  • parameter_names (list[str] | None) – Ordered parameter names.

  • config (CMAESConfig | None) – Low-level CMAESConfig; None for 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 NLSQResult from 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:
  • cov (ndarray) – Covariance matrix of shape (n_params, n_params).

  • lower (ndarray) – Lower bound array of shape (n_params,).

  • upper (ndarray) – Upper bound array of shape (n_params,).

Return type:

ndarray

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: object

Configuration 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_starts exceeds this threshold.

enable: bool = False
n_starts: int = 10
seed: int | None = None
sampling_strategy: str = 'latin_hypercube'
custom_starts: list[list[float]] | None = None
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
max_workers: int | None = None
worker_timeout: float = 1800.0
max_data_points_for_parallel: int = 500000
classmethod from_dict(d)[source]

Create from dictionary, ignoring unknown keys.

Parameters:

d (dict[str, Any]) – Dictionary of configuration values.

Return type:

MultiStartConfig

Returns:

Populated MultiStartConfig instance.

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:

Any

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: object

Result from a single optimization start.

result

The NLSQResult returned 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: object

Aggregated result from a multi-start optimization run.

best_result

The NLSQResult with the lowest final cost.

all_starts

Ordered list of SingleStartResult for every start.

n_successful

Number of starts that reported success=True.

n_total

Total number of starts attempted.

config

The MultiStartConfig used 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
screening_costs: ndarray[tuple[Any, ...], dtype[float64]] | None = None
basin_labels: ndarray[tuple[Any, ...], dtype[int64]] | None = None
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_time was always 0.0 because fit() only set wall_time_total).

property all_results: list[NLSQResult]

Backward-compatible accessor returning all NLSQResult objects.

to_nlsq_result()[source]

Convert the best result to an NLSQResult with multistart metadata.

Attaches a "multistart" key to result.metadata containing summary statistics for the full multi-start run.

Return type:

NLSQResult

Returns:

The best NLSQResult with 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:

Any

__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: object

Multi-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 remaining n_starts - 1 points are drawn via Latin Hypercube Sampling within the supplied bounds.

Parallel execution is available via MultiStartConfig.parallel=True but 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.

Parameters:
  • adapter (NLSQAdapterBase) – NLSQ adapter used to run each individual fit.

  • config (MultiStartConfig | None) – Full MultiStartConfig. When provided, n_starts and seed keyword arguments are ignored.

  • n_starts (int | None) – Legacy argument — number of starting points.

  • seed (int | None) – Legacy argument — random seed.

generate_starting_points(initial_params, bounds)[source]

Generate starting points using Latin Hypercube Sampling.

The first point is always initial_params. The remaining n_starts - 1 points are drawn via LHS within bounds.

Parameters:
  • initial_params (ndarray) – User-provided initial values (used as first point).

  • bounds (tuple[ndarray, ndarray]) – (lower, upper) bound arrays of shape (n_params,).

Return type:

ndarray

Returns:

Array of shape (n_starts, n_params).

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:

MultiStartResult with 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:
  • bounds_lower (ndarray) – Lower bound array of shape (n_params,).

  • bounds_upper (ndarray) – Upper bound array of shape (n_params,).

Return type:

list[int]

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 via check_zero_volume_bounds() and excluded from LHS sampling. All starts receive the fixed value for those dimensions.

Parameters:
  • n_starts (int) – Number of starting points to generate.

  • bounds_lower (ndarray) – Lower bound array of shape (n_params,).

  • bounds_upper (ndarray) – Upper bound array of shape (n_params,).

  • seed (int) – Random seed for reproducibility.

Return type:

ndarray

Returns:

Array of shape (n_starts, n_params) containing starting points.

Raises:

ValueError – If n_starts < 1 or 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.

Parameters:
  • n_starts (int) – Requested number of starting points.

  • n_params (int) – Number of parameters (dimensions).

  • warn (bool) – Whether to emit warnings for suboptimal settings.

Returns:

Validated n_starts (unchanged if valid).

Return type:

int

heterodyne.optimization.nlsq.multistart.generate_random_starts(bounds, n_starts, seed=42)[source]

Generate starting points via random uniform sampling.

Parameters:
  • bounds (ndarray[tuple[Any, ...], dtype[double]]) – Parameter bounds as (n_params, 2) array.

  • n_starts (int) – Number of starting points to generate.

  • seed (int) – Random seed for reproducibility.

Returns:

Starting points as (n_starts, n_params) array.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

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:
Returns:

Combined starting points with custom starts first.

Return type:

ndarray[tuple[Any, ...], dtype[double]]

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.

Parameters:
  • config (MultiStartConfig) – Multi-start configuration.

  • n_starts (int) – Number of starting points.

Returns:

Number of workers to use.

Return type:

int

heterodyne.optimization.nlsq.multistart.detect_degeneracy(results, chi_sq_threshold=0.1, param_threshold=0.2)[source]

Detect parameter degeneracy from multiple optimization results.

Parameters:
  • results (list[SingleStartResult]) – List of optimization results (heterodyne SingleStartResult wrapping an inner NLSQResult).

  • chi_sq_threshold (float) – Maximum relative chi-squared difference to consider similar.

  • param_threshold (float) – Maximum relative parameter distance to consider same basin.

Returns:

(degeneracy_detected, n_unique_basins, basin_labels)

Return type:

tuple[bool, int, ndarray[tuple[Any, ...], dtype[int_]] | None]

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 — structural NLSQResult quality 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 parameters

  • validate_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 bounds

  • ConvergenceValidator: assesses convergence quality

  • FitQualityConfig / FitQualityReport / validate_fit_quality: homodyne-parity post-fit quality validator with configurable thresholds

  • classify_fit_quality: legacy 3-bin fit-quality classifier

  • ValidationIssue / ValidationReport / ValidationSeverity: structured report types (heterodyne-native)