scirs2_optimize/constrained/
cobyla.rs

1//! COBYLA (Constrained Optimization BY Linear Approximations) algorithm
2
3use crate::constrained::{Constraint, ConstraintFn, ConstraintKind, Options};
4use crate::error::{OptimizeError, OptimizeResult};
5use crate::result::OptimizeResults;
6use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Data, Ix1};
7use scirs2_core::validation::check_finite;
8
9/// COBYLA optimizer for constrained optimization
10///
11/// COBYLA is a numerical optimization method for constrained problems where
12/// the objective function and the constraints are not required to be differentiable.
13/// It works by building linear approximations of the objective function and
14/// constraints, then solving linear programming subproblems.
15///
16/// # Arguments
17///
18/// * `func` - The objective function to minimize
19/// * `x0` - Initial guess for the solution
20/// * `constraints` - Vector of constraint functions
21/// * `options` - Optimization options
22///
23/// # References
24///
25/// Powell, M. J. D. (1994). "A Direct Search Optimization Method That Models
26/// the Objective and Constraint Functions by Linear Interpolation."
27/// In Advances in Optimization and Numerical Analysis, pp. 51-67.
28#[allow(dead_code)]
29pub fn minimize_cobyla<F, S>(
30    func: F,
31    x0: &ArrayBase<S, Ix1>,
32    constraints: &[Constraint<ConstraintFn>],
33    options: &Options,
34) -> OptimizeResult<OptimizeResults<f64>>
35where
36    F: Fn(&[f64]) -> f64,
37    S: Data<Elem = f64>,
38{
39    let n = x0.len();
40    let x0_vec = x0.to_vec();
41
42    // Validate inputs
43    for (i, &val) in x0_vec.iter().enumerate() {
44        check_finite(val, format!("x0[{}]", i))?;
45    }
46    if n == 0 {
47        return Err(OptimizeError::ValueError("x0 cannot be empty".to_string()));
48    }
49
50    // Set up algorithm parameters
51    let maxiter = options.maxiter.unwrap_or(1000);
52    let ftol = options.ftol.unwrap_or(1e-8);
53    let ctol = options.ctol.unwrap_or(1e-8);
54
55    // Initial trust region radius
56    let mut rho = 0.1;
57    let rho_end = 1e-8;
58    let rho_beg = 0.5;
59
60    if rho_beg < rho_end {
61        return Err(OptimizeError::ValueError(
62            "Initial trust region radius must be >= final radius".to_string(),
63        ));
64    }
65
66    // Initialize state
67    let mut x = Array1::from_vec(x0_vec);
68    let mut f = func(&x.to_vec());
69    let mut nfev = 1;
70    let mut nit = 0;
71
72    // Count constraints
73    let num_constraints = constraints.len();
74
75    // Evaluate initial constraints
76    let mut constraint_values = Array1::zeros(num_constraints);
77    for (i, constraint) in constraints.iter().enumerate() {
78        constraint_values[i] = (constraint.fun)(&x.to_vec());
79        nfev += 1;
80    }
81
82    // Check initial constraint satisfaction
83    let mut max_constraint_violation: f64 = 0.0;
84    for (i, constraint) in constraints.iter().enumerate() {
85        let violation = match constraint.kind {
86            ConstraintKind::Equality => constraint_values[i].abs(),
87            ConstraintKind::Inequality => {
88                if constraint_values[i] < 0.0 {
89                    -constraint_values[i]
90                } else {
91                    0.0
92                }
93            }
94        };
95        max_constraint_violation = max_constraint_violation.max(violation);
96    }
97
98    // Build initial interpolation set
99    let npt = 2 * n + 1; // Number of interpolation points
100    let mut xpt = Array2::zeros((npt, n)); // Interpolation points
101    let mut fval = Array1::zeros(npt); // Function values at interpolation points
102    let mut con = Array2::zeros((npt, num_constraints)); // Constraint values
103
104    // First point is the starting point
105    xpt.row_mut(0).assign(&x);
106    fval[0] = f;
107    con.row_mut(0).assign(&constraint_values);
108
109    // Create additional interpolation points
110    for i in 1..=n {
111        let mut xi = x.clone();
112        xi[i - 1] += rho;
113        xpt.row_mut(i).assign(&xi);
114        fval[i] = func(&xi.to_vec());
115        nfev += 1;
116
117        for (j, constraint) in constraints.iter().enumerate() {
118            con[[i, j]] = (constraint.fun)(&xi.to_vec());
119            nfev += 1;
120        }
121    }
122
123    for i in (n + 1)..npt {
124        let mut xi = x.clone();
125        xi[i - n - 1] -= rho;
126        xpt.row_mut(i).assign(&xi);
127        fval[i] = func(&xi.to_vec());
128        nfev += 1;
129
130        for (j, constraint) in constraints.iter().enumerate() {
131            con[[i, j]] = (constraint.fun)(&xi.to_vec());
132            nfev += 1;
133        }
134    }
135
136    // Main optimization loop
137    let mut success = false;
138    let mut message = "Maximum number of iterations reached".to_string();
139
140    while nit < maxiter && rho > rho_end {
141        nit += 1;
142
143        // Build linear models for objective and constraints
144        let (grad_f, grad_c) = build_linear_models(&xpt, &fval, &con, &x, n, num_constraints)?;
145
146        // Solve trust region subproblem
147        let step = solve_trust_region_subproblem(&grad_f, &grad_c, constraints, rho, n)?;
148
149        // Compute trial point
150        let mut x_trial = x.clone();
151        for i in 0..n {
152            x_trial[i] += step[i];
153        }
154
155        // Evaluate objective and constraints at trial point
156        let f_trial = func(&x_trial.to_vec());
157        nfev += 1;
158
159        let mut c_trial = Array1::zeros(num_constraints);
160        for (i, constraint) in constraints.iter().enumerate() {
161            c_trial[i] = (constraint.fun)(&x_trial.to_vec());
162            nfev += 1;
163        }
164
165        // Compute predicted reduction
166        let pred_reduction = -grad_f.dot(&step);
167        let actual_reduction = f - f_trial;
168
169        // Compute constraint violation
170        let mut trial_violation: f64 = 0.0;
171        for (i, constraint) in constraints.iter().enumerate() {
172            let violation = match constraint.kind {
173                ConstraintKind::Equality => c_trial[i].abs(),
174                ConstraintKind::Inequality => {
175                    if c_trial[i] < 0.0 {
176                        -c_trial[i]
177                    } else {
178                        0.0
179                    }
180                }
181            };
182            trial_violation = trial_violation.max(violation);
183        }
184
185        // Accept or reject the step
186        let ratio = if pred_reduction.abs() > 1e-15 {
187            actual_reduction / pred_reduction
188        } else {
189            0.0
190        };
191
192        let accept_step = ratio > 0.1 && trial_violation <= max_constraint_violation * 1.1;
193
194        if accept_step {
195            x = x_trial;
196            f = f_trial;
197            constraint_values = c_trial;
198            max_constraint_violation = trial_violation;
199
200            // Update interpolation set
201            update_interpolation_set(&mut xpt, &mut fval, &mut con, &x, f, &constraint_values, 0);
202        }
203
204        // Update trust region radius
205        if ratio > 0.75 && step.dot(&step).sqrt() > 0.9 * rho {
206            rho = (2.0 * rho).min(rho_beg);
207        } else if ratio <= 0.25 {
208            rho *= 0.5;
209        }
210
211        // Check convergence
212        if max_constraint_violation <= ctol && step.dot(&step).sqrt() <= ftol {
213            success = true;
214            message = "Optimization terminated successfully".to_string();
215            break;
216        }
217
218        if rho <= rho_end {
219            if max_constraint_violation <= ctol {
220                success = true;
221                message = "Optimization terminated successfully".to_string();
222            } else {
223                message = "Trust region radius became too small".to_string();
224            }
225            break;
226        }
227    }
228
229    Ok(OptimizeResults::<f64> {
230        x,
231        fun: f,
232        jac: None,
233        hess: None,
234        constr: Some(constraint_values),
235        nit,
236        nfev,
237        njev: 0,
238        nhev: 0,
239        maxcv: 0,
240        message,
241        success,
242        status: if success { 0 } else { 1 },
243    })
244}
245
246/// Build linear models for the objective function and constraints
247#[allow(clippy::too_many_arguments)]
248#[allow(dead_code)]
249fn build_linear_models(
250    xpt: &Array2<f64>,
251    fval: &Array1<f64>,
252    con: &Array2<f64>,
253    _x: &Array1<f64>,
254    n: usize,
255    num_constraints: usize,
256) -> OptimizeResult<(Array1<f64>, Array2<f64>)> {
257    // Simple finite difference approximation for gradients
258    let mut grad_f = Array1::zeros(n);
259    let mut grad_c = Array2::zeros((num_constraints, n));
260
261    let _h = 1e-8;
262
263    // Build gradient of objective function
264    for i in 0..n {
265        if i + 1 < xpt.nrows() {
266            grad_f[i] = (fval[i + 1] - fval[0]) / (xpt[[i + 1, i]] - xpt[[0, i]]);
267        }
268    }
269
270    // Build gradients of constraint functions
271    for j in 0..num_constraints {
272        for i in 0..n {
273            if i + 1 < xpt.nrows() {
274                grad_c[[j, i]] = (con[[i + 1, j]] - con[[0, j]]) / (xpt[[i + 1, i]] - xpt[[0, i]]);
275            }
276        }
277    }
278
279    Ok((grad_f, grad_c))
280}
281
282/// Solve the trust region subproblem
283#[allow(dead_code)]
284fn solve_trust_region_subproblem(
285    grad_f: &Array1<f64>,
286    grad_c: &Array2<f64>,
287    constraints: &[Constraint<ConstraintFn>],
288    rho: f64,
289    n: usize,
290) -> OptimizeResult<Array1<f64>> {
291    // Simple approach: project the negative gradient onto the feasible region
292    let mut step = -grad_f.clone();
293
294    // Scale to trust region
295    let step_norm = step.dot(&step).sqrt();
296    if step_norm > rho {
297        step *= rho / step_norm;
298    }
299
300    // Simple constraint handling: if step violates linear constraints, scale it back
301    for (i, constraint) in constraints.iter().enumerate() {
302        if i < grad_c.nrows() {
303            let grad_ci = grad_c.row(i);
304            let pred_constraint = grad_ci.dot(&step);
305
306            match constraint.kind {
307                ConstraintKind::Inequality => {
308                    if pred_constraint < -0.1 * rho {
309                        // Scale back the step
310                        let scale = -0.1 * rho / pred_constraint;
311                        step *= scale;
312                    }
313                }
314                ConstraintKind::Equality => {
315                    // For equality constraints, project the step
316                    let norm_sq = grad_ci.dot(&grad_ci);
317                    if norm_sq > 1e-15 {
318                        let projection = pred_constraint / norm_sq;
319                        for j in 0..n {
320                            step[j] -= projection * grad_ci[j];
321                        }
322                    }
323                }
324            }
325        }
326    }
327
328    Ok(step)
329}
330
331/// Update the interpolation set with a new point
332#[allow(clippy::too_many_arguments)]
333#[allow(dead_code)]
334fn update_interpolation_set(
335    xpt: &mut Array2<f64>,
336    fval: &mut Array1<f64>,
337    con: &mut Array2<f64>,
338    x: &Array1<f64>,
339    f: f64,
340    constraint_values: &Array1<f64>,
341    index: usize,
342) {
343    // Update the interpolation point
344    xpt.row_mut(index).assign(x);
345    fval[index] = f;
346    con.row_mut(index).assign(constraint_values);
347}
348
349// Implement error conversion for validation errors
350impl From<scirs2_core::error::CoreError> for OptimizeError {
351    fn from(error: scirs2_core::error::CoreError) -> Self {
352        OptimizeError::ValueError(error.to_string())
353    }
354}