scirs2_optimize/constrained/
trust_constr.rs

1//! Trust-region algorithm for constrained optimization
2//!
3//! This module implements the trust-region constrained optimization algorithm,
4//! supporting both numerical and user-supplied gradients and Hessians.
5//!
6//! # Example with custom gradient
7//!
8//! ```
9//! use scirs2_optimize::constrained::trust_constr::{
10//!     minimize_trust_constr_with_derivatives, HessianUpdate,
11//! };
12//! use scirs2_optimize::constrained::{Constraint, Options};
13//! use scirs2_core::ndarray::{array, Array1};
14//!
15//! // Objective: f(x) = (x[0] - 1)² + (x[1] - 2.5)²
16//! fn objective(x: &[f64]) -> f64 {
17//!     (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
18//! }
19//!
20//! // Analytical gradient: [2(x[0] - 1), 2(x[1] - 2.5)]
21//! fn gradient(x: &[f64]) -> Array1<f64> {
22//!     array![2.0 * (x[0] - 1.0), 2.0 * (x[1] - 2.5)]
23//! }
24//!
25//! // Constraint: x[0] + x[1] <= 3  =>  g(x) = 3 - x[0] - x[1] >= 0
26//! fn constraint(x: &[f64]) -> f64 {
27//!     3.0 - x[0] - x[1]
28//! }
29//!
30//! let x0 = array![0.0, 0.0];
31//! let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
32//! let options = Options::default();
33//!
34//! let result = minimize_trust_constr_with_derivatives(
35//!     objective,
36//!     &x0,
37//!     &constraints,
38//!     &options,
39//!     Some(gradient),           // Custom gradient
40//!     None::<fn(&[f64]) -> _>,  // No custom Hessian (use BFGS)
41//!     HessianUpdate::BFGS,
42//! ).unwrap();
43//!
44//! assert!(result.success);
45//! ```
46
47use crate::constrained::{Constraint, ConstraintFn, ConstraintKind, Options};
48use crate::error::OptimizeResult;
49use crate::result::OptimizeResults;
50use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1};
51
52/// Hessian update strategy for quasi-Newton methods
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub enum HessianUpdate {
55    /// BFGS update (default) - good for general problems
56    #[default]
57    BFGS,
58    /// SR1 update - can capture negative curvature
59    SR1,
60    /// Use exact Hessian provided by user
61    Exact,
62}
63
64/// Type alias for gradient function
65pub type GradientFn = fn(&[f64]) -> Array1<f64>;
66
67/// Type alias for Hessian function
68pub type HessianFn = fn(&[f64]) -> Array2<f64>;
69
70#[allow(clippy::many_single_char_names)]
71#[allow(dead_code)]
72pub fn minimize_trust_constr<F, S>(
73    func: F,
74    x0: &ArrayBase<S, Ix1>,
75    constraints: &[Constraint<ConstraintFn>],
76    options: &Options,
77) -> OptimizeResult<OptimizeResults<f64>>
78where
79    F: Fn(&[f64]) -> f64,
80    S: Data<Elem = f64>,
81{
82    // Get options or use defaults
83    let ftol = options.ftol.unwrap_or(1e-8);
84    let gtol = options.gtol.unwrap_or(1e-8);
85    let ctol = options.ctol.unwrap_or(1e-8);
86    let maxiter = options.maxiter.unwrap_or(100 * x0.len());
87    let eps = options.eps.unwrap_or(1e-8);
88
89    // Initialize variables
90    let n = x0.len();
91    let mut x = x0.to_owned();
92    let mut f = func(x.as_slice().expect("Operation failed"));
93    let mut nfev = 1;
94
95    // Initialize the Lagrange multipliers
96    let mut lambda = Array1::zeros(constraints.len());
97
98    // Calculate initial gradient using finite differences
99    let mut g = Array1::zeros(n);
100    for i in 0..n {
101        let mut x_h = x.clone();
102        x_h[i] += eps;
103        let f_h = func(x_h.as_slice().expect("Operation failed"));
104        g[i] = (f_h - f) / eps;
105        nfev += 1;
106    }
107
108    // Evaluate initial constraints
109    let mut c = Array1::zeros(constraints.len());
110    for (i, constraint) in constraints.iter().enumerate() {
111        if !constraint.is_bounds() {
112            let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
113
114            match constraint.kind {
115                ConstraintKind::Inequality => {
116                    c[i] = val; // g(x) >= 0 constraint
117                }
118                ConstraintKind::Equality => {
119                    c[i] = val; // h(x) = 0 constraint
120                }
121            }
122        }
123    }
124
125    // Calculate constraint Jacobian
126    let mut a = Array2::zeros((constraints.len(), n));
127    for (i, constraint) in constraints.iter().enumerate() {
128        if !constraint.is_bounds() {
129            for j in 0..n {
130                let mut x_h = x.clone();
131                x_h[j] += eps;
132                let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
133                a[[i, j]] = (c_h - c[i]) / eps;
134                nfev += 1;
135            }
136        }
137    }
138
139    // Initialize trust region radius
140    let mut delta = 1.0;
141
142    // Initialize approximation of the Hessian of the Lagrangian
143    let mut b = Array2::eye(n);
144
145    // Main optimization loop
146    let mut iter = 0;
147
148    while iter < maxiter {
149        // Check constraint violation
150        let mut max_constraint_violation = 0.0;
151        for (i, &ci) in c.iter().enumerate() {
152            match constraints[i].kind {
153                ConstraintKind::Inequality => {
154                    if ci < -ctol {
155                        max_constraint_violation = f64::max(max_constraint_violation, -ci);
156                    }
157                }
158                ConstraintKind::Equality => {
159                    // For equality constraints, violation is |h(x)|
160                    let violation = ci.abs();
161                    if violation > ctol {
162                        max_constraint_violation = f64::max(max_constraint_violation, violation);
163                    }
164                }
165            }
166        }
167
168        // Check convergence on gradient and constraints
169        if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
170            break;
171        }
172
173        // Compute the Lagrangian gradient
174        let mut lag_grad = g.clone();
175        for (i, &li) in lambda.iter().enumerate() {
176            let include_constraint = match constraints[i].kind {
177                ConstraintKind::Inequality => {
178                    // For inequality constraints: include if active (lambda > 0) or violated (c < 0)
179                    li > 0.0 || c[i] < -ctol
180                }
181                ConstraintKind::Equality => {
182                    // For equality constraints: always include (always active)
183                    true
184                }
185            };
186
187            if include_constraint {
188                for j in 0..n {
189                    // L = f - sum(lambda_i * c_i) for constraints
190                    // So gradient of L is grad(f) - sum(lambda_i * grad(c_i))
191                    lag_grad[j] -= li * a[[i, j]];
192                }
193            }
194        }
195
196        // Compute the step using a trust-region approach
197        // Solve the constrained quadratic subproblem:
198        // min 0.5 * p^T B p + g^T p  s.t. ||p|| <= delta and linearized constraints
199
200        let (p, predicted_reduction) =
201            compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
202
203        // Try the step
204        let x_new = &x + &p;
205
206        // Evaluate function and constraints at new point
207        let f_new = func(x_new.as_slice().expect("Operation failed"));
208        nfev += 1;
209
210        let mut c_new = Array1::zeros(constraints.len());
211        for (i, constraint) in constraints.iter().enumerate() {
212            if !constraint.is_bounds() {
213                c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
214                nfev += 1;
215            }
216        }
217
218        // Compute change in merit function (includes constraint violation)
219        let mut merit = f;
220        let mut merit_new = f_new;
221
222        // Add constraint violation penalty
223        let penalty = 10.0; // Simple fixed penalty parameter
224        for (i, &ci) in c.iter().enumerate() {
225            match constraints[i].kind {
226                ConstraintKind::Inequality => {
227                    // For inequality constraints: penalize only violations (g(x) < 0)
228                    merit += penalty * f64::max(0.0, -ci);
229                    merit_new += penalty * f64::max(0.0, -c_new[i]);
230                }
231                ConstraintKind::Equality => {
232                    // For equality constraints: penalize any deviation from zero
233                    merit += penalty * ci.abs();
234                    merit_new += penalty * c_new[i].abs();
235                }
236            }
237        }
238
239        // Compute actual reduction in merit function
240        let actual_reduction = merit - merit_new;
241
242        // Compute ratio of actual to predicted reduction
243        let rho = if predicted_reduction > 0.0 {
244            actual_reduction / predicted_reduction
245        } else {
246            0.0
247        };
248
249        // Update trust region radius based on the quality of the step
250        if rho < 0.25 {
251            delta *= 0.5;
252        } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
253            delta *= 2.0;
254        }
255
256        // Accept or reject the step
257        if rho > 0.1 {
258            // Accept the step
259            x = x_new;
260            f = f_new;
261            c = c_new;
262
263            // Check convergence on function value
264            if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
265                break;
266            }
267
268            // Compute new gradient
269            let mut g_new = Array1::zeros(n);
270            for i in 0..n {
271                let mut x_h = x.clone();
272                x_h[i] += eps;
273                let f_h = func(x_h.as_slice().expect("Operation failed"));
274                g_new[i] = (f_h - f) / eps;
275                nfev += 1;
276            }
277
278            // Compute new constraint Jacobian
279            let mut a_new = Array2::zeros((constraints.len(), n));
280            for (i, constraint) in constraints.iter().enumerate() {
281                if !constraint.is_bounds() {
282                    for j in 0..n {
283                        let mut x_h = x.clone();
284                        x_h[j] += eps;
285                        let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
286                        a_new[[i, j]] = (c_h - c[i]) / eps;
287                        nfev += 1;
288                    }
289                }
290            }
291
292            // Update Lagrange multipliers using projected gradient method
293            for (i, constraint) in constraints.iter().enumerate() {
294                match constraint.kind {
295                    ConstraintKind::Inequality => {
296                        if c[i] < -ctol {
297                            // Active or violated constraint
298                            // Increase multiplier if constraint is violated
299                            lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
300                        } else {
301                            // Decrease multiplier towards zero
302                            lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
303                        }
304                    }
305                    ConstraintKind::Equality => {
306                        // For equality constraints, multipliers can be positive or negative
307                        // Update based on constraint violation to drive h(x) -> 0
308                        let step_size = 0.1;
309                        lambda[i] -= step_size * c[i] * penalty;
310
311                        // Optional: add some damping to prevent oscillations
312                        lambda[i] *= 0.9;
313                    }
314                }
315            }
316
317            // Update Hessian approximation using BFGS or SR1
318            let s = &p;
319            let y = &g_new - &g;
320
321            // Simple BFGS update for the Hessian approximation
322            let s_dot_y = s.dot(&y);
323            if s_dot_y > 1e-10 {
324                let s_col = s.clone().insert_axis(Axis(1));
325                let s_row = s.clone().insert_axis(Axis(0));
326
327                let bs = b.dot(s);
328                let bs_col = bs.clone().insert_axis(Axis(1));
329                let bs_row = bs.clone().insert_axis(Axis(0));
330
331                let term1 = s_dot_y + s.dot(&bs);
332                let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
333
334                let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
335
336                b = &b + &term2 - &(&term3 / s_dot_y);
337            }
338
339            // Update variables for next iteration
340            g = g_new;
341            a = a_new;
342        }
343
344        iter += 1;
345    }
346
347    // Prepare constraint values for the result
348    let mut c_result = Array1::zeros(constraints.len());
349    for (i, constraint) in constraints.iter().enumerate() {
350        if !constraint.is_bounds() {
351            c_result[i] = c[i];
352        }
353    }
354
355    // Create and return result
356    let mut result = OptimizeResults::default();
357    result.x = x;
358    result.fun = f;
359    result.jac = Some(g.into_raw_vec_and_offset().0);
360    result.constr = Some(c_result);
361    result.nfev = nfev;
362    result.nit = iter;
363    result.success = iter < maxiter;
364
365    if result.success {
366        result.message = "Optimization terminated successfully.".to_string();
367    } else {
368        result.message = "Maximum iterations reached.".to_string();
369    }
370
371    Ok(result)
372}
373
374/// Minimizes a function with constraints using trust-region method with custom derivatives.
375///
376/// This function extends [`minimize_trust_constr`] by allowing user-supplied gradient
377/// and Hessian functions, which can significantly improve performance and accuracy
378/// compared to finite-difference approximations.
379///
380/// # Arguments
381///
382/// * `func` - The objective function to minimize
383/// * `x0` - Initial guess for the optimization
384/// * `constraints` - Vector of constraints (equality and/or inequality)
385/// * `options` - Optimization options (tolerances, max iterations, etc.)
386/// * `jac` - Optional gradient function. If `None`, finite differences are used.
387/// * `hess` - Optional Hessian function. Only used when `hess_update` is [`HessianUpdate::Exact`].
388/// * `hess_update` - Strategy for Hessian updates (BFGS, SR1, or Exact)
389///
390/// # Returns
391///
392/// * `OptimizeResults` containing the optimization solution
393///
394/// # Example
395///
396/// ```
397/// use scirs2_optimize::constrained::trust_constr::{
398///     minimize_trust_constr_with_derivatives, HessianUpdate,
399/// };
400/// use scirs2_optimize::constrained::{Constraint, Options};
401/// use scirs2_core::ndarray::{array, Array1, Array2};
402///
403/// // Rosenbrock function
404/// fn rosenbrock(x: &[f64]) -> f64 {
405///     (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2)
406/// }
407///
408/// // Analytical gradient
409/// fn rosenbrock_grad(x: &[f64]) -> Array1<f64> {
410///     array![
411///         -2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0].powi(2)),
412///         200.0 * (x[1] - x[0].powi(2))
413///     ]
414/// }
415///
416/// // Constraint: x[0]^2 + x[1]^2 <= 2
417/// fn circle_constraint(x: &[f64]) -> f64 {
418///     2.0 - x[0].powi(2) - x[1].powi(2)
419/// }
420///
421/// let x0 = array![0.0, 0.0];
422/// let constraints = vec![Constraint::new(circle_constraint, Constraint::INEQUALITY)];
423///
424/// let result = minimize_trust_constr_with_derivatives(
425///     rosenbrock,
426///     &x0,
427///     &constraints,
428///     &Options::default(),
429///     Some(rosenbrock_grad),
430///     None::<fn(&[f64]) -> Array2<f64>>,
431///     HessianUpdate::BFGS,
432/// ).unwrap();
433/// ```
434#[allow(clippy::many_single_char_names)]
435#[allow(clippy::too_many_arguments)]
436#[allow(dead_code)]
437pub fn minimize_trust_constr_with_derivatives<F, S, G, H>(
438    func: F,
439    x0: &ArrayBase<S, Ix1>,
440    constraints: &[Constraint<ConstraintFn>],
441    options: &Options,
442    jac: Option<G>,
443    hess: Option<H>,
444    hess_update: HessianUpdate,
445) -> OptimizeResult<OptimizeResults<f64>>
446where
447    F: Fn(&[f64]) -> f64,
448    S: Data<Elem = f64>,
449    G: Fn(&[f64]) -> Array1<f64>,
450    H: Fn(&[f64]) -> Array2<f64>,
451{
452    // Get options or use defaults
453    let ftol = options.ftol.unwrap_or(1e-8);
454    let gtol = options.gtol.unwrap_or(1e-8);
455    let ctol = options.ctol.unwrap_or(1e-8);
456    let maxiter = options.maxiter.unwrap_or(100 * x0.len());
457    let eps = options.eps.unwrap_or(1e-8);
458
459    // Initialize variables
460    let n = x0.len();
461    let mut x = x0.to_owned();
462    let mut f = func(x.as_slice().expect("Operation failed"));
463    let mut nfev = 1;
464    let mut njev = 0;
465    let mut nhev = 0;
466
467    // Initialize the Lagrange multipliers
468    let mut lambda = Array1::zeros(constraints.len());
469
470    // Calculate initial gradient (using custom jac if provided)
471    let mut g = if let Some(ref grad_fn) = jac {
472        njev += 1;
473        grad_fn(x.as_slice().expect("Operation failed"))
474    } else {
475        // Finite difference gradient
476        let mut grad = Array1::zeros(n);
477        for i in 0..n {
478            let mut x_h = x.clone();
479            x_h[i] += eps;
480            let f_h = func(x_h.as_slice().expect("Operation failed"));
481            grad[i] = (f_h - f) / eps;
482            nfev += 1;
483        }
484        grad
485    };
486
487    // Evaluate initial constraints
488    let mut c = Array1::zeros(constraints.len());
489    for (i, constraint) in constraints.iter().enumerate() {
490        if !constraint.is_bounds() {
491            let val = (constraint.fun)(x.as_slice().expect("Operation failed"));
492            match constraint.kind {
493                ConstraintKind::Inequality => c[i] = val,
494                ConstraintKind::Equality => c[i] = val,
495            }
496        }
497    }
498
499    // Calculate constraint Jacobian
500    let mut a = Array2::zeros((constraints.len(), n));
501    for (i, constraint) in constraints.iter().enumerate() {
502        if !constraint.is_bounds() {
503            for j in 0..n {
504                let mut x_h = x.clone();
505                x_h[j] += eps;
506                let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
507                a[[i, j]] = (c_h - c[i]) / eps;
508                nfev += 1;
509            }
510        }
511    }
512
513    // Initialize trust region radius
514    let mut delta = 1.0;
515
516    // Initialize Hessian or its approximation
517    let mut b = if hess_update == HessianUpdate::Exact {
518        if let Some(ref hess_fn) = hess {
519            nhev += 1;
520            hess_fn(x.as_slice().expect("Operation failed"))
521        } else {
522            // Fallback to identity if exact requested but no Hessian provided
523            Array2::eye(n)
524        }
525    } else {
526        Array2::eye(n)
527    };
528
529    // Main optimization loop
530    let mut iter = 0;
531
532    while iter < maxiter {
533        // Check constraint violation
534        let mut max_constraint_violation = 0.0;
535        for (i, &ci) in c.iter().enumerate() {
536            match constraints[i].kind {
537                ConstraintKind::Inequality => {
538                    if ci < -ctol {
539                        max_constraint_violation = f64::max(max_constraint_violation, -ci);
540                    }
541                }
542                ConstraintKind::Equality => {
543                    let violation = ci.abs();
544                    if violation > ctol {
545                        max_constraint_violation = f64::max(max_constraint_violation, violation);
546                    }
547                }
548            }
549        }
550
551        // Check convergence
552        if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
553            break;
554        }
555
556        // Compute the Lagrangian gradient
557        let mut lag_grad = g.clone();
558        for (i, &li) in lambda.iter().enumerate() {
559            let include_constraint = match constraints[i].kind {
560                ConstraintKind::Inequality => li > 0.0 || c[i] < -ctol,
561                ConstraintKind::Equality => true,
562            };
563
564            if include_constraint {
565                for j in 0..n {
566                    lag_grad[j] -= li * a[[i, j]];
567                }
568            }
569        }
570
571        // Compute the step
572        let (p, predicted_reduction) =
573            compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
574
575        // Try the step
576        let x_new = &x + &p;
577        let f_new = func(x_new.as_slice().expect("Operation failed"));
578        nfev += 1;
579
580        let mut c_new = Array1::zeros(constraints.len());
581        for (i, constraint) in constraints.iter().enumerate() {
582            if !constraint.is_bounds() {
583                c_new[i] = (constraint.fun)(x_new.as_slice().expect("Operation failed"));
584                nfev += 1;
585            }
586        }
587
588        // Compute merit function
589        let mut merit = f;
590        let mut merit_new = f_new;
591        let penalty = 10.0;
592
593        for (i, &ci) in c.iter().enumerate() {
594            match constraints[i].kind {
595                ConstraintKind::Inequality => {
596                    merit += penalty * f64::max(0.0, -ci);
597                    merit_new += penalty * f64::max(0.0, -c_new[i]);
598                }
599                ConstraintKind::Equality => {
600                    merit += penalty * ci.abs();
601                    merit_new += penalty * c_new[i].abs();
602                }
603            }
604        }
605
606        let actual_reduction = merit - merit_new;
607        let rho = if predicted_reduction > 0.0 {
608            actual_reduction / predicted_reduction
609        } else {
610            0.0
611        };
612
613        // Update trust region radius
614        if rho < 0.25 {
615            delta *= 0.5;
616        } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
617            delta *= 2.0;
618        }
619
620        // Accept or reject the step
621        if rho > 0.1 {
622            x = x_new;
623            f = f_new;
624            c = c_new;
625
626            if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
627                break;
628            }
629
630            // Compute new gradient
631            let g_new = if let Some(ref grad_fn) = jac {
632                njev += 1;
633                grad_fn(x.as_slice().expect("Operation failed"))
634            } else {
635                let mut grad = Array1::zeros(n);
636                for i in 0..n {
637                    let mut x_h = x.clone();
638                    x_h[i] += eps;
639                    let f_h = func(x_h.as_slice().expect("Operation failed"));
640                    grad[i] = (f_h - f) / eps;
641                    nfev += 1;
642                }
643                grad
644            };
645
646            // Compute new constraint Jacobian
647            let mut a_new = Array2::zeros((constraints.len(), n));
648            for (i, constraint) in constraints.iter().enumerate() {
649                if !constraint.is_bounds() {
650                    for j in 0..n {
651                        let mut x_h = x.clone();
652                        x_h[j] += eps;
653                        let c_h = (constraint.fun)(x_h.as_slice().expect("Operation failed"));
654                        a_new[[i, j]] = (c_h - c[i]) / eps;
655                        nfev += 1;
656                    }
657                }
658            }
659
660            // Update Lagrange multipliers
661            for (i, constraint) in constraints.iter().enumerate() {
662                match constraint.kind {
663                    ConstraintKind::Inequality => {
664                        if c[i] < -ctol {
665                            lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
666                        } else {
667                            lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
668                        }
669                    }
670                    ConstraintKind::Equality => {
671                        let step_size = 0.1;
672                        lambda[i] -= step_size * c[i] * penalty;
673                        lambda[i] *= 0.9;
674                    }
675                }
676            }
677
678            // Update Hessian based on strategy
679            match hess_update {
680                HessianUpdate::Exact => {
681                    if let Some(ref hess_fn) = hess {
682                        nhev += 1;
683                        b = hess_fn(x.as_slice().expect("Operation failed"));
684                    }
685                }
686                HessianUpdate::BFGS => {
687                    let s = &p;
688                    let y = &g_new - &g;
689                    let s_dot_y = s.dot(&y);
690                    if s_dot_y > 1e-10 {
691                        let s_col = s.clone().insert_axis(Axis(1));
692                        let s_row = s.clone().insert_axis(Axis(0));
693                        let bs = b.dot(s);
694                        let bs_col = bs.clone().insert_axis(Axis(1));
695                        let bs_row = bs.clone().insert_axis(Axis(0));
696                        let term1 = s_dot_y + s.dot(&bs);
697                        let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
698                        let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
699                        b = &b + &term2 - &(&term3 / s_dot_y);
700                    }
701                }
702                HessianUpdate::SR1 => {
703                    let s = &p;
704                    let y = &g_new - &g;
705                    let bs = b.dot(s);
706                    let diff = &y - &bs;
707                    let s_dot_diff = s.dot(&diff);
708                    // SR1 update with skipping condition
709                    if s_dot_diff.abs() > 1e-8 * s.dot(s).sqrt() * diff.dot(&diff).sqrt() {
710                        let diff_col = diff.clone().insert_axis(Axis(1));
711                        let diff_row = diff.clone().insert_axis(Axis(0));
712                        let update = &diff_col.dot(&diff_row) / s_dot_diff;
713                        b = &b + &update;
714                    }
715                }
716            }
717
718            g = g_new;
719            a = a_new;
720        }
721
722        iter += 1;
723    }
724
725    // Prepare result
726    let mut c_result = Array1::zeros(constraints.len());
727    for (i, constraint) in constraints.iter().enumerate() {
728        if !constraint.is_bounds() {
729            c_result[i] = c[i];
730        }
731    }
732
733    let mut result = OptimizeResults::default();
734    result.x = x;
735    result.fun = f;
736    result.jac = Some(g.into_raw_vec_and_offset().0);
737    result.constr = Some(c_result);
738    result.nfev = nfev;
739    result.njev = njev;
740    result.nhev = nhev;
741    result.nit = iter;
742    result.success = iter < maxiter;
743
744    if result.success {
745        result.message = "Optimization terminated successfully.".to_string();
746    } else {
747        result.message = "Maximum iterations reached.".to_string();
748    }
749
750    Ok(result)
751}
752
753/// Compute a trust-region step for constrained optimization
754#[allow(clippy::many_single_char_names)]
755#[allow(dead_code)]
756fn compute_trust_region_step_constrained(
757    g: &Array1<f64>,
758    b: &Array2<f64>,
759    a: &Array2<f64>,
760    c: &Array1<f64>,
761    delta: f64,
762    constraints: &[Constraint<ConstraintFn>],
763    ctol: f64,
764) -> (Array1<f64>, f64) {
765    let n = g.len();
766    let n_constr = constraints.len();
767
768    // Compute the unconstrained Cauchy point (steepest descent direction)
769    let p_unconstrained = compute_unconstrained_cauchy_point(g, b, delta);
770
771    // Check if unconstrained Cauchy point satisfies linearized constraints
772    let mut constraint_violated = false;
773    for i in 0..n_constr {
774        let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p_unconstrained[j]).sum::<f64>();
775
776        match constraints[i].kind {
777            ConstraintKind::Inequality => {
778                // For inequality constraints: check if g(x) + grad_g^T p >= -ctol
779                if c[i] + grad_c_dot_p < -ctol {
780                    constraint_violated = true;
781                    break;
782                }
783            }
784            ConstraintKind::Equality => {
785                // For equality constraints: check if |h(x) + grad_h^T p| <= ctol
786                if (c[i] + grad_c_dot_p).abs() > ctol {
787                    constraint_violated = true;
788                    break;
789                }
790            }
791        }
792    }
793
794    // If unconstrained point is feasible, use it
795    if !constraint_violated {
796        // Compute predicted reduction
797        let g_dot_p = g.dot(&p_unconstrained);
798        let bp = b.dot(&p_unconstrained);
799        let p_dot_bp = p_unconstrained.dot(&bp);
800        let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
801
802        return (p_unconstrained, predicted_reduction);
803    }
804
805    // Otherwise, project onto the linearized feasible region
806    // This is a simplified approach - in practice, you would solve a CQP
807
808    // Start with the steepest descent direction
809    let mut p = Array1::zeros(n);
810    for i in 0..n {
811        p[i] = -g[i];
812    }
813
814    // Normalize to trust region radius
815    let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
816    if p_norm > 1e-10 {
817        p = &p * (delta / p_norm);
818    }
819
820    // Project onto each constraint
821    for _iter in 0..5 {
822        // Limited iterations for projection
823        let mut max_viol = 0.0;
824        let mut most_violated = 0;
825
826        // Find most violated constraint
827        for i in 0..n_constr {
828            let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p[j]).sum::<f64>();
829
830            let viol = match constraints[i].kind {
831                ConstraintKind::Inequality => {
832                    // For inequality constraints: violation is max(0, -(g + grad_g^T p))
833                    f64::max(0.0, -(c[i] + grad_c_dot_p))
834                }
835                ConstraintKind::Equality => {
836                    // For equality constraints: violation is |h + grad_h^T p|
837                    (c[i] + grad_c_dot_p).abs()
838                }
839            };
840
841            if viol > max_viol {
842                max_viol = viol;
843                most_violated = i;
844            }
845        }
846
847        if max_viol < ctol {
848            break;
849        }
850
851        // Project p onto the constraint
852        let mut a_norm_sq = 0.0;
853        for j in 0..n {
854            a_norm_sq += a[[most_violated, j]] * a[[most_violated, j]];
855        }
856
857        if a_norm_sq > 1e-10 {
858            let grad_c_dot_p = (0..n).map(|j| a[[most_violated, j]] * p[j]).sum::<f64>();
859
860            let proj_dist = match constraints[most_violated].kind {
861                ConstraintKind::Inequality => {
862                    // For inequality constraints: project to boundary when violated
863                    if c[most_violated] + grad_c_dot_p < 0.0 {
864                        -(c[most_violated] + grad_c_dot_p) / a_norm_sq
865                    } else {
866                        0.0
867                    }
868                }
869                ConstraintKind::Equality => {
870                    // For equality constraints: always project to satisfy h + grad_h^T p = 0
871                    -(c[most_violated] + grad_c_dot_p) / a_norm_sq
872                }
873            };
874
875            // Project p
876            for j in 0..n {
877                p[j] += a[[most_violated, j]] * proj_dist;
878            }
879
880            // Rescale to trust region
881            let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
882            if p_norm > delta {
883                p = &p * (delta / p_norm);
884            }
885        }
886    }
887
888    // Compute predicted reduction
889    let g_dot_p = g.dot(&p);
890    let bp = b.dot(&p);
891    let p_dot_bp = p.dot(&bp);
892    let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
893
894    (p, predicted_reduction)
895}
896
897/// Compute the unconstrained Cauchy point (steepest descent to trust region boundary)
898#[allow(dead_code)]
899fn compute_unconstrained_cauchy_point(g: &Array1<f64>, b: &Array2<f64>, delta: f64) -> Array1<f64> {
900    let n = g.len();
901
902    // Compute the steepest descent direction: -g
903    let mut p = Array1::zeros(n);
904    for i in 0..n {
905        p[i] = -g[i];
906    }
907
908    // Compute g^T B g and ||g||^2
909    let bg = b.dot(g);
910    let g_dot_bg = g.dot(&bg);
911    let g_norm_sq = g.dot(g);
912
913    // Check if the gradient is practically zero
914    if g_norm_sq < 1e-10 {
915        // If gradient is practically zero, don't move
916        return Array1::zeros(n);
917    }
918
919    // Compute tau (step to the boundary)
920    let tau = if g_dot_bg <= 0.0 {
921        // Negative curvature or zero curvature case
922        delta / g_norm_sq.sqrt()
923    } else {
924        // Positive curvature case
925        f64::min(delta / g_norm_sq.sqrt(), g_norm_sq / g_dot_bg)
926    };
927
928    // Scale the direction by tau
929    for i in 0..n {
930        p[i] *= tau;
931    }
932
933    p
934}