Skip to main content

apex_solver/optimizer/
dog_leg.rs

1//! Dog Leg trust region optimization algorithm implementation.
2//!
3//! The Dog Leg method is a robust trust region algorithm for solving nonlinear least squares problems:
4//!
5//! ```text
6//! min f(x) = ½||r(x)||² = ½Σᵢ rᵢ(x)²
7//! ```
8//!
9//! where `r: ℝⁿ → ℝᵐ` is the residual vector function.
10//!
11//! # Algorithm Overview
12//!
13//! Powell's Dog Leg method constructs a piecewise linear path within a spherical trust region
14//! of radius Δ, connecting three key points:
15//!
16//! 1. **Origin** (current position)
17//! 2. **Cauchy Point** p_c = -α·g (optimal steepest descent step)
18//! 3. **Gauss-Newton Point** h_gn (full Newton step solving J^T·J·h = -J^T·r)
19//!
20//! The "dog leg" path travels from the origin to the Cauchy point, then from the Cauchy point
21//! toward the Gauss-Newton point, stopping at the trust region boundary.
22//!
23//! ## Step Selection Strategy
24//!
25//! Given trust region radius Δ, the algorithm selects a step h based on three cases:
26//!
27//! **Case 1: GN step inside trust region** (`||h_gn|| ≤ Δ`)
28//! ```text
29//! h = h_gn  (full Gauss-Newton step)
30//! ```
31//!
32//! **Case 2: Even Cauchy point outside trust region** (`||p_c|| ≥ Δ`)
33//! ```text
34//! h = (Δ / ||g||) · (-g)  (scaled steepest descent to boundary)
35//! ```
36//!
37//! **Case 3: Dog leg interpolation** (`||p_c|| < Δ < ||h_gn||`)
38//! ```text
39//! h(β) = p_c + β·(h_gn - p_c),  where β ∈ [0,1] satisfies ||h(β)|| = Δ
40//! ```
41//!
42//! ## Cauchy Point Computation
43//!
44//! The Cauchy point is the optimal step along the steepest descent direction -g:
45//!
46//! ```text
47//! α = (g^T·g) / (g^T·H·g)  where H = J^T·J
48//! p_c = -α·g
49//! ```
50//!
51//! This minimizes the quadratic model along the gradient direction.
52//!
53//! ## Trust Region Management
54//!
55//! The trust region radius Δ adapts based on the gain ratio:
56//!
57//! ```text
58//! ρ = (actual reduction) / (predicted reduction)
59//! ```
60//!
61//! **Good step** (`ρ > 0.75`): Increase radius `Δ ← max(Δ, 3·||h||)`
62//! **Poor step** (`ρ < 0.25`): Decrease radius `Δ ← Δ/2`
63//! **Moderate step**: Keep radius unchanged
64//!
65//! # Advanced Features (Ceres Solver Enhancements)
66//!
67//! This implementation includes several improvements from Google's Ceres Solver:
68//!
69//! ## 1. Adaptive μ Regularization
70//!
71//! Instead of solving `J^T·J·h = -J^T·r` directly, we solve:
72//!
73//! ```text
74//! (J^T·J + μI)·h = -J^T·r
75//! ```
76//!
77//! where μ adapts to handle ill-conditioned Hessians:
78//! - **Increases** (×10) when linear solve fails
79//! - **Decreases** (÷5) when steps are accepted
80//! - **Bounded** between `min_mu` (1e-8) and `max_mu` (1.0)
81//!
82//! Default `initial_mu = 1e-4` provides good numerical stability.
83//!
84//! ## 2. Numerically Robust Beta Computation
85//!
86//! When computing β for dog leg interpolation, solves: `||p_c + β·v||² = Δ²`
87//!
88//! Uses two formulas to avoid catastrophic cancellation:
89//! ```text
90//! If b ≤ 0:  β = (-b + √(b²-ac)) / a    (standard formula)
91//! If b > 0:  β = -c / (b + √(b²-ac))    (alternative, avoids cancellation)
92//! ```
93//!
94//! ## 3. Step Reuse Mechanism
95//!
96//! When a step is rejected, caches the Gauss-Newton step, Cauchy point, and gradient.
97//! On the next iteration (with smaller Δ), reuses these cached values instead of
98//! recomputing them. This avoids expensive linear solves when trust region shrinks.
99//!
100//! **Safety limits:**
101//! - Maximum 5 consecutive reuses before forcing fresh computation
102//! - Cache invalidated when steps are accepted (parameters have moved)
103//!
104//! ## 4. Jacobi Scaling (Diagonal Preconditioning)
105//!
106//! Optionally applies column scaling to J before forming J^T·J:
107//! ```text
108//! S_ii = 1 / (1 + ||J_i||)  where J_i is column i
109//! ```
110//!
111//! This creates an **elliptical trust region** instead of spherical, improving
112//! convergence for problems with mixed parameter scales.
113//!
114//! # Mathematical Background
115//!
116//! ## Why Dog Leg Works
117//!
118//! The dog leg path is a cheap approximation to the optimal trust region step
119//! (which would require solving a constrained optimization problem). It:
120//!
121//! 1. Exploits the fact that optimal steps often lie near the 2D subspace
122//!    spanned by the gradient and Gauss-Newton directions
123//! 2. Provides global convergence guarantees (always finds descent direction)
124//! 3. Achieves local quadratic convergence (like Gauss-Newton near solution)
125//! 4. Requires only one linear solve per iteration (same as Gauss-Newton)
126//!
127//! ## Convergence Properties
128//!
129//! - **Global convergence**: Guaranteed descent at each iteration
130//! - **Local quadratic convergence**: Reduces to Gauss-Newton near solution
131//! - **Robustness**: Handles ill-conditioning via trust region + μ regularization
132//! - **Efficiency**: Comparable cost to Gauss-Newton with better reliability
133//!
134//! # When to Use
135//!
136//! Dog Leg is an excellent choice when:
137//! - You want explicit control over step size (via trust region radius)
138//! - The problem may be ill-conditioned
139//! - You need guaranteed descent at each iteration
140//! - Initial guess may be poor but you want reliable convergence
141//!
142//! Compared to alternatives:
143//! - **vs Gauss-Newton**: More robust but similar computational cost
144//! - **vs Levenberg-Marquardt**: Explicit trust region vs implicit damping
145//! - Both Dog Leg and LM are excellent general-purpose choices
146//!
147//! # Examples
148//!
149//! ## Basic usage
150//!
151//! ```no_run
152//! use apex_solver::optimizer::DogLeg;
153//! use apex_solver::core::problem::Problem;
154//! use apex_solver::JacobianMode;
155//!
156//! # type TestResult = Result<(), Box<dyn std::error::Error>>;
157//! # fn main() -> TestResult {
158//! let mut problem = Problem::new(JacobianMode::Sparse);
159//! // ... add residual blocks (factors) to problem ...
160//!
161//! let mut solver = DogLeg::new();
162//! let result = solver.optimize(&mut problem)?;
163//! # Ok(())
164//! # }
165//! ```
166//!
167//! ## Advanced configuration with Ceres enhancements
168//!
169//! ```no_run
170//! use apex_solver::optimizer::dog_leg::{DogLegConfig, DogLeg};
171//! use apex_solver::linalg::LinearSolverType;
172//!
173//! # fn main() {
174//! let config = DogLegConfig::new()
175//!     .with_max_iterations(100)
176//!     .with_trust_region_radius(1e4)  // Large initial radius
177//!     .with_trust_region_bounds(1e-3, 1e6)  // Min/max radius
178//!     .with_mu_params(1e-4, 1e-8, 1.0, 10.0)  // Conservative regularization
179//!     .with_jacobi_scaling(true)  // Enable elliptical trust regions
180//!     .with_step_reuse(true);  // Enable Ceres-style caching
181//!
182//! let mut solver = DogLeg::with_config(config);
183//! # }
184//! ```
185//!
186//! # References
187//!
188//! - Powell, M. J. D. (1970). "A Hybrid Method for Nonlinear Equations". *Numerical Methods for Nonlinear Algebraic Equations*. Gordon and Breach.
189//! - Nocedal, J. & Wright, S. (2006). *Numerical Optimization* (2nd ed.). Springer. Chapter 4 (Trust Region Methods).
190//! - Madsen, K., Nielsen, H. B., & Tingleff, O. (2004). *Methods for Non-Linear Least Squares Problems* (2nd ed.). Chapter 6.
191//! - Conn, A. R., Gould, N. I. M., & Toint, P. L. (2000). *Trust-Region Methods*. SIAM.
192//! - Ceres Solver: <http://ceres-solver.org/> - Google's C++ nonlinear least squares library
193
194use crate::{core::problem, error, linalg, optimizer};
195use std::{fmt, time};
196use tracing::debug;
197
198use crate::linalg::{
199    DenseCholeskySolver, DenseMode, DenseQRSolver, JacobianMode, LinearSolver, LinearSolverType,
200    SparseCholeskySolver, SparseMode, SparseQRSolver,
201};
202use crate::optimizer::{AssemblyBackend, IterationStats};
203
204/// Configuration parameters for the Dog Leg trust region optimizer.
205///
206/// Controls trust region management, convergence criteria, adaptive regularization,
207/// and Ceres Solver enhancements for the Dog Leg algorithm.
208///
209/// # Builder Pattern
210///
211/// All configuration options can be set using the builder pattern:
212///
213/// ```
214/// use apex_solver::optimizer::dog_leg::DogLegConfig;
215///
216/// let config = DogLegConfig::new()
217///     .with_max_iterations(100)
218///     .with_trust_region_radius(1e4)
219///     .with_mu_params(1e-4, 1e-8, 1.0, 10.0)
220///     .with_jacobi_scaling(true)
221///     .with_step_reuse(true);
222/// ```
223///
224/// # Trust Region Behavior
225///
226/// The trust region radius Δ controls the maximum allowed step size:
227///
228/// - **Initial radius** (`trust_region_radius`): Starting value (default: 1e4)
229/// - **Bounds** (`trust_region_min`, `trust_region_max`): Valid range (default: 1e-3 to 1e6)
230/// - **Adaptation**: Increases for good steps, decreases for poor steps
231///
232/// # Adaptive μ Regularization (Ceres Enhancement)
233///
234/// Controls the regularization parameter in `(J^T·J + μI)·h = -J^T·r`:
235///
236/// - `initial_mu`: Starting value (default: 1e-4 for numerical stability)
237/// - `min_mu`, `max_mu`: Bounds (default: 1e-8 to 1.0)
238/// - `mu_increase_factor`: Multiplier when solve fails (default: 10.0)
239///
240/// # Convergence Criteria
241///
242/// The optimizer terminates when ANY of the following conditions is met:
243///
244/// - **Cost tolerance**: `|cost_k - cost_{k-1}| < cost_tolerance`
245/// - **Parameter tolerance**: `||step|| < parameter_tolerance`
246/// - **Gradient tolerance**: `||J^T·r|| < gradient_tolerance`
247/// - **Maximum iterations**: `iteration >= max_iterations`
248/// - **Timeout**: `elapsed_time >= timeout`
249///
250/// # See Also
251///
252/// - [`DogLeg`] - The solver that uses this configuration
253/// - [`LevenbergMarquardtConfig`](crate::optimizer::levenberg_marquardt::LevenbergMarquardtConfig) - Alternative damping approach
254/// - [`GaussNewtonConfig`](crate::optimizer::gauss_newton::GaussNewtonConfig) - Undamped variant
255#[derive(Clone)]
256pub struct DogLegConfig {
257    /// Type of linear solver for the linear systems
258    pub linear_solver_type: linalg::LinearSolverType,
259    /// Maximum number of iterations
260    pub max_iterations: usize,
261    /// Convergence tolerance for cost function
262    pub cost_tolerance: f64,
263    /// Convergence tolerance for parameter updates
264    pub parameter_tolerance: f64,
265    /// Convergence tolerance for gradient norm
266    pub gradient_tolerance: f64,
267    /// Timeout duration
268    pub timeout: Option<time::Duration>,
269    /// Initial trust region radius
270    pub trust_region_radius: f64,
271    /// Minimum trust region radius
272    pub trust_region_min: f64,
273    /// Maximum trust region radius
274    pub trust_region_max: f64,
275    /// Trust region increase factor (for good steps, rho > 0.75)
276    pub trust_region_increase_factor: f64,
277    /// Trust region decrease factor (for poor steps, rho < 0.25)
278    pub trust_region_decrease_factor: f64,
279    /// Minimum step quality for acceptance (typically 0.0)
280    pub min_step_quality: f64,
281    /// Good step quality threshold (typically 0.75)
282    pub good_step_quality: f64,
283    /// Poor step quality threshold (typically 0.25)
284    pub poor_step_quality: f64,
285    /// Use Jacobi column scaling (preconditioning)
286    pub use_jacobi_scaling: bool,
287
288    // Ceres-style adaptive mu regularization parameters
289    /// Initial mu regularization parameter for Gauss-Newton step
290    pub initial_mu: f64,
291    /// Minimum mu regularization parameter
292    pub min_mu: f64,
293    /// Maximum mu regularization parameter
294    pub max_mu: f64,
295    /// Factor to increase mu when linear solver fails
296    pub mu_increase_factor: f64,
297
298    // Ceres-style step reuse optimization
299    /// Enable step reuse after rejection (Ceres-style efficiency optimization)
300    pub enable_step_reuse: bool,
301
302    /// Minimum objective function cutoff (optional early termination)
303    ///
304    /// If set, optimization terminates when cost falls below this threshold.
305    /// Useful for early stopping when a "good enough" solution is acceptable.
306    ///
307    /// Default: None (disabled)
308    pub min_cost_threshold: Option<f64>,
309
310    /// Maximum condition number for Jacobian matrix (optional check)
311    ///
312    /// If set, the optimizer checks if condition_number(J^T*J) exceeds this
313    /// threshold and terminates with IllConditionedJacobian status.
314    /// Note: Computing condition number is expensive, so this is disabled by default.
315    ///
316    /// Default: None (disabled)
317    pub max_condition_number: Option<f64>,
318
319    /// Minimum relative cost decrease for step acceptance
320    ///
321    /// Used in computing step quality (rho = actual_reduction / predicted_reduction).
322    /// Steps with rho < min_relative_decrease are rejected. Matches Ceres Solver's
323    /// min_relative_decrease parameter.
324    ///
325    /// Default: 1e-3 (Ceres-compatible)
326    pub min_relative_decrease: f64,
327
328    /// Compute per-variable covariance matrices (uncertainty estimation)
329    ///
330    /// When enabled, computes covariance by inverting the Hessian matrix after
331    /// convergence. The full covariance matrix is extracted into per-variable
332    /// blocks stored in both Variable structs and optimizer::SolverResult.
333    ///
334    /// Default: false (to avoid performance overhead)
335    pub compute_covariances: bool,
336
337    /// Enable real-time visualization (graphical debugging).
338    ///
339    /// When enabled, optimization progress is logged to a Rerun viewer.
340    /// **Note:** Requires the `visualization` feature to be enabled in `Cargo.toml`.
341    ///
342    /// Default: false
343    #[cfg(feature = "visualization")]
344    pub enable_visualization: bool,
345}
346
347impl Default for DogLegConfig {
348    fn default() -> Self {
349        Self {
350            linear_solver_type: linalg::LinearSolverType::default(),
351            // Ceres Solver default: 50 (changed from 100 for compatibility)
352            max_iterations: 50,
353            // Ceres Solver default: 1e-6 (changed from 1e-8 for compatibility)
354            cost_tolerance: 1e-6,
355            // Ceres Solver default: 1e-8 (unchanged)
356            parameter_tolerance: 1e-8,
357            // Ceres Solver default: 1e-10 (changed from 1e-8 for compatibility)
358            gradient_tolerance: 1e-10,
359            timeout: None,
360            // Ceres-style: larger initial radius for better global convergence
361            trust_region_radius: 1e4,
362            trust_region_min: 1e-12,
363            trust_region_max: 1e12,
364            // Ceres uses adaptive increase (max(radius, 3*step_norm)),
365            // but we keep factor for simpler config
366            trust_region_increase_factor: 3.0,
367            trust_region_decrease_factor: 0.5,
368            min_step_quality: 0.0,
369            good_step_quality: 0.75,
370            poor_step_quality: 0.25,
371            // Ceres-style: Enable diagonal scaling by default for elliptical trust region
372            use_jacobi_scaling: true,
373
374            // Ceres-style adaptive mu regularization defaults
375            // Start with more conservative regularization to avoid singular Hessian
376            initial_mu: 1e-4,
377            min_mu: 1e-8,
378            max_mu: 1.0,
379            mu_increase_factor: 10.0,
380
381            // Ceres-style step reuse optimization
382            enable_step_reuse: true,
383
384            // New Ceres-compatible termination parameters
385            min_cost_threshold: None,
386            max_condition_number: None,
387            min_relative_decrease: 1e-3,
388
389            compute_covariances: false,
390            #[cfg(feature = "visualization")]
391            enable_visualization: false,
392        }
393    }
394}
395
396impl DogLegConfig {
397    /// Create a new Dog Leg configuration with default values.
398    pub fn new() -> Self {
399        Self::default()
400    }
401
402    /// Set the linear solver type
403    pub fn with_linear_solver_type(mut self, linear_solver_type: linalg::LinearSolverType) -> Self {
404        self.linear_solver_type = linear_solver_type;
405        self
406    }
407
408    /// Set the maximum number of iterations
409    pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
410        self.max_iterations = max_iterations;
411        self
412    }
413
414    /// Set the cost tolerance
415    pub fn with_cost_tolerance(mut self, cost_tolerance: f64) -> Self {
416        self.cost_tolerance = cost_tolerance;
417        self
418    }
419
420    /// Set the parameter tolerance
421    pub fn with_parameter_tolerance(mut self, parameter_tolerance: f64) -> Self {
422        self.parameter_tolerance = parameter_tolerance;
423        self
424    }
425
426    /// Set the gradient tolerance
427    pub fn with_gradient_tolerance(mut self, gradient_tolerance: f64) -> Self {
428        self.gradient_tolerance = gradient_tolerance;
429        self
430    }
431
432    /// Set the timeout duration
433    pub fn with_timeout(mut self, timeout: time::Duration) -> Self {
434        self.timeout = Some(timeout);
435        self
436    }
437
438    /// Set the initial trust region radius
439    pub fn with_trust_region_radius(mut self, radius: f64) -> Self {
440        self.trust_region_radius = radius;
441        self
442    }
443
444    /// Set the trust region radius bounds
445    pub fn with_trust_region_bounds(mut self, min: f64, max: f64) -> Self {
446        self.trust_region_min = min;
447        self.trust_region_max = max;
448        self
449    }
450
451    /// Set the trust region adjustment factors
452    pub fn with_trust_region_factors(mut self, increase: f64, decrease: f64) -> Self {
453        self.trust_region_increase_factor = increase;
454        self.trust_region_decrease_factor = decrease;
455        self
456    }
457
458    /// Set the trust region quality thresholds
459    pub fn with_step_quality_thresholds(
460        mut self,
461        min_quality: f64,
462        poor_quality: f64,
463        good_quality: f64,
464    ) -> Self {
465        self.min_step_quality = min_quality;
466        self.poor_step_quality = poor_quality;
467        self.good_step_quality = good_quality;
468        self
469    }
470
471    /// Enable or disable Jacobi column scaling (preconditioning)
472    pub fn with_jacobi_scaling(mut self, use_jacobi_scaling: bool) -> Self {
473        self.use_jacobi_scaling = use_jacobi_scaling;
474        self
475    }
476
477    /// Set adaptive mu regularization parameters (Ceres-style)
478    pub fn with_mu_params(
479        mut self,
480        initial_mu: f64,
481        min_mu: f64,
482        max_mu: f64,
483        increase_factor: f64,
484    ) -> Self {
485        self.initial_mu = initial_mu;
486        self.min_mu = min_mu;
487        self.max_mu = max_mu;
488        self.mu_increase_factor = increase_factor;
489        self
490    }
491
492    /// Enable or disable step reuse optimization (Ceres-style)
493    pub fn with_step_reuse(mut self, enable_step_reuse: bool) -> Self {
494        self.enable_step_reuse = enable_step_reuse;
495        self
496    }
497
498    /// Set minimum objective function cutoff for early termination.
499    ///
500    /// When set, optimization terminates with MinCostThresholdReached status
501    /// if the cost falls below this threshold. Useful for early stopping when
502    /// a "good enough" solution is acceptable.
503    pub fn with_min_cost_threshold(mut self, min_cost: f64) -> Self {
504        self.min_cost_threshold = Some(min_cost);
505        self
506    }
507
508    /// Set maximum condition number for Jacobian matrix.
509    ///
510    /// If set, the optimizer checks if condition_number(J^T*J) exceeds this
511    /// threshold and terminates with IllConditionedJacobian status.
512    /// Note: Computing condition number is expensive, disabled by default.
513    pub fn with_max_condition_number(mut self, max_cond: f64) -> Self {
514        self.max_condition_number = Some(max_cond);
515        self
516    }
517
518    /// Set minimum relative cost decrease for step acceptance.
519    ///
520    /// Steps with rho = (actual_reduction / predicted_reduction) below this
521    /// threshold are rejected. Default: 1e-3 (Ceres-compatible)
522    pub fn with_min_relative_decrease(mut self, min_decrease: f64) -> Self {
523        self.min_relative_decrease = min_decrease;
524        self
525    }
526
527    /// Enable or disable covariance computation (uncertainty estimation).
528    ///
529    /// When enabled, computes the full covariance matrix by inverting the Hessian
530    /// after convergence, then extracts per-variable covariance blocks.
531    pub fn with_compute_covariances(mut self, compute_covariances: bool) -> Self {
532        self.compute_covariances = compute_covariances;
533        self
534    }
535
536    /// Enable real-time visualization.
537    ///
538    /// **Note:** Requires the `visualization` feature to be enabled in `Cargo.toml`.
539    ///
540    /// # Arguments
541    ///
542    /// * `enable` - Whether to enable visualization
543    #[cfg(feature = "visualization")]
544    pub fn with_visualization(mut self, enable: bool) -> Self {
545        self.enable_visualization = enable;
546        self
547    }
548
549    /// Print configuration parameters (info level logging)
550    pub fn print_configuration(&self) {
551        debug!(
552            "Configuration:\n  Solver:        Dog-Leg\n  Linear solver: {:?}\n  Loss function: N/A\n\nConvergence Criteria:\n  Max iterations:      {}\n  Cost tolerance:      {:.2e}\n  Parameter tolerance: {:.2e}\n  Gradient tolerance:  {:.2e}\n  Timeout:             {:?}\n\nTrust Region:\n  Initial radius:      {:.2e}\n  Radius range:        [{:.2e}, {:.2e}]\n  Min step quality:    {:.2}\n  Good step quality:   {:.2}\n  Poor step quality:   {:.2}\n\nRegularization:\n  Initial mu:          {:.2e}\n  Mu range:            [{:.2e}, {:.2e}]\n  Mu increase factor:  {:.2}\n\nNumerical Settings:\n  Jacobi scaling:      {}\n  Step reuse:          {}\n  Compute covariances: {}",
553            self.linear_solver_type,
554            self.max_iterations,
555            self.cost_tolerance,
556            self.parameter_tolerance,
557            self.gradient_tolerance,
558            self.timeout,
559            self.trust_region_radius,
560            self.trust_region_min,
561            self.trust_region_max,
562            self.min_step_quality,
563            self.good_step_quality,
564            self.poor_step_quality,
565            self.initial_mu,
566            self.min_mu,
567            self.max_mu,
568            self.mu_increase_factor,
569            if self.use_jacobi_scaling {
570                "enabled"
571            } else {
572                "disabled"
573            },
574            if self.enable_step_reuse {
575                "enabled"
576            } else {
577                "disabled"
578            },
579            if self.compute_covariances {
580                "enabled"
581            } else {
582                "disabled"
583            }
584        );
585    }
586}
587
588/// Result from step computation
589struct StepResult {
590    step: faer::Mat<f64>,
591    gradient_norm: f64,
592    predicted_reduction: f64,
593}
594
595/// Type of step taken
596#[derive(Debug, Clone, Copy)]
597enum StepType {
598    /// Full Gauss-Newton step
599    GaussNewton,
600    /// Scaled steepest descent (Cauchy point)
601    SteepestDescent,
602    /// Dog leg interpolation
603    DogLeg,
604}
605
606impl fmt::Display for StepType {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        match self {
609            StepType::GaussNewton => write!(f, "GN"),
610            StepType::SteepestDescent => write!(f, "SD"),
611            StepType::DogLeg => write!(f, "DL"),
612        }
613    }
614}
615
616/// Result from step evaluation
617struct StepEvaluation {
618    accepted: bool,
619    cost_reduction: f64,
620    rho: f64,
621}
622
623/// Dog Leg trust region solver for nonlinear least squares optimization.
624///
625/// Implements Powell's Dog Leg algorithm with Ceres Solver enhancements including
626/// adaptive μ regularization, numerically robust beta computation, and step reuse caching.
627///
628/// # Algorithm
629///
630/// At each iteration k:
631/// 1. Compute residual `r(xₖ)` and Jacobian `J(xₖ)`
632/// 2. Solve for Gauss-Newton step: `(J^T·J + μI)·h_gn = -J^T·r`
633/// 3. Compute steepest descent direction: `-g` where `g = J^T·r`
634/// 4. Compute Cauchy point: `p_c = -α·g` (optimal along steepest descent)
635/// 5. Construct dog leg step based on trust region radius Δ:
636///    - If `||h_gn|| ≤ Δ`: Take full GN step
637///    - Else if `||p_c|| ≥ Δ`: Take scaled SD to boundary
638///    - Else: Interpolate `h = p_c + β·(h_gn - p_c)` where `||h|| = Δ`
639/// 6. Evaluate gain ratio: `ρ = (actual reduction) / (predicted reduction)`
640/// 7. Update trust region radius based on ρ
641/// 8. Accept/reject step and update parameters
642///
643/// # Ceres Solver Enhancements
644///
645/// This implementation includes four major improvements from Google's Ceres Solver:
646///
647/// **1. Adaptive μ Regularization:** Dynamically adjusts regularization parameter
648/// to handle ill-conditioned Hessians (increases on failure, decreases on success).
649///
650/// **2. Numerically Robust Beta:** Uses two formulas for computing dog leg
651/// interpolation parameter β to avoid catastrophic cancellation.
652///
653/// **3. Step Reuse Mechanism:** Caches GN step, Cauchy point, and gradient when
654/// steps are rejected. Limited to 5 consecutive reuses to prevent staleness.
655///
656/// **4. Jacobi Scaling:** Optional diagonal preconditioning creates elliptical
657/// trust regions for better handling of mixed-scale problems.
658///
659/// # Examples
660///
661/// ```no_run
662/// use apex_solver::optimizer::DogLeg;
663/// use apex_solver::core::problem::Problem;
664/// use apex_solver::JacobianMode;
665///
666/// # type TestResult = Result<(), Box<dyn std::error::Error>>;
667/// # fn main() -> TestResult {
668/// let mut problem = Problem::new(JacobianMode::Sparse);
669/// // ... add factors to problem ...
670///
671/// let mut solver = DogLeg::new();
672/// let result = solver.optimize(&mut problem)?;
673/// # Ok(())
674/// # }
675/// ```
676///
677/// # See Also
678///
679/// - [`DogLegConfig`] - Configuration options
680/// - [`LevenbergMarquardt`](crate::optimizer::LevenbergMarquardt) - Alternative adaptive damping
681/// - [`GaussNewton`](crate::optimizer::GaussNewton) - Undamped variant
682pub struct DogLeg {
683    config: DogLegConfig,
684    jacobi_scaling: Option<Vec<f64>>,
685    observers: optimizer::OptObserverVec,
686
687    // Adaptive mu regularization (Ceres-style)
688    mu: f64,
689    min_mu: f64,
690    max_mu: f64,
691    mu_increase_factor: f64,
692
693    // Step reuse mechanism (Ceres-style efficiency optimization)
694    reuse_step_on_rejection: bool,
695    cached_gn_step: Option<faer::Mat<f64>>,
696    cached_cauchy_point: Option<faer::Mat<f64>>,
697    cached_gradient: Option<faer::Mat<f64>>,
698    cached_alpha: Option<f64>,
699    cache_reuse_count: usize, // Track consecutive reuses to prevent staleness
700}
701
702impl Default for DogLeg {
703    fn default() -> Self {
704        Self::new()
705    }
706}
707
708impl DogLeg {
709    /// Create a new Dog Leg solver with default configuration.
710    pub fn new() -> Self {
711        Self::with_config(DogLegConfig::default())
712    }
713
714    /// Create a new Dog Leg solver with the given configuration.
715    pub fn with_config(config: DogLegConfig) -> Self {
716        Self {
717            // Initialize adaptive mu from config
718            mu: config.initial_mu,
719            min_mu: config.min_mu,
720            max_mu: config.max_mu,
721            mu_increase_factor: config.mu_increase_factor,
722
723            // Initialize step reuse mechanism (disabled initially, enabled after first rejection)
724            reuse_step_on_rejection: false,
725            cached_gn_step: None,
726            cached_cauchy_point: None,
727            cached_gradient: None,
728            cached_alpha: None,
729            cache_reuse_count: 0,
730
731            config,
732            jacobi_scaling: None,
733            observers: optimizer::OptObserverVec::new(),
734        }
735    }
736
737    /// Add an observer to the solver.
738    ///
739    /// Observers are notified at each iteration with the current variable values.
740    /// This enables real-time visualization, logging, metrics collection, etc.
741    ///
742    /// # Examples
743    ///
744    /// ```no_run
745    /// use apex_solver::optimizer::DogLeg;
746    /// # use apex_solver::optimizer::OptObserver;
747    /// # use apex_solver::core::VarKey;
748    /// # use apex_solver::core::variable::ManifoldVariable;
749    /// # use slotmap::SlotMap;
750    ///
751    /// # struct MyObserver;
752    /// # impl OptObserver for MyObserver {
753    /// #     fn on_step(&self, _: &SlotMap<VarKey, Box<dyn ManifoldVariable>>, _: usize) {}
754    /// # }
755    /// let mut solver = DogLeg::new();
756    /// solver.add_observer(MyObserver);
757    /// ```
758    pub fn add_observer(&mut self, observer: impl optimizer::OptObserver + 'static) {
759        self.observers.add(observer);
760    }
761
762    /// Compute Cauchy point and optimal step length for steepest descent (generic).
763    ///
764    /// Returns (alpha, cauchy_point) where:
765    /// - alpha: optimal step length α = ||g||² / (g^T H g)
766    /// - cauchy_point: p_c = -α * g (the Cauchy point)
767    fn compute_cauchy_point_and_alpha_generic<M: AssemblyBackend>(
768        &self,
769        gradient: &faer::Mat<f64>,
770        hessian: &M::Hessian,
771    ) -> (f64, faer::Mat<f64>) {
772        // Optimal step size along steepest descent: α = (g^T*g) / (g^T*H*g)
773        let g_norm_sq_mat = gradient.transpose() * gradient;
774        let g_norm_sq = g_norm_sq_mat[(0, 0)];
775
776        let h_g = M::hessian_vec_product(hessian, gradient);
777        let g_h_g_mat = gradient.transpose() * &h_g;
778        let g_h_g = g_h_g_mat[(0, 0)];
779
780        // Avoid division by zero
781        let alpha = if g_h_g.abs() > 1e-15 {
782            g_norm_sq / g_h_g
783        } else {
784            1.0
785        };
786
787        // Compute Cauchy point: p_c = -α * gradient
788        let mut cauchy_point = faer::Mat::zeros(gradient.nrows(), 1);
789        for i in 0..gradient.nrows() {
790            cauchy_point[(i, 0)] = -alpha * gradient[(i, 0)];
791        }
792
793        (alpha, cauchy_point)
794    }
795
796    /// Compute the dog leg step using Powell's Dog Leg method
797    ///
798    /// The dog leg path consists of two segments:
799    /// 1. From origin to Cauchy point (optimal along steepest descent)
800    /// 2. From Cauchy point to Gauss-Newton step
801    ///
802    /// Arguments:
803    /// - steepest_descent_dir: -gradient (steepest descent direction, not scaled)
804    /// - cauchy_point: p_c = α * (-gradient), the optimal steepest descent step
805    /// - h_gn: Gauss-Newton step
806    /// - delta: Trust region radius
807    ///
808    /// Returns: (step, step_type)
809    fn compute_dog_leg_step(
810        &self,
811        steepest_descent_dir: &faer::Mat<f64>,
812        cauchy_point: &faer::Mat<f64>,
813        h_gn: &faer::Mat<f64>,
814        delta: f64,
815    ) -> (faer::Mat<f64>, StepType) {
816        let gn_norm = h_gn.norm_l2();
817        let cauchy_norm = cauchy_point.norm_l2();
818        let sd_norm = steepest_descent_dir.norm_l2();
819
820        // Case 1: Full Gauss-Newton step fits in trust region
821        if gn_norm <= delta {
822            return (h_gn.clone(), StepType::GaussNewton);
823        }
824
825        // Case 2: Even Cauchy point is outside trust region
826        // Scale steepest descent direction to boundary: (delta / ||δ_sd||) * δ_sd
827        if cauchy_norm >= delta {
828            let mut scaled_sd = faer::Mat::zeros(steepest_descent_dir.nrows(), 1);
829            let scale = delta / sd_norm;
830            for i in 0..steepest_descent_dir.nrows() {
831                scaled_sd[(i, 0)] = steepest_descent_dir[(i, 0)] * scale;
832            }
833            return (scaled_sd, StepType::SteepestDescent);
834        }
835
836        // Case 3: Dog leg interpolation between Cauchy point and GN step
837        // Use Ceres-style numerically robust formula
838        //
839        // Following Ceres solver implementation for numerical stability:
840        // Compute intersection of trust region boundary with line from Cauchy point to GN step
841        //
842        // Let v = δ_gn - p_c
843        // Solve: ||p_c + β*v||² = delta²
844        // This gives: a*β² + 2*b*β + c = 0
845        // where:
846        //   a = v^T·v = ||v||²
847        //   b = p_c^T·v
848        //   c = p_c^T·p_c - delta² = ||p_c||² - delta²
849
850        let mut v = faer::Mat::zeros(cauchy_point.nrows(), 1);
851        for i in 0..cauchy_point.nrows() {
852            v[(i, 0)] = h_gn[(i, 0)] - cauchy_point[(i, 0)];
853        }
854
855        // Compute coefficients
856        let v_squared_norm = v.transpose() * &v;
857        let a = v_squared_norm[(0, 0)];
858
859        let pc_dot_v = cauchy_point.transpose() * &v;
860        let b = pc_dot_v[(0, 0)];
861
862        let c = cauchy_norm * cauchy_norm - delta * delta;
863
864        // Ceres-style numerically robust beta computation
865        // Uses two different formulas based on sign of b to avoid catastrophic cancellation
866        let d_squared = b * b - a * c;
867
868        let beta = if d_squared < 0.0 {
869            // Should not happen geometrically, but handle gracefully
870            1.0
871        } else if a.abs() < 1e-15 {
872            // Degenerate case: v is nearly zero
873            1.0
874        } else {
875            let d = d_squared.sqrt();
876
877            // Ceres formula: choose formula based on sign of b to avoid cancellation
878            // If b <= 0: beta = (-b + d) / a  (standard formula, no cancellation)
879            // If b > 0:  beta = -c / (b + d)  (alternative formula, avoids cancellation)
880            if b <= 0.0 { (-b + d) / a } else { -c / (b + d) }
881        };
882
883        // Clamp beta to [0, 1] for safety
884        let beta = beta.clamp(0.0, 1.0);
885
886        // Compute dog leg step: p_dl = p_c + β*(δ_gn - p_c)
887        let mut dog_leg = faer::Mat::zeros(cauchy_point.nrows(), 1);
888        for i in 0..cauchy_point.nrows() {
889            dog_leg[(i, 0)] = cauchy_point[(i, 0)] + beta * v[(i, 0)];
890        }
891
892        (dog_leg, StepType::DogLeg)
893    }
894
895    /// Update trust region radius based on step quality (Ceres-style)
896    fn update_trust_region(&mut self, rho: f64, step_norm: f64) -> bool {
897        if rho > self.config.good_step_quality {
898            // Good step, increase trust region (Ceres-style: max(radius, 3*step_norm))
899            let new_radius = self.config.trust_region_radius.max(3.0 * step_norm);
900            self.config.trust_region_radius = new_radius.min(self.config.trust_region_max);
901
902            // Decrease mu on successful step (Ceres-style adaptive regularization)
903            self.mu = (self.mu / (0.5 * self.mu_increase_factor)).max(self.min_mu);
904
905            // Clear reuse flag and invalidate cache on acceptance (parameters have moved)
906            self.reuse_step_on_rejection = false;
907            self.cached_gn_step = None;
908            self.cached_cauchy_point = None;
909            self.cached_gradient = None;
910            self.cached_alpha = None;
911            self.cache_reuse_count = 0;
912
913            true
914        } else if rho < self.config.poor_step_quality {
915            // Poor step, decrease trust region
916            self.config.trust_region_radius = (self.config.trust_region_radius
917                * self.config.trust_region_decrease_factor)
918                .max(self.config.trust_region_min);
919
920            // Enable step reuse flag for next iteration (Ceres-style)
921            self.reuse_step_on_rejection = self.config.enable_step_reuse;
922
923            false
924        } else {
925            // Moderate step, keep trust region unchanged
926            // Clear reuse flag and invalidate cache on acceptance (parameters have moved)
927            self.reuse_step_on_rejection = false;
928            self.cached_gn_step = None;
929            self.cached_cauchy_point = None;
930            self.cached_gradient = None;
931            self.cached_alpha = None;
932            self.cache_reuse_count = 0;
933
934            true
935        }
936    }
937
938    /// Compute predicted cost reduction from linear model (generic over assembly mode).
939    fn compute_predicted_reduction_generic<M: AssemblyBackend>(
940        &self,
941        step: &faer::Mat<f64>,
942        gradient: &faer::Mat<f64>,
943        hessian: &M::Hessian,
944    ) -> f64 {
945        // Dog Leg predicted reduction: -step^T * gradient - 0.5 * step^T * H * step
946        let linear_term = step.transpose() * gradient;
947        let hessian_step = M::hessian_vec_product(hessian, step);
948        let quadratic_term = step.transpose() * &hessian_step;
949
950        -linear_term[(0, 0)] - 0.5 * quadratic_term[(0, 0)]
951    }
952
953    /// Compute dog leg optimization step (generic over assembly mode).
954    fn compute_optimization_step_generic<M: AssemblyBackend>(
955        &mut self,
956        residuals: &faer::Mat<f64>,
957        scaled_jacobian: &M::Jacobian,
958        linear_solver: &mut dyn LinearSolver<M>,
959    ) -> Option<StepResult> {
960        // Check if we can reuse cached step (Ceres-style optimization)
961        // Safety limit: prevent excessive reuse that could lead to stale gradient/Hessian
962        const MAX_CACHE_REUSE: usize = 5;
963
964        if self.reuse_step_on_rejection
965            && self.config.enable_step_reuse
966            && self.cache_reuse_count < MAX_CACHE_REUSE
967            && let (Some(cached_gn), Some(cached_cauchy), Some(cached_grad), Some(_cached_a)) = (
968                &self.cached_gn_step,
969                &self.cached_cauchy_point,
970                &self.cached_gradient,
971                &self.cached_alpha,
972            )
973        {
974            // Increment reuse counter
975            self.cache_reuse_count += 1;
976
977            let gradient_norm = cached_grad.norm_l2();
978            let mut steepest_descent = faer::Mat::zeros(cached_grad.nrows(), 1);
979            for i in 0..cached_grad.nrows() {
980                steepest_descent[(i, 0)] = -cached_grad[(i, 0)];
981            }
982
983            let (scaled_step, _step_type) = self.compute_dog_leg_step(
984                &steepest_descent,
985                cached_cauchy,
986                cached_gn,
987                self.config.trust_region_radius,
988            );
989
990            let step = if self.config.use_jacobi_scaling {
991                let scaling = self.jacobi_scaling.as_ref()?;
992                M::apply_inverse_scaling(&scaled_step, scaling)
993            } else {
994                scaled_step.clone()
995            };
996
997            // For cached reuse, we need the hessian for predicted reduction.
998            // Use hessian from linear solver if available.
999            let hessian = linear_solver.get_hessian()?;
1000            let predicted_reduction =
1001                self.compute_predicted_reduction_generic::<M>(&scaled_step, cached_grad, hessian);
1002
1003            return Some(StepResult {
1004                step,
1005                gradient_norm,
1006                predicted_reduction,
1007            });
1008        }
1009
1010        // Not reusing, compute fresh step
1011        // 1. Solve for Gauss-Newton step with adaptive mu regularization (Ceres-style)
1012        let residuals_owned = residuals.as_ref().to_owned();
1013        let mut scaled_gn_step = None;
1014        let mut mu_attempts = 0;
1015
1016        // Try to solve with current mu, increasing if necessary
1017        while mu_attempts < 10 && self.mu <= self.max_mu {
1018            let damping = self.mu;
1019
1020            if let Ok(step) =
1021                linear_solver.solve_augmented_equation(&residuals_owned, scaled_jacobian, damping)
1022            {
1023                scaled_gn_step = Some(step);
1024                break;
1025            }
1026
1027            // Increase mu (Ceres-style)
1028            self.mu = (self.mu * self.mu_increase_factor).min(self.max_mu);
1029            mu_attempts += 1;
1030        }
1031
1032        let scaled_gn_step = scaled_gn_step?;
1033
1034        // 2. Get gradient and Hessian (cached by solve_augmented_equation)
1035        let gradient = linear_solver.get_gradient()?;
1036        let hessian = linear_solver.get_hessian()?;
1037        let gradient_norm = gradient.norm_l2();
1038
1039        // 3. Compute steepest descent direction: δ_sd = -gradient
1040        let mut steepest_descent = faer::Mat::zeros(gradient.nrows(), 1);
1041        for i in 0..gradient.nrows() {
1042            steepest_descent[(i, 0)] = -gradient[(i, 0)];
1043        }
1044
1045        // 4. Compute Cauchy point and optimal step length
1046        let (alpha, cauchy_point) =
1047            self.compute_cauchy_point_and_alpha_generic::<M>(gradient, hessian);
1048
1049        // 5. Compute dog leg step based on trust region radius
1050        let (scaled_step, _step_type) = self.compute_dog_leg_step(
1051            &steepest_descent,
1052            &cauchy_point,
1053            &scaled_gn_step,
1054            self.config.trust_region_radius,
1055        );
1056
1057        // 6. Apply inverse Jacobi scaling if enabled
1058        let step = if self.config.use_jacobi_scaling {
1059            let scaling = self.jacobi_scaling.as_ref()?;
1060            M::apply_inverse_scaling(&scaled_step, scaling)
1061        } else {
1062            scaled_step.clone()
1063        };
1064
1065        // 7. Compute predicted reduction
1066        let predicted_reduction =
1067            self.compute_predicted_reduction_generic::<M>(&scaled_step, gradient, hessian);
1068
1069        // 8. Cache step components for potential reuse (Ceres-style)
1070        self.cached_gn_step = Some(scaled_gn_step.clone());
1071        self.cached_cauchy_point = Some(cauchy_point.clone());
1072        self.cached_gradient = Some(gradient.clone());
1073        self.cached_alpha = Some(alpha);
1074
1075        Some(StepResult {
1076            step,
1077            gradient_norm,
1078            predicted_reduction,
1079        })
1080    }
1081
1082    /// Evaluate and apply step
1083    fn evaluate_and_apply_step(
1084        &mut self,
1085        step_result: &StepResult,
1086        state: &mut optimizer::InitializedState,
1087        problem: &problem::Problem,
1088    ) -> error::ApexSolverResult<StepEvaluation> {
1089        // Apply parameter updates
1090        let step_norm = optimizer::apply_parameter_step(
1091            &mut state.variables,
1092            step_result.step.as_ref(),
1093            &state.sorted_vars,
1094        );
1095
1096        // Compute new cost (residual only, no Jacobian needed for step evaluation)
1097        let new_residual = problem.compute_residual_sparse(&state.variables)?;
1098        let new_cost = optimizer::compute_cost(&new_residual);
1099
1100        // Compute step quality
1101        let rho = optimizer::compute_step_quality(
1102            state.current_cost,
1103            new_cost,
1104            step_result.predicted_reduction,
1105        );
1106
1107        // Update trust region and decide acceptance
1108        // Filter out numerical noise with small threshold
1109        let accepted = rho > 1e-4;
1110        let _trust_region_updated = self.update_trust_region(rho, step_norm);
1111
1112        let cost_reduction = if accepted {
1113            let reduction = state.current_cost - new_cost;
1114            state.current_cost = new_cost;
1115            reduction
1116        } else {
1117            // Reject step - revert changes
1118            optimizer::apply_negative_parameter_step(
1119                &mut state.variables,
1120                step_result.step.as_ref(),
1121                &state.sorted_vars,
1122            );
1123            0.0
1124        };
1125
1126        Ok(StepEvaluation {
1127            accepted,
1128            cost_reduction,
1129            rho,
1130        })
1131    }
1132
1133    /// Run optimization using the specified assembly mode and linear solver.
1134    fn optimize_with_mode<M: AssemblyBackend>(
1135        &mut self,
1136        problem: &mut problem::Problem,
1137        linear_solver: &mut dyn LinearSolver<M>,
1138    ) -> optimizer::OptimizeResult {
1139        let start_time = time::Instant::now();
1140        let mut iteration = 0;
1141        let mut cost_evaluations = 1;
1142        let mut jacobian_evaluations = 0;
1143        let mut successful_steps = 0;
1144        let mut unsuccessful_steps = 0;
1145
1146        let mut state = optimizer::initialize_optimization_state(problem)?;
1147
1148        let mut max_gradient_norm: f64 = 0.0;
1149        let mut max_parameter_update_norm: f64 = 0.0;
1150        let mut total_cost_reduction = 0.0;
1151        let mut final_gradient_norm;
1152        let mut final_parameter_update_norm;
1153
1154        // Initialize iteration statistics tracking
1155        let mut iteration_stats = Vec::with_capacity(self.config.max_iterations);
1156        let mut previous_cost = state.current_cost;
1157
1158        // Print configuration and header if debug level is enabled
1159        if tracing::enabled!(tracing::Level::DEBUG) {
1160            self.config.print_configuration();
1161            IterationStats::print_header();
1162        }
1163
1164        loop {
1165            let iter_start = time::Instant::now();
1166
1167            // Evaluate residuals and Jacobian using the assembly mode
1168            let (residuals, jacobian) = M::assemble(
1169                problem,
1170                &state.variables,
1171                &state.variable_index_map,
1172                state.symbolic_structure.as_ref(),
1173                state.total_dof,
1174            )?;
1175            jacobian_evaluations += 1;
1176
1177            // Process Jacobian (apply scaling if enabled)
1178            let scaled_jacobian = if self.config.use_jacobi_scaling {
1179                optimizer::process_jacobian_generic::<M>(
1180                    &jacobian,
1181                    &mut self.jacobi_scaling,
1182                    iteration,
1183                )?
1184            } else {
1185                jacobian
1186            };
1187
1188            // Compute dog leg step
1189            let step_result = match self.compute_optimization_step_generic::<M>(
1190                &residuals,
1191                &scaled_jacobian,
1192                linear_solver,
1193            ) {
1194                Some(result) => result,
1195                None => {
1196                    return Err(optimizer::OptimizerError::LinearSolveFailed(
1197                        "Linear solver failed to solve system".to_string(),
1198                    )
1199                    .into());
1200                }
1201            };
1202
1203            // Update tracking
1204            max_gradient_norm = max_gradient_norm.max(step_result.gradient_norm);
1205            final_gradient_norm = step_result.gradient_norm;
1206            let step_norm = step_result.step.norm_l2();
1207            max_parameter_update_norm = max_parameter_update_norm.max(step_norm);
1208            final_parameter_update_norm = step_norm;
1209
1210            // Evaluate and apply step
1211            let step_eval = self.evaluate_and_apply_step(&step_result, &mut state, problem)?;
1212            cost_evaluations += 1;
1213
1214            if step_eval.accepted {
1215                successful_steps += 1;
1216                total_cost_reduction += step_eval.cost_reduction;
1217            } else {
1218                unsuccessful_steps += 1;
1219            }
1220
1221            // OPTIMIZATION: Only collect iteration statistics if debug level is enabled
1222            if tracing::enabled!(tracing::Level::DEBUG) {
1223                let iter_elapsed_ms = iter_start.elapsed().as_secs_f64() * 1000.0;
1224                let total_elapsed_ms = start_time.elapsed().as_secs_f64() * 1000.0;
1225
1226                let stats = IterationStats {
1227                    iteration,
1228                    cost: state.current_cost,
1229                    cost_change: previous_cost - state.current_cost,
1230                    gradient_norm: step_result.gradient_norm,
1231                    step_norm,
1232                    tr_ratio: step_eval.rho,
1233                    tr_radius: self.config.trust_region_radius,
1234                    ls_iter: 0,
1235                    iter_time_ms: iter_elapsed_ms,
1236                    total_time_ms: total_elapsed_ms,
1237                    accepted: step_eval.accepted,
1238                };
1239
1240                iteration_stats.push(stats.clone());
1241                stats.print_line();
1242            }
1243
1244            previous_cost = state.current_cost;
1245
1246            // Notify all observers with current state
1247            optimizer::notify_observers_generic::<M>(
1248                &mut self.observers,
1249                &state.variables,
1250                iteration,
1251                state.current_cost,
1252                step_result.gradient_norm,
1253                Some(self.config.trust_region_radius),
1254                step_norm,
1255                Some(step_eval.rho),
1256                linear_solver,
1257            );
1258
1259            // Check convergence
1260            let elapsed = start_time.elapsed();
1261            let parameter_norm = optimizer::compute_parameter_norm(&state.variables);
1262            let new_cost = state.current_cost;
1263
1264            let cost_before_step = if step_eval.accepted {
1265                state.current_cost + step_eval.cost_reduction
1266            } else {
1267                state.current_cost
1268            };
1269
1270            if let Some(status) = optimizer::check_convergence(&optimizer::ConvergenceParams {
1271                iteration,
1272                current_cost: cost_before_step,
1273                new_cost,
1274                parameter_norm,
1275                parameter_update_norm: step_norm,
1276                gradient_norm: step_result.gradient_norm,
1277                elapsed,
1278                step_accepted: step_eval.accepted,
1279                max_iterations: self.config.max_iterations,
1280                gradient_tolerance: self.config.gradient_tolerance,
1281                parameter_tolerance: self.config.parameter_tolerance,
1282                cost_tolerance: self.config.cost_tolerance,
1283                min_cost_threshold: self.config.min_cost_threshold,
1284                timeout: self.config.timeout,
1285                trust_region_radius: Some(self.config.trust_region_radius),
1286                min_trust_region_radius: Some(self.config.trust_region_min),
1287            }) {
1288                // Print summary only if debug level is enabled
1289                if tracing::enabled!(tracing::Level::DEBUG) {
1290                    let summary = optimizer::create_optimizer_summary(
1291                        "Dog-Leg",
1292                        state.initial_cost,
1293                        state.current_cost,
1294                        iteration + 1,
1295                        Some(successful_steps),
1296                        Some(unsuccessful_steps),
1297                        max_gradient_norm,
1298                        final_gradient_norm,
1299                        max_parameter_update_norm,
1300                        final_parameter_update_norm,
1301                        total_cost_reduction,
1302                        elapsed,
1303                        iteration_stats.clone(),
1304                        status.clone(),
1305                        None,
1306                        Some(self.config.trust_region_radius),
1307                        None,
1308                    );
1309                    debug!("{}", summary);
1310                }
1311
1312                // Compute covariances if enabled
1313                let covariances = if self.config.compute_covariances {
1314                    problem.compute_and_set_covariances_generic::<M>(
1315                        linear_solver,
1316                        &mut state.variables,
1317                        &state.variable_index_map,
1318                    )
1319                } else {
1320                    None
1321                };
1322
1323                return Ok(optimizer::build_solver_result(
1324                    status,
1325                    iteration + 1,
1326                    state,
1327                    elapsed,
1328                    final_gradient_norm,
1329                    final_parameter_update_norm,
1330                    cost_evaluations,
1331                    jacobian_evaluations,
1332                    covariances,
1333                ));
1334            }
1335
1336            iteration += 1;
1337        }
1338    }
1339
1340    /// Run optimization, automatically selecting sparse or dense path based on config.
1341    pub fn optimize(&mut self, problem: &mut problem::Problem) -> optimizer::OptimizeResult {
1342        match problem.jacobian_mode {
1343            JacobianMode::Dense => match self.config.linear_solver_type {
1344                LinearSolverType::DenseQR => {
1345                    let mut solver = DenseQRSolver::new();
1346                    self.optimize_with_mode::<DenseMode>(problem, &mut solver)
1347                }
1348                _ => {
1349                    let mut solver = DenseCholeskySolver::new();
1350                    self.optimize_with_mode::<DenseMode>(problem, &mut solver)
1351                }
1352            },
1353            JacobianMode::Sparse => match self.config.linear_solver_type {
1354                linalg::LinearSolverType::SparseQR => {
1355                    let mut solver = SparseQRSolver::new();
1356                    self.optimize_with_mode::<SparseMode>(problem, &mut solver)
1357                }
1358                _ => {
1359                    // SparseCholesky (default), SparseSchurComplement or DenseCholesky with
1360                    // sparse mode → SparseCholeskySolver
1361                    let mut solver = SparseCholeskySolver::new();
1362                    self.optimize_with_mode::<SparseMode>(problem, &mut solver)
1363                }
1364            },
1365        }
1366    }
1367}
1368
1369impl optimizer::Optimizer for DogLeg {
1370    fn optimize(&mut self, problem: &mut problem::Problem) -> optimizer::OptimizeResult {
1371        self.optimize(problem)
1372    }
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377    use super::*;
1378    use crate::factors;
1379    use apex_manifolds as manifold;
1380    use faer::prelude::ReborrowMut;
1381    use nalgebra;
1382
1383    type TestResult = Result<(), Box<dyn std::error::Error>>;
1384
1385    /// Custom Rosenbrock Factor 1: r1 = 10(x2 - x1²)
1386    /// Demonstrates extensibility - custom factors can be defined outside of factors.rs
1387    #[derive(Debug, Clone)]
1388    struct RosenbrockFactor1;
1389
1390    impl factors::Factor for RosenbrockFactor1 {
1391        fn linearize(
1392            &self,
1393            params: &[&[f64]],
1394            residual: &mut [f64],
1395            jacobian: Option<faer::mat::MatMut<'_, f64>>,
1396        ) {
1397            let x1 = params[0][0];
1398            let x2 = params[1][0];
1399            residual[0] = 10.0 * (x2 - x1 * x1);
1400            if let Some(mut jac) = jacobian {
1401                *jac.rb_mut().get_mut(0, 0) = -20.0 * x1;
1402                *jac.rb_mut().get_mut(0, 1) = 10.0;
1403            }
1404        }
1405        fn residual_dim(&self) -> usize {
1406            1
1407        }
1408        fn jacobian_shape(&self) -> (usize, usize) {
1409            (1, 2)
1410        }
1411    }
1412
1413    /// Custom Rosenbrock Factor 2: r2 = 1 - x1
1414    /// Demonstrates extensibility - custom factors can be defined outside of factors.rs
1415    #[derive(Debug, Clone)]
1416    struct RosenbrockFactor2;
1417
1418    impl factors::Factor for RosenbrockFactor2 {
1419        fn linearize(
1420            &self,
1421            params: &[&[f64]],
1422            residual: &mut [f64],
1423            jacobian: Option<faer::mat::MatMut<'_, f64>>,
1424        ) {
1425            residual[0] = 1.0 - params[0][0];
1426            if let Some(mut jac) = jacobian {
1427                *jac.rb_mut().get_mut(0, 0) = -1.0;
1428            }
1429        }
1430        fn residual_dim(&self) -> usize {
1431            1
1432        }
1433        fn jacobian_shape(&self) -> (usize, usize) {
1434            (1, 1)
1435        }
1436    }
1437
1438    #[test]
1439    fn test_rosenbrock_optimization() -> TestResult {
1440        // Rosenbrock function test:
1441        // Minimize: r1² + r2² where
1442        //   r1 = 10(x2 - x1²)
1443        //   r2 = 1 - x1
1444        // Starting point: [-1.2, 1.0]
1445        // Expected minimum: [1.0, 1.0]
1446
1447        let mut problem = problem::Problem::new(JacobianMode::Sparse);
1448        let x1 = problem.add_variable(manifold::ManifoldType::RN, nalgebra::dvector![-1.2]);
1449        let x2 = problem.add_variable(manifold::ManifoldType::RN, nalgebra::dvector![1.0]);
1450
1451        // Add custom factors (demonstrates extensibility!)
1452        problem.add_residual_block(&[x1, x2], Box::new(RosenbrockFactor1), None);
1453        problem.add_residual_block(&[x1], Box::new(RosenbrockFactor2), None);
1454
1455        // Configure Dog Leg optimizer with appropriate trust region
1456        let config = DogLegConfig::new()
1457            .with_max_iterations(100)
1458            .with_cost_tolerance(1e-8)
1459            .with_parameter_tolerance(1e-8)
1460            .with_gradient_tolerance(1e-10)
1461            .with_trust_region_radius(10.0); // Start with larger trust region
1462
1463        let mut solver = DogLeg::with_config(config);
1464        let result = solver.optimize(&mut problem)?;
1465
1466        // Extract final values
1467        let x1_final = result.parameters[x1].as_param_slice()[0];
1468        let x2_final = result.parameters[x2].as_param_slice()[0];
1469
1470        // Verify convergence to [1.0, 1.0]
1471        assert!(
1472            matches!(
1473                result.status,
1474                optimizer::OptimizationStatus::Converged
1475                    | optimizer::OptimizationStatus::CostToleranceReached
1476                    | optimizer::OptimizationStatus::ParameterToleranceReached
1477                    | optimizer::OptimizationStatus::GradientToleranceReached
1478            ),
1479            "Optimization should converge"
1480        );
1481        assert!(
1482            (x1_final - 1.0).abs() < 1e-4,
1483            "x1 should converge to 1.0, got {}",
1484            x1_final
1485        );
1486        assert!(
1487            (x2_final - 1.0).abs() < 1e-4,
1488            "x2 should converge to 1.0, got {}",
1489            x2_final
1490        );
1491        assert!(
1492            result.final_cost < 1e-6,
1493            "Final cost should be near zero, got {}",
1494            result.final_cost
1495        );
1496        Ok(())
1497    }
1498
1499    /// Trivial factor: r = x - target, J = [[1.0]]
1500    struct LinearFactor {
1501        target: f64,
1502    }
1503
1504    impl factors::Factor for LinearFactor {
1505        fn linearize(
1506            &self,
1507            params: &[&[f64]],
1508            residual: &mut [f64],
1509            jacobian: Option<faer::mat::MatMut<'_, f64>>,
1510        ) {
1511            residual[0] = params[0][0] - self.target;
1512            if let Some(mut jac) = jacobian {
1513                *jac.rb_mut().get_mut(0, 0) = 1.0;
1514            }
1515        }
1516        fn residual_dim(&self) -> usize {
1517            1
1518        }
1519        fn jacobian_shape(&self) -> (usize, usize) {
1520            (1, 1)
1521        }
1522    }
1523
1524    fn rosenbrock_problem() -> problem::Problem {
1525        let mut prob = problem::Problem::new(JacobianMode::Sparse);
1526        let x1 = prob.add_variable(manifold::ManifoldType::RN, nalgebra::dvector![-1.2]);
1527        let x2 = prob.add_variable(manifold::ManifoldType::RN, nalgebra::dvector![1.0]);
1528        prob.add_residual_block(&[x1, x2], Box::new(RosenbrockFactor1), None);
1529        prob.add_residual_block(&[x1], Box::new(RosenbrockFactor2), None);
1530        prob
1531    }
1532
1533    fn linear_problem(start: f64) -> problem::Problem {
1534        let mut prob = problem::Problem::new(JacobianMode::Sparse);
1535        let x = prob.add_variable(manifold::ManifoldType::RN, nalgebra::dvector![start]);
1536        prob.add_residual_block(&[x], Box::new(LinearFactor { target: 0.0 }), None);
1537        prob
1538    }
1539
1540    // -------------------------------------------------------------------------
1541    // DogLegConfig builder tests
1542    // -------------------------------------------------------------------------
1543
1544    #[test]
1545    fn test_dl_config_default() {
1546        let cfg = DogLegConfig::default();
1547        assert_eq!(cfg.max_iterations, 50);
1548        assert!((cfg.cost_tolerance - 1e-6).abs() < 1e-15);
1549        assert!(cfg.use_jacobi_scaling); // DogLeg enables Jacobi scaling by default
1550        assert!(!cfg.compute_covariances);
1551        assert!(cfg.enable_step_reuse);
1552    }
1553
1554    #[test]
1555    fn test_dl_config_builders() {
1556        use crate::linalg::LinearSolverType;
1557        let cfg = DogLegConfig::new()
1558            .with_max_iterations(20)
1559            .with_cost_tolerance(1e-5)
1560            .with_parameter_tolerance(1e-6)
1561            .with_gradient_tolerance(1e-7)
1562            .with_trust_region_radius(200.0)
1563            .with_trust_region_bounds(1e-15, 1e15)
1564            .with_trust_region_factors(4.0, 0.3)
1565            .with_step_quality_thresholds(0.01, 0.3, 0.7)
1566            .with_jacobi_scaling(false)
1567            .with_mu_params(1e-3, 1e-10, 2.0, 5.0)
1568            .with_step_reuse(false)
1569            .with_min_cost_threshold(1e-9)
1570            .with_compute_covariances(true)
1571            .with_linear_solver_type(LinearSolverType::SparseQR);
1572        assert_eq!(cfg.max_iterations, 20);
1573        assert!((cfg.trust_region_radius - 200.0).abs() < 1e-10);
1574        assert!(!cfg.use_jacobi_scaling);
1575        assert!(!cfg.enable_step_reuse);
1576        assert!(cfg.min_cost_threshold.is_some());
1577        assert!(cfg.compute_covariances);
1578        assert!((cfg.min_step_quality - 0.01).abs() < 1e-15);
1579        assert!((cfg.poor_step_quality - 0.3).abs() < 1e-15);
1580        assert!((cfg.good_step_quality - 0.7).abs() < 1e-15);
1581        assert!((cfg.initial_mu - 1e-3).abs() < 1e-15);
1582    }
1583
1584    #[test]
1585    fn test_dl_print_configuration_no_panic() {
1586        DogLegConfig::default().print_configuration();
1587    }
1588
1589    #[test]
1590    fn test_dl_default_equals_new() {
1591        let _a = DogLeg::new();
1592        let _b = DogLeg::default();
1593    }
1594
1595    #[test]
1596    fn test_dl_with_config_method() {
1597        let cfg = DogLegConfig::new().with_max_iterations(5);
1598        let _solver = DogLeg::with_config(cfg);
1599    }
1600
1601    // -------------------------------------------------------------------------
1602    // Convergence termination paths
1603    // -------------------------------------------------------------------------
1604
1605    #[test]
1606    fn test_dl_max_iterations_termination() -> TestResult {
1607        let mut problem = rosenbrock_problem();
1608        let cfg = DogLegConfig::new().with_max_iterations(2);
1609        let mut solver = DogLeg::with_config(cfg);
1610        let result = solver.optimize(&mut problem)?;
1611        assert_eq!(
1612            result.status,
1613            optimizer::OptimizationStatus::MaxIterationsReached
1614        );
1615        assert!(result.iterations <= 3, "iterations={}", result.iterations);
1616        Ok(())
1617    }
1618
1619    #[test]
1620    fn test_dl_gradient_tolerance_convergence() -> TestResult {
1621        let mut problem = linear_problem(1.0);
1622        let cfg = DogLegConfig::new()
1623            .with_gradient_tolerance(1e3)
1624            .with_cost_tolerance(1e-20)
1625            .with_parameter_tolerance(1e-20);
1626        let mut solver = DogLeg::with_config(cfg);
1627        let result = solver.optimize(&mut problem)?;
1628        assert_eq!(
1629            result.status,
1630            optimizer::OptimizationStatus::GradientToleranceReached
1631        );
1632        Ok(())
1633    }
1634
1635    #[test]
1636    fn test_dl_cost_tolerance_convergence() -> TestResult {
1637        let mut problem = rosenbrock_problem();
1638        let cfg = DogLegConfig::new()
1639            .with_cost_tolerance(1e2) // very loose
1640            .with_gradient_tolerance(1e-20)
1641            .with_parameter_tolerance(1e-20);
1642        let mut solver = DogLeg::with_config(cfg);
1643        let result = solver.optimize(&mut problem)?;
1644        assert!(matches!(
1645            result.status,
1646            optimizer::OptimizationStatus::CostToleranceReached
1647                | optimizer::OptimizationStatus::GradientToleranceReached
1648                | optimizer::OptimizationStatus::ParameterToleranceReached
1649                | optimizer::OptimizationStatus::Converged
1650        ));
1651        Ok(())
1652    }
1653
1654    #[test]
1655    fn test_dl_qr_solver() -> TestResult {
1656        use crate::linalg::LinearSolverType;
1657        let mut problem = rosenbrock_problem();
1658        let cfg = DogLegConfig::new()
1659            .with_linear_solver_type(LinearSolverType::SparseQR)
1660            .with_max_iterations(100);
1661        let mut solver = DogLeg::with_config(cfg);
1662        let result = solver.optimize(&mut problem)?;
1663        assert!(result.final_cost < 1e-6);
1664        Ok(())
1665    }
1666
1667    #[test]
1668    fn test_dl_jacobi_scaling_disabled() -> TestResult {
1669        // DogLeg has Jacobi scaling ON by default; test with it explicitly disabled
1670        let mut problem = rosenbrock_problem();
1671        let cfg = DogLegConfig::new()
1672            .with_jacobi_scaling(false)
1673            .with_max_iterations(100);
1674        let mut solver = DogLeg::with_config(cfg);
1675        let result = solver.optimize(&mut problem)?;
1676        assert!(result.final_cost < 1e-6);
1677        Ok(())
1678    }
1679
1680    #[test]
1681    fn test_dl_min_cost_threshold() -> TestResult {
1682        let mut problem = rosenbrock_problem();
1683        let cfg = DogLegConfig::new()
1684            .with_min_cost_threshold(1e10)
1685            .with_cost_tolerance(1e-20)
1686            .with_gradient_tolerance(1e-20)
1687            .with_parameter_tolerance(1e-20);
1688        let mut solver = DogLeg::with_config(cfg);
1689        let result = solver.optimize(&mut problem)?;
1690        assert_eq!(
1691            result.status,
1692            optimizer::OptimizationStatus::MinCostThresholdReached
1693        );
1694        Ok(())
1695    }
1696
1697    #[test]
1698    fn test_dl_trust_region_radius_in_config() -> TestResult {
1699        let mut problem = rosenbrock_problem();
1700        let cfg = DogLegConfig::new()
1701            .with_trust_region_radius(0.1) // small initial radius
1702            .with_max_iterations(200);
1703        let mut solver = DogLeg::with_config(cfg);
1704        let result = solver.optimize(&mut problem)?;
1705        assert!(result.iterations > 0);
1706        Ok(())
1707    }
1708
1709    #[test]
1710    fn test_dl_step_reuse_config() -> TestResult {
1711        let mut problem = rosenbrock_problem();
1712        let cfg = DogLegConfig::new()
1713            .with_step_reuse(true)
1714            .with_max_iterations(100);
1715        let mut solver = DogLeg::with_config(cfg);
1716        let result = solver.optimize(&mut problem)?;
1717        assert!(result.final_cost < 1e-6);
1718        Ok(())
1719    }
1720
1721    #[test]
1722    fn test_dl_mu_params_config() {
1723        let cfg = DogLegConfig::new().with_mu_params(1e-2, 1e-9, 10.0, 20.0);
1724        assert!((cfg.initial_mu - 1e-2).abs() < 1e-15);
1725        assert!((cfg.min_mu - 1e-9).abs() < 1e-20);
1726        assert!((cfg.max_mu - 10.0).abs() < 1e-12);
1727        assert!((cfg.mu_increase_factor - 20.0).abs() < 1e-12);
1728    }
1729
1730    #[test]
1731    fn test_dl_step_quality_thresholds() {
1732        let cfg = DogLegConfig::new().with_step_quality_thresholds(0.05, 0.25, 0.8);
1733        assert!((cfg.min_step_quality - 0.05).abs() < 1e-15);
1734        assert!((cfg.poor_step_quality - 0.25).abs() < 1e-15);
1735        assert!((cfg.good_step_quality - 0.8).abs() < 1e-15);
1736    }
1737
1738    #[test]
1739    fn test_dl_result_fields() -> TestResult {
1740        let mut problem = rosenbrock_problem();
1741        let mut solver = DogLeg::new();
1742        let result = solver.optimize(&mut problem)?;
1743        assert!(result.initial_cost > result.final_cost);
1744        assert!(result.iterations > 0);
1745        assert!(result.convergence_info.is_some());
1746        Ok(())
1747    }
1748
1749    #[test]
1750    fn test_dl_timeout_config() {
1751        let cfg = DogLegConfig::new().with_timeout(time::Duration::from_secs(60));
1752        assert!(cfg.timeout.is_some());
1753    }
1754}