1use numra_core::Scalar;
11use numra_linalg::{DenseMatrix, Matrix};
12
13use crate::error::OptimError;
14use crate::problem::{finite_diff_gradient, Constraint, ConstraintKind};
15use crate::types::{IterationRecord, OptimResult, OptimStatus};
16
17#[derive(Clone, Debug)]
19pub struct SqpOptions<S: Scalar> {
20 pub max_iter: usize,
21 pub tol: S,
22 pub verbose: bool,
23}
24
25impl<S: Scalar> Default for SqpOptions<S> {
26 fn default() -> Self {
27 Self {
28 max_iter: 500,
29 tol: S::from_f64(1e-6),
30 verbose: false,
31 }
32 }
33}
34
35pub fn sqp_minimize<S, F, G>(
39 f: F,
40 grad_f: G,
41 constraints: &[Constraint<S>],
42 x0: &[S],
43 opts: &SqpOptions<S>,
44) -> Result<OptimResult<S>, OptimError>
45where
46 S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField,
47 F: Fn(&[S]) -> S,
48 G: Fn(&[S], &mut [S]),
49{
50 let start = std::time::Instant::now();
51 let n = x0.len();
52
53 let eq_indices: Vec<usize> = constraints
54 .iter()
55 .enumerate()
56 .filter(|(_, c)| c.kind == ConstraintKind::Equality)
57 .map(|(i, _)| i)
58 .collect();
59 let ineq_indices: Vec<usize> = constraints
60 .iter()
61 .enumerate()
62 .filter(|(_, c)| c.kind == ConstraintKind::Inequality)
63 .map(|(i, _)| i)
64 .collect();
65 let n_eq = eq_indices.len();
66 let n_ineq = ineq_indices.len();
67
68 let mut x = x0.to_vec();
69 let mut grad = vec![S::ZERO; n];
70 grad_f(&x, &mut grad);
71
72 let mut hess = DenseMatrix::<S>::zeros(n, n);
74 for i in 0..n {
75 hess.set(i, i, S::ONE);
76 }
77
78 let mut n_feval = 1_usize;
79 let mut n_geval = 1_usize;
80 let mut history: Vec<IterationRecord<S>> = Vec::new();
81 let mut converged = false;
82 let mut iterations = 0;
83
84 let mut mu = S::from_f64(10.0);
86 let mut prev_step_norm = S::INFINITY;
87 let mut prev_violation = S::INFINITY;
88 let mut stagnation_count = 0_usize;
89
90 for iter in 0..opts.max_iter {
91 iterations = iter + 1;
92 let f_val = f(&x);
93 n_feval += 1;
94
95 let c_vals: Vec<S> = constraints.iter().map(|c| (c.func)(&x)).collect();
97
98 let max_violation = constraint_violation(&c_vals, &eq_indices, &ineq_indices);
100
101 let mut c_grads: Vec<Vec<S>> = Vec::with_capacity(constraints.len());
103 for c in constraints {
104 let mut cg = vec![S::ZERO; n];
105 if let Some(ref g) = c.grad {
106 g(&x, &mut cg);
107 } else {
108 finite_diff_gradient(&*c.func, &x, &mut cg);
109 }
110 c_grads.push(cg);
111 }
112
113 let grad_norm = grad.iter().map(|&g| g * g).sum::<S>().sqrt();
115 let kkt_norm = if !eq_indices.is_empty() || !ineq_indices.is_empty() {
116 lagrangian_gradient_norm(&grad, &c_vals, &c_grads, &eq_indices, &ineq_indices, n)
117 } else {
118 grad_norm
119 };
120
121 if opts.verbose {
122 eprintln!(
123 "SQP iter {}: f={:.6e}, ||kkt||={:.2e}, cv={:.2e}, mu={:.1e}, step={:.2e}",
124 iter,
125 f_val.to_f64(),
126 kkt_norm.to_f64(),
127 max_violation.to_f64(),
128 mu.to_f64(),
129 prev_step_norm.to_f64()
130 );
131 }
132
133 history.push(IterationRecord {
134 iteration: iter,
135 objective: f_val,
136 gradient_norm: kkt_norm,
137 step_size: prev_step_norm,
138 constraint_violation: max_violation,
139 });
140
141 if kkt_norm < opts.tol && max_violation < opts.tol {
143 converged = true;
144 break;
145 }
146 if prev_step_norm < opts.tol && max_violation < opts.tol {
148 converged = true;
149 break;
150 }
151
152 if max_violation > opts.tol {
155 let cv_decrease = prev_violation - max_violation;
156 if cv_decrease < S::from_f64(1e-8) * max_violation {
157 stagnation_count += 1;
158 } else {
159 stagnation_count = 0;
160 }
161
162 if stagnation_count >= 5 {
163 let m = constraints.len();
166 let mut jtj = vec![S::ZERO; n * n];
167 let mut jtc = vec![S::ZERO; n];
168 for i in 0..m {
169 let ci = c_vals[i];
170 if constraints[i].kind == ConstraintKind::Inequality && ci < S::ZERO {
172 continue;
173 }
174 for j in 0..n {
175 jtc[j] += c_grads[i][j] * ci;
176 for k in 0..n {
177 jtj[j * n + k] += c_grads[i][j] * c_grads[i][k];
178 }
179 }
180 }
181 for j in 0..n {
183 jtj[j * n + j] += S::from_f64(1e-6);
184 }
185 let jtj_mat = DenseMatrix::<S>::from_row_major(n, n, &jtj);
186 let neg_jtc: Vec<S> = jtc.iter().map(|&v| -v).collect();
187 if let Ok(d_feas) = jtj_mat.solve(&neg_jtc) {
188 let mut alpha_f = S::ONE;
190 for _ in 0..10 {
191 let x_trial: Vec<S> = (0..n).map(|j| x[j] + alpha_f * d_feas[j]).collect();
192 let c_trial: Vec<S> =
193 constraints.iter().map(|c| (c.func)(&x_trial)).collect();
194 let cv_trial = constraint_violation(&c_trial, &eq_indices, &ineq_indices);
195 if cv_trial < max_violation {
196 x = x_trial;
197 grad_f(&x, &mut grad);
198 n_geval += 1;
199 n_feval += 1;
200 for i in 0..n {
202 for j in 0..n {
203 hess.set(i, j, if i == j { S::ONE } else { S::ZERO });
204 }
205 }
206 stagnation_count = 0;
207 break;
208 }
209 alpha_f *= S::HALF;
210 }
211 }
212 prev_violation = max_violation;
213 continue;
214 }
215 } else {
216 stagnation_count = 0;
217 }
218 prev_violation = max_violation;
219
220 let (d, max_mult) = solve_qp_subproblem(
222 &hess,
223 &grad,
224 &c_vals,
225 &c_grads,
226 &eq_indices,
227 &ineq_indices,
228 n,
229 );
230
231 let d_norm = d.iter().map(|&di| di * di).sum::<S>().sqrt();
232 if d_norm < S::from_f64(1e-15) {
233 if max_violation < opts.tol {
234 converged = true;
235 }
236 break;
237 }
238
239 let mu_min = max_mult + S::from_f64(0.1);
241 if mu < mu_min {
242 mu = mu_min;
243 }
244
245 let df_dd: S = grad.iter().zip(d.iter()).map(|(&gi, &di)| gi * di).sum();
247 let merit_deriv = df_dd - mu * l1_penalty(&c_vals, &eq_indices, &ineq_indices);
248
249 if merit_deriv > S::ZERO {
251 mu *= S::TWO;
252 }
253
254 let merit_x = f_val + mu * l1_penalty(&c_vals, &eq_indices, &ineq_indices);
255
256 let mut alpha = S::ONE;
258 let eta = S::from_f64(1e-4);
259 let mut step_evals = 0;
260
261 for _ in 0..30 {
262 let x_trial: Vec<S> = (0..n).map(|j| x[j] + alpha * d[j]).collect();
263 let f_trial = f(&x_trial);
264 let c_trial: Vec<S> = constraints.iter().map(|c| (c.func)(&x_trial)).collect();
265 step_evals += 1;
266
267 let merit_trial = f_trial + mu * l1_penalty(&c_trial, &eq_indices, &ineq_indices);
268
269 if merit_trial <= merit_x + eta * alpha * merit_deriv {
271 break;
272 }
273 if merit_trial < merit_x && alpha < S::from_f64(0.1) {
275 break;
276 }
277
278 alpha *= S::HALF;
279 }
280 n_feval += step_evals;
281
282 let old_x = x.clone();
284 let old_grad = grad.clone();
285 for j in 0..n {
286 x[j] += alpha * d[j];
287 }
288 prev_step_norm = alpha * d_norm;
289
290 grad_f(&x, &mut grad);
292 n_geval += 1;
293
294 let s: Vec<S> = (0..n).map(|j| x[j] - old_x[j]).collect();
296 let y: Vec<S> = (0..n).map(|j| grad[j] - old_grad[j]).collect();
297
298 let ss: S = s.iter().map(|&si| si * si).sum();
299 if ss > S::from_f64(1e-20) {
300 let sy: S = s.iter().zip(y.iter()).map(|(&si, &yi)| si * yi).sum();
301 let mut hs = vec![S::ZERO; n];
302 hess.mul_vec(&s, &mut hs);
303 let shs: S = s.iter().zip(hs.iter()).map(|(&si, &hi)| si * hi).sum();
304
305 if shs > S::from_f64(1e-20) {
306 let theta = if sy >= S::from_f64(0.2) * shs {
308 S::ONE
309 } else if (shs - sy).to_f64().abs() > 1e-20 {
310 S::from_f64(0.8) * shs / (shs - sy)
311 } else {
312 S::ONE
313 };
314
315 let r: Vec<S> = (0..n)
316 .map(|j| theta * y[j] + (S::ONE - theta) * hs[j])
317 .collect();
318 let sr: S = s.iter().zip(r.iter()).map(|(&si, &ri)| si * ri).sum();
319
320 if sr > S::from_f64(1e-20) {
321 for i in 0..n {
322 for j in 0..n {
323 let val = hess.get(i, j) - hs[i] * hs[j] / shs + r[i] * r[j] / sr;
324 hess.set(i, j, val);
325 }
326 }
327 }
328 }
329 }
330 }
331
332 let f_final = f(&x);
333 let c_final: Vec<S> = constraints.iter().map(|c| (c.func)(&x)).collect();
334 let final_violation = constraint_violation(&c_final, &eq_indices, &ineq_indices);
335
336 let (status, message) = if converged {
337 (
338 OptimStatus::GradientConverged,
339 format!("SQP converged after {} iterations", iterations),
340 )
341 } else {
342 (
343 OptimStatus::MaxIterations,
344 format!("SQP: max iterations ({}) reached", opts.max_iter),
345 )
346 };
347
348 Ok(OptimResult {
349 x,
350 f: f_final,
351 grad,
352 iterations,
353 n_feval,
354 n_geval,
355 converged,
356 message,
357 status,
358 history,
359 lambda_eq: vec![S::ZERO; n_eq],
360 lambda_ineq: vec![S::ZERO; n_ineq],
361 active_bounds: Vec::new(),
362 constraint_violation: final_violation,
363 wall_time_secs: 0.0,
364 pareto: None,
365 sensitivity: None,
366 }
367 .with_wall_time(start))
368}
369
370fn lagrangian_gradient_norm<S: Scalar>(
376 grad: &[S],
377 c_vals: &[S],
378 c_grads: &[Vec<S>],
379 eq_indices: &[usize],
380 ineq_indices: &[usize],
381 n: usize,
382) -> S {
383 let mut active_grads: Vec<&Vec<S>> = Vec::new();
385 for &i in eq_indices {
386 active_grads.push(&c_grads[i]);
387 }
388 for &i in ineq_indices {
389 if c_vals[i] > S::from_f64(-1e-6) {
390 active_grads.push(&c_grads[i]);
391 }
392 }
393
394 if active_grads.is_empty() {
395 return grad.iter().map(|&g| g * g).sum::<S>().sqrt();
396 }
397
398 let m = active_grads.len();
400 if m == 1 {
405 let a = &active_grads[0];
406 let ag: S = a.iter().zip(grad.iter()).map(|(&ai, &gi)| ai * gi).sum();
407 let aa: S = a.iter().map(|&ai| ai * ai).sum();
408 if aa < S::from_f64(1e-20) {
409 return grad.iter().map(|&g| g * g).sum::<S>().sqrt();
410 }
411 let lambda = -ag / aa;
413 let mut residual_sq = S::ZERO;
414 for j in 0..n {
415 let r = grad[j] + lambda * a[j];
416 residual_sq += r * r;
417 }
418 return residual_sq.sqrt();
419 }
420
421 let mut aat = vec![S::ZERO; m * m];
424 let mut ag = vec![S::ZERO; m];
425 for i in 0..m {
426 for j in 0..m {
427 let mut dot = S::ZERO;
428 for (ak_i, ak_j) in active_grads[i].iter().zip(active_grads[j].iter()).take(n) {
429 dot += *ak_i * *ak_j;
430 }
431 aat[i * m + j] = dot;
432 }
433 let mut dot = S::ZERO;
434 for k in 0..n {
435 dot += active_grads[i][k] * grad[k];
436 }
437 ag[i] = -dot;
438 }
439
440 let mut aug = vec![S::ZERO; m * (m + 1)];
443 for i in 0..m {
444 for j in 0..m {
445 aug[i * (m + 1) + j] = aat[i * m + j];
446 }
447 aug[i * (m + 1) + m] = ag[i];
448 }
449
450 for col in 0..m {
451 let mut max_abs = S::ZERO;
453 let mut pivot_row = col;
454 for row in col..m {
455 let v = aug[row * (m + 1) + col].abs();
456 if v > max_abs {
457 max_abs = v;
458 pivot_row = row;
459 }
460 }
461 if max_abs < S::from_f64(1e-20) {
462 return grad.iter().map(|&g| g * g).sum::<S>().sqrt();
464 }
465 if pivot_row != col {
467 for j in 0..=m {
468 aug.swap(col * (m + 1) + j, pivot_row * (m + 1) + j);
469 }
470 }
471 let pivot = aug[col * (m + 1) + col];
473 for row in (col + 1)..m {
474 let factor = aug[row * (m + 1) + col] / pivot;
475 for j in col..=m {
476 let val = aug[col * (m + 1) + j];
477 aug[row * (m + 1) + j] -= factor * val;
478 }
479 }
480 }
481
482 let mut lambda = vec![S::ZERO; m];
484 for i in (0..m).rev() {
485 let mut s = aug[i * (m + 1) + m];
486 for j in (i + 1)..m {
487 s -= aug[i * (m + 1) + j] * lambda[j];
488 }
489 lambda[i] = s / aug[i * (m + 1) + i];
490 }
491
492 let mut residual_sq = S::ZERO;
494 for j in 0..n {
495 let mut r = grad[j];
496 for (i, ag_i) in active_grads.iter().enumerate() {
497 r += lambda[i] * ag_i[j];
498 }
499 residual_sq += r * r;
500 }
501 residual_sq.sqrt()
502}
503
504fn constraint_violation<S: Scalar>(
506 c_vals: &[S],
507 eq_indices: &[usize],
508 ineq_indices: &[usize],
509) -> S {
510 let mut v = S::ZERO;
511 for &i in eq_indices {
512 let cv = c_vals[i].abs();
513 if cv > v {
514 v = cv;
515 }
516 }
517 for &i in ineq_indices {
518 if c_vals[i] > v {
519 v = c_vals[i];
520 }
521 }
522 v
523}
524
525fn l1_penalty<S: Scalar>(c_vals: &[S], eq_indices: &[usize], ineq_indices: &[usize]) -> S {
527 let mut p = S::ZERO;
528 for &i in eq_indices {
529 p += c_vals[i].abs();
530 }
531 for &i in ineq_indices {
532 if c_vals[i] > S::ZERO {
533 p += c_vals[i];
534 }
535 }
536 p
537}
538
539fn solve_qp_subproblem<S>(
542 hess: &DenseMatrix<S>,
543 grad: &[S],
544 c_vals: &[S],
545 c_grads: &[Vec<S>],
546 eq_indices: &[usize],
547 ineq_indices: &[usize],
548 n: usize,
549) -> (Vec<S>, S)
550where
551 S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField,
552{
553 let active_ineq: Vec<usize> = ineq_indices
555 .iter()
556 .copied()
557 .filter(|&i| c_vals[i] > S::from_f64(-1e-6))
558 .collect();
559
560 let mut independent_eq: Vec<usize> = Vec::new();
563 let mut independent_ineq: Vec<usize> = Vec::new();
564 let mut accepted_grads: Vec<&Vec<S>> = Vec::new();
565
566 for &ci in eq_indices {
567 if is_independent(&c_grads[ci], &accepted_grads, n) {
568 accepted_grads.push(&c_grads[ci]);
569 independent_eq.push(ci);
570 }
571 }
572 for &ci in &active_ineq {
573 if is_independent(&c_grads[ci], &accepted_grads, n) {
574 accepted_grads.push(&c_grads[ci]);
575 independent_ineq.push(ci);
576 }
577 }
578
579 let n_ind_eq = independent_eq.len();
580 let n_ind_ineq = independent_ineq.len();
581 let n_total = n_ind_eq + n_ind_ineq;
582
583 if n_total == 0 {
584 let neg_grad: Vec<S> = grad.iter().map(|&g| -g).collect();
586 let d = hess.solve(&neg_grad).unwrap_or(neg_grad);
587 return (d, S::ZERO);
588 }
589
590 let kkt_n = n + n_total;
592 let mut kkt = DenseMatrix::<S>::zeros(kkt_n, kkt_n);
593 let mut rhs = vec![S::ZERO; kkt_n];
594
595 let diag_max = (0..n)
597 .map(|i| hess.get(i, i).abs())
598 .fold(S::ZERO, |a, b| if b > a { b } else { a });
599 let reg = S::from_f64(1e-8) * (S::ONE + diag_max);
600 for i in 0..n {
601 for j in 0..n {
602 kkt.set(i, j, hess.get(i, j));
603 }
604 kkt.set(i, i, kkt.get(i, i) + reg);
605 rhs[i] = -grad[i];
606 }
607
608 for (row, &ci) in independent_eq.iter().enumerate() {
610 let cg = &c_grads[ci];
611 for (j, &cgj) in cg.iter().enumerate().take(n) {
612 kkt.set(n + row, j, cgj);
613 kkt.set(j, n + row, cgj);
614 }
615 rhs[n + row] = -c_vals[ci];
616 }
617
618 for (row, &ci) in independent_ineq.iter().enumerate() {
620 let kkt_row = n + n_ind_eq + row;
621 let cg = &c_grads[ci];
622 for (j, &cgj) in cg.iter().enumerate().take(n) {
623 kkt.set(kkt_row, j, cgj);
624 kkt.set(j, kkt_row, cgj);
625 }
626 rhs[kkt_row] = -c_vals[ci];
627 }
628
629 for i in 0..n_total {
631 kkt.set(n + i, n + i, kkt.get(n + i, n + i) - S::from_f64(1e-10));
632 }
633
634 match kkt.solve(&rhs) {
635 Ok(sol) => {
636 let d = sol[..n].to_vec();
637 let max_mult =
639 sol[n..]
640 .iter()
641 .map(|&v| v.abs())
642 .fold(S::ZERO, |a, b| if b > a { b } else { a });
643 (d, max_mult)
644 }
645 Err(_) => {
646 let mut d: Vec<S> = grad.iter().map(|&g| -g).collect();
648 for &i in eq_indices {
649 let cv = c_vals[i];
650 for j in 0..n {
651 d[j] -= cv * c_grads[i][j];
652 }
653 }
654 for &i in ineq_indices {
655 if c_vals[i] > S::ZERO {
656 for j in 0..n {
657 d[j] -= c_vals[i] * c_grads[i][j];
658 }
659 }
660 }
661 (d, S::from_f64(100.0))
662 }
663 }
664}
665
666fn is_independent<S: Scalar>(g: &[S], accepted: &[&Vec<S>], n: usize) -> bool {
670 let g_norm_sq: S = g.iter().map(|&v| v * v).sum();
671 if g_norm_sq < S::from_f64(1e-20) {
672 return false; }
674
675 if accepted.is_empty() {
676 return true;
677 }
678
679 let mut residual: Vec<S> = g.to_vec();
681 for &a in accepted {
682 let a_norm_sq: S = a.iter().map(|&v| v * v).sum();
683 if a_norm_sq < S::from_f64(1e-20) {
684 continue;
685 }
686 let dot: S = residual
687 .iter()
688 .zip(a.iter())
689 .take(n)
690 .map(|(&ri, &ai)| ri * ai)
691 .sum();
692 let coeff = dot / a_norm_sq;
693 for j in 0..n.min(residual.len()) {
694 residual[j] -= coeff * a[j];
695 }
696 }
697
698 let res_norm_sq: S = residual.iter().map(|&v| v * v).sum();
699 res_norm_sq > S::from_f64(1e-4) * g_norm_sq
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::problem::Constraint;
707
708 #[test]
709 fn test_sqp_equality_circle() {
710 let constraints = vec![Constraint {
713 func: Box::new(|x: &[f64]| x[0] * x[0] + x[1] * x[1] - 1.0),
714 grad: Some(Box::new(|x: &[f64], g: &mut [f64]| {
715 g[0] = 2.0 * x[0];
716 g[1] = 2.0 * x[1];
717 })),
718 kind: ConstraintKind::Equality,
719 }];
720
721 let result = sqp_minimize(
722 |x: &[f64]| x[0] + x[1],
723 |_x: &[f64], g: &mut [f64]| {
724 g[0] = 1.0;
725 g[1] = 1.0;
726 },
727 &constraints,
728 &[1.0, 0.0],
729 &SqpOptions::default(),
730 )
731 .unwrap();
732
733 assert!(result.converged, "SQP did not converge: {}", result.message);
734 let expected = -1.0 / 2.0_f64.sqrt();
735 assert!(
736 (result.x[0] - expected).abs() < 0.05,
737 "x0={}, expected {}",
738 result.x[0],
739 expected
740 );
741 assert!(result.constraint_violation < 1e-3);
742 }
743
744 #[test]
745 fn test_sqp_inequality() {
746 let constraints = vec![Constraint {
749 func: Box::new(|x: &[f64]| x[0] + x[1] - 2.0),
750 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
751 g[0] = 1.0;
752 g[1] = 1.0;
753 })),
754 kind: ConstraintKind::Inequality,
755 }];
756
757 let result = sqp_minimize(
758 |x: &[f64]| (x[0] - 2.0).powi(2) + (x[1] - 2.0).powi(2),
759 |x: &[f64], g: &mut [f64]| {
760 g[0] = 2.0 * (x[0] - 2.0);
761 g[1] = 2.0 * (x[1] - 2.0);
762 },
763 &constraints,
764 &[0.0, 0.0],
765 &SqpOptions::default(),
766 )
767 .unwrap();
768
769 assert!(result.converged, "SQP did not converge: {}", result.message);
770 assert!(
771 (result.x[0] - 1.0).abs() < 0.1,
772 "x0={}, expected ~1.0",
773 result.x[0]
774 );
775 }
776
777 #[test]
778 fn test_sqp_mixed_constraints() {
779 let constraints = vec![
783 Constraint {
784 func: Box::new(|x: &[f64]| x[0] + x[1] - 1.0),
785 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
786 g[0] = 1.0;
787 g[1] = 1.0;
788 })),
789 kind: ConstraintKind::Equality,
790 },
791 Constraint {
792 func: Box::new(|x: &[f64]| 0.6 - x[0]),
793 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
794 g[0] = -1.0;
795 g[1] = 0.0;
796 })),
797 kind: ConstraintKind::Inequality,
798 },
799 ];
800
801 let result = sqp_minimize(
802 |x: &[f64]| x[0] * x[0] + x[1] * x[1],
803 |x: &[f64], g: &mut [f64]| {
804 g[0] = 2.0 * x[0];
805 g[1] = 2.0 * x[1];
806 },
807 &constraints,
808 &[1.0, 1.0],
809 &SqpOptions::default(),
810 )
811 .unwrap();
812
813 assert!(result.converged, "SQP did not converge: {}", result.message);
814 assert!(
815 (result.x[0] - 0.6).abs() < 0.1,
816 "x0={}, expected ~0.6",
817 result.x[0]
818 );
819 }
820
821 #[test]
822 fn test_sqp_multiple_inequality_constraints() {
823 let constraints = vec![
827 Constraint {
828 func: Box::new(|x: &[f64]| -x[0] + 1.0), grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
830 g[0] = -1.0;
831 g[1] = 0.0;
832 })),
833 kind: ConstraintKind::Inequality,
834 },
835 Constraint {
836 func: Box::new(|x: &[f64]| -x[1] + 1.0), grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
838 g[0] = 0.0;
839 g[1] = -1.0;
840 })),
841 kind: ConstraintKind::Inequality,
842 },
843 Constraint {
844 func: Box::new(|x: &[f64]| x[0] + x[1] - 4.0), grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
846 g[0] = 1.0;
847 g[1] = 1.0;
848 })),
849 kind: ConstraintKind::Inequality,
850 },
851 ];
852
853 let result = sqp_minimize(
854 |x: &[f64]| x[0] * x[0] + x[1] * x[1],
855 |x: &[f64], g: &mut [f64]| {
856 g[0] = 2.0 * x[0];
857 g[1] = 2.0 * x[1];
858 },
859 &constraints,
860 &[0.5, 0.5],
861 &SqpOptions::default(),
862 )
863 .unwrap();
864
865 assert!(result.converged, "SQP did not converge: {}", result.message);
866 assert!(
867 (result.x[0] - 1.0).abs() < 0.15,
868 "x0={}, expected ~1.0",
869 result.x[0]
870 );
871 assert!(
872 (result.x[1] - 1.0).abs() < 0.15,
873 "x1={}, expected ~1.0",
874 result.x[1]
875 );
876 }
877
878 #[test]
879 fn test_sqp_dependent_constraints() {
880 let constraints = vec![
883 Constraint {
884 func: Box::new(|x: &[f64]| x[0] + x[1] - 2.0),
885 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
886 g[0] = 1.0;
887 g[1] = 1.0;
888 })),
889 kind: ConstraintKind::Inequality,
890 },
891 Constraint {
892 func: Box::new(|x: &[f64]| x[0] + 1.001 * x[1] - 2.001),
893 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
894 g[0] = 1.0;
895 g[1] = 1.001;
896 })),
897 kind: ConstraintKind::Inequality,
898 },
899 ];
900
901 let result = sqp_minimize(
902 |x: &[f64]| (x[0] - 3.0).powi(2) + (x[1] - 3.0).powi(2),
903 |x: &[f64], g: &mut [f64]| {
904 g[0] = 2.0 * (x[0] - 3.0);
905 g[1] = 2.0 * (x[1] - 3.0);
906 },
907 &constraints,
908 &[0.0, 0.0],
909 &SqpOptions::default(),
910 )
911 .unwrap();
912
913 assert!(result.converged, "SQP did not converge: {}", result.message);
915 assert!(
916 result.x[0] + result.x[1] <= 2.0 + 0.01,
917 "constraint violated: x0+x1={}",
918 result.x[0] + result.x[1]
919 );
920 }
921
922 #[test]
923 fn test_sqp_many_constraints() {
924 let constraints = vec![
928 Constraint {
929 func: Box::new(|x: &[f64]| x[0] + x[1] + x[2] - 3.0),
930 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
931 g[0] = 1.0;
932 g[1] = 1.0;
933 g[2] = 1.0;
934 })),
935 kind: ConstraintKind::Equality,
936 },
937 Constraint {
938 func: Box::new(|x: &[f64]| x[0] - x[1]),
939 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
940 g[0] = 1.0;
941 g[1] = -1.0;
942 g[2] = 0.0;
943 })),
944 kind: ConstraintKind::Equality,
945 },
946 Constraint {
947 func: Box::new(|x: &[f64]| -x[0]),
948 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
949 g[0] = -1.0;
950 g[1] = 0.0;
951 g[2] = 0.0;
952 })),
953 kind: ConstraintKind::Inequality,
954 },
955 Constraint {
956 func: Box::new(|x: &[f64]| -x[1]),
957 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
958 g[0] = 0.0;
959 g[1] = -1.0;
960 g[2] = 0.0;
961 })),
962 kind: ConstraintKind::Inequality,
963 },
964 Constraint {
965 func: Box::new(|x: &[f64]| -x[2]),
966 grad: Some(Box::new(|_x: &[f64], g: &mut [f64]| {
967 g[0] = 0.0;
968 g[1] = 0.0;
969 g[2] = -1.0;
970 })),
971 kind: ConstraintKind::Inequality,
972 },
973 ];
974
975 let result = sqp_minimize(
976 |x: &[f64]| x[0] * x[0] + x[1] * x[1] + x[2] * x[2],
977 |x: &[f64], g: &mut [f64]| {
978 g[0] = 2.0 * x[0];
979 g[1] = 2.0 * x[1];
980 g[2] = 2.0 * x[2];
981 },
982 &constraints,
983 &[2.0, 1.0, 0.5],
984 &SqpOptions::default(),
985 )
986 .unwrap();
987
988 assert!(result.converged, "SQP did not converge: {}", result.message);
989 assert!(
990 (result.x[0] - 1.0).abs() < 0.15,
991 "x0={}, expected ~1.0",
992 result.x[0]
993 );
994 assert!(
995 (result.x[1] - 1.0).abs() < 0.15,
996 "x1={}, expected ~1.0",
997 result.x[1]
998 );
999 assert!(
1000 (result.x[2] - 1.0).abs() < 0.15,
1001 "x2={}, expected ~1.0",
1002 result.x[2]
1003 );
1004 }
1005
1006 #[test]
1007 fn test_sqp_unconstrained() {
1008 let result = sqp_minimize(
1009 |x: &[f64]| x[0] * x[0] + 4.0 * x[1] * x[1],
1010 |x: &[f64], g: &mut [f64]| {
1011 g[0] = 2.0 * x[0];
1012 g[1] = 8.0 * x[1];
1013 },
1014 &[],
1015 &[5.0, 3.0],
1016 &SqpOptions::default(),
1017 )
1018 .unwrap();
1019
1020 assert!(result.converged, "did not converge: {}", result.message);
1021 assert!(result.x[0].abs() < 1e-3, "x0={}", result.x[0]);
1022 assert!(result.x[1].abs() < 1e-3, "x1={}", result.x[1]);
1023 }
1024}