1use super::{
12 DspError, LeastSquaresResult, XScale, bound_step, dot, feasible, minimize, norm, scaling,
13 termination,
14};
15
16pub trait JacobianOperator {
24 fn residual(&mut self, x: &[f64]) -> Vec<f64>;
25 fn linearize(&mut self, x: &[f64]) -> Result<Vec<f64>, DspError>;
26 fn jvp(&self, direction: &[f64], out: &mut [f64]) -> Result<(), DspError>;
27 fn vjp(&self, residual_direction: &[f64], out: &mut [f64]) -> Result<(), DspError>;
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct OperatorOptions {
32 pub max_nfev: usize,
33 pub max_cg_iterations: usize,
35 pub cg_rtol: f64,
37 pub x_scale: XScale,
38 pub ftol: f64,
39 pub xtol: f64,
40 pub gtol: f64,
41}
42
43impl Default for OperatorOptions {
44 fn default() -> Self {
45 Self {
46 max_nfev: 100,
47 max_cg_iterations: 64,
48 cg_rtol: 1e-3,
49 x_scale: XScale::Jac,
50 ftol: 1e-8,
51 xtol: 1e-8,
52 gtol: 1e-8,
53 }
54 }
55}
56
57fn checked_linearization<P: JacobianOperator>(
58 problem: &mut P,
59 x: &[f64],
60 f: &[f64],
61) -> Result<(Vec<f64>, Vec<f64>), DspError> {
62 let diagonal = problem.linearize(x)?;
63 if diagonal.len() != x.len() || diagonal.iter().any(|v| !v.is_finite() || *v < 0.0) {
64 return Err(crate::invalid("invalid operator Jacobian diagonal"));
65 }
66 let mut gradient = vec![0.0; x.len()];
67 problem.vjp(f, &mut gradient)?;
68 if gradient.iter().any(|v| !v.is_finite()) {
69 return Err(crate::invalid(
70 "nonfinite operator Jacobian transpose product",
71 ));
72 }
73 Ok((diagonal, gradient))
74}
75
76fn jac_scale(diagonal: &[f64], old: Option<&[f64]>) -> Vec<f64> {
77 diagonal
78 .iter()
79 .enumerate()
80 .map(|(i, v)| {
81 let s = v.sqrt();
82 match old {
83 Some(old) => s.max(old[i]),
84 None if s == 0.0 => 1.0,
85 None => s,
86 }
87 })
88 .collect()
89}
90
91struct Model<'a, P> {
92 problem: &'a P,
93 d: &'a [f64],
94 diagonal: &'a [f64],
95 gradient: &'a [f64],
96 rows: usize,
97}
98
99impl<P: JacobianOperator> Model<'_, P> {
100 fn product(&self, s: &[f64]) -> Result<Vec<f64>, DspError> {
101 let physical: Vec<_> = s.iter().zip(self.d).map(|(s, d)| s * d).collect();
102 let mut jv = vec![0.0; self.rows];
103 self.problem.jvp(&physical, &mut jv)?;
104 if jv.iter().any(|v| !v.is_finite()) {
105 return Err(crate::invalid("nonfinite operator Jacobian product"));
106 }
107 let mut out = vec![0.0; s.len()];
108 self.problem.vjp(&jv, &mut out)?;
109 for i in 0..s.len() {
110 out[i] = self.d[i] * out[i] + self.diagonal[i] * s[i];
111 }
112 if out.iter().any(|v| !v.is_finite()) {
113 return Err(crate::invalid("nonfinite operator trust-region model"));
114 }
115 Ok(out)
116 }
117
118 fn value(&self, s: &[f64]) -> Result<f64, DspError> {
119 Ok(0.5 * dot(s, &self.product(s)?) + dot(s, self.gradient))
120 }
121
122 fn line(&self, s: &[f64], s0: Option<&[f64]>) -> Result<(f64, f64, f64), DspError> {
123 let hs = self.product(s)?;
124 let a = 0.5 * dot(s, &hs);
125 let mut b = dot(self.gradient, s);
126 let mut c = 0.0;
127 if let Some(s0) = s0 {
128 b += dot(s0, &hs);
129 c = self.value(s0)?;
130 }
131 Ok((a, b, c))
132 }
133}
134
135fn to_boundary(s: &[f64], p: &[f64], delta: f64) -> f64 {
138 let a = dot(p, p);
139 let b = dot(s, p);
140 let c = dot(s, s) - delta * delta;
141 let root = (b * b - a * c).max(0.0).sqrt();
142 if b >= 0.0 {
143 if root + b == 0.0 {
144 0.0
145 } else {
146 -c / (root + b)
147 }
148 } else {
149 (root - b) / a
150 }
151}
152
153fn truncated_cg<P: JacobianOperator>(
154 model: &Model<'_, P>,
155 column_norms: &[f64],
156 delta: f64,
157 options: &OperatorOptions,
158) -> Result<Vec<f64>, DspError> {
159 let n = model.gradient.len();
160 let preconditioner: Vec<_> = (0..n)
161 .map(|i| {
162 let v = model.d[i] * model.d[i] * column_norms[i] + model.diagonal[i];
163 if v > 0.0 { v } else { 1.0 }
164 })
165 .collect();
166 let mut s = vec![0.0; n];
167 let mut r: Vec<_> = model.gradient.iter().map(|g| -g).collect();
168 let mut z: Vec<_> = r.iter().zip(&preconditioner).map(|(r, d)| r / d).collect();
169 let mut direction = z.clone();
170 let mut rz = dot(&r, &z);
171 if !rz.is_finite() || rz < 0.0 {
172 return Err(crate::invalid("invalid operator CG preconditioner"));
173 }
174 let threshold = options.cg_rtol * options.cg_rtol * rz;
175 if rz == 0.0 {
176 return Ok(s);
177 }
178 for _ in 0..options.max_cg_iterations.min(n) {
179 let hd = model.product(&direction)?;
180 let curvature = dot(&direction, &hd);
181 if curvature <= 0.0 {
182 let tau = to_boundary(&s, &direction, delta);
183 for i in 0..n {
184 s[i] += tau * direction[i];
185 }
186 return Ok(s);
187 }
188 let alpha = rz / curvature;
189 let next: Vec<_> = (0..n).map(|i| s[i] + alpha * direction[i]).collect();
190 if norm(&next) >= delta {
191 let tau = to_boundary(&s, &direction, delta);
192 for i in 0..n {
193 s[i] += tau * direction[i];
194 }
195 return Ok(s);
196 }
197 s = next;
198 for i in 0..n {
199 r[i] -= alpha * hd[i];
200 z[i] = r[i] / preconditioner[i];
201 }
202 let next_rz = dot(&r, &z);
203 if !next_rz.is_finite() || next_rz < 0.0 {
204 return Err(crate::invalid("nonfinite operator CG iteration"));
205 }
206 if next_rz <= threshold {
207 return Ok(s);
208 }
209 let beta = next_rz / rz;
210 for i in 0..n {
211 direction[i] = z[i] + beta * direction[i];
212 }
213 rz = next_rz;
214 }
215 Ok(s)
216}
217
218#[allow(clippy::too_many_arguments)]
221fn select<P: JacobianOperator>(
222 x: &[f64],
223 model: &Model<'_, P>,
224 mut ph: Vec<f64>,
225 delta: f64,
226 lb: &[f64],
227 ub: &[f64],
228 theta: f64,
229) -> Result<(Vec<f64>, Vec<f64>, f64), DspError> {
230 let mut p: Vec<_> = ph.iter().zip(model.d).map(|(p, d)| p * d).collect();
231 if (0..x.len()).all(|i| x[i] + p[i] >= lb[i] && x[i] + p[i] <= ub[i]) {
232 let value = model.value(&ph)?;
233 return Ok((p, ph, -value));
234 }
235 let (stride, hits) = bound_step(x, &p, lb, ub);
236 let mut rh: Vec<_> = ph
237 .iter()
238 .zip(hits)
239 .map(|(v, hit)| if hit { -v } else { *v })
240 .collect();
241 let r: Vec<_> = rh.iter().zip(model.d).map(|(a, b)| a * b).collect();
242 for v in &mut p {
243 *v *= stride;
244 }
245 for v in &mut ph {
246 *v *= stride;
247 }
248 let xb: Vec<_> = x.iter().zip(&p).map(|(x, p)| x + p).collect();
249 let tr = to_boundary(&ph, &rh, delta);
250 let (bound, _) = bound_step(&xb, &r, lb, ub);
251 let rs = bound.min(tr);
252 let (lo, hi) = if rs > 0.0 {
253 (
254 (1.0 - theta) * stride / rs,
255 if rs == bound { theta * bound } else { tr },
256 )
257 } else {
258 (0.0, -1.0)
259 };
260 let mut reflected_value = f64::INFINITY;
261 if lo <= hi {
262 let (t, value) = minimize(model.line(&rh, Some(&ph))?, lo, hi);
263 reflected_value = value;
264 for i in 0..rh.len() {
265 rh[i] = rh[i] * t + ph[i];
266 }
267 }
268 for v in &mut p {
269 *v *= theta;
270 }
271 for v in &mut ph {
272 *v *= theta;
273 }
274 let step_value = model.value(&ph)?;
275 let mut ah: Vec<_> = model.gradient.iter().map(|v| -v).collect();
276 let mut ag: Vec<_> = ah.iter().zip(model.d).map(|(a, b)| a * b).collect();
277 let tr = delta / norm(&ah);
278 let (bound, _) = bound_step(x, &ag, lb, ub);
279 let upper = if bound < tr { theta * bound } else { tr };
280 let (stride, gradient_value) = minimize(model.line(&ah, None)?, 0.0, upper);
281 for v in &mut ah {
282 *v *= stride;
283 }
284 for v in &mut ag {
285 *v *= stride;
286 }
287 if step_value < reflected_value && step_value < gradient_value {
288 Ok((p, ph, -step_value))
289 } else if reflected_value < step_value && reflected_value < gradient_value {
290 Ok((
291 rh.iter().zip(model.d).map(|(a, b)| a * b).collect(),
292 rh,
293 -reflected_value,
294 ))
295 } else {
296 Ok((ag, ah, -gradient_value))
297 }
298}
299
300pub fn least_squares_operator<P: JacobianOperator>(
307 problem: &mut P,
308 x0: &[f64],
309 lb: &[f64],
310 ub: &[f64],
311 options: &OperatorOptions,
312) -> Result<LeastSquaresResult, DspError> {
313 let n = x0.len();
314 let tolerances = [options.ftol, options.xtol, options.gtol];
315 if n == 0
316 || lb.len() != n
317 || ub.len() != n
318 || options.max_nfev == 0
319 || options.max_cg_iterations == 0
320 || !options.cg_rtol.is_finite()
321 || options.cg_rtol <= 0.0
322 || options.cg_rtol >= 1.0
323 || tolerances.iter().any(|v| !v.is_finite() || *v < 0.0)
324 || tolerances.iter().all(|v| *v <= f64::EPSILON)
325 || !lb.iter().chain(ub).any(|v| v.is_finite())
326 || (0..n).any(|i| {
327 !x0[i].is_finite()
328 || lb[i].is_nan()
329 || ub[i].is_nan()
330 || lb[i] >= ub[i]
331 || x0[i] < lb[i]
332 || x0[i] > ub[i]
333 })
334 {
335 return Err(crate::invalid("invalid operator TRF start/bounds/options"));
336 }
337 let mut x = x0.to_vec();
338 feasible(&mut x, lb, ub, true);
339 let mut f = problem.residual(&x);
340 let rows = f.len();
341 if rows == 0 || f.iter().any(|v| !v.is_finite()) {
342 return Err(crate::invalid("invalid initial operator residual"));
343 }
344 let (mut column_norms, mut gradient) = checked_linearization(problem, &x, &f)?;
345 let mut scale_inv = match options.x_scale {
346 XScale::Unit => vec![1.0; n],
347 XScale::Jac => jac_scale(&column_norms, None),
348 };
349 let (mut v, dv) = scaling(&x, &gradient, lb, ub);
350 for i in 0..n {
351 if dv[i] != 0.0 {
352 v[i] *= scale_inv[i];
353 }
354 }
355 let mut delta = norm(
356 &(0..n)
357 .map(|i| x[i] * scale_inv[i] / v[i].sqrt())
358 .collect::<Vec<_>>(),
359 );
360 if delta == 0.0 {
361 delta = 1.0;
362 }
363 let mut cost = 0.5 * dot(&f, &f);
364 if !cost.is_finite() || !delta.is_finite() {
365 return Err(crate::invalid("nonfinite initial operator cost/radius"));
366 }
367 let (mut nfev, mut njev, mut status) = (1, 1, 0);
368 let optimality = loop {
369 let (mut v, dv) = scaling(&x, &gradient, lb, ub);
370 let gn = gradient
371 .iter()
372 .zip(&v)
373 .map(|(g, v)| (g * v).abs())
374 .fold(0.0, f64::max);
375 if gn < options.gtol {
376 status = 1;
377 }
378 if status != 0 || nfev == options.max_nfev {
379 break gn;
380 }
381 for i in 0..n {
382 if dv[i] != 0.0 {
383 v[i] *= scale_inv[i];
384 }
385 }
386 let d: Vec<_> = (0..n).map(|i| v[i].sqrt() / scale_inv[i]).collect();
387 let diagonal: Vec<_> = (0..n).map(|i| gradient[i] * dv[i] / scale_inv[i]).collect();
388 let gh: Vec<_> = (0..n).map(|i| d[i] * gradient[i]).collect();
389 if d.iter().chain(&diagonal).chain(&gh).any(|v| !v.is_finite()) {
390 return Err(crate::invalid("nonfinite operator TRF scaling"));
391 }
392 let theta = 0.995_f64.max(1.0 - gn);
393 let mut actual = -1.0;
394 let mut candidate = x.clone();
395 let mut candidate_f = f.clone();
396 let mut candidate_cost = cost;
397 while actual <= 0.0 && nfev < options.max_nfev {
398 let model = Model {
399 problem: &*problem,
400 d: &d,
401 diagonal: &diagonal,
402 gradient: &gh,
403 rows,
404 };
405 let ph = truncated_cg(&model, &column_norms, delta, options)?;
406 let (step, sh, predicted) = select(&x, &model, ph, delta, lb, ub, theta)?;
407 if !predicted.is_finite() || step.iter().any(|v| !v.is_finite()) {
408 return Err(crate::invalid("nonfinite operator trust-region step"));
409 }
410 candidate = x.iter().zip(&step).map(|(x, s)| x + s).collect();
411 feasible(&mut candidate, lb, ub, false);
412 candidate_f = problem.residual(&candidate);
413 nfev += 1;
414 if candidate_f.len() != rows {
415 return Err(crate::invalid("residual length changed"));
416 }
417 let shn = norm(&sh);
418 if candidate_f.iter().any(|v| !v.is_finite()) {
419 delta = 0.25 * shn;
420 continue;
421 }
422 candidate_cost = 0.5 * dot(&candidate_f, &candidate_f);
423 actual = cost - candidate_cost;
424 let ratio = if predicted > 0.0 {
425 actual / predicted
426 } else if predicted == 0.0 && actual == 0.0 {
427 1.0
428 } else {
429 0.0
430 };
431 let next_delta = if ratio < 0.25 {
432 0.25 * shn
433 } else if ratio > 0.75 && shn > 0.95 * delta {
434 2.0 * delta
435 } else {
436 delta
437 };
438 status = termination(actual, cost, &step, &x, ratio, options.ftol, options.xtol);
439 if status != 0 {
440 break;
441 }
442 delta = next_delta;
443 if delta <= 0.0 || !delta.is_finite() {
444 return Err(crate::invalid("operator trust radius exhausted"));
445 }
446 }
447 if actual > 0.0 {
448 x = candidate;
449 f = candidate_f;
450 cost = candidate_cost;
451 (column_norms, gradient) = checked_linearization(problem, &x, &f)?;
452 njev += 1;
453 if options.x_scale == XScale::Jac {
454 scale_inv = jac_scale(&column_norms, Some(&scale_inv));
455 }
456 }
457 };
458 Ok(LeastSquaresResult {
459 x,
460 cost,
461 nfev,
462 status,
463 njev,
464 optimality,
465 })
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 struct Linear {
473 a: Vec<Vec<f64>>,
474 b: Vec<f64>,
475 }
476 impl JacobianOperator for Linear {
477 fn residual(&mut self, x: &[f64]) -> Vec<f64> {
478 self.a
479 .iter()
480 .zip(&self.b)
481 .map(|(a, b)| dot(a, x) - b)
482 .collect()
483 }
484 fn linearize(&mut self, x: &[f64]) -> Result<Vec<f64>, DspError> {
485 Ok((0..x.len())
486 .map(|j| self.a.iter().map(|a| a[j] * a[j]).sum())
487 .collect())
488 }
489 fn jvp(&self, v: &[f64], out: &mut [f64]) -> Result<(), DspError> {
490 for (out, a) in out.iter_mut().zip(&self.a) {
491 *out = dot(a, v);
492 }
493 Ok(())
494 }
495 fn vjp(&self, v: &[f64], out: &mut [f64]) -> Result<(), DspError> {
496 for (j, out) in out.iter_mut().enumerate() {
497 *out = self.a.iter().zip(v).map(|(a, v)| a[j] * v).sum();
498 }
499 Ok(())
500 }
501 }
502
503 #[test]
504 fn operator_matches_dense_bounded_solution() {
505 let mut problem = Linear {
506 a: vec![vec![1.0, 0.5], vec![-0.2, 2.0], vec![0.4, -0.1]],
507 b: vec![-2.0, 4.0, -1.0],
508 };
509 let (x, lb, ub) = ([0.4, 0.6], [0.0, 0.0], [1.0, 1.0]);
510 let dense =
511 super::super::least_squares(|x| problem.residual(x), &x, &lb, &ub, 100).unwrap();
512 let iterative =
513 least_squares_operator(&mut problem, &x, &lb, &ub, &OperatorOptions::default())
514 .unwrap();
515 assert!(
516 iterative
517 .x
518 .iter()
519 .zip(dense.x)
520 .all(|(a, b)| (a - b).abs() < 1e-7)
521 );
522 assert!((iterative.cost - dense.cost).abs() < 1e-8);
523 assert!(
524 iterative
525 .x
526 .iter()
527 .zip(lb)
528 .zip(ub)
529 .all(|((&x, l), u)| x >= l && x <= u)
530 );
531 }
532
533 #[test]
534 fn operator_handles_rank_deficiency_and_invalid_options() {
535 let mut problem = Linear {
536 a: vec![vec![1.0, 1.0, 0.0], vec![2.0, 2.0, 0.0]],
537 b: vec![3.0, 6.0],
538 };
539 let (x, lb, ub) = ([0.0, 0.0, 0.7], [-10.0; 3], [10.0; 3]);
540 let options = OperatorOptions::default();
541 let result = least_squares_operator(&mut problem, &x, &lb, &ub, &options).unwrap();
542 assert!((result.x[0] + result.x[1] - 3.0).abs() < 1e-7);
543 assert_eq!(result.x[2], 0.7);
544 assert!(
545 least_squares_operator(
546 &mut problem,
547 &x,
548 &lb,
549 &ub,
550 &OperatorOptions {
551 max_cg_iterations: 0,
552 ..options
553 }
554 )
555 .is_err()
556 );
557 }
558}