Skip to main content

scirs2_optimize/constrained/
mod.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//! ```no_run
9//! use scirs2_core::ndarray::{array, Array1};
10//! use scirs2_optimize::constrained::{minimize_constrained, Method, Constraint};
11//!
12//! // Define a simple function to minimize: f(x) = (x[0] - 1)² + (x[1] - 2.5)²
13//! // Unconstrained minimum is at (1.0, 2.5), but we add a constraint.
14//! fn objective(x: &[f64]) -> f64 {
15//!     (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
16//! }
17//!
18//! // Define a constraint: x[0] + x[1] <= 3
19//! // Written as g(x) >= 0, so: g(x) = 3 - x[0] - x[1]
20//! fn constraint(x: &[f64]) -> f64 {
21//!     3.0 - x[0] - x[1]  // Should be >= 0
22//! }
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! // Minimize the function starting at [1.0, 1.0]
26//! // Note: Initial point should be feasible (satisfy constraints) for best convergence
27//! let initial_point = array![1.0, 1.0];
28//! let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
29//!
30//! let result = minimize_constrained(
31//!     objective,
32//!     &initial_point,
33//!     &constraints,
34//!     Method::SLSQP,
35//!     None
36//! )?;
37//!
38//! // The constrained minimum is at [0.75, 2.25] with f(x) = 0.125
39//! // This is where the gradient of f is parallel to the constraint boundary,
40//! // solved via Lagrange multipliers on x[0] + x[1] = 3.
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! Note: This function requires LAPACK libraries to be linked for certain optimization methods.
46
47use crate::error::OptimizeResult;
48use crate::result::OptimizeResults;
49use scirs2_core::ndarray::{Array1, ArrayBase, Data, Ix1};
50use std::fmt;
51
52// Re-export optimization methods
53pub mod augmented_lagrangian;
54pub mod cobyla;
55pub mod enhanced_sqp;
56pub mod epsilon_constraint;
57pub mod feasibility_rules;
58pub mod interior_point;
59pub mod lp_qp_interior;
60pub mod penalty;
61pub mod slsqp;
62pub mod sqp;
63pub mod sqp_advanced;
64pub mod trust_constr;
65pub mod trust_constr_advanced;
66
67// Re-export main functions
68pub use augmented_lagrangian::{
69    minimize_augmented_lagrangian, minimize_equality_constrained, minimize_inequality_constrained,
70    AugmentedLagrangianOptions, AugmentedLagrangianResult,
71};
72pub use cobyla::minimize_cobyla;
73pub use interior_point::{
74    minimize_interior_point, minimize_interior_point_constrained, InteriorPointOptions,
75    InteriorPointResult,
76};
77pub use slsqp::minimize_slsqp;
78pub use sqp::{minimize_sqp, SqpOptions, SqpResult};
79pub use trust_constr::{
80    minimize_trust_constr, minimize_trust_constr_with_derivatives, GradientFn, HessianFn,
81    HessianUpdate,
82};
83
84#[cfg(test)]
85mod tests;
86
87/// Type alias for constraint functions that take a slice of f64 and return f64
88pub type ConstraintFn = fn(&[f64]) -> f64;
89
90/// Optimization methods for constrained minimization.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Method {
93    /// Sequential Least SQuares Programming
94    SLSQP,
95
96    /// Trust-region constrained algorithm
97    TrustConstr,
98
99    /// Linear programming using the simplex algorithm
100    COBYLA,
101
102    /// Interior point method
103    InteriorPoint,
104
105    /// Augmented Lagrangian method
106    AugmentedLagrangian,
107
108    /// Sequential Quadratic Programming
109    SQP,
110}
111
112impl fmt::Display for Method {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        match self {
115            Method::SLSQP => write!(f, "SLSQP"),
116            Method::TrustConstr => write!(f, "trust-constr"),
117            Method::COBYLA => write!(f, "COBYLA"),
118            Method::InteriorPoint => write!(f, "interior-point"),
119            Method::AugmentedLagrangian => write!(f, "augmented-lagrangian"),
120            Method::SQP => write!(f, "SQP"),
121        }
122    }
123}
124
125/// Options for the constrained optimizer.
126#[derive(Debug, Clone)]
127pub struct Options {
128    /// Maximum number of iterations to perform
129    pub maxiter: Option<usize>,
130
131    /// Precision goal for the value in the stopping criterion
132    pub ftol: Option<f64>,
133
134    /// Precision goal for the gradient in the stopping criterion (relative)
135    pub gtol: Option<f64>,
136
137    /// Precision goal for constraint violation
138    pub ctol: Option<f64>,
139
140    /// Step size used for numerical approximation of the jacobian
141    pub eps: Option<f64>,
142
143    /// Whether to print convergence messages
144    pub disp: bool,
145
146    /// Return the optimization result after each iteration
147    pub return_all: bool,
148}
149
150impl Default for Options {
151    fn default() -> Self {
152        Options {
153            maxiter: None,
154            ftol: Some(1e-8),
155            gtol: Some(1e-8),
156            ctol: Some(1e-8),
157            eps: Some(1e-8),
158            disp: false,
159            return_all: false,
160        }
161    }
162}
163
164/// Constraint type for constrained optimization
165pub struct Constraint<F> {
166    /// The constraint function
167    pub fun: F,
168
169    /// The type of constraint (equality or inequality)
170    pub kind: ConstraintKind,
171
172    /// Lower bound for a box constraint
173    pub lb: Option<f64>,
174
175    /// Upper bound for a box constraint
176    pub ub: Option<f64>,
177}
178
179/// The kind of constraint
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum ConstraintKind {
182    /// Equality constraint: fun(x) = 0
183    Equality,
184
185    /// Inequality constraint: fun(x) >= 0
186    Inequality,
187}
188
189impl Constraint<fn(&[f64]) -> f64> {
190    /// Constant for equality constraint
191    pub const EQUALITY: ConstraintKind = ConstraintKind::Equality;
192
193    /// Constant for inequality constraint
194    pub const INEQUALITY: ConstraintKind = ConstraintKind::Inequality;
195
196    /// Create a new constraint
197    pub fn new(fun: fn(&[f64]) -> f64, kind: ConstraintKind) -> Self {
198        Constraint {
199            fun,
200            kind,
201            lb: None,
202            ub: None,
203        }
204    }
205
206    /// Create a new box constraint
207    pub fn new_bounds(lb: Option<f64>, ub: Option<f64>) -> Self {
208        Constraint {
209            fun: |_| 0.0, // Dummy function for box constraints
210            kind: ConstraintKind::Inequality,
211            lb,
212            ub,
213        }
214    }
215}
216
217impl<F> Constraint<F> {
218    /// Check if this is a box constraint
219    pub fn is_bounds(&self) -> bool {
220        self.lb.is_some() || self.ub.is_some()
221    }
222}
223
224/// Minimizes a scalar function of one or more variables with constraints.
225///
226/// # Arguments
227///
228/// * `func` - A function that takes a slice of values and returns a scalar
229/// * `x0` - The initial guess
230/// * `constraints` - Vector of constraints
231/// * `method` - The optimization method to use
232/// * `options` - Options for the optimizer
233///
234/// # Returns
235///
236/// * `OptimizeResults` containing the optimization results
237///
238/// # Example
239///
240/// ```no_run
241/// use scirs2_core::ndarray::array;
242/// use scirs2_optimize::constrained::{minimize_constrained, Method, Constraint};
243///
244/// // Function to minimize
245/// fn objective(x: &[f64]) -> f64 {
246///     (x[0] - 1.0).powi(2) + (x[1] - 2.5).powi(2)
247/// }
248///
249/// // Constraint: x[0] + x[1] <= 3
250/// fn constraint(x: &[f64]) -> f64 {
251///     3.0 - x[0] - x[1]  // Should be >= 0
252/// }
253///
254/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
255/// let initial_point = array![0.0, 0.0];
256/// let constraints = vec![Constraint::new(constraint, Constraint::INEQUALITY)];
257///
258/// let result = minimize_constrained(
259///     objective,
260///     &initial_point,
261///     &constraints,
262///     Method::SLSQP,
263///     None
264/// )?;
265/// # Ok(())
266/// # }
267/// ```
268#[allow(dead_code)]
269pub fn minimize_constrained<F, S>(
270    func: F,
271    x0: &ArrayBase<S, Ix1>,
272    constraints: &[Constraint<ConstraintFn>],
273    method: Method,
274    options: Option<Options>,
275) -> OptimizeResult<OptimizeResults<f64>>
276where
277    F: Fn(&[f64]) -> f64 + Clone,
278    S: Data<Elem = f64>,
279{
280    let options = options.unwrap_or_default();
281
282    // Implementation of various methods will go here
283    match method {
284        Method::SLSQP => minimize_slsqp(func, x0, constraints, &options),
285        Method::TrustConstr => minimize_trust_constr(func, x0, constraints, &options),
286        Method::COBYLA => minimize_cobyla(func, x0, constraints, &options),
287        Method::InteriorPoint => {
288            // Convert constraints to interior point format
289            let x0_arr = Array1::from_vec(x0.to_vec());
290
291            // Create interior point options from general options
292            let ip_options = InteriorPointOptions {
293                max_iter: options.maxiter.unwrap_or(100),
294                tol: options.gtol.unwrap_or(1e-8),
295                feas_tol: options.ctol.unwrap_or(1e-8),
296                ..Default::default()
297            };
298
299            // Convert to OptimizeResults format
300            match minimize_interior_point_constrained(func, x0_arr, constraints, Some(ip_options)) {
301                Ok(result) => {
302                    let opt_result = OptimizeResults::<f64> {
303                        x: result.x,
304                        fun: result.fun,
305                        nit: result.nit,
306                        nfev: result.nfev,
307                        success: result.success,
308                        message: result.message,
309                        jac: None,
310                        hess: None,
311                        constr: None,
312                        njev: 0,  // Not tracked by interior point method
313                        nhev: 0,  // Not tracked by interior point method
314                        maxcv: 0, // Not applicable for interior point
315                        status: if result.success { 0 } else { 1 },
316                    };
317                    Ok(opt_result)
318                }
319                Err(e) => Err(e),
320            }
321        }
322        Method::AugmentedLagrangian => {
323            use scirs2_core::ndarray::{Array1, ArrayView1};
324            use std::sync::Arc;
325
326            let x0_arr = Array1::from_vec(x0.to_vec());
327
328            // Partition constraints into equality and inequality groups
329            let eq_fns: Arc<Vec<ConstraintFn>> = Arc::new(
330                constraints
331                    .iter()
332                    .filter(|c| c.kind == ConstraintKind::Equality)
333                    .map(|c| c.fun)
334                    .collect(),
335            );
336
337            let ineq_fns: Arc<Vec<ConstraintFn>> = Arc::new(
338                constraints
339                    .iter()
340                    .filter(|c| c.kind == ConstraintKind::Inequality)
341                    .map(|c| c.fun)
342                    .collect(),
343            );
344
345            // Wrap the objective to accept an ArrayView
346            let func_clone = func.clone();
347            let al_fun = move |x: &ArrayView1<f64>| func_clone(x.as_slice().unwrap_or(&[]));
348
349            // Build combined equality constraint closure (Clone via Arc)
350            let al_options = AugmentedLagrangianOptions {
351                max_iter: options.maxiter.unwrap_or(100),
352                constraint_tol: options.ctol.unwrap_or(1e-8),
353                optimality_tol: options.gtol.unwrap_or(1e-8),
354                ..Default::default()
355            };
356
357            // Helper: emit a slice-backed value for a contiguous ArrayView1
358            #[inline]
359            fn view_to_slice(x: &ArrayView1<f64>) -> Vec<f64> {
360                x.iter().copied().collect()
361            }
362
363            let result = if !eq_fns.is_empty() && !ineq_fns.is_empty() {
364                let eq_arc = Arc::clone(&eq_fns);
365                let eq_closure = move |x: &ArrayView1<f64>| {
366                    let xs = view_to_slice(x);
367                    Array1::from_vec(eq_arc.iter().map(|f| f(&xs)).collect())
368                };
369                let ineq_arc = Arc::clone(&ineq_fns);
370                let ineq_closure = move |x: &ArrayView1<f64>| {
371                    let xs = view_to_slice(x);
372                    Array1::from_vec(ineq_arc.iter().map(|f| f(&xs)).collect())
373                };
374                minimize_augmented_lagrangian(
375                    al_fun,
376                    x0_arr,
377                    Some(eq_closure),
378                    Some(ineq_closure),
379                    Some(al_options),
380                )?
381            } else if !eq_fns.is_empty() {
382                let eq_arc = Arc::clone(&eq_fns);
383                let eq_closure = move |x: &ArrayView1<f64>| {
384                    let xs = view_to_slice(x);
385                    Array1::from_vec(eq_arc.iter().map(|f| f(&xs)).collect())
386                };
387                minimize_augmented_lagrangian(
388                    al_fun,
389                    x0_arr,
390                    Some(eq_closure),
391                    None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
392                    Some(al_options),
393                )?
394            } else {
395                let ineq_arc = Arc::clone(&ineq_fns);
396                let ineq_closure = move |x: &ArrayView1<f64>| {
397                    let xs = view_to_slice(x);
398                    Array1::from_vec(ineq_arc.iter().map(|f| f(&xs)).collect())
399                };
400                minimize_augmented_lagrangian(
401                    al_fun,
402                    x0_arr,
403                    None::<fn(&ArrayView1<f64>) -> Array1<f64>>,
404                    Some(ineq_closure),
405                    Some(al_options),
406                )?
407            };
408
409            Ok(OptimizeResults::<f64> {
410                x: result.x,
411                fun: result.fun,
412                nit: result.nit,
413                nfev: result.nfev,
414                success: result.success,
415                message: result.message,
416                jac: None,
417                hess: None,
418                constr: None,
419                njev: 0,
420                nhev: 0,
421                maxcv: 0,
422                status: if result.success { 0 } else { 1 },
423            })
424        }
425        Method::SQP => sqp::minimize_sqp_compat(func, x0, constraints, &options),
426    }
427}