scirs2_optimize/constrained/
cobyla.rs1use 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#[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 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 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 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 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 let num_constraints = constraints.len();
74
75 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 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 let npt = 2 * n + 1; let mut xpt = Array2::zeros((npt, n)); let mut fval = Array1::zeros(npt); let mut con = Array2::zeros((npt, num_constraints)); xpt.row_mut(0).assign(&x);
106 fval[0] = f;
107 con.row_mut(0).assign(&constraint_values);
108
109 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 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 let (grad_f, grad_c) = build_linear_models(&xpt, &fval, &con, &x, n, num_constraints)?;
145
146 let step = solve_trust_region_subproblem(&grad_f, &grad_c, constraints, rho, n)?;
148
149 let mut x_trial = x.clone();
151 for i in 0..n {
152 x_trial[i] += step[i];
153 }
154
155 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 let pred_reduction = -grad_f.dot(&step);
167 let actual_reduction = f - f_trial;
168
169 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 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(&mut xpt, &mut fval, &mut con, &x, f, &constraint_values, 0);
202 }
203
204 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 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#[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 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 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 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#[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 let mut step = -grad_f.clone();
293
294 let step_norm = step.dot(&step).sqrt();
296 if step_norm > rho {
297 step *= rho / step_norm;
298 }
299
300 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 let scale = -0.1 * rho / pred_constraint;
311 step *= scale;
312 }
313 }
314 ConstraintKind::Equality => {
315 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#[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 xpt.row_mut(index).assign(x);
345 fval[index] = f;
346 con.row_mut(index).assign(constraint_values);
347}
348
349impl From<scirs2_core::error::CoreError> for OptimizeError {
351 fn from(error: scirs2_core::error::CoreError) -> Self {
352 OptimizeError::ValueError(error.to_string())
353 }
354}