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}