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>>(
62 obj: &mut O,
63 x: &[F],
64 d: &[F],
65 f_x: F,
66 grad_x: &[F],
67 params: &ArmijoParams<F>,
68) -> Option<LineSearchResult<F>> {
69 let n = x.len();
70
71 if !(params.rho > F::zero() && params.rho < F::one() && params.alpha_min > F::zero()) {
78 return None;
79 }
80
81 let dg = dot(grad_x, d);
82
83 if dg >= F::zero() {
85 return None;
86 }
87
88 let mut alpha = params.alpha_init;
89 let mut x_new = vec![F::zero(); n];
90 let mut evals = 0;
91
92 loop {
93 if alpha < params.alpha_min {
94 return None;
95 }
96
97 for i in 0..n {
98 x_new[i] = x[i] + alpha * d[i];
99 }
100
101 let (f_new, g_new) = obj.eval_grad(&x_new);
102 evals += 1;
103
104 if !f_new.is_finite() || !g_new.iter().all(|g| g.is_finite()) {
110 alpha = alpha * params.rho;
111 continue;
112 }
113
114 if f_new <= f_x + params.c * alpha * dg {
116 return Some(LineSearchResult {
117 alpha,
118 value: f_new,
119 gradient: g_new,
120 evals,
121 });
122 }
123
124 alpha = alpha * params.rho;
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 struct Quadratic;
134
135 impl Objective<f64> for Quadratic {
136 fn dim(&self) -> usize {
137 2
138 }
139
140 fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
141 let f = 0.5 * (x[0] * x[0] + x[1] * x[1]);
142 let g = vec![x[0], x[1]];
143 (f, g)
144 }
145 }
146
147 #[test]
148 fn armijo_quadratic_descent() {
149 let mut obj = Quadratic;
150 let x = vec![2.0, 3.0];
151 let (f_x, grad) = obj.eval_grad(&x);
152 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
154
155 let result =
156 backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default()).unwrap();
157
158 assert!(result.alpha > 0.0);
159 assert!(result.value < f_x, "line search should decrease objective");
160 }
161
162 #[test]
163 fn armijo_full_step_on_quadratic() {
164 let mut obj = Quadratic;
165 let x = vec![2.0, 3.0];
166 let (f_x, grad) = obj.eval_grad(&x);
167 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
168
169 let result =
170 backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default()).unwrap();
171
172 assert!(
174 (result.alpha - 1.0).abs() < 1e-12,
175 "full step should be accepted on quadratic, got alpha={}",
176 result.alpha
177 );
178 }
179
180 #[test]
181 fn armijo_non_descent_returns_none() {
182 let mut obj = Quadratic;
183 let x = vec![2.0, 3.0];
184 let (f_x, grad) = obj.eval_grad(&x);
185 let d = grad.clone();
187
188 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &ArmijoParams::default());
189 assert!(result.is_none());
190 }
191
192 #[test]
195 fn armijo_rejects_rho_ge_one() {
196 let mut obj = Quadratic;
197 let x = vec![2.0, 3.0];
198 let (f_x, grad) = obj.eval_grad(&x);
199 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
200 let params = ArmijoParams {
202 rho: 1.0,
203 ..Default::default()
204 };
205 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, ¶ms);
206 assert!(result.is_none(), "rho >= 1 must be rejected, not looped");
207 }
208
209 #[test]
210 fn armijo_rejects_nonpositive_alpha_min() {
211 let mut obj = Quadratic;
212 let x = vec![2.0, 3.0];
213 let (f_x, grad) = obj.eval_grad(&x);
214 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
215 let params = ArmijoParams {
217 alpha_min: 0.0,
218 ..Default::default()
219 };
220 let result = backtracking_armijo(&mut obj, &x, &d, f_x, &grad, ¶ms);
221 assert!(result.is_none(), "alpha_min <= 0 must be rejected");
222 }
223
224 #[test]
225 fn armijo_rejects_nan_params() {
226 let mut obj = Quadratic;
227 let x = vec![2.0, 3.0];
228 let (f_x, grad) = obj.eval_grad(&x);
229 let d: Vec<f64> = grad.iter().map(|&g| -g).collect();
230 let nan_rho = ArmijoParams {
233 rho: f64::NAN,
234 ..Default::default()
235 };
236 assert!(backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &nan_rho).is_none());
237 let nan_amin = ArmijoParams {
238 alpha_min: f64::NAN,
239 ..Default::default()
240 };
241 assert!(backtracking_armijo(&mut obj, &x, &d, f_x, &grad, &nan_amin).is_none());
242 }
243}