use crate::ipopt_cq::IpoptCqHandle;
use crate::ipopt_data::IpoptDataHandle;
use crate::ipopt_nlp::IpoptNlp;
use crate::iterates_vector::IteratesVector;
use crate::kkt::pd_search_dir_calc::PdSearchDirCalc;
use crate::line_search::filter_acceptor::AcceptDecision;
use crate::line_search::ls_acceptor::BacktrackingLsAcceptor;
use pounce_common::types::Number;
use std::cell::RefCell;
use std::rc::Rc;
const ALPHA_INTERP_MIN_TRIALS: i32 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Accepted,
TinyStep,
Failed,
Deadline,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlphaForY {
Primal,
BoundMult,
Full,
Min,
Max,
Average,
}
impl AlphaForY {
pub fn alpha_y(self, alpha_primal: Number, alpha_dual: Number) -> Number {
match self {
AlphaForY::Primal => alpha_primal,
AlphaForY::BoundMult => alpha_dual,
AlphaForY::Full => 1.0,
AlphaForY::Min => alpha_primal.min(alpha_dual),
AlphaForY::Max => alpha_primal.max(alpha_dual),
AlphaForY::Average => 0.5 * (alpha_primal + alpha_dual),
}
}
}
pub struct BacktrackingLineSearch {
pub acceptor: Box<dyn BacktrackingLsAcceptor>,
pub alpha_red_factor: Number,
pub alpha_red_factor_min: Number,
pub max_soc: i32,
pub kappa_soc: Number,
pub soc_method: i32,
pub watchdog_shortened_iter_trigger: i32,
pub watchdog_trial_iter_max: i32,
pub alpha_min: Number,
pub max_trials: i32,
in_watchdog: bool,
watchdog_iterate: Option<IteratesVector>,
watchdog_delta: Option<IteratesVector>,
watchdog_trial_iter: i32,
watchdog_shortened_iter: i32,
last_mu: Number,
watchdog_theta: Number,
watchdog_phi: Number,
watchdog_d_phi: Number,
pub soft_resto_pderror_reduction_factor: Number,
pub max_soft_resto_iters: i32,
in_soft_resto_phase: bool,
soft_resto_counter: i32,
pub accept_every_trial_step: bool,
pub alpha_for_y: AlphaForY,
pub accept_after_max_steps: i32,
}
enum AlphaResult {
Accepted { n_steps: i32 },
TinyStep { n_steps: i32, last_alpha: Number },
Failed {
n_steps: i32,
last_alpha: Number,
evaluation_error: bool,
},
Deadline,
}
impl BacktrackingLineSearch {
pub fn new(acceptor: Box<dyn BacktrackingLsAcceptor>) -> Self {
Self {
acceptor,
alpha_red_factor: 0.5,
alpha_red_factor_min: 0.05,
max_soc: 4,
kappa_soc: 0.99,
soc_method: 0,
watchdog_shortened_iter_trigger: 10,
watchdog_trial_iter_max: 3,
alpha_min: 1e-12,
max_trials: 50,
in_watchdog: false,
watchdog_iterate: None,
watchdog_delta: None,
watchdog_trial_iter: 0,
watchdog_shortened_iter: 0,
last_mu: -1.0,
watchdog_theta: 0.0,
watchdog_phi: 0.0,
watchdog_d_phi: 0.0,
soft_resto_pderror_reduction_factor: 1.0 - 1e-4,
max_soft_resto_iters: 10,
in_soft_resto_phase: false,
soft_resto_counter: 0,
accept_every_trial_step: false,
alpha_for_y: AlphaForY::Primal,
accept_after_max_steps: -1,
}
}
pub(crate) fn in_watchdog(&self) -> bool {
self.in_watchdog
}
#[cfg(test)]
pub(crate) fn watchdog_shortened_iter(&self) -> i32 {
self.watchdog_shortened_iter
}
pub fn acceptor(&self) -> &dyn BacktrackingLsAcceptor {
&*self.acceptor
}
pub fn acceptor_mut(&mut self) -> &mut dyn BacktrackingLsAcceptor {
&mut *self.acceptor
}
pub fn reset(&mut self) {
self.acceptor.reset();
}
pub fn reset_after_restoration(&mut self) {
self.in_soft_resto_phase = false;
self.soft_resto_counter = 0;
self.watchdog_shortened_iter = 0;
}
#[allow(clippy::too_many_arguments)]
pub fn find_acceptable_trial_point(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
delta: &IteratesVector,
alpha_init: Number,
alpha_dual: Number,
nlp: Option<&Rc<RefCell<dyn IpoptNlp>>>,
search_dir: Option<&mut PdSearchDirCalc>,
) -> Outcome {
if self.accept_every_trial_step {
let curr = match data.borrow().curr.clone() {
Some(c) => c,
None => return Outcome::Failed,
};
let alpha_y = self.alpha_for_y.alpha_y(alpha_init, alpha_dual);
let trial_iv = scaled_step(&curr, delta, alpha_init, alpha_y, alpha_dual);
let mut d = data.borrow_mut();
d.set_trial(trial_iv);
d.info_alpha_primal = alpha_init;
d.info_alpha_dual = alpha_dual;
d.info_alpha_primal_char = ' ';
d.info_ls_count = 1;
return Outcome::Accepted;
}
if self.in_soft_resto_phase {
self.soft_resto_counter += 1;
if self.soft_resto_counter > self.max_soft_resto_iters {
self.in_soft_resto_phase = false;
self.soft_resto_counter = 0;
return self.fail_to_restoration(data);
}
self.acceptor.init_this_line_search(data, cq, delta);
return match self.try_soft_resto_step(data, cq, delta) {
Some(satisfies_original) => {
if satisfies_original {
self.in_soft_resto_phase = false;
self.soft_resto_counter = 0;
data.borrow_mut().info_alpha_primal_char = 'S';
} else {
data.borrow_mut().info_alpha_primal_char = 's';
}
Outcome::Accepted
}
None => {
self.in_soft_resto_phase = false;
self.soft_resto_counter = 0;
self.fail_to_restoration(data)
}
};
}
let outcome =
self.run_filter_line_search(data, cq, delta, alpha_init, alpha_dual, nlp, search_dir);
if outcome == Outcome::Accepted {
return Outcome::Accepted;
}
if outcome == Outcome::Deadline {
return Outcome::Deadline;
}
let reference_theta = cq.borrow().curr_constraint_violation();
let reference_barr = cq.borrow().curr_barrier_obj();
self.acceptor
.prepare_resto_phase_start(reference_theta, reference_barr);
match self.try_soft_resto_step(data, cq, delta) {
Some(satisfies_original) => {
if satisfies_original {
data.borrow_mut().info_alpha_primal_char = 'S';
} else {
self.in_soft_resto_phase = true;
self.soft_resto_counter = 0;
data.borrow_mut().info_alpha_primal_char = 's';
}
Outcome::Accepted
}
None => outcome,
}
}
fn fail_to_restoration(&self, data: &IpoptDataHandle) -> Outcome {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = 0.0;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = 0;
Outcome::Failed
}
fn try_soft_resto_step(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
delta: &IteratesVector,
) -> Option<bool> {
if self.soft_resto_pderror_reduction_factor == 0.0 {
return None;
}
let curr = data.borrow().curr.clone()?;
let tau = data.borrow().curr_tau;
let alpha = {
let cq_ref = cq.borrow();
cq_ref
.aff_step_alpha_primal_max(delta, tau)
.min(cq_ref.aff_step_alpha_dual_max(delta, tau))
};
let trial_iv = scaled_step(&curr, delta, alpha, alpha, alpha);
data.borrow_mut().set_trial(trial_iv);
let theta_trial = cq.borrow().trial_constraint_violation();
let phi_trial = cq.borrow().trial_barrier_obj();
if !theta_trial.is_finite() || !phi_trial.is_finite() {
return None;
}
let theta = cq.borrow().curr_constraint_violation();
let phi = cq.borrow().curr_barrier_obj();
let d_phi = self.compute_d_phi(cq, delta);
if self
.acceptor
.check_trial_point(0.0, theta, phi, d_phi, theta_trial, phi_trial)
== AcceptDecision::Accept
{
let mut d = data.borrow_mut();
d.info_alpha_primal = alpha;
d.info_alpha_dual = alpha;
d.info_ls_count = 1;
return Some(true);
}
let mu = data.borrow().curr_mu;
let curr_pderror = cq.borrow().curr_primal_dual_system_error(mu);
let trial_pderror = cq.borrow().trial_primal_dual_system_error(mu);
if !trial_pderror.is_finite() {
return None;
}
if trial_pderror <= self.soft_resto_pderror_reduction_factor * curr_pderror {
let mut d = data.borrow_mut();
d.info_alpha_primal = alpha;
d.info_alpha_dual = alpha;
d.info_ls_count = 1;
return Some(false);
}
None
}
#[allow(clippy::too_many_arguments)]
fn run_filter_line_search(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
delta: &IteratesVector,
alpha_init: Number,
alpha_dual: Number,
nlp: Option<&Rc<RefCell<dyn IpoptNlp>>>,
search_dir: Option<&mut PdSearchDirCalc>,
) -> Outcome {
let curr_mu = data.borrow().curr_mu;
if self.last_mu < 0.0 || self.last_mu != curr_mu {
self.in_watchdog = false;
self.watchdog_iterate = None;
self.watchdog_delta = None;
self.watchdog_shortened_iter = 0;
self.last_mu = curr_mu;
}
if !self.in_watchdog
&& self.watchdog_shortened_iter_trigger > 0
&& self.watchdog_shortened_iter >= self.watchdog_shortened_iter_trigger
{
self.start_watchdog(data, cq, delta);
}
self.acceptor
.set_theta_rows(cq.borrow().constraint_violation_rows() as Number);
self.acceptor.init_this_line_search(data, cq, delta);
let (theta, phi, d_phi) = if self.in_watchdog {
(self.watchdog_theta, self.watchdog_phi, self.watchdog_d_phi)
} else {
let theta = cq.borrow().curr_constraint_violation();
let phi = cq.borrow().curr_barrier_obj();
let d_phi = self.compute_d_phi(cq, delta);
(theta, phi, d_phi)
};
let result = self.run_alpha_loop(
data, cq, delta, alpha_init, alpha_dual, nlp, search_dir, theta, phi, d_phi,
false,
);
match result {
AlphaResult::Accepted { n_steps } => {
if n_steps == 0 {
self.watchdog_shortened_iter = 0;
} else {
self.watchdog_shortened_iter += 1;
}
if self.in_watchdog {
self.in_watchdog = false;
self.watchdog_iterate = None;
self.watchdog_delta = None;
self.watchdog_shortened_iter = 0;
}
Outcome::Accepted
}
AlphaResult::TinyStep {
n_steps,
last_alpha,
} => {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = last_alpha;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = n_steps + 1;
Outcome::TinyStep
}
AlphaResult::Failed {
n_steps,
last_alpha,
evaluation_error,
} => {
if self.in_watchdog {
self.handle_watchdog_failure(
data,
cq,
alpha_dual,
nlp,
n_steps,
last_alpha,
evaluation_error,
)
} else {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = last_alpha;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = n_steps + 1;
Outcome::Failed
}
}
AlphaResult::Deadline => Outcome::Deadline,
}
}
fn start_watchdog(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
delta: &IteratesVector,
) {
let curr = data.borrow().curr.clone();
let Some(curr) = curr else {
return;
};
self.in_watchdog = true;
self.watchdog_iterate = Some(curr);
self.watchdog_delta = Some(delta.clone());
self.watchdog_trial_iter = 0;
self.watchdog_theta = cq.borrow().curr_constraint_violation();
self.watchdog_phi = cq.borrow().curr_barrier_obj();
self.watchdog_d_phi = self.compute_d_phi(cq, delta);
}
fn handle_watchdog_failure(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
alpha_dual: Number,
nlp: Option<&Rc<RefCell<dyn IpoptNlp>>>,
n_steps: i32,
last_alpha: Number,
evaluation_error: bool,
) -> Outcome {
self.watchdog_trial_iter += 1;
if evaluation_error || self.watchdog_trial_iter > self.watchdog_trial_iter_max {
let snapshot_iter = self.watchdog_iterate.take();
let snapshot_delta = self.watchdog_delta.take();
self.in_watchdog = false;
self.watchdog_shortened_iter = 0;
let (Some(snap), Some(snap_delta)) = (snapshot_iter, snapshot_delta) else {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = last_alpha;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = n_steps + 1;
return Outcome::Failed;
};
{
let mut d = data.borrow_mut();
d.set_curr(snap);
}
let theta = cq.borrow().curr_constraint_violation();
let phi = cq.borrow().curr_barrier_obj();
let d_phi = self.compute_d_phi(cq, &snap_delta);
let tau = data.borrow().curr_tau;
let (alpha_primal_retry, alpha_dual_retry) = {
let cq_ref = cq.borrow();
(
1.0_f64.min(cq_ref.aff_step_alpha_primal_max(&snap_delta, tau)),
1.0_f64.min(cq_ref.aff_step_alpha_dual_max(&snap_delta, tau)),
)
};
let result2 = self.run_alpha_loop(
data,
cq,
&snap_delta,
alpha_primal_retry,
alpha_dual_retry,
nlp,
None,
theta,
phi,
d_phi,
true,
);
match result2 {
AlphaResult::Accepted { n_steps: ns2 } => {
if ns2 == 0 {
self.watchdog_shortened_iter = 0;
} else {
self.watchdog_shortened_iter += 1;
}
Outcome::Accepted
}
AlphaResult::TinyStep {
n_steps: ns2,
last_alpha: la2,
} => {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = la2;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = ns2 + 1;
Outcome::TinyStep
}
AlphaResult::Failed {
n_steps: ns2,
last_alpha: la2,
evaluation_error: _,
} => {
let mut d = data.borrow_mut();
d.trial = None;
d.info_alpha_primal = la2;
d.info_alpha_dual = 0.0;
d.info_alpha_primal_char = 'R';
d.info_ls_count = ns2 + 1;
Outcome::Failed
}
AlphaResult::Deadline => Outcome::Deadline,
}
} else {
let mut d = data.borrow_mut();
d.info_alpha_primal = last_alpha;
d.info_alpha_dual = alpha_dual;
d.info_alpha_primal_char = 'w';
d.info_ls_count = n_steps + 1;
Outcome::Accepted
}
}
#[allow(clippy::too_many_arguments)]
fn run_alpha_loop(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
delta: &IteratesVector,
alpha_init: Number,
alpha_dual: Number,
nlp: Option<&Rc<RefCell<dyn IpoptNlp>>>,
search_dir: Option<&mut PdSearchDirCalc>,
theta: Number,
phi: Number,
d_phi: Number,
skip_first: bool,
) -> AlphaResult {
let curr = match data.borrow().curr.clone() {
Some(c) => c,
None => {
return AlphaResult::Failed {
n_steps: 0,
last_alpha: 0.0,
evaluation_error: false,
};
}
};
let mut evaluation_error = false;
let mut soc_search_dir = search_dir;
let (mut c_soc_buf, mut dms_soc_buf) =
if soc_search_dir.is_some() && nlp.is_some() && self.max_soc > 0 && !skip_first {
let cq_ref = cq.borrow();
let curr_c = cq_ref.curr_c();
let curr_dms = cq_ref.curr_d_minus_s();
let mut c_soc = curr_c.make_new();
c_soc.copy(&*curr_c);
let mut dms_soc = curr_dms.make_new();
dms_soc.copy(&*curr_dms);
(Some(c_soc), Some(dms_soc))
} else {
(None, None)
};
let mut alpha = if skip_first {
alpha_init * self.alpha_red_factor
} else {
alpha_init
};
let mut last_alpha = alpha;
let mut n_steps: i32 = 0;
let alpha_min_eff = if self.in_watchdog {
alpha_init
} else {
let acceptor_alpha_min = self.acceptor.calc_alpha_min(d_phi, theta);
self.alpha_min.max(acceptor_alpha_min)
};
for trial in 0..self.max_trials {
if data
.borrow()
.deadline
.as_ref()
.is_some_and(|dl| dl.exceeded().is_some())
{
return AlphaResult::Deadline;
}
if alpha < alpha_min_eff {
return AlphaResult::TinyStep {
n_steps,
last_alpha,
};
}
last_alpha = alpha;
n_steps = trial;
let alpha_y = self.alpha_for_y.alpha_y(alpha, alpha_dual);
let trial_iv = scaled_step(&curr, delta, alpha, alpha_y, alpha_dual);
data.borrow_mut().set_trial(trial_iv);
let theta_trial = cq.borrow().trial_constraint_violation();
let phi_trial = cq.borrow().trial_barrier_obj();
if !theta_trial.is_finite() || !phi_trial.is_finite() {
evaluation_error = true;
if self.in_watchdog {
return AlphaResult::Failed {
n_steps: trial,
last_alpha: alpha,
evaluation_error: true,
};
}
alpha *= self.alpha_red_factor;
continue;
}
let force_accept =
self.accept_after_max_steps >= 0 && trial >= self.accept_after_max_steps;
let decision = if force_accept {
self.in_soft_resto_phase = false;
self.soft_resto_counter = 0;
self.acceptor.reset();
AcceptDecision::Accept
} else {
self.acceptor
.check_trial_point(alpha, theta, phi, d_phi, theta_trial, phi_trial)
};
if decision == AcceptDecision::Accept {
let mode = self
.acceptor
.update_for_next_iteration(alpha, theta, phi, d_phi, phi_trial);
if std::env::var_os("POUNCE_DBG_LS").is_some() {
let d = data.borrow();
tracing::debug!(target: "pounce::linesearch",
"[PN_LS] iter={} mu={:.3e} alpha={:.3e} alpha_d={:.3e} mode={} theta={:.6e} theta_trial={:.6e} phi={:.6e} phi_trial={:.6e} n_steps={}",
d.iter_count, d.curr_mu, alpha, alpha_dual, mode, theta, theta_trial, phi, phi_trial, trial
);
}
let mut d = data.borrow_mut();
d.info_alpha_primal = alpha;
d.info_alpha_dual = alpha_dual;
d.info_ls_count = trial + 1;
d.info_alpha_primal_char = mode;
return AlphaResult::Accepted { n_steps: trial };
}
if self.in_watchdog {
return AlphaResult::Failed {
n_steps: trial,
last_alpha: alpha,
evaluation_error,
};
}
if trial == 0
&& !skip_first
&& self.max_soc > 0
&& theta <= theta_trial
&& c_soc_buf.is_some()
&& dms_soc_buf.is_some()
{
let alpha_test = alpha;
let mut count_soc: i32 = 0;
let mut theta_soc_old: Number = 0.0;
let mut theta_trial_local = theta_trial;
let mut alpha_primal_soc = alpha;
let mut soc_accepted = false;
while count_soc < self.max_soc
&& !soc_accepted
&& (count_soc == 0 || theta_trial_local <= self.kappa_soc * theta_soc_old)
{
theta_soc_old = theta_trial_local;
{
let cq_ref = cq.borrow();
let trial_c = cq_ref.trial_c();
let trial_dms = cq_ref.trial_d_minus_s();
if let Some(c_soc) = c_soc_buf.as_mut() {
c_soc.scal(alpha_primal_soc);
c_soc.axpy(1.0, &*trial_c);
}
if let Some(dms_soc) = dms_soc_buf.as_mut() {
dms_soc.scal(alpha_primal_soc);
dms_soc.axpy(1.0, &*trial_dms);
}
}
let delta_soc_opt = {
let sd = soc_search_dir
.as_deref_mut()
.expect("SOC: search_dir is gated above");
let nlp_ref = nlp.expect("SOC: nlp is gated above");
let c_soc = c_soc_buf.as_deref().expect("SOC: c_soc_buf is gated above");
let dms_soc = dms_soc_buf
.as_deref()
.expect("SOC: dms_soc_buf is gated above");
sd.compute_soc_step(
data,
cq,
nlp_ref,
c_soc,
dms_soc,
alpha_primal_soc,
self.soc_method,
)
};
let Some(delta_soc) = delta_soc_opt else {
break;
};
let tau = data.borrow().curr_tau;
alpha_primal_soc = cq.borrow().aff_step_alpha_primal_max(&delta_soc, tau);
let alpha_dual_soc = cq.borrow().aff_step_alpha_dual_max(&delta_soc, tau);
let mut trial_iv = curr.deep_copy();
trial_iv.x.axpy(alpha_primal_soc, &*delta_soc.x);
trial_iv.s.axpy(alpha_primal_soc, &*delta_soc.s);
trial_iv.y_c.axpy(alpha_primal_soc, &*delta_soc.y_c);
trial_iv.y_d.axpy(alpha_primal_soc, &*delta_soc.y_d);
trial_iv.z_l.axpy(alpha_dual_soc, &*delta_soc.z_l);
trial_iv.z_u.axpy(alpha_dual_soc, &*delta_soc.z_u);
trial_iv.v_l.axpy(alpha_dual_soc, &*delta_soc.v_l);
trial_iv.v_u.axpy(alpha_dual_soc, &*delta_soc.v_u);
let trial_iv = trial_iv.freeze();
data.borrow_mut().set_trial(trial_iv);
let theta_soc = cq.borrow().trial_constraint_violation();
let phi_soc = cq.borrow().trial_barrier_obj();
if !theta_soc.is_finite() || !phi_soc.is_finite() {
break;
}
let dec = self
.acceptor
.check_trial_point(alpha_test, theta, phi, d_phi, theta_soc, phi_soc);
if dec == AcceptDecision::Accept {
let mode = self
.acceptor
.update_for_next_iteration(alpha_test, theta, phi, d_phi, phi_soc);
let mut d = data.borrow_mut();
d.info_alpha_primal = alpha_primal_soc;
d.info_alpha_dual = alpha_dual_soc;
d.info_ls_count = trial + 1;
d.info_alpha_primal_char = mode.to_ascii_uppercase();
return AlphaResult::Accepted { n_steps: trial };
}
count_soc += 1;
theta_trial_local = theta_soc;
soc_accepted = false;
}
}
alpha = if trial < ALPHA_INTERP_MIN_TRIALS {
alpha * self.alpha_red_factor
} else {
self.next_alpha(alpha, phi, d_phi, phi_trial)
};
}
AlphaResult::Failed {
n_steps,
last_alpha,
evaluation_error,
}
}
fn next_alpha(&self, alpha: Number, phi: Number, d_phi: Number, phi_trial: Number) -> Number {
let fixed = alpha * self.alpha_red_factor;
if !(d_phi < 0.0) || !phi_trial.is_finite() || !phi.is_finite() {
return fixed;
}
let denom = 2.0 * (phi_trial - phi - d_phi * alpha);
if !(denom > 0.0) {
return fixed;
}
let alpha_q = -d_phi * alpha * alpha / denom;
if !alpha_q.is_finite() {
return fixed;
}
let floor = (alpha * self.alpha_red_factor_min).min(fixed);
alpha_q.clamp(floor, fixed)
}
fn compute_d_phi(&self, cq: &IpoptCqHandle, delta: &IteratesVector) -> Number {
let cq_ref = cq.borrow();
let g_x = cq_ref.curr_grad_barrier_obj_x();
let g_s = cq_ref.curr_grad_barrier_obj_s();
g_x.dot(&*delta.x) + g_s.dot(&*delta.s)
}
}
fn scaled_step(
curr: &IteratesVector,
delta: &IteratesVector,
alpha_primal: Number,
alpha_y: Number,
alpha_dual: Number,
) -> IteratesVector {
let mut out = curr.make_new_zeroed();
out.add_one_vector(1.0, curr, 0.0); out.x.axpy(alpha_primal, &*delta.x);
out.s.axpy(alpha_primal, &*delta.s);
out.y_c.axpy(alpha_y, &*delta.y_c);
out.y_d.axpy(alpha_y, &*delta.y_d);
out.z_l.axpy(alpha_dual, &*delta.z_l);
out.z_u.axpy(alpha_dual, &*delta.z_u);
out.v_l.axpy(alpha_dual, &*delta.v_l);
out.v_u.axpy(alpha_dual, &*delta.v_u);
out.freeze()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ipopt_cq::IpoptCalculatedQuantities;
use crate::ipopt_data::IpoptData;
use crate::ipopt_nlp::Nlp;
use crate::iterates_vector::IteratesVector;
use crate::line_search::filter_acceptor::FilterLsAcceptor;
use pounce_common::types::Index;
use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
use pounce_linalg::expansion_matrix::{ExpansionMatrix, ExpansionMatrixSpace};
use pounce_linalg::{Matrix, SymMatrix, Vector};
use std::rc::Rc;
fn dense(n: i32, vals: &[Number]) -> Rc<dyn Vector> {
let mut v = DenseVectorSpace::new(n).make_new_dense();
v.set(0.0);
if !vals.is_empty() {
v.values_mut().copy_from_slice(vals);
}
Rc::new(v)
}
fn dvec(vals: &[Number]) -> DenseVector {
let mut v = DenseVectorSpace::new(vals.len() as Index).make_new_dense();
v.set(0.0);
if !vals.is_empty() {
v.values_mut().copy_from_slice(vals);
}
v
}
struct F4MockNlp {
x_l: DenseVector,
x_u: DenseVector,
d_l: DenseVector,
d_u: DenseVector,
px_l: Rc<dyn Matrix>,
px_u: Rc<dyn Matrix>,
pd_l: Rc<dyn Matrix>,
pd_u: Rc<dyn Matrix>,
}
impl F4MockNlp {
fn new() -> Self {
Self {
x_l: dvec(&[0.0]),
x_u: dvec(&[]),
d_l: dvec(&[]),
d_u: dvec(&[]),
px_l: Rc::new(ExpansionMatrix::new(ExpansionMatrixSpace::new(
1,
1,
&[0],
0,
))),
px_u: Rc::new(ExpansionMatrix::new(ExpansionMatrixSpace::new(
1,
0,
&[],
0,
))),
pd_l: Rc::new(ExpansionMatrix::new(ExpansionMatrixSpace::new(
0,
0,
&[],
0,
))),
pd_u: Rc::new(ExpansionMatrix::new(ExpansionMatrixSpace::new(
0,
0,
&[],
0,
))),
}
}
}
impl Nlp for F4MockNlp {
fn n(&self) -> Index {
1
}
fn m_eq(&self) -> Index {
0
}
fn m_ineq(&self) -> Index {
0
}
fn eval_f(&mut self, x: &dyn Vector) -> Number {
let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
xx.values()[0] * xx.values()[0]
}
fn eval_grad_f(&mut self, x: &dyn Vector, g: &mut dyn Vector) {
let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
let gg = g.as_any_mut().downcast_mut::<DenseVector>().unwrap();
gg.values_mut()[0] = 2.0 * xx.values()[0];
}
fn eval_c(&mut self, _x: &dyn Vector, _c: &mut dyn Vector) {}
fn eval_d(&mut self, _x: &dyn Vector, _d: &mut dyn Vector) {}
fn eval_jac_c(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
unimplemented!("no equality constraints in the F4 watchdog fixture")
}
fn eval_jac_d(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
unimplemented!("no inequality constraints in the F4 watchdog fixture")
}
fn eval_h(
&mut self,
_x: &dyn Vector,
_obj_factor: Number,
_y_c: &dyn Vector,
_y_d: &dyn Vector,
) -> Rc<dyn SymMatrix> {
unimplemented!("Hessian not exercised by the line search")
}
}
impl IpoptNlp for F4MockNlp {
fn x_l(&self) -> &dyn Vector {
&self.x_l
}
fn x_u(&self) -> &dyn Vector {
&self.x_u
}
fn d_l(&self) -> &dyn Vector {
&self.d_l
}
fn d_u(&self) -> &dyn Vector {
&self.d_u
}
fn px_l(&self) -> Rc<dyn Matrix> {
self.px_l.clone()
}
fn px_u(&self) -> Rc<dyn Matrix> {
self.px_u.clone()
}
fn pd_l(&self) -> Rc<dyn Matrix> {
self.pd_l.clone()
}
fn pd_u(&self) -> Rc<dyn Matrix> {
self.pd_u.clone()
}
}
struct RecordingAcceptor {
first_alpha: Rc<RefCell<Option<Number>>>,
}
impl BacktrackingLsAcceptor for RecordingAcceptor {
fn reset(&mut self) {}
fn check_trial_point(
&mut self,
alpha_primal: Number,
_theta: Number,
_phi: Number,
_d_phi: Number,
_theta_trial: Number,
_phi_trial: Number,
) -> AcceptDecision {
let mut slot = self.first_alpha.borrow_mut();
if slot.is_none() {
*slot = Some(alpha_primal);
}
AcceptDecision::Accept
}
}
fn empty() -> Rc<dyn Vector> {
dense(0, &[])
}
#[test]
fn stop_watchdog_retry_recomputes_ftb_cap_from_snapshot_direction() {
let nlp: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(F4MockNlp::new()));
let data: IpoptDataHandle = Rc::new(RefCell::new(IpoptData::new()));
let snap = IteratesVector::new(
dense(1, &[2.0]),
empty(),
empty(),
empty(),
dense(1, &[0.5]),
empty(),
empty(),
empty(),
);
{
let mut d = data.borrow_mut();
d.curr_mu = 0.1;
d.curr_tau = 1.0;
d.set_curr(snap.clone());
}
let cq: IpoptCqHandle = Rc::new(RefCell::new(IpoptCalculatedQuantities::new(
data.clone(),
nlp,
)));
let snap_delta = IteratesVector::new(
dense(1, &[-4.0]),
empty(),
empty(),
empty(),
dense(1, &[0.0]),
empty(),
empty(),
empty(),
);
let recorded = Rc::new(RefCell::new(None));
let mut bls = BacktrackingLineSearch::new(Box::new(RecordingAcceptor {
first_alpha: recorded.clone(),
}));
bls.in_watchdog = true;
bls.watchdog_iterate = Some(snap.clone());
bls.watchdog_delta = Some(snap_delta);
bls.watchdog_trial_iter = bls.watchdog_trial_iter_max;
let outcome = bls.handle_watchdog_failure(
&data, &cq, 1.0, None, 0, 1.0,
false,
);
assert_eq!(outcome, Outcome::Accepted);
let a = recorded
.borrow()
.expect("acceptor must have seen at least one trial");
assert!(
(a - 0.25).abs() < 1e-12,
"retry first alpha = {a}, expected 0.25 (snapshot FTB cap 0.5 × red 0.5)"
);
}
#[test]
fn deadline_short_circuits_the_alpha_loop() {
let nlp: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(F4MockNlp::new()));
let data: IpoptDataHandle = Rc::new(RefCell::new(IpoptData::new()));
let curr = IteratesVector::new(
dense(1, &[2.0]),
empty(),
empty(),
empty(),
dense(1, &[0.5]),
empty(),
empty(),
empty(),
);
{
let mut d = data.borrow_mut();
d.curr_mu = 0.1;
d.curr_tau = 1.0;
d.set_curr(curr.clone());
d.deadline = Some(pounce_common::timing::Deadline::new(0.0, 1e6));
}
let cq: IpoptCqHandle = Rc::new(RefCell::new(IpoptCalculatedQuantities::new(
data.clone(),
nlp.clone(),
)));
let delta = IteratesVector::new(
dense(1, &[-1.0]),
empty(),
empty(),
empty(),
dense(1, &[0.0]),
empty(),
empty(),
empty(),
);
let mut bls = BacktrackingLineSearch::new(Box::new(FilterLsAcceptor::new()));
let outcome = bls.find_acceptable_trial_point(
&data,
&cq,
&delta,
1.0,
1.0,
Some(&nlp),
None,
);
assert_eq!(outcome, Outcome::Deadline);
assert!(data.borrow().trial.is_none());
}
fn iv_from(x: &[Number], s: &[Number]) -> IteratesVector {
IteratesVector::new(
dense(x.len() as i32, x),
dense(s.len() as i32, s),
dense(0, &[]),
dense(0, &[]),
dense(0, &[]),
dense(0, &[]),
dense(0, &[]),
dense(0, &[]),
)
}
#[test]
fn driver_constructs_with_defaults() {
let bls = BacktrackingLineSearch::new(Box::new(FilterLsAcceptor::new()));
assert_eq!(bls.alpha_red_factor, 0.5);
assert_eq!(bls.alpha_red_factor_min, 0.05);
assert_eq!(bls.max_soc, 4);
}
#[test]
fn scaled_step_writes_curr_plus_alpha_delta() {
let curr = iv_from(&[0.0, 0.0], &[0.0]);
let delta = iv_from(&[1.0, 1.0], &[2.0]);
let trial = scaled_step(&curr, &delta, 0.5, 0.5, 0.5);
let xv = trial
.x
.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
.unwrap()
.values()
.to_vec();
assert_eq!(xv, vec![0.5, 0.5]);
let sv = trial
.s
.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
.unwrap()
.values()
.to_vec();
assert_eq!(sv, vec![1.0]); }
#[test]
fn outcome_variants_are_distinct() {
assert_ne!(Outcome::Accepted, Outcome::Failed);
assert_ne!(Outcome::Accepted, Outcome::TinyStep);
assert_ne!(Outcome::Failed, Outcome::TinyStep);
}
#[test]
fn watchdog_state_starts_inactive() {
let bls = BacktrackingLineSearch::new(Box::new(FilterLsAcceptor::new()));
assert!(!bls.in_watchdog());
assert_eq!(bls.watchdog_shortened_iter(), 0);
assert!(bls.last_mu < 0.0);
assert_eq!(bls.watchdog_shortened_iter_trigger, 10);
assert_eq!(bls.watchdog_trial_iter_max, 3);
}
#[test]
fn restoration_resets_the_shortened_iter_counter() {
let mut bls = BacktrackingLineSearch::new(Box::new(FilterLsAcceptor::new()));
bls.watchdog_shortened_iter = 5;
bls.in_soft_resto_phase = true;
bls.soft_resto_counter = 4;
bls.reset_after_restoration();
assert_eq!(bls.watchdog_shortened_iter, 0);
assert!(!bls.in_soft_resto_phase);
assert_eq!(bls.soft_resto_counter, 0);
bls.watchdog_shortened_iter += 5;
assert!(bls.watchdog_shortened_iter < bls.watchdog_shortened_iter_trigger);
}
#[test]
fn alpha_result_failed_carries_n_steps_and_last_alpha() {
let r = AlphaResult::Failed {
n_steps: 7,
last_alpha: 1e-6,
evaluation_error: false,
};
match r {
AlphaResult::Failed {
n_steps,
last_alpha,
evaluation_error,
} => {
assert_eq!(n_steps, 7);
assert!((last_alpha - 1e-6).abs() < 1e-20);
assert!(!evaluation_error);
}
_ => unreachable!(),
}
}
fn ls_for_next_alpha(red: Number, red_min: Number) -> BacktrackingLineSearch {
let mut bls = BacktrackingLineSearch::new(Box::new(FilterLsAcceptor::default()));
bls.alpha_red_factor = red;
bls.alpha_red_factor_min = red_min;
bls
}
#[test]
fn next_alpha_jumps_to_the_interpolated_minimizer() {
let bls = ls_for_next_alpha(0.5, 1e-4);
let a = bls.next_alpha(1.0, 1.0, -1.0, 50.0);
assert!((a - 0.01).abs() < 1e-12, "got {a}");
}
#[test]
fn next_alpha_is_never_slower_than_the_fixed_factor() {
let bls = ls_for_next_alpha(0.5, 1e-4);
let a = bls.next_alpha(1.0, 1.0, -1.0, 0.001);
assert_eq!(a, 0.5, "must clamp up to alpha_red_factor * alpha");
}
#[test]
fn next_alpha_is_floored_by_alpha_red_factor_min() {
let bls = ls_for_next_alpha(0.5, 0.05);
let a = bls.next_alpha(1.0, 1.0, -1.0, 5e5);
assert!((a - 0.05).abs() < 1e-15, "got {a}");
}
#[test]
fn next_alpha_degenerates_to_the_fixed_factor_when_the_clamp_is_closed() {
let bls = ls_for_next_alpha(0.5, 0.5);
for phi_trial in [0.001, 2.0, 50.0, 5e5] {
assert_eq!(bls.next_alpha(1.0, 1.0, -1.0, phi_trial), 0.5);
}
}
#[test]
fn next_alpha_survives_an_inverted_safeguard_pair() {
let bls = ls_for_next_alpha(0.01, 0.05);
for phi_trial in [0.001, 2.0, 50.0, 5e5] {
let a = bls.next_alpha(1.0, 1.0, -1.0, phi_trial);
assert_eq!(a, 0.01, "inverted pair must fall back to the cap");
}
let bls = ls_for_next_alpha(0.5, 0.05);
assert!((bls.next_alpha(1.0, 1.0, -1.0, 5e5) - 0.05).abs() < 1e-15);
}
#[test]
fn next_alpha_falls_back_on_every_undefined_fit() {
let bls = ls_for_next_alpha(0.5, 0.05);
assert_eq!(bls.next_alpha(1.0, 1.0, 0.0, 2.0), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, 1.0, 2.0), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, Number::NAN, 2.0), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, -1.0, Number::INFINITY), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, -1.0, Number::NAN), 0.5);
assert_eq!(bls.next_alpha(1.0, Number::NAN, -1.0, 2.0), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, -1.0, 0.0), 0.5);
assert_eq!(bls.next_alpha(1.0, 1.0, -1.0, -5.0), 0.5);
}
}