1use num_traits::Float;
2
3use crate::convergence::dot;
4use crate::objective::Objective;
5
6#[derive(Debug, Clone)]
8pub struct ArmijoParams<F> {
9 pub c: F,
11 pub rho: F,
13 pub alpha_init: F,
15 pub alpha_min: F,
17}
18
19impl Default for ArmijoParams<f64> {
20 fn default() -> Self {
21 ArmijoParams {
22 c: 1e-4,
23 rho: 0.5,
24 alpha_init: 1.0,
25 alpha_min: 1e-16,
26 }
27 }
28}
29
30impl Default for ArmijoParams<f32> {
31 fn default() -> Self {
32 ArmijoParams {
33 c: 1e-4,
34 rho: 0.5,
35 alpha_init: 1.0,
36 alpha_min: 1e-8,
37 }
38 }
39}
40
41#[derive(Debug)]
43pub struct LineSearchResult<F> {
44 pub alpha: F,
46 pub value: F,
48 pub gradient: Vec<F>,
50 pub evals: usize,
52}
53
54pub fn backtracking_armijo<F: Float, O: Objective<F>>(
66 obj: &mut O,
67 x: &[F],
68 d: &[F],
69 f_x: F,
70 grad_x: &[F],
71 params: &ArmijoParams<F>,
72) -> Option<LineSearchResult<F>> {
73 let mut discarded = 0;
74 backtracking_armijo_with_evals(obj, x, d, f_x, grad_x, params, &mut discarded)
75}
76
77pub fn backtracking_armijo_with_evals<F: Float, O: Objective<F>>(
86 obj: &mut O,
87 x: &[F],
88 d: &[F],
89 f_x: F,
90 grad_x: &[F],
91 params: &ArmijoParams<F>,
92 func_evals: &mut usize,
93) -> Option<LineSearchResult<F>> {
94 let n = x.len();
95
96 if !(params.rho > F::zero() && params.rho < F::one() && params.alpha_min > F::zero()) {
103 return None;
104 }
105
106 let dg = dot(grad_x, d);
107
108 if dg >= F::zero() {
110 return None;
111 }
112
113 let mut alpha = params.alpha_init;
114 let mut x_new = vec![F::zero(); n];
115 let mut evals = 0;
116
117 loop {
118 if alpha < params.alpha_min {
119 return None;
120 }
121
122 for i in 0..n {
123 x_new[i] = x[i] + alpha * d[i];
124 }
125
126 let (f_new, g_new) = obj.eval_grad(&x_new);
127 evals += 1;
128 *func_evals += 1;
129
130 if !f_new.is_finite() || !g_new.iter().all(|g| g.is_finite()) {
136 alpha = alpha * params.rho;
137 continue;
138 }
139
140 if f_new <= f_x + params.c * alpha * dg {
142 return Some(LineSearchResult {
143 alpha,
144 value: f_new,
145 gradient: g_new,
146 evals,
147 });
148 }
149
150 alpha = alpha * params.rho;
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 struct Quadratic;
160
161 impl Objective<f64> for Quadratic {
162 fn dim(&self) -> usize {
163 2
164 }
165
166 fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
167 let f = 0.5 * (x[0] * x[0] + x[1] * x[1]);
168 let g = vec![x[0], x[1]];
169 (f, g)
170 }
171 }
172
173 #[test]
174 fn armijo_quadratic_descent() {
175 let mut obj = Quadratic;
176 let x = vec![2.0, 3.0];
177 let (f_x, grad) = obj.eval_grad(&x);
178 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
180
181 let result =
182 backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default()).unwrap();
183
184 assert!(result.alpha > 0.0);
185 assert!(result.value < f_x, "line search should decrease objective");
186 }
187
188 #[test]
189 fn armijo_full_step_on_quadratic() {
190 let mut obj = Quadratic;
191 let x = vec![2.0, 3.0];
192 let (f_x, grad) = obj.eval_grad(&x);
193 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
194
195 let result =
196 backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default()).unwrap();
197
198 assert!(
200 (result.alpha - 1.0).abs() < 1e-12,
201 "full step should be accepted on quadratic, got alpha={}",
202 result.alpha
203 );
204 }
205
206 #[test]
207 fn armijo_non_descent_returns_none() {
208 let mut obj = Quadratic;
209 let x = vec![2.0, 3.0];
210 let (f_x, grad) = obj.eval_grad(&x);
211 let d = grad.clone();
213
214 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default());
215 assert!(result.is_none());
216 }
217
218 #[test]
221 fn armijo_rejects_rho_ge_one() {
222 let mut obj = Quadratic;
223 let x = vec![2.0, 3.0];
224 let (f_x, grad) = obj.eval_grad(&x);
225 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
226 let params = ArmijoParams {
228 rho: 1.0,
229 ..Default::default()
230 };
231 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, ¶ms);
232 assert!(result.is_none(), "rho >= 1 must be rejected, not looped");
233 }
234
235 #[test]
236 fn armijo_rejects_nonpositive_alpha_min() {
237 let mut obj = Quadratic;
238 let x = vec![2.0, 3.0];
239 let (f_x, grad) = obj.eval_grad(&x);
240 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
241 let params = ArmijoParams {
243 alpha_min: 0.0,
244 ..Default::default()
245 };
246 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, ¶ms);
247 assert!(result.is_none(), "alpha_min <= 0 must be rejected");
248 }
249
250 #[test]
251 fn armijo_rejects_nan_params() {
252 let mut obj = Quadratic;
253 let x = vec![2.0, 3.0];
254 let (f_x, grad) = obj.eval_grad(&x);
255 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
256 let nan_rho = ArmijoParams {
259 rho: f64::NAN,
260 ..Default::default()
261 };
262 assert!(backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &nan_rho).is_none());
263 let nan_amin = ArmijoParams {
264 alpha_min: f64::NAN,
265 ..Default::default()
266 };
267 assert!(backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &nan_amin).is_none());
268 }
269}