scirs2_optimize/constrained/
slsqp.rs

1//! SLSQP (Sequential Least SQuares Programming) algorithm for constrained optimization
2
3use 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/// Implements the SLSQP algorithm for constrained optimization
9#[allow(clippy::many_single_char_names)]
10pub fn minimize_slsqp<F, S>(
11    func: F,
12    x0: &ArrayBase<S, Ix1>,
13    constraints: &[Constraint<ConstraintFn>],
14    options: &Options,
15) -> OptimizeResult<OptimizeResults<f64>>
16where
17    F: Fn(&[f64]) -> f64,
18    S: Data<Elem = f64>,
19{
20    // Get options or use defaults
21    let ftol = options.ftol.unwrap_or(1e-8);
22    let gtol = options.gtol.unwrap_or(1e-8);
23    let ctol = options.ctol.unwrap_or(1e-8);
24    let maxiter = options.maxiter.unwrap_or(100 * x0.len());
25    let eps = options.eps.unwrap_or(1e-8);
26
27    // Initialize variables
28    let n = x0.len();
29    let mut x = x0.to_owned();
30    let mut f = func(x.as_slice().unwrap());
31    let mut nfev = 1;
32
33    // Initialize the Lagrange multipliers for inequality constraints
34    let mut lambda = Array1::zeros(constraints.len());
35
36    // Calculate initial gradient using finite differences
37    let mut g = Array1::zeros(n);
38    for i in 0..n {
39        let mut x_h = x.clone();
40        x_h[i] += eps;
41        let f_h = func(x_h.as_slice().unwrap());
42        g[i] = (f_h - f) / eps;
43        nfev += 1;
44    }
45
46    // Evaluate initial constraints
47    let mut c = Array1::zeros(constraints.len());
48    let _ceq: Array1<f64> = Array1::zeros(0); // No equality constraints support for now
49    for (i, constraint) in constraints.iter().enumerate() {
50        if !constraint.is_bounds() {
51            let val = (constraint.fun)(x.as_slice().unwrap());
52
53            match constraint.kind {
54                ConstraintKind::Inequality => {
55                    c[i] = val; // g(x) >= 0 constraint
56                }
57                ConstraintKind::Equality => {
58                    // For simplicity, we don't fully support equality constraints yet
59                    return Err(OptimizeError::NotImplementedError(
60                        "Equality constraints not fully implemented in SLSQP yet".to_string(),
61                    ));
62                }
63            }
64        }
65    }
66
67    // Calculate constraint Jacobian
68    let mut a = Array2::zeros((constraints.len(), n));
69    for (i, constraint) in constraints.iter().enumerate() {
70        if !constraint.is_bounds() {
71            for j in 0..n {
72                let mut x_h = x.clone();
73                x_h[j] += eps;
74                let c_h = (constraint.fun)(x_h.as_slice().unwrap());
75                a[[i, j]] = (c_h - c[i]) / eps;
76                nfev += 1;
77            }
78        }
79    }
80
81    // Initialize working matrices
82    let mut h_inv = Array2::eye(n); // Approximate inverse Hessian
83
84    // Main optimization loop
85    let mut iter = 0;
86
87    while iter < maxiter {
88        // Check constraint violation
89        let mut max_constraint_violation = 0.0;
90        for (i, &ci) in c.iter().enumerate() {
91            if constraints[i].kind == ConstraintKind::Inequality && ci < -ctol {
92                max_constraint_violation = f64::max(max_constraint_violation, -ci);
93            }
94        }
95
96        // Check convergence on gradient
97        if g.iter().all(|&gi| gi.abs() < gtol) && max_constraint_violation < ctol {
98            break;
99        }
100
101        // Compute the search direction using QP subproblem
102        // For simplicity, we'll use a projected gradient approach
103        let mut p = Array1::zeros(n);
104
105        if max_constraint_violation > ctol {
106            // If constraints are violated, move toward feasibility
107            for (i, &ci) in c.iter().enumerate() {
108                if ci < -ctol {
109                    // This constraint is violated
110                    for j in 0..n {
111                        p[j] -= a[[i, j]] * ci; // Move along constraint gradient
112                    }
113                }
114            }
115        } else {
116            // Otherwise, use BFGS direction with constraints
117            p = -&h_inv.dot(&g);
118
119            // Project gradient on active constraints
120            for (i, &ci) in c.iter().enumerate() {
121                if ci.abs() < ctol {
122                    // Active constraint
123                    let mut normal = Array1::zeros(n);
124                    for j in 0..n {
125                        normal[j] = a[[i, j]];
126                    }
127                    let norm = normal.dot(&normal).sqrt();
128                    if norm > 1e-10 {
129                        normal = &normal / norm;
130                        let p_dot_normal = p.dot(&normal);
131
132                        // If moving in the wrong direction (constraint violation), project out
133                        if p_dot_normal < 0.0 {
134                            p = &p - &(&normal * p_dot_normal);
135                        }
136                    }
137                }
138            }
139        }
140
141        // Line search with constraint awareness
142        let mut alpha = 1.0;
143        let c1 = 1e-4; // Sufficient decrease parameter
144        let rho = 0.5; // Backtracking parameter
145
146        // Initial step
147        let mut x_new = &x + &(&p * alpha);
148        let mut f_new = func(x_new.as_slice().unwrap());
149        nfev += 1;
150
151        // Evaluate constraints at new point
152        let mut c_new = Array1::zeros(constraints.len());
153        for (i, constraint) in constraints.iter().enumerate() {
154            if !constraint.is_bounds() {
155                c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
156                nfev += 1;
157            }
158        }
159
160        // Check if constraint violation is reduced and objective decreases
161        let mut max_viol = 0.0;
162        let mut max_viol_new = 0.0;
163
164        for (i, constraint) in constraints.iter().enumerate() {
165            if constraint.kind == ConstraintKind::Inequality {
166                max_viol = f64::max(max_viol, f64::max(0.0, -c[i]));
167                max_viol_new = f64::max(max_viol_new, f64::max(0.0, -c_new[i]));
168            }
169        }
170
171        // Compute directional derivative
172        let g_dot_p = g.dot(&p);
173
174        // Backtracking line search
175        while (f_new > f + c1 * alpha * g_dot_p && max_viol <= ctol)
176            || (max_viol_new > max_viol && max_viol > ctol)
177        {
178            alpha *= rho;
179
180            // Prevent tiny steps
181            if alpha < 1e-10 {
182                break;
183            }
184
185            x_new = &x + &(&p * alpha);
186            f_new = func(x_new.as_slice().unwrap());
187            nfev += 1;
188
189            // Evaluate constraints
190            for (i, constraint) in constraints.iter().enumerate() {
191                if !constraint.is_bounds() {
192                    c_new[i] = (constraint.fun)(x_new.as_slice().unwrap());
193                    nfev += 1;
194                }
195            }
196
197            max_viol_new = 0.0;
198            for (i, constraint) in constraints.iter().enumerate() {
199                if constraint.kind == ConstraintKind::Inequality {
200                    max_viol_new = f64::max(max_viol_new, f64::max(0.0, -c_new[i]));
201                }
202            }
203        }
204
205        // Check convergence on function value and step size
206        if ((f - f_new).abs() < ftol * (1.0 + f.abs())) && alpha * p.dot(&p).sqrt() < ftol {
207            break;
208        }
209
210        // Calculate new gradient
211        let mut g_new = Array1::zeros(n);
212        for i in 0..n {
213            let mut x_h = x_new.clone();
214            x_h[i] += eps;
215            let f_h = func(x_h.as_slice().unwrap());
216            g_new[i] = (f_h - f_new) / eps;
217            nfev += 1;
218        }
219
220        // Calculate new constraint Jacobian
221        let mut a_new = Array2::zeros((constraints.len(), n));
222        for (i, constraint) in constraints.iter().enumerate() {
223            if !constraint.is_bounds() {
224                for j in 0..n {
225                    let mut x_h = x_new.clone();
226                    x_h[j] += eps;
227                    let c_h = (constraint.fun)(x_h.as_slice().unwrap());
228                    a_new[[i, j]] = (c_h - c_new[i]) / eps;
229                    nfev += 1;
230                }
231            }
232        }
233
234        // Update Lagrange multipliers (rudimentary)
235        for (i, constraint) in constraints.iter().enumerate() {
236            if constraint.kind == ConstraintKind::Inequality && c_new[i].abs() < ctol {
237                // For active inequality constraints
238                let mut normal = Array1::zeros(n);
239                for j in 0..n {
240                    normal[j] = a_new[[i, j]];
241                }
242
243                let norm = normal.dot(&normal).sqrt();
244                if norm > 1e-10 {
245                    normal = &normal / norm;
246                    lambda[i] = -g_new.dot(&normal);
247                }
248            } else {
249                lambda[i] = 0.0;
250            }
251        }
252
253        // BFGS update for the Hessian approximation
254        let s = &x_new - &x;
255        let y = &g_new - &g;
256
257        // Include constraints in y by adding Lagrangian term
258        let mut y_lag = y.clone();
259        for (i, &li) in lambda.iter().enumerate() {
260            if li > 0.0 {
261                // Only active constraints
262                for j in 0..n {
263                    // L = f - sum(lambda_i * c_i)
264                    // So gradient of L includes -lambda_i * grad(c_i)
265                    y_lag[j] += li * (a_new[[i, j]] - a[[i, j]]);
266                }
267            }
268        }
269
270        // BFGS update formula
271        let rho_bfgs = 1.0 / y_lag.dot(&s);
272        if rho_bfgs.is_finite() && rho_bfgs > 0.0 {
273            let i_mat = Array2::eye(n);
274            let y_row = y_lag.clone().insert_axis(Axis(0));
275            let s_col = s.clone().insert_axis(Axis(1));
276            let y_s_t = y_row.dot(&s_col);
277
278            let term1 = &i_mat - &(&y_s_t * rho_bfgs);
279            let s_row = s.clone().insert_axis(Axis(0));
280            let y_col = y_lag.clone().insert_axis(Axis(1));
281            let s_y_t = s_row.dot(&y_col);
282
283            let term2 = &i_mat - &(&s_y_t * rho_bfgs);
284            let term3 = &term1.dot(&h_inv);
285            h_inv = term3.dot(&term2) + rho_bfgs * s_col.dot(&s_row);
286        }
287
288        // Update for next iteration
289        x = x_new;
290        f = f_new;
291        g = g_new;
292        c = c_new;
293        a = a_new;
294
295        iter += 1;
296    }
297
298    // Prepare constraint values for the result
299    let mut c_result = Array1::zeros(constraints.len());
300    for (i, constraint) in constraints.iter().enumerate() {
301        if !constraint.is_bounds() {
302            c_result[i] = c[i];
303        }
304    }
305
306    // Create and return result
307    let mut result = OptimizeResults::default();
308    result.x = x;
309    result.fun = f;
310    result.jac = Some(g.into_raw_vec_and_offset().0);
311    result.constr = Some(c_result);
312    result.nfev = nfev;
313    result.nit = iter;
314    result.success = iter < maxiter;
315
316    if result.success {
317        result.message = "Optimization terminated successfully.".to_string();
318    } else {
319        result.message = "Maximum iterations reached.".to_string();
320    }
321
322    Ok(result)
323}