scirs2_optimize/
constrained.rs

1//! Constrained optimization algorithms
2//!
3//! This module provides methods for constrained optimization of scalar
4//! functions of one or more variables.
5//!
6//! ## Example
7//!
8//! ```
9//! use ndarray::{array, Array1};
10//! use scirs2_optimize::constrained::{minimize_constrained, Method, Constraint};
11//!
12//! // Define a simple function to minimize
13//! fn objective(x: &[f64]) -> f64 {
14//!     (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
15//! }
16//!
17//! // Define a constraint: x[0] + x[1] <= 3
18//! fn constraint(x: &[f64]) -> f64 {
19//!     3.0 - x[0] - x[1]  // Should be >= 0
20//! }
21//!
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! // Minimize the function starting at [0.0, 0.0]
24//! let initial_point = array![0.0, 0.0];
25//! let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
26//!
27//! let result = minimize_constrained(
28//!     objective,
29//!     &initial_point,
30//!     &constraints,
31//!     Method::SLSQP,
32//!     None
33//! )?;
34//!
35//! // The constrained minimum should be at [0.5, 2.5]
36//! # Ok(())
37//! # }
38//! ```
39
40use crate::error::{OptimizeError, OptimizeResult};
41use crate::result::OptimizeResults;
42use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1};
43use std::fmt;
44
45/// Type alias for constraint functions that take a slice of f64 and return f64
46pub type ConstraintFn = fn(&[f64]) -> f64;
47
48/// Optimization methods for constrained minimization.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Method {
51    /// Sequential Least SQuares Programming
52    SLSQP,
53
54    /// Trust-region constrained algorithm
55    TrustConstr,
56
57    /// Linear programming using the simplex algorithm
58    COBYLA,
59}
60
61impl fmt::Display for Method {
62    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63        match self {
64            Method::SLSQP => write!(f, "SLSQP"),
65            Method::TrustConstr => write!(f, "trust-constr"),
66            Method::COBYLA => write!(f, "COBYLA"),
67        }
68    }
69}
70
71/// Options for the constrained optimizer.
72#[derive(Debug, Clone)]
73pub struct Options {
74    /// Maximum number of iterations to perform
75    pub maxiter: Option<usize>,
76
77    /// Precision goal for the value in the stopping criterion
78    pub ftol: Option<f64>,
79
80    /// Precision goal for the gradient in the stopping criterion (relative)
81    pub gtol: Option<f64>,
82
83    /// Precision goal for constraint violation
84    pub ctol: Option<f64>,
85
86    /// Step size used for numerical approximation of the jacobian
87    pub eps: Option<f64>,
88
89    /// Whether to print convergence messages
90    pub disp: bool,
91
92    /// Return the optimization result after each iteration
93    pub return_all: bool,
94}
95
96impl Default for Options {
97    fn default() -> Self {
98        Options {
99            maxiter: None,
100            ftol: Some(1e-8),
101            gtol: Some(1e-8),
102            ctol: Some(1e-8),
103            eps: Some(1e-8),
104            disp: false,
105            return_all: false,
106        }
107    }
108}
109
110/// Constraint type for constrained optimization
111pub struct Constraint<F> {
112    /// The constraint function
113    pub fun: F,
114
115    /// The type of constraint (equality or inequality)
116    pub kind: ConstraintKind,
117
118    /// Lower bound for a box constraint
119    pub lb: Option<f64>,
120
121    /// Upper bound for a box constraint
122    pub ub: Option<f64>,
123}
124
125/// The kind of constraint
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum ConstraintKind {
128    /// Equality constraint: fun(x) = 0
129    Equality,
130
131    /// Inequality constraint: fun(x) >= 0
132    Inequality,
133}
134
135impl Constraint<fn(&[f64]) -> f64> {
136    /// Constant for equality constraint
137    pub const EQUALITY: ConstraintKind = ConstraintKind::Equality;
138
139    /// Constant for inequality constraint
140    pub const INEQUALITY: ConstraintKind = ConstraintKind::Inequality;
141
142    /// Create a new constraint
143    pub fn new(fun: fn(&[f64]) -> f64, kind: ConstraintKind) -> Self {
144        Constraint {
145            fun,
146            kind,
147            lb: None,
148            ub: None,
149        }
150    }
151
152    /// Create a new box constraint
153    pub fn new_bounds(lb: Option<f64>, ub: Option<f64>) -> Self {
154        Constraint {
155            fun: |_| 0.0, // Dummy function for box constraints
156            kind: ConstraintKind::Inequality,
157            lb,
158            ub,
159        }
160    }
161}
162
163impl<F> Constraint<F> {
164    /// Check if this is a box constraint
165    pub fn is_bounds(&self) -> bool {
166        self.lb.is_some() || self.ub.is_some()
167    }
168}
169
170/// Minimizes a scalar function of one or more variables with constraints.
171///
172/// # Arguments
173///
174/// * `func` - A function that takes a slice of values and returns a scalar
175/// * `x0` - The initial guess
176/// * `constraints` - Vector of constraints
177/// * `method` - The optimization method to use
178/// * `options` - Options for the optimizer
179///
180/// # Returns
181///
182/// * `OptimizeResults` containing the optimization results
183///
184/// # Example
185///
186/// ```
187/// use ndarray::array;
188/// use scirs2_optimize::constrained::{minimize_constrained, Method, Constraint};
189///
190/// // Function to minimize
191/// fn objective(x: &[f64]) -> f64 {
192///     (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
193/// }
194///
195/// // Constraint: x[0] + x[1] <= 3
196/// fn constraint(x: &[f64]) -> f64 {
197///     3.0 - x[0] - x[1]  // Should be >= 0
198/// }
199///
200/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
201/// let initial_point = array![0.0, 0.0];
202/// let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
203///
204/// let result = minimize_constrained(
205///     objective,
206///     &initial_point,
207///     &constraints,
208///     Method::SLSQP,
209///     None
210/// )?;
211/// # Ok(())
212/// # }
213/// ```
214pub fn minimize_constrained<F, S>(
215    func: F,
216    x0: &ArrayBase<S, Ix1>,
217    constraints: &[Constraint<ConstraintFn>],
218    method: Method,
219    options: Option<Options>,
220) -> OptimizeResult<OptimizeResults<f64>>
221where
222    F: Fn(&[f64]) -> f64,
223    S: Data<Elem = f64>,
224{
225    let options = options.unwrap_or_default();
226
227    // Implementation of various methods will go here
228    match method {
229        Method::SLSQP => minimize_slsqp(func, x0, constraints, &options),
230        Method::TrustConstr => minimize_trust_constr(func, x0, constraints, &options),
231        _ => Err(OptimizeError::NotImplementedError(format!(
232            "Method {:?} is not yet implemented",
233            method
234        ))),
235    }
236}
237
238/// Implements the SLSQP algorithm for constrained optimization
239fn minimize_slsqp<F, S>(
240    func: F,
241    x0: &ArrayBase<S, Ix1>,
242    constraints: &[Constraint<ConstraintFn>],
243    options: &Options,
244) -> OptimizeResult<OptimizeResults<f64>>
245where
246    F: Fn(&[f64]) -> f64,
247    S: Data<Elem = f64>,
248{
249    // Get options or use defaults
250    let ftol = options.ftol.unwrap_or(1e-8);
251    let gtol = options.gtol.unwrap_or(1e-8);
252    let ctol = options.ctol.unwrap_or(1e-8);
253    let maxiter = options.maxiter.unwrap_or(100 * x0.len());
254    let eps = options.eps.unwrap_or(1e-8);
255
256    // Initialize variables
257    let n = x0.len();
258    let mut x = x0.to_owned();
259    let mut f = func(x.as_slice().unwrap());
260    let mut nfev = 1;
261
262    // Initialize the Lagrange multipliers for inequality constraints
263    let mut lambda = Array1::zeros(constraints.len());
264
265    // Calculate initial gradient using finite differences
266    let mut g = Array1::zeros(n);
267    for i in 0..n {
268        let mut x_h = x.clone();
269        x_h[i] += eps;
270        let f_h = func(x_h.as_slice().unwrap());
271        g[i] = (f_h - f) / eps;
272        nfev += 1;
273    }
274
275    // Evaluate initial constraints
276    let mut c = Array1::zeros(constraints.len());
277    let _ceq: Array1<f64> = Array1::zeros(0); // No equality constraints support for now
278    for (i, constraint) in constraints.iter().enumerate() {
279        if !constraint.is_bounds() {
280            let val = (constraint.fun)(x.as_slice().unwrap());
281
282            match constraint.kind {
283                ConstraintKind::Inequality => {
284                    c[i] = val; // g(x) >= 0 constraint
285                }
286                ConstraintKind::Equality => {
287                    // For simplicity, we don't fully support equality constraints yet
288                    return Err(OptimizeError::NotImplementedError(
289                        "Equality constraints not fully implemented in SLSQP yet".to_string(),
290                    ));
291                }
292            }
293        }
294    }
295
296    // Calculate constraint Jacobian
297    let mut a = Array2::zeros((constraints.len(), n));
298    for (i, constraint) in constraints.iter().enumerate() {
299        if !constraint.is_bounds() {
300            for j in 0..n {
301                let mut x_h = x.clone();
302                x_h[j] += eps;
303                let c_h = (constraint.fun)(x_h.as_slice().unwrap());
304                a[[i, j]] = (c_h - c[i]) / eps;
305                nfev += 1;
306            }
307        }
308    }
309
310    // Initialize working matrices
311    let mut h_inv = Array2::eye(n); // Approximate inverse Hessian
312
313    // Main optimization loop
314    let mut iter = 0;
315
316    while iter < maxiter {
317        // Check constraint violation
318        let mut max_constraint_violation = 0.0;
319        for (i, &ci) in c.iter().enumerate() {
320            if constraints[i].kind == ConstraintKind::Inequality && ci < -ctol {
321                max_constraint_violation = f64::max(max_constraint_violation, -ci);
322            }
323        }
324
325        // Check convergence on gradient
326        if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
327            break;
328        }
329
330        // Compute the search direction using QP subproblem
331        // For simplicity, we'll use a projected gradient approach
332        let mut p = Array1::zeros(n);
333
334        if max_constraint_violation > ctol {
335            // If constraints are violated, move toward feasibility
336            for (i, &ci) in c.iter().enumerate() {
337                if ci < -ctol {
338                    // This constraint is violated
339                    for j in 0..n {
340                        p[j] -= a[[i, j]] * ci; // Move along constraint gradient
341                    }
342                }
343            }
344        } else {
345            // Otherwise, use BFGS direction with constraints
346            p = -&h_inv.dot(&g);
347
348            // Project gradient on active constraints
349            for (i, &ci) in c.iter().enumerate() {
350                if ci.abs() < ctol {
351                    // Active constraint
352                    let mut normal = Array1::zeros(n);
353                    for j in 0..n {
354                        normal[j] = a[[i, j]];
355                    }
356                    let norm = normal.dot(&normal).sqrt();
357                    if norm > 1e-10 {
358                        normal = &normal / norm;
359                        let p_dot_normal = p.dot(&normal);
360
361                        // If moving in the wrong direction (constraint violation), project out
362                        if p_dot_normal < 0.0 {
363                            p = &p - &(&normal * p_dot_normal);
364                        }
365                    }
366                }
367            }
368        }
369
370        // Line search with constraint awareness
371        let mut alpha = 1.0;
372        let c1 = 1e-4; // Sufficient decrease parameter
373        let rho = 0.5; // Backtracking parameter
374
375        // Initial step
376        let mut x_new = &x + &(&p * alpha);
377        let mut f_new = func(x_new.as_slice().unwrap());
378        nfev += 1;
379
380        // Evaluate constraints at new point
381        let mut c_new = Array1::zeros(constraints.len());
382        for (i, constraint) in constraints.iter().enumerate() {
383            if !constraint.is_bounds() {
384                c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
385                nfev += 1;
386            }
387        }
388
389        // Check if constraint violation is reduced and objective decreases
390        let mut max_viol = 0.0;
391        let mut max_viol_new = 0.0;
392
393        for (i, constraint) in constraints.iter().enumerate() {
394            if constraint.kind == ConstraintKind::Inequality {
395                max_viol = f64::max(max_viol, f64::max(0.0, -c[i]));
396                max_viol_new = f64::max(max_viol_new, f64::max(0.0, -c_new[i]));
397            }
398        }
399
400        // Compute directional derivative
401        let g_dot_p = g.dot(&p);
402
403        // Backtracking line search
404        while (f_new > f + c1 * alpha * g_dot_p && max_viol <= ctol)
405            || (max_viol_new > max_viol && max_viol > ctol)
406        {
407            alpha *= rho;
408
409            // Prevent tiny steps
410            if alpha < 1e-10 {
411                break;
412            }
413
414            x_new = &x + &(&p * alpha);
415            f_new = func(x_new.as_slice().unwrap());
416            nfev += 1;
417
418            // Evaluate constraints
419            for (i, constraint) in constraints.iter().enumerate() {
420                if !constraint.is_bounds() {
421                    c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
422                    nfev += 1;
423                }
424            }
425
426            max_viol_new = 0.0;
427            for (i, constraint) in constraints.iter().enumerate() {
428                if constraint.kind == ConstraintKind::Inequality {
429                    max_viol_new = f64::max(max_viol_new, f64::max(0.0, -c_new[i]));
430                }
431            }
432        }
433
434        // Check convergence on function value and step size
435        if ((f - f_new).abs() < ftol * (1.0 + f.abs())) && alpha * p.dot(&p).sqrt() < ftol {
436            break;
437        }
438
439        // Calculate new gradient
440        let mut g_new = Array1::zeros(n);
441        for i in 0..n {
442            let mut x_h = x_new.clone();
443            x_h[i] += eps;
444            let f_h = func(x_h.as_slice().unwrap());
445            g_new[i] = (f_h - f_new) / eps;
446            nfev += 1;
447        }
448
449        // Calculate new constraint Jacobian
450        let mut a_new = Array2::zeros((constraints.len(), n));
451        for (i, constraint) in constraints.iter().enumerate() {
452            if !constraint.is_bounds() {
453                for j in 0..n {
454                    let mut x_h = x_new.clone();
455                    x_h[j] += eps;
456                    let c_h = (constraint.fun)(x_h.as_slice().unwrap());
457                    a_new[[i, j]] = (c_h - c_new[i]) / eps;
458                    nfev += 1;
459                }
460            }
461        }
462
463        // Update Lagrange multipliers (rudimentary)
464        for (i, constraint) in constraints.iter().enumerate() {
465            if constraint.kind == ConstraintKind::Inequality && c_new[i].abs() < ctol {
466                // For active inequality constraints
467                let mut normal = Array1::zeros(n);
468                for j in 0..n {
469                    normal[j] = a_new[[i, j]];
470                }
471
472                let norm = normal.dot(&normal).sqrt();
473                if norm > 1e-10 {
474                    normal = &normal / norm;
475                    lambda[i] = -g_new.dot(&normal);
476                }
477            } else {
478                lambda[i] = 0.0;
479            }
480        }
481
482        // BFGS update for the Hessian approximation
483        let s = &x_new - &x;
484        let y = &g_new - &g;
485
486        // Include constraints in y by adding Lagrangian term
487        let mut y_lag = y.clone();
488        for (i, &li) in lambda.iter().enumerate() {
489            if li > 0.0 {
490                // Only active constraints
491                for j in 0..n {
492                    // L = f - sum(lambda_i * c_i)
493                    // So gradient of L includes -lambda_i * grad(c_i)
494                    y_lag[j] += li * (a_new[[i, j]] - a[[i, j]]);
495                }
496            }
497        }
498
499        // BFGS update formula
500        let rho_bfgs = 1.0 / y_lag.dot(&s);
501        if rho_bfgs.is_finite() && rho_bfgs > 0.0 {
502            let i_mat = Array2::eye(n);
503            let y_row = y_lag.clone().insert_axis(Axis(0));
504            let s_col = s.clone().insert_axis(Axis(1));
505            let y_s_t = y_row.dot(&s_col);
506
507            let term1 = &i_mat - &(&y_s_t * rho_bfgs);
508            let s_row = s.clone().insert_axis(Axis(0));
509            let y_col = y_lag.clone().insert_axis(Axis(1));
510            let s_y_t = s_row.dot(&y_col);
511
512            let term2 = &i_mat - &(&s_y_t * rho_bfgs);
513            let term3 = &term1.dot(&h_inv);
514            h_inv = term3.dot(&term2) + rho_bfgs * s_col.dot(&s_row);
515        }
516
517        // Update for next iteration
518        x = x_new;
519        f = f_new;
520        g = g_new;
521        c = c_new;
522        a = a_new;
523
524        iter += 1;
525    }
526
527    // Prepare constraint values for the result
528    let mut c_result = Array1::zeros(constraints.len());
529    for (i, constraint) in constraints.iter().enumerate() {
530        if !constraint.is_bounds() {
531            c_result[i] = c[i];
532        }
533    }
534
535    // Create and return result
536    let mut result = OptimizeResults::default();
537    result.x = x;
538    result.fun = f;
539    result.jac = Some(g.into_raw_vec_and_offset().0);
540    result.constr = Some(c_result);
541    result.nfev = nfev;
542    result.nit = iter;
543    result.success = iter < maxiter;
544
545    if result.success {
546        result.message = "Optimization terminated successfully.".to_string();
547    } else {
548        result.message = "Maximum iterations reached.".to_string();
549    }
550
551    Ok(result)
552}
553
554/// Implements the Trust Region Constrained algorithm for constrained optimization
555fn minimize_trust_constr<F, S>(
556    func: F,
557    x0: &ArrayBase<S, Ix1>,
558    constraints: &[Constraint<ConstraintFn>],
559    options: &Options,
560) -> OptimizeResult<OptimizeResults<f64>>
561where
562    F: Fn(&[f64]) -> f64,
563    S: Data<Elem = f64>,
564{
565    // Get options or use defaults
566    let ftol = options.ftol.unwrap_or(1e-8);
567    let gtol = options.gtol.unwrap_or(1e-8);
568    let ctol = options.ctol.unwrap_or(1e-8);
569    let maxiter = options.maxiter.unwrap_or(100 * x0.len());
570    let eps = options.eps.unwrap_or(1e-8);
571
572    // Initialize variables
573    let n = x0.len();
574    let mut x = x0.to_owned();
575    let mut f = func(x.as_slice().unwrap());
576    let mut nfev = 1;
577
578    // Initialize the Lagrange multipliers
579    let mut lambda = Array1::zeros(constraints.len());
580
581    // Calculate initial gradient using finite differences
582    let mut g = Array1::zeros(n);
583    for i in 0..n {
584        let mut x_h = x.clone();
585        x_h[i] += eps;
586        let f_h = func(x_h.as_slice().unwrap());
587        g[i] = (f_h - f) / eps;
588        nfev += 1;
589    }
590
591    // Evaluate initial constraints
592    let mut c = Array1::zeros(constraints.len());
593    for (i, constraint) in constraints.iter().enumerate() {
594        if !constraint.is_bounds() {
595            let val = (constraint.fun)(x.as_slice().unwrap());
596
597            match constraint.kind {
598                ConstraintKind::Inequality => {
599                    c[i] = val; // g(x) >= 0 constraint
600                }
601                ConstraintKind::Equality => {
602                    // For simplicity, we don't fully support equality constraints yet
603                    return Err(OptimizeError::NotImplementedError(
604                        "Equality constraints not fully implemented in Trust-Region yet"
605                            .to_string(),
606                    ));
607                }
608            }
609        }
610    }
611
612    // Calculate constraint Jacobian
613    let mut a = Array2::zeros((constraints.len(), n));
614    for (i, constraint) in constraints.iter().enumerate() {
615        if !constraint.is_bounds() {
616            for j in 0..n {
617                let mut x_h = x.clone();
618                x_h[j] += eps;
619                let c_h = (constraint.fun)(x_h.as_slice().unwrap());
620                a[[i, j]] = (c_h - c[i]) / eps;
621                nfev += 1;
622            }
623        }
624    }
625
626    // Initialize trust region radius
627    let mut delta = 1.0;
628
629    // Initialize approximation of the Hessian of the Lagrangian
630    let mut b = Array2::eye(n);
631
632    // Main optimization loop
633    let mut iter = 0;
634
635    while iter < maxiter {
636        // Check constraint violation
637        let mut max_constraint_violation = 0.0;
638        for (i, &ci) in c.iter().enumerate() {
639            if constraints[i].kind == ConstraintKind::Inequality && ci < -ctol {
640                max_constraint_violation = f64::max(max_constraint_violation, -ci);
641            }
642        }
643
644        // Check convergence on gradient and constraints
645        if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
646            break;
647        }
648
649        // Compute the Lagrangian gradient
650        let mut lag_grad = g.clone();
651        for (i, &li) in lambda.iter().enumerate() {
652            if li > 0.0 || c[i] < ctol {
653                // Active or violated constraint
654                for j in 0..n {
655                    // L = f - sum(lambda_i * c_i) for inequality constraints
656                    // So gradient of L is grad(f) - sum(lambda_i * grad(c_i))
657                    lag_grad[j] -= li * a[[i, j]];
658                }
659            }
660        }
661
662        // Compute the step using a trust-region approach
663        // Solve the constrained quadratic subproblem:
664        // min 0.5 * p^T B p + g^T p  s.t. ||p|| <= delta and linearized constraints
665
666        let (p, predicted_reduction) =
667            compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
668
669        // Try the step
670        let x_new = &x + &p;
671
672        // Evaluate function and constraints at new point
673        let f_new = func(x_new.as_slice().unwrap());
674        nfev += 1;
675
676        let mut c_new = Array1::zeros(constraints.len());
677        for (i, constraint) in constraints.iter().enumerate() {
678            if !constraint.is_bounds() {
679                c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
680                nfev += 1;
681            }
682        }
683
684        // Compute change in merit function (includes constraint violation)
685        let mut merit = f;
686        let mut merit_new = f_new;
687
688        // Add constraint violation penalty
689        let penalty = 10.0; // Simple fixed penalty parameter
690        for (i, &ci) in c.iter().enumerate() {
691            if constraints[i].kind == ConstraintKind::Inequality {
692                merit += penalty * f64::max(0.0, -ci);
693                merit_new += penalty * f64::max(0.0, -c_new[i]);
694            }
695        }
696
697        // Compute actual reduction in merit function
698        let actual_reduction = merit - merit_new;
699
700        // Compute ratio of actual to predicted reduction
701        let rho = if predicted_reduction > 0.0 {
702            actual_reduction / predicted_reduction
703        } else {
704            0.0
705        };
706
707        // Update trust region radius based on the quality of the step
708        if rho < 0.25 {
709            delta *= 0.5;
710        } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
711            delta *= 2.0;
712        }
713
714        // Accept or reject the step
715        if rho > 0.1 {
716            // Accept the step
717            x = x_new;
718            f = f_new;
719            c = c_new;
720
721            // Check convergence on function value
722            if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
723                break;
724            }
725
726            // Compute new gradient
727            let mut g_new = Array1::zeros(n);
728            for i in 0..n {
729                let mut x_h = x.clone();
730                x_h[i] += eps;
731                let f_h = func(x_h.as_slice().unwrap());
732                g_new[i] = (f_h - f) / eps;
733                nfev += 1;
734            }
735
736            // Compute new constraint Jacobian
737            let mut a_new = Array2::zeros((constraints.len(), n));
738            for (i, constraint) in constraints.iter().enumerate() {
739                if !constraint.is_bounds() {
740                    for j in 0..n {
741                        let mut x_h = x.clone();
742                        x_h[j] += eps;
743                        let c_h = (constraint.fun)(x_h.as_slice().unwrap());
744                        a_new[[i, j]] = (c_h - c[i]) / eps;
745                        nfev += 1;
746                    }
747                }
748            }
749
750            // Update Lagrange multipliers using projected gradient method
751            for (i, constraint) in constraints.iter().enumerate() {
752                if constraint.kind == ConstraintKind::Inequality {
753                    if c[i] < ctol {
754                        // Active or violated constraint
755                        // Increase multiplier if constraint is violated
756                        lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
757                    } else {
758                        // Decrease multiplier towards zero
759                        lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
760                    }
761                }
762            }
763
764            // Update Hessian approximation using BFGS or SR1
765            let s = &p;
766            let y = &g_new - &g;
767
768            // Simple BFGS update for the Hessian approximation
769            let s_dot_y = s.dot(&y);
770            if s_dot_y > 1e-10 {
771                let s_col = s.clone().insert_axis(Axis(1));
772                let s_row = s.clone().insert_axis(Axis(0));
773
774                let bs = b.dot(s);
775                let bs_col = bs.clone().insert_axis(Axis(1));
776                let bs_row = bs.clone().insert_axis(Axis(0));
777
778                let term1 = s_dot_y + s.dot(&bs);
779                let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
780
781                let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
782
783                b = &b + &term2 - &(&term3 / s_dot_y);
784            }
785
786            // Update variables for next iteration
787            g = g_new;
788            a = a_new;
789        }
790
791        iter += 1;
792    }
793
794    // Prepare constraint values for the result
795    let mut c_result = Array1::zeros(constraints.len());
796    for (i, constraint) in constraints.iter().enumerate() {
797        if !constraint.is_bounds() {
798            c_result[i] = c[i];
799        }
800    }
801
802    // Create and return result
803    let mut result = OptimizeResults::default();
804    result.x = x;
805    result.fun = f;
806    result.jac = Some(g.into_raw_vec_and_offset().0);
807    result.constr = Some(c_result);
808    result.nfev = nfev;
809    result.nit = iter;
810    result.success = iter < maxiter;
811
812    if result.success {
813        result.message = "Optimization terminated successfully.".to_string();
814    } else {
815        result.message = "Maximum iterations reached.".to_string();
816    }
817
818    Ok(result)
819}
820
821/// Compute a trust-region step for constrained optimization
822fn compute_trust_region_step_constrained(
823    g: &Array1<f64>,
824    b: &Array2<f64>,
825    a: &Array2<f64>,
826    c: &Array1<f64>,
827    delta: f64,
828    constraints: &[Constraint<ConstraintFn>],
829    ctol: f64,
830) -> (Array1<f64>, f64) {
831    let n = g.len();
832    let n_constr = constraints.len();
833
834    // Compute the unconstrained Cauchy point (steepest descent direction)
835    let p_unconstrained = compute_unconstrained_cauchy_point(g, b, delta);
836
837    // Check if unconstrained Cauchy point satisfies linearized constraints
838    let mut constraint_violated = false;
839    for i in 0..n_constr {
840        if constraints[i].kind == ConstraintKind::Inequality {
841            let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p_unconstrained[j]).sum::<f64>();
842            if c[i] + grad_c_dot_p < -ctol {
843                constraint_violated = true;
844                break;
845            }
846        }
847    }
848
849    // If unconstrained point is feasible, use it
850    if !constraint_violated {
851        // Compute predicted reduction
852        let g_dot_p = g.dot(&p_unconstrained);
853        let bp = b.dot(&p_unconstrained);
854        let p_dot_bp = p_unconstrained.dot(&bp);
855        let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
856
857        return (p_unconstrained, predicted_reduction);
858    }
859
860    // Otherwise, project onto the linearized feasible region
861    // This is a simplified approach - in practice, you would solve a CQP
862
863    // Start with the steepest descent direction
864    let mut p = Array1::zeros(n);
865    for i in 0..n {
866        p[i] = -g[i];
867    }
868
869    // Normalize to trust region radius
870    let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
871    if p_norm > 1e-10 {
872        p = &p * (delta / p_norm);
873    }
874
875    // Project onto each constraint
876    for _iter in 0..5 {
877        // Limited iterations for projection
878        let mut max_viol = 0.0;
879        let mut most_violated = 0;
880
881        // Find most violated constraint
882        for i in 0..n_constr {
883            if constraints[i].kind == ConstraintKind::Inequality {
884                let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p[j]).sum::<f64>();
885                let viol = -(c[i] + grad_c_dot_p);
886                if viol > max_viol {
887                    max_viol = viol;
888                    most_violated = i;
889                }
890            }
891        }
892
893        if max_viol < ctol {
894            break;
895        }
896
897        // Project p onto the constraint
898        let mut a_norm_sq = 0.0;
899        for j in 0..n {
900            a_norm_sq += a[[most_violated, j]] * a[[most_violated, j]];
901        }
902
903        if a_norm_sq > 1e-10 {
904            let grad_c_dot_p = (0..n).map(|j| a[[most_violated, j]] * p[j]).sum::<f64>();
905            let proj_dist = (c[most_violated] + grad_c_dot_p) / a_norm_sq;
906
907            // Project p
908            for j in 0..n {
909                p[j] += a[[most_violated, j]] * proj_dist;
910            }
911
912            // Rescale to trust region
913            let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
914            if p_norm > delta {
915                p = &p * (delta / p_norm);
916            }
917        }
918    }
919
920    // Compute predicted reduction
921    let g_dot_p = g.dot(&p);
922    let bp = b.dot(&p);
923    let p_dot_bp = p.dot(&bp);
924    let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
925
926    (p, predicted_reduction)
927}
928
929/// Compute the unconstrained Cauchy point (steepest descent to trust region boundary)
930fn compute_unconstrained_cauchy_point(g: &Array1<f64>, b: &Array2<f64>, delta: f64) -> Array1<f64> {
931    let n = g.len();
932
933    // Compute the steepest descent direction: -g
934    let mut p = Array1::zeros(n);
935    for i in 0..n {
936        p[i] = -g[i];
937    }
938
939    // Compute g^T B g and ||g||^2
940    let bg = b.dot(g);
941    let g_dot_bg = g.dot(&bg);
942    let g_norm_sq = g.dot(g);
943
944    // Check if the gradient is practically zero
945    if g_norm_sq < 1e-10 {
946        // If gradient is practically zero, don't move
947        return Array1::zeros(n);
948    }
949
950    // Compute tau (step to the boundary)
951    let tau = if g_dot_bg <= 0.0 {
952        // Negative curvature or zero curvature case
953        delta / g_norm_sq.sqrt()
954    } else {
955        // Positive curvature case
956        f64::min(delta / g_norm_sq.sqrt(), g_norm_sq / g_dot_bg)
957    };
958
959    // Scale the direction by tau
960    for i in 0..n {
961        p[i] *= tau;
962    }
963
964    p
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use ndarray::array;
971
972    fn objective(x: &[f64]) -> f64 {
973        (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
974    }
975
976    fn constraint(x: &[f64]) -> f64 {
977        3.0 - x[0] - x[1] // Should be >= 0
978    }
979
980    #[test]
981    fn test_minimize_constrained_placeholder() {
982        // We're now using the real implementation, so this test needs to be adjusted
983        let x0 = array![0.0, 0.0];
984        let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
985
986        // Use minimal iterations to check basic algorithm behavior
987        let options = Options {
988            maxiter: Some(1), // Just a single iteration
989            ..Options::default()
990        };
991
992        let result = minimize_constrained(
993            objective,
994            &x0.view(),
995            &constraints,
996            Method::SLSQP,
997            Some(options),
998        )
999        .unwrap();
1000
1001        // With limited iterations, we expect it not to converge
1002        assert!(!result.success);
1003
1004        // Check that constraint value was computed
1005        assert!(result.constr.is_some());
1006        let constr = result.constr.unwrap();
1007        assert_eq!(constr.len(), 1);
1008    }
1009
1010    // Test the SLSQP algorithm on a simple constrained problem
1011    #[test]
1012    fn test_minimize_slsqp() {
1013        // Problem:
1014        // Minimize (x-1)^2 + (y-2.5)^2
1015        // Subject to: x + y <= 3
1016
1017        let x0 = array![0.0, 0.0];
1018        let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
1019
1020        let options = Options {
1021            maxiter: Some(100),
1022            gtol: Some(1e-6),
1023            ftol: Some(1e-6),
1024            ctol: Some(1e-6),
1025            ..Options::default()
1026        };
1027
1028        let result = minimize_constrained(
1029            objective,
1030            &x0.view(),
1031            &constraints,
1032            Method::SLSQP,
1033            Some(options),
1034        )
1035        .unwrap();
1036
1037        // For the purpose of this test, we're just checking that the algorithm runs
1038        // and produces reasonable output. The convergence may vary.
1039
1040        // Check that we're moving in the right direction
1041        assert!(result.x[0] >= 0.0);
1042        assert!(result.x[1] >= 0.0);
1043
1044        // Function value should be decreasing from initial point
1045        let initial_value = objective(&[0.0, 0.0]);
1046        assert!(result.fun <= initial_value);
1047
1048        // Check that constraint values are computed
1049        assert!(result.constr.is_some());
1050
1051        // Output the result for inspection
1052        println!(
1053            "SLSQP result: x = {:?}, f = {}, iterations = {}",
1054            result.x, result.fun, result.nit
1055        );
1056    }
1057
1058    // Test the Trust Region Constrained algorithm
1059    #[test]
1060    fn test_minimize_trust_constr() {
1061        // Problem:
1062        // Minimize (x-1)^2 + (y-2.5)^2
1063        // Subject to: x + y <= 3
1064
1065        let x0 = array![0.0, 0.0];
1066        let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
1067
1068        let options = Options {
1069            maxiter: Some(500), // Increased iterations for convergence
1070            gtol: Some(1e-6),
1071            ftol: Some(1e-6),
1072            ctol: Some(1e-6),
1073            ..Options::default()
1074        };
1075
1076        let result = minimize_constrained(
1077            objective,
1078            &x0.view(),
1079            &constraints,
1080            Method::TrustConstr,
1081            Some(options.clone()),
1082        )
1083        .unwrap();
1084
1085        // Check that we're moving in the right direction
1086        assert!(result.x[0] >= 0.0);
1087        assert!(result.x[1] >= 0.0);
1088
1089        // Function value should be decreasing from initial point
1090        let initial_value = objective(&[0.0, 0.0]);
1091        assert!(result.fun <= initial_value);
1092
1093        // Check that constraint values are computed
1094        assert!(result.constr.is_some());
1095
1096        // Output the result for inspection
1097        println!(
1098            "TrustConstr result: x = {:?}, f = {}, iterations = {}",
1099            result.x, result.fun, result.nit
1100        );
1101    }
1102
1103    // Test both constrained optimization methods on a more complex problem
1104    #[test]
1105    fn test_constrained_rosenbrock() {
1106        // Rosenbrock function with a constraint
1107        fn rosenbrock(x: &[f64]) -> f64 {
1108            100.0 * (x[1] - x[0].powi(2)).powi(2) + (1.0 - x[0]).powi(2)
1109        }
1110
1111        // Constraint: x[0]^2 + x[1]^2 <= 1.5
1112        fn circle_constraint(x: &[f64]) -> f64 {
1113            1.5 - (x[0].powi(2) + x[1].powi(2)) // Should be >= 0
1114        }
1115
1116        let x0 = array![0.0, 0.0];
1117        let constraints = vec![Constraint::new(circle_constraint, Constraint::INEQUALITY)];
1118
1119        let options = Options {
1120            maxiter: Some(1000), // More iterations for this harder problem
1121            gtol: Some(1e-4),    // Relaxed tolerances
1122            ftol: Some(1e-4),
1123            ctol: Some(1e-4),
1124            ..Options::default()
1125        };
1126
1127        // For this test, we'll clone options at each stage to avoid move issues
1128        let options_copy1 = options.clone();
1129        let options_copy2 = options.clone();
1130
1131        // Test SLSQP
1132        let result_slsqp = minimize_constrained(
1133            rosenbrock,
1134            &x0.view(),
1135            &constraints,
1136            Method::SLSQP,
1137            Some(options_copy1),
1138        )
1139        .unwrap();
1140
1141        // Test TrustConstr
1142        let result_trust = minimize_constrained(
1143            rosenbrock,
1144            &x0.view(),
1145            &constraints,
1146            Method::TrustConstr,
1147            Some(options_copy2),
1148        )
1149        .unwrap();
1150
1151        // Check that both methods find a reasonable solution
1152        println!(
1153            "SLSQP Rosenbrock result: x = {:?}, f = {}, iterations = {}",
1154            result_slsqp.x, result_slsqp.fun, result_slsqp.nit
1155        );
1156        println!(
1157            "TrustConstr Rosenbrock result: x = {:?}, f = {}, iterations = {}",
1158            result_trust.x, result_trust.fun, result_trust.nit
1159        );
1160
1161        // Check that function value is better than initial point
1162        let initial_value = rosenbrock(&[0.0, 0.0]);
1163        assert!(result_slsqp.fun < initial_value);
1164        assert!(result_trust.fun < initial_value);
1165
1166        // Check that constraint is satisfied
1167        let constr_slsqp = result_slsqp.constr.unwrap();
1168        let constr_trust = result_trust.constr.unwrap();
1169        assert!(constr_slsqp[0] >= -0.01); // Relaxed tolerance for the test
1170        assert!(constr_trust[0] >= -0.01); // Relaxed tolerance for the test
1171    }
1172}