scirs2_optimize/constrained/
trust_constr.rs1use crate::constrained::{Constraint, ConstraintFn, ConstraintKind, Options};
4use crate::error::{OptimizeError, OptimizeResult};
5use crate::result::OptimizeResults;
6use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1};
7
8#[allow(clippy::many_single_char_names)]
9pub fn minimize_trust_constr<F, S>(
10 func: F,
11 x0: &ArrayBase<S, Ix1>,
12 constraints: &[Constraint<ConstraintFn>],
13 options: &Options,
14) -> OptimizeResult<OptimizeResults<f64>>
15where
16 F: Fn(&[f64]) -> f64,
17 S: Data<Elem = f64>,
18{
19 let ftol = options.ftol.unwrap_or(1e-8);
21 let gtol = options.gtol.unwrap_or(1e-8);
22 let ctol = options.ctol.unwrap_or(1e-8);
23 let maxiter = options.maxiter.unwrap_or(100 * x0.len());
24 let eps = options.eps.unwrap_or(1e-8);
25
26 let n = x0.len();
28 let mut x = x0.to_owned();
29 let mut f = func(x.as_slice().unwrap());
30 let mut nfev = 1;
31
32 let mut lambda = Array1::zeros(constraints.len());
34
35 let mut g = Array1::zeros(n);
37 for i in 0..n {
38 let mut x_h = x.clone();
39 x_h[i] += eps;
40 let f_h = func(x_h.as_slice().unwrap());
41 g[i] = (f_h - f) / eps;
42 nfev += 1;
43 }
44
45 let mut c = Array1::zeros(constraints.len());
47 for (i, constraint) in constraints.iter().enumerate() {
48 if !constraint.is_bounds() {
49 let val = (constraint.fun)(x.as_slice().unwrap());
50
51 match constraint.kind {
52 ConstraintKind::Inequality => {
53 c[i] = val; }
55 ConstraintKind::Equality => {
56 return Err(OptimizeError::NotImplementedError(
58 "Equality constraints not fully implemented in Trust-Region yet"
59 .to_string(),
60 ));
61 }
62 }
63 }
64 }
65
66 let mut a = Array2::zeros((constraints.len(), n));
68 for (i, constraint) in constraints.iter().enumerate() {
69 if !constraint.is_bounds() {
70 for j in 0..n {
71 let mut x_h = x.clone();
72 x_h[j] += eps;
73 let c_h = (constraint.fun)(x_h.as_slice().unwrap());
74 a[[i, j]] = (c_h - c[i]) / eps;
75 nfev += 1;
76 }
77 }
78 }
79
80 let mut delta = 1.0;
82
83 let mut b = Array2::eye(n);
85
86 let mut iter = 0;
88
89 while iter < maxiter {
90 let mut max_constraint_violation = 0.0;
92 for (i, &ci) in c.iter().enumerate() {
93 if constraints[i].kind == ConstraintKind::Inequality && ci < -ctol {
94 max_constraint_violation = f64::max(max_constraint_violation, -ci);
95 }
96 }
97
98 if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
100 break;
101 }
102
103 let mut lag_grad = g.clone();
105 for (i, &li) in lambda.iter().enumerate() {
106 if li > 0.0 || c[i] < ctol {
107 for j in 0..n {
109 lag_grad[j] -= li * a[[i, j]];
112 }
113 }
114 }
115
116 let (p, predicted_reduction) =
121 compute_trust_region_step_constrained(&lag_grad, &b, &a, &c, delta, constraints, ctol);
122
123 let x_new = &x + &p;
125
126 let f_new = func(x_new.as_slice().unwrap());
128 nfev += 1;
129
130 let mut c_new = Array1::zeros(constraints.len());
131 for (i, constraint) in constraints.iter().enumerate() {
132 if !constraint.is_bounds() {
133 c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
134 nfev += 1;
135 }
136 }
137
138 let mut merit = f;
140 let mut merit_new = f_new;
141
142 let penalty = 10.0; for (i, &ci) in c.iter().enumerate() {
145 if constraints[i].kind == ConstraintKind::Inequality {
146 merit += penalty * f64::max(0.0, -ci);
147 merit_new += penalty * f64::max(0.0, -c_new[i]);
148 }
149 }
150
151 let actual_reduction = merit - merit_new;
153
154 let rho = if predicted_reduction > 0.0 {
156 actual_reduction / predicted_reduction
157 } else {
158 0.0
159 };
160
161 if rho < 0.25 {
163 delta *= 0.5;
164 } else if rho > 0.75 && p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt() >= 0.9 * delta {
165 delta *= 2.0;
166 }
167
168 if rho > 0.1 {
170 x = x_new;
172 f = f_new;
173 c = c_new;
174
175 if (merit - merit_new).abs() < ftol * (1.0 + merit.abs()) {
177 break;
178 }
179
180 let mut g_new = Array1::zeros(n);
182 for i in 0..n {
183 let mut x_h = x.clone();
184 x_h[i] += eps;
185 let f_h = func(x_h.as_slice().unwrap());
186 g_new[i] = (f_h - f) / eps;
187 nfev += 1;
188 }
189
190 let mut a_new = Array2::zeros((constraints.len(), n));
192 for (i, constraint) in constraints.iter().enumerate() {
193 if !constraint.is_bounds() {
194 for j in 0..n {
195 let mut x_h = x.clone();
196 x_h[j] += eps;
197 let c_h = (constraint.fun)(x_h.as_slice().unwrap());
198 a_new[[i, j]] = (c_h - c[i]) / eps;
199 nfev += 1;
200 }
201 }
202 }
203
204 for (i, constraint) in constraints.iter().enumerate() {
206 if constraint.kind == ConstraintKind::Inequality {
207 if c[i] < ctol {
208 lambda[i] = f64::max(0.0, lambda[i] - c[i] * penalty);
211 } else {
212 lambda[i] = f64::max(0.0, lambda[i] - 0.1 * lambda[i]);
214 }
215 }
216 }
217
218 let s = &p;
220 let y = &g_new - &g;
221
222 let s_dot_y = s.dot(&y);
224 if s_dot_y > 1e-10 {
225 let s_col = s.clone().insert_axis(Axis(1));
226 let s_row = s.clone().insert_axis(Axis(0));
227
228 let bs = b.dot(s);
229 let bs_col = bs.clone().insert_axis(Axis(1));
230 let bs_row = bs.clone().insert_axis(Axis(0));
231
232 let term1 = s_dot_y + s.dot(&bs);
233 let term2 = &s_col.dot(&s_row) * (term1 / (s_dot_y * s_dot_y));
234
235 let term3 = &bs_col.dot(&s_row) + &s_col.dot(&bs_row);
236
237 b = &b + &term2 - &(&term3 / s_dot_y);
238 }
239
240 g = g_new;
242 a = a_new;
243 }
244
245 iter += 1;
246 }
247
248 let mut c_result = Array1::zeros(constraints.len());
250 for (i, constraint) in constraints.iter().enumerate() {
251 if !constraint.is_bounds() {
252 c_result[i] = c[i];
253 }
254 }
255
256 let mut result = OptimizeResults::default();
258 result.x = x;
259 result.fun = f;
260 result.jac = Some(g.into_raw_vec_and_offset().0);
261 result.constr = Some(c_result);
262 result.nfev = nfev;
263 result.nit = iter;
264 result.success = iter < maxiter;
265
266 if result.success {
267 result.message = "Optimization terminated successfully.".to_string();
268 } else {
269 result.message = "Maximum iterations reached.".to_string();
270 }
271
272 Ok(result)
273}
274
275#[allow(clippy::many_single_char_names)]
277fn compute_trust_region_step_constrained(
278 g: &Array1<f64>,
279 b: &Array2<f64>,
280 a: &Array2<f64>,
281 c: &Array1<f64>,
282 delta: f64,
283 constraints: &[Constraint<ConstraintFn>],
284 ctol: f64,
285) -> (Array1<f64>, f64) {
286 let n = g.len();
287 let n_constr = constraints.len();
288
289 let p_unconstrained = compute_unconstrained_cauchy_point(g, b, delta);
291
292 let mut constraint_violated = false;
294 for i in 0..n_constr {
295 if constraints[i].kind == ConstraintKind::Inequality {
296 let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p_unconstrained[j]).sum::<f64>();
297 if c[i] + grad_c_dot_p < -ctol {
298 constraint_violated = true;
299 break;
300 }
301 }
302 }
303
304 if !constraint_violated {
306 let g_dot_p = g.dot(&p_unconstrained);
308 let bp = b.dot(&p_unconstrained);
309 let p_dot_bp = p_unconstrained.dot(&bp);
310 let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
311
312 return (p_unconstrained, predicted_reduction);
313 }
314
315 let mut p = Array1::zeros(n);
320 for i in 0..n {
321 p[i] = -g[i];
322 }
323
324 let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
326 if p_norm > 1e-10 {
327 p = &p * (delta / p_norm);
328 }
329
330 for _iter in 0..5 {
332 let mut max_viol = 0.0;
334 let mut most_violated = 0;
335
336 for i in 0..n_constr {
338 if constraints[i].kind == ConstraintKind::Inequality {
339 let grad_c_dot_p = (0..n).map(|j| a[[i, j]] * p[j]).sum::<f64>();
340 let viol = -(c[i] + grad_c_dot_p);
341 if viol > max_viol {
342 max_viol = viol;
343 most_violated = i;
344 }
345 }
346 }
347
348 if max_viol < ctol {
349 break;
350 }
351
352 let mut a_norm_sq = 0.0;
354 for j in 0..n {
355 a_norm_sq += a[[most_violated, j]] * a[[most_violated, j]];
356 }
357
358 if a_norm_sq > 1e-10 {
359 let grad_c_dot_p = (0..n).map(|j| a[[most_violated, j]] * p[j]).sum::<f64>();
360 let proj_dist = (c[most_violated] + grad_c_dot_p) / a_norm_sq;
361
362 for j in 0..n {
364 p[j] += a[[most_violated, j]] * proj_dist;
365 }
366
367 let p_norm = p.iter().map(|&pi| pi * pi).sum::<f64>().sqrt();
369 if p_norm > delta {
370 p = &p * (delta / p_norm);
371 }
372 }
373 }
374
375 let g_dot_p = g.dot(&p);
377 let bp = b.dot(&p);
378 let p_dot_bp = p.dot(&bp);
379 let predicted_reduction = -g_dot_p - 0.5 * p_dot_bp;
380
381 (p, predicted_reduction)
382}
383
384fn compute_unconstrained_cauchy_point(g: &Array1<f64>, b: &Array2<f64>, delta: f64) -> Array1<f64> {
386 let n = g.len();
387
388 let mut p = Array1::zeros(n);
390 for i in 0..n {
391 p[i] = -g[i];
392 }
393
394 let bg = b.dot(g);
396 let g_dot_bg = g.dot(&bg);
397 let g_norm_sq = g.dot(g);
398
399 if g_norm_sq < 1e-10 {
401 return Array1::zeros(n);
403 }
404
405 let tau = if g_dot_bg <= 0.0 {
407 delta / g_norm_sq.sqrt()
409 } else {
410 f64::min(delta / g_norm_sq.sqrt(), g_norm_sq / g_dot_bg)
412 };
413
414 for i in 0..n {
416 p[i] *= tau;
417 }
418
419 p
420}