1use crate::ipopt_cq::IpoptCqHandle;
24use crate::ipopt_data::IpoptDataHandle;
25use crate::ipopt_nlp::IpoptNlp;
26use crate::iterates_vector::IteratesVector;
27use crate::kkt::pd_search_dir_calc::PdSearchDirCalc;
28use crate::mu::oracle::r#trait::MuOracle;
29use pounce_common::types::Number;
30use pounce_linalg::Vector;
31use std::cell::RefCell;
32use std::rc::Rc;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum NormType {
36 OneNorm,
37 TwoNormSquared,
40 TwoNorm,
41 MaxNorm,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum CentralityType {
46 None,
47 LogCenter,
48 ReciprocalCenter,
49 CubedReciprocalCenter,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum BalancingTermType {
54 None,
55 CubicTerm,
56}
57
58pub struct QualityFunctionMuOracle {
59 pub norm_type: NormType,
60 pub centrality_type: CentralityType,
61 pub balancing_term: BalancingTermType,
62 pub max_section_steps: i32,
63 pub section_sigma_tol: Number,
64 pub section_qf_tol: Number,
65 pub sigma_max: Number,
66 pub sigma_min: Number,
67 pub mu_min: Number,
68 pub mu_max: Number,
69}
70
71impl Default for QualityFunctionMuOracle {
72 fn default() -> Self {
73 Self {
75 norm_type: NormType::TwoNormSquared,
76 centrality_type: CentralityType::None,
77 balancing_term: BalancingTermType::None,
78 max_section_steps: 8,
79 section_sigma_tol: 1e-2,
80 section_qf_tol: 0.0,
81 sigma_max: 100.0,
82 sigma_min: 1e-6,
91 mu_min: 1e-11,
92 mu_max: 1e5,
93 }
94 }
95}
96
97impl QualityFunctionMuOracle {
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 #[allow(clippy::too_many_lines)]
113 pub fn calculate_mu_with_predictor_centering(
114 &mut self,
115 data: &IpoptDataHandle,
116 cq: &IpoptCqHandle,
117 nlp: &Rc<RefCell<dyn IpoptNlp>>,
118 pd_search_dir: &mut PdSearchDirCalc,
119 ) -> Option<Number> {
120 if !pd_search_dir.compute_affine_step(data, cq, nlp) {
121 return None;
122 }
123 if !pd_search_dir.compute_centering_step(data, cq, nlp) {
124 return None;
125 }
126
127 let delta_aff: IteratesVector = data.borrow().delta_aff.clone()?;
128 let delta_cen: IteratesVector = data.borrow().delta_cen.clone()?;
129
130 let nlp_ref = nlp.borrow();
134 let cq_ref = cq.borrow();
135 let curr_iv = cq_ref.curr_iv();
136 let curr_slack_x_l = cq_ref.curr_slack_x_l();
137 let curr_slack_x_u = cq_ref.curr_slack_x_u();
138 let curr_slack_s_l = cq_ref.curr_slack_s_l();
139 let curr_slack_s_u = cq_ref.curr_slack_s_u();
140 let avrg_compl = cq_ref.curr_avrg_compl();
141
142 let project = |sign_l_x: Number,
143 sign_u_x: Number,
144 step_x: &dyn Vector,
145 step_s: &dyn Vector|
146 -> [Rc<dyn Vector>; 4] {
147 let mut x_l = curr_slack_x_l.make_new();
148 nlp_ref
149 .px_l()
150 .trans_mult_vector(sign_l_x, step_x, 0.0, &mut *x_l);
151 let mut x_u = curr_slack_x_u.make_new();
152 nlp_ref
153 .px_u()
154 .trans_mult_vector(sign_u_x, step_x, 0.0, &mut *x_u);
155 let mut s_l = curr_slack_s_l.make_new();
156 nlp_ref
157 .pd_l()
158 .trans_mult_vector(sign_l_x, step_s, 0.0, &mut *s_l);
159 let mut s_u = curr_slack_s_u.make_new();
160 nlp_ref
161 .pd_u()
162 .trans_mult_vector(sign_u_x, step_s, 0.0, &mut *s_u);
163 [Rc::from(x_l), Rc::from(x_u), Rc::from(s_l), Rc::from(s_u)]
164 };
165
166 let [step_aff_x_l, step_aff_x_u, step_aff_s_l, step_aff_s_u] =
167 project(1.0, -1.0, &*delta_aff.x, &*delta_aff.s);
168 let [step_cen_x_l, step_cen_x_u, step_cen_s_l, step_cen_s_u] =
169 project(1.0, -1.0, &*delta_cen.x, &*delta_cen.s);
170
171 let step_aff_z_l = delta_aff.z_l.clone();
174 let step_aff_z_u = delta_aff.z_u.clone();
175 let step_aff_v_l = delta_aff.v_l.clone();
176 let step_aff_v_u = delta_aff.v_u.clone();
177 let step_cen_z_l = delta_cen.z_l.clone();
178 let step_cen_z_u = delta_cen.z_u.clone();
179 let step_cen_v_l = delta_cen.v_l.clone();
180 let step_cen_v_u = delta_cen.v_u.clone();
181
182 drop(nlp_ref);
186
187 let grad_lag_x = cq_ref.curr_grad_lag_x();
191 let grad_lag_s = cq_ref.curr_grad_lag_s();
192 let c = cq_ref.curr_c();
193 let d_minus_s = cq_ref.curr_d_minus_s();
194 let dual_aggr = match self.norm_type {
195 NormType::OneNorm => grad_lag_x.asum() + grad_lag_s.asum(),
196 NormType::TwoNormSquared => {
197 let nx = grad_lag_x.nrm2();
198 let ns = grad_lag_s.nrm2();
199 nx * nx + ns * ns
200 }
201 NormType::TwoNorm => {
202 let nx = grad_lag_x.nrm2();
203 let ns = grad_lag_s.nrm2();
204 (nx * nx + ns * ns).sqrt()
205 }
206 NormType::MaxNorm => grad_lag_x.amax().max(grad_lag_s.amax()),
207 };
208 let primal_aggr = match self.norm_type {
209 NormType::OneNorm => c.asum() + d_minus_s.asum(),
210 NormType::TwoNormSquared => {
211 let nc = c.nrm2();
212 let nd = d_minus_s.nrm2();
213 nc * nc + nd * nd
214 }
215 NormType::TwoNorm => {
216 let nc = c.nrm2();
217 let nd = d_minus_s.nrm2();
218 (nc * nc + nd * nd).sqrt()
219 }
220 NormType::MaxNorm => c.amax().max(d_minus_s.amax()),
221 };
222
223 let n_dual = curr_iv.x.dim() + curr_iv.s.dim();
224 let n_pri = curr_iv.y_c.dim() + curr_iv.y_d.dim();
225 let n_comp = curr_iv.z_l.dim() + curr_iv.z_u.dim() + curr_iv.v_l.dim() + curr_iv.v_u.dim();
226 let tau = data.borrow().curr_tau;
227
228 let curr_z_l = curr_iv.z_l.clone();
229 let curr_z_u = curr_iv.z_u.clone();
230 let curr_v_l = curr_iv.v_l.clone();
231 let curr_v_u = curr_iv.v_u.clone();
232
233 drop(cq_ref);
234
235 let norm_type = self.norm_type;
236 let centrality = self.centrality_type;
237 let balancing = self.balancing_term;
238
239 let mut stp_x_l = step_aff_x_l.make_new();
260 let mut stp_x_u = step_aff_x_u.make_new();
261 let mut stp_s_l = step_aff_s_l.make_new();
262 let mut stp_s_u = step_aff_s_u.make_new();
263 let mut stp_z_l = step_aff_z_l.make_new();
264 let mut stp_z_u = step_aff_z_u.make_new();
265 let mut stp_v_l = step_aff_v_l.make_new();
266 let mut stp_v_u = step_aff_v_u.make_new();
267 let mut trial_s_x_l = curr_slack_x_l.make_new();
268 let mut trial_s_x_u = curr_slack_x_u.make_new();
269 let mut trial_s_s_l = curr_slack_s_l.make_new();
270 let mut trial_s_s_u = curr_slack_s_u.make_new();
271 let mut trial_z_l = curr_z_l.make_new();
272 let mut trial_z_u = curr_z_u.make_new();
273 let mut trial_v_l = curr_v_l.make_new();
274 let mut trial_v_u = curr_v_u.make_new();
275 for b in [
276 &mut stp_x_l,
277 &mut stp_x_u,
278 &mut stp_s_l,
279 &mut stp_s_u,
280 &mut stp_z_l,
281 &mut stp_z_u,
282 &mut stp_v_l,
283 &mut stp_v_u,
284 &mut trial_s_x_l,
285 &mut trial_s_x_u,
286 &mut trial_s_s_l,
287 &mut trial_s_s_u,
288 &mut trial_z_l,
289 &mut trial_z_u,
290 &mut trial_v_l,
291 &mut trial_v_u,
292 ] {
293 b.set(0.0);
294 }
295
296 let dbg_aggr = std::env::var_os("POUNCE_DBG_QF_AGGR").is_some();
299
300 let mut eval_q = |sigma: Number| -> Number {
301 stp_x_l.add_two_vectors(1.0, &*step_aff_x_l, sigma, &*step_cen_x_l, 0.0);
303 stp_x_u.add_two_vectors(1.0, &*step_aff_x_u, sigma, &*step_cen_x_u, 0.0);
304 stp_s_l.add_two_vectors(1.0, &*step_aff_s_l, sigma, &*step_cen_s_l, 0.0);
305 stp_s_u.add_two_vectors(1.0, &*step_aff_s_u, sigma, &*step_cen_s_u, 0.0);
306 stp_z_l.add_two_vectors(1.0, &*step_aff_z_l, sigma, &*step_cen_z_l, 0.0);
307 stp_z_u.add_two_vectors(1.0, &*step_aff_z_u, sigma, &*step_cen_z_u, 0.0);
308 stp_v_l.add_two_vectors(1.0, &*step_aff_v_l, sigma, &*step_cen_v_l, 0.0);
309 stp_v_u.add_two_vectors(1.0, &*step_aff_v_u, sigma, &*step_cen_v_u, 0.0);
310
311 let alpha_pri = curr_slack_x_l
313 .frac_to_bound(&*stp_x_l, tau)
314 .min(curr_slack_x_u.frac_to_bound(&*stp_x_u, tau))
315 .min(curr_slack_s_l.frac_to_bound(&*stp_s_l, tau))
316 .min(curr_slack_s_u.frac_to_bound(&*stp_s_u, tau));
317 let alpha_du = curr_z_l
318 .frac_to_bound(&*stp_z_l, tau)
319 .min(curr_z_u.frac_to_bound(&*stp_z_u, tau))
320 .min(curr_v_l.frac_to_bound(&*stp_v_l, tau))
321 .min(curr_v_u.frac_to_bound(&*stp_v_u, tau));
322
323 trial_s_x_l.add_two_vectors(1.0, &*curr_slack_x_l, alpha_pri, &*stp_x_l, 0.0);
325 trial_s_x_u.add_two_vectors(1.0, &*curr_slack_x_u, alpha_pri, &*stp_x_u, 0.0);
326 trial_s_s_l.add_two_vectors(1.0, &*curr_slack_s_l, alpha_pri, &*stp_s_l, 0.0);
327 trial_s_s_u.add_two_vectors(1.0, &*curr_slack_s_u, alpha_pri, &*stp_s_u, 0.0);
328
329 trial_z_l.add_two_vectors(1.0, &*curr_z_l, alpha_du, &*stp_z_l, 0.0);
330 trial_z_u.add_two_vectors(1.0, &*curr_z_u, alpha_du, &*stp_z_u, 0.0);
331 trial_v_l.add_two_vectors(1.0, &*curr_v_l, alpha_du, &*stp_v_l, 0.0);
332 trial_v_u.add_two_vectors(1.0, &*curr_v_u, alpha_du, &*stp_v_u, 0.0);
333
334 trial_s_x_l.element_wise_multiply(&*trial_z_l);
336 trial_s_x_u.element_wise_multiply(&*trial_z_u);
337 trial_s_s_l.element_wise_multiply(&*trial_v_l);
338 trial_s_s_u.element_wise_multiply(&*trial_v_u);
339
340 let compl_aggr = match norm_type {
341 NormType::OneNorm => {
342 trial_s_x_l.asum()
343 + trial_s_x_u.asum()
344 + trial_s_s_l.asum()
345 + trial_s_s_u.asum()
346 }
347 NormType::TwoNormSquared => {
348 let a = trial_s_x_l.nrm2();
349 let b = trial_s_x_u.nrm2();
350 let c = trial_s_s_l.nrm2();
351 let d = trial_s_s_u.nrm2();
352 a * a + b * b + c * c + d * d
353 }
354 NormType::TwoNorm => {
355 let a = trial_s_x_l.nrm2();
356 let b = trial_s_x_u.nrm2();
357 let c = trial_s_s_l.nrm2();
358 let d = trial_s_s_u.nrm2();
359 (a * a + b * b + c * c + d * d).sqrt()
360 }
361 NormType::MaxNorm => trial_s_x_l
362 .amax()
363 .max(trial_s_x_u.amax())
364 .max(trial_s_s_l.amax())
365 .max(trial_s_s_u.amax()),
366 };
367
368 let xi = if matches!(centrality, CentralityType::None) {
369 1.0
370 } else {
371 let total = trial_s_x_l.asum()
375 + trial_s_x_u.asum()
376 + trial_s_s_l.asum()
377 + trial_s_s_u.asum();
378 let avg = if n_comp > 0 {
379 total / n_comp as Number
380 } else {
381 1.0
382 };
383 let mn = trial_s_x_l
384 .min()
385 .min(trial_s_x_u.min())
386 .min(trial_s_s_l.min())
387 .min(trial_s_s_u.min());
388 if avg > 0.0 { mn / avg } else { 1.0 }
389 };
390
391 let aggr = QualityFunctionAggregates {
392 dual_aggr,
393 primal_aggr,
394 compl_aggr,
395 n_dual,
396 n_pri,
397 n_comp,
398 };
399
400 if dbg_aggr {
401 tracing::debug!(target: "pounce::mu",
402 "[QF_AGGR] σ={:.6e} α_pri={:.6e} α_du={:.6e} xi={:.6e} dual_aggr={:.6e} primal_aggr={:.6e} compl_aggr={:.6e} n_dual={} n_pri={} n_comp={}",
403 sigma, alpha_pri, alpha_du, xi,
404 dual_aggr, primal_aggr, compl_aggr,
405 n_dual, n_pri, n_comp
406 );
407 }
408
409 evaluate_quality_function(
410 norm_type, centrality, balancing, alpha_pri, alpha_du, xi, aggr,
411 )
412 };
413
414 if let Ok(s) = std::env::var("POUNCE_DBG_QF_SWEEP") {
418 if let Ok(target_iter) = s.parse::<i32>() {
419 if data.borrow().iter_count == target_iter {
420 let lo = self.sigma_min.max(self.mu_min / avrg_compl);
421 let hi = self.sigma_max.min(self.mu_max / avrg_compl).max(lo * 10.0);
422 let log_lo = lo.ln();
423 let log_hi = hi.ln();
424 tracing::debug!(target: "pounce::mu", "[QF_SWEEP] iter={} avrg_compl={:.6e} σ_range=[{:.3e},{:.3e}] sigma_min={:.3e} sigma_max={:.3e} mu_min={:.3e} mu_max={:.3e}",
425 target_iter, avrg_compl, lo, hi,
426 self.sigma_min, self.sigma_max, self.mu_min, self.mu_max);
427 let n = 21;
428 for i in 0..n {
429 let frac = i as f64 / (n - 1) as f64;
430 let sig = (log_lo + frac * (log_hi - log_lo)).exp();
431 let q = eval_q(sig);
432 tracing::debug!(target: "pounce::mu", "[QF_SWEEP] σ={:.6e} q={:.10e}", sig, q);
433 }
434 let q1 = eval_q(1.0);
435 let s1m = 1.0 - self.section_sigma_tol.max(1e-4);
436 let q1m = eval_q(s1m);
437 tracing::debug!(target: "pounce::mu",
438 "[QF_SWEEP] σ=1.0 q={:.10e} σ={:.6e} q={:.10e} (q_1minus>q_1: {})",
439 q1,
440 s1m,
441 q1m,
442 q1m > q1
443 );
444 }
445 }
446 }
447
448 let sigma = pick_sigma(
449 self.sigma_min,
450 self.sigma_max,
451 self.mu_min,
452 self.mu_max,
453 avrg_compl,
454 self.section_sigma_tol,
455 self.section_qf_tol,
456 self.max_section_steps,
457 &mut eval_q,
458 );
459
460 let mu_new = sigma * avrg_compl;
461 let mu_clamped = mu_new.clamp(self.mu_min, self.mu_max);
462 if std::env::var("POUNCE_DBG_QF").is_ok() {
463 let iter_count = data.borrow().iter_count;
464 let curr_mu = data.borrow().curr_mu;
465 let sigma_floor = self.sigma_min.max(self.mu_min / avrg_compl);
466 let sigma_up_dn = sigma_floor
467 .max(1.0 - self.section_sigma_tol.max(1e-4))
468 .min(self.mu_max / avrg_compl);
469 tracing::debug!(target: "pounce::mu",
470 "[QF] iter={} curr_mu={:.3e} avrg_compl={:.3e} sigma={:.3e} mu_new={:.3e} mu_clamped={:.3e} | sigma_min={:.3e} mu_min={:.3e} sigma_lo_dn={:.3e} sigma_up_dn={:.3e} mu_min/avrg={:.3e}",
471 iter_count, curr_mu, avrg_compl, sigma, mu_new, mu_clamped,
472 self.sigma_min, self.mu_min, sigma_floor, sigma_up_dn,
473 self.mu_min / avrg_compl,
474 );
475 }
476 Some(mu_clamped)
477 }
478}
479
480impl MuOracle for QualityFunctionMuOracle {
481 fn calculate_mu(&mut self) -> Option<Number> {
482 None
487 }
488}
489
490pub fn golden_section(
504 sigma_lo_in: Number,
505 sigma_up_in: Number,
506 q_lo_in: Number,
507 q_up_in: Number,
508 sigma_tol: Number,
509 qf_tol: Number,
510 max_steps: i32,
511 mut q: impl FnMut(Number) -> Number,
512) -> Number {
513 let mut sigma_lo = sigma_lo_in;
514 let mut sigma_up = sigma_up_in;
515 let mut q_lo = q_lo_in;
516 let mut q_up = q_up_in;
517
518 let gfac = (3.0 - 5.0_f64.sqrt()) / 2.0;
519 let mut sigma_mid1 = sigma_lo + gfac * (sigma_up - sigma_lo);
520 let mut sigma_mid2 = sigma_lo + (1.0 - gfac) * (sigma_up - sigma_lo);
521 let mut qmid1 = q(sigma_mid1);
522 let mut qmid2 = q(sigma_mid2);
523
524 let mut nsections = 0;
525 let mut width_ok;
526 let mut qf_ok;
527 loop {
528 width_ok = (sigma_up - sigma_lo) >= sigma_tol * sigma_up;
529 let qmin = q_lo.min(q_up).min(qmid1).min(qmid2);
530 let qmax = q_lo.max(q_up).max(qmid1).max(qmid2);
531 qf_ok = qmax > 0.0 && (1.0 - qmin / qmax) >= qf_tol;
532 if !(width_ok && qf_ok && nsections < max_steps) {
533 break;
534 }
535 nsections += 1;
536 if qmid1 > qmid2 {
537 sigma_lo = sigma_mid1;
538 q_lo = qmid1;
539 sigma_mid1 = sigma_mid2;
540 qmid1 = qmid2;
541 sigma_mid2 = sigma_lo + (1.0 - gfac) * (sigma_up - sigma_lo);
542 qmid2 = q(sigma_mid2);
543 } else {
544 sigma_up = sigma_mid2;
545 q_up = qmid2;
546 sigma_mid2 = sigma_mid1;
547 qmid2 = qmid1;
548 sigma_mid1 = sigma_lo + gfac * (sigma_up - sigma_lo);
549 qmid1 = q(sigma_mid1);
550 }
551 }
552
553 if width_ok && !qf_ok {
577 if sigma_lo == sigma_lo_in && q_lo < 0.0 {
585 q_lo = q(sigma_lo);
586 }
587 if sigma_up == sigma_up_in && q_up < 0.0 {
588 q_up = q(sigma_up);
589 }
590 let mut best_s = sigma_lo;
591 let mut best_q = q_lo;
592 if q_up < best_q {
593 best_s = sigma_up;
594 best_q = q_up;
595 }
596 if qmid1 < best_q {
597 best_s = sigma_mid1;
598 best_q = qmid1;
599 }
600 if qmid2 < best_q {
601 best_s = sigma_mid2;
602 }
603 return best_s;
604 }
605 let (mut sigma, mut qval) = if qmid1 < qmid2 {
606 (sigma_mid1, qmid1)
607 } else {
608 (sigma_mid2, qmid2)
609 };
610 if sigma_up == sigma_up_in {
611 let qtmp = if q_up < 0.0 { q(sigma_up) } else { q_up };
612 if qtmp < qval {
613 sigma = sigma_up;
614 qval = qtmp;
615 }
616 } else if sigma_lo == sigma_lo_in {
617 let qtmp = if q_lo < 0.0 { q(sigma_lo) } else { q_lo };
618 if qtmp < qval {
619 sigma = sigma_lo;
620 }
621 }
622 let _ = qval;
623 sigma
624}
625
626#[derive(Debug, Clone, Copy)]
641pub struct QualityFunctionAggregates {
642 pub dual_aggr: Number,
643 pub primal_aggr: Number,
644 pub compl_aggr: Number,
645 pub n_dual: i32,
646 pub n_pri: i32,
647 pub n_comp: i32,
648}
649
650pub fn evaluate_quality_function(
659 norm: NormType,
660 centrality: CentralityType,
661 balancing: BalancingTermType,
662 alpha_primal: Number,
663 alpha_dual: Number,
664 xi: Number,
665 aggr: QualityFunctionAggregates,
666) -> Number {
667 let (mut dual_inf, mut primal_inf, mut compl_inf) = match norm {
668 NormType::OneNorm => {
669 let mut d = (1.0 - alpha_dual) * aggr.dual_aggr;
670 let mut p = (1.0 - alpha_primal) * aggr.primal_aggr;
671 let mut c = aggr.compl_aggr;
672 d /= aggr.n_dual as Number;
673 if aggr.n_pri > 0 {
674 p /= aggr.n_pri as Number;
675 }
676 debug_assert!(aggr.n_comp > 0);
677 c /= aggr.n_comp as Number;
678 (d, p, c)
679 }
680 NormType::TwoNormSquared => {
681 let mut d = (1.0 - alpha_dual).powi(2) * aggr.dual_aggr;
685 let mut p = (1.0 - alpha_primal).powi(2) * aggr.primal_aggr;
686 let mut c = aggr.compl_aggr;
687 d /= aggr.n_dual as Number;
688 if aggr.n_pri > 0 {
689 p /= aggr.n_pri as Number;
690 }
691 debug_assert!(aggr.n_comp > 0);
692 c /= aggr.n_comp as Number;
693 (d, p, c)
694 }
695 NormType::MaxNorm => (
696 (1.0 - alpha_dual) * aggr.dual_aggr,
697 (1.0 - alpha_primal) * aggr.primal_aggr,
698 aggr.compl_aggr,
699 ),
700 NormType::TwoNorm => {
701 let mut d = (1.0 - alpha_dual) * aggr.dual_aggr;
702 let mut p = (1.0 - alpha_primal) * aggr.primal_aggr;
703 let mut c = aggr.compl_aggr;
704 d /= (aggr.n_dual as Number).sqrt();
705 if aggr.n_pri > 0 {
706 p /= (aggr.n_pri as Number).sqrt();
707 }
708 debug_assert!(aggr.n_comp > 0);
709 c /= (aggr.n_comp as Number).sqrt();
710 (d, p, c)
711 }
712 };
713
714 if dual_inf.is_nan() {
716 dual_inf = 0.0;
717 }
718 if primal_inf.is_nan() {
719 primal_inf = 0.0;
720 }
721 if compl_inf.is_nan() {
722 compl_inf = 0.0;
723 }
724
725 let mut q = dual_inf + primal_inf + compl_inf;
726
727 match centrality {
728 CentralityType::None => {}
729 CentralityType::LogCenter => q -= compl_inf * xi.ln(),
730 CentralityType::ReciprocalCenter => q += compl_inf / xi,
731 CentralityType::CubedReciprocalCenter => q += compl_inf / xi.powi(3),
732 }
733
734 match balancing {
735 BalancingTermType::None => {}
736 BalancingTermType::CubicTerm => {
737 let dom = dual_inf.max(primal_inf) - compl_inf;
738 q += dom.max(0.0).powi(3);
739 }
740 }
741
742 q
743}
744
745#[allow(clippy::too_many_arguments)]
756pub fn pick_sigma(
757 sigma_min: Number,
758 sigma_max: Number,
759 mu_min: Number,
760 mu_max: Number,
761 avrg_compl: Number,
762 sigma_tol: Number,
763 qf_tol: Number,
764 max_steps: i32,
765 mut q: impl FnMut(Number) -> Number,
766) -> Number {
767 let qf_1 = q(1.0);
768 let sigma_1minus = 1.0 - sigma_tol.max(1e-4);
769 let qf_1minus = q(sigma_1minus);
770
771 if qf_1minus > qf_1 {
772 let sigma_up = sigma_max.min(mu_max / avrg_compl);
774 let sigma_lo = 1.0;
775 if sigma_lo >= sigma_up {
776 sigma_up
777 } else {
778 golden_section(
779 sigma_lo, sigma_up, qf_1, -100.0, sigma_tol, qf_tol, max_steps, q,
780 )
781 }
782 } else {
783 let sigma_lo = sigma_min.max(mu_min / avrg_compl);
785 let sigma_up = sigma_lo.max(sigma_1minus).min(mu_max / avrg_compl);
786 if sigma_lo >= sigma_up {
787 sigma_lo
788 } else {
789 golden_section(
790 sigma_lo, sigma_up, -100.0, qf_1minus, sigma_tol, qf_tol, max_steps, q,
791 )
792 }
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn golden_section_minimizes_parabola() {
802 let f = |s: f64| (s - 0.3).powi(2);
804 let s = golden_section(0.0, 1.0, f(0.0), f(1.0), 1e-6, 0.0, 50, f);
805 assert!((s - 0.3).abs() < 1e-3);
806 }
807
808 #[test]
809 fn golden_section_respects_max_steps() {
810 let f = |s: f64| (s - 0.5).powi(2);
812 let s = golden_section(0.0, 1.0, f(0.0), f(1.0), 1e-12, 0.0, 5, f);
813 assert!((s - 0.5).abs() < 0.2);
814 }
815
816 #[test]
817 fn golden_section_handles_monotone() {
818 let f = |s: f64| s;
820 let s = golden_section(0.1, 2.0, 0.1, 2.0, 1e-6, 0.0, 50, f);
821 assert!(s < 0.2, "got s = {}", s);
822 }
823
824 #[test]
825 fn golden_section_never_returns_unevaluated_sentinel() {
826 let sigma_lo = 1.0_f64;
837 let sigma_up = 3.0_f64;
838 let q = move |s: f64| if s == sigma_up { 50.0 } else { -s };
841 let s = golden_section(sigma_lo, sigma_up, q(sigma_lo), -100.0, 1e-3, 0.0, 50, q);
843 assert!(
844 s < sigma_up,
845 "golden_section returned the unevaluated sentinel endpoint σ = {} \
846 (true q there = {}, the bracket maximum); it must re-evaluate the \
847 sentinel before selecting a minimum",
848 s,
849 q(s)
850 );
851 }
852
853 #[test]
854 fn calculate_mu_returns_none_until_plumbed() {
855 let mut o = QualityFunctionMuOracle::new();
856 assert!(o.calculate_mu().is_none());
857 }
858
859 fn aggr(
860 d: Number,
861 p: Number,
862 c: Number,
863 nd: i32,
864 np: i32,
865 nc: i32,
866 ) -> QualityFunctionAggregates {
867 QualityFunctionAggregates {
868 dual_aggr: d,
869 primal_aggr: p,
870 compl_aggr: c,
871 n_dual: nd,
872 n_pri: np,
873 n_comp: nc,
874 }
875 }
876
877 #[test]
878 fn evaluate_one_norm_averages_by_n() {
879 let q = evaluate_quality_function(
881 NormType::OneNorm,
882 CentralityType::None,
883 BalancingTermType::None,
884 0.5, 0.25, 1.0,
887 aggr(8.0, 4.0, 6.0, 4, 2, 3),
888 );
889 assert!((q - 4.5).abs() < 1e-12, "got {}", q);
891 }
892
893 #[test]
894 fn evaluate_max_norm_does_not_divide() {
895 let q = evaluate_quality_function(
896 NormType::MaxNorm,
897 CentralityType::None,
898 BalancingTermType::None,
899 0.0,
900 0.0,
901 1.0,
902 aggr(2.0, 3.0, 5.0, 10, 10, 10),
903 );
904 assert!((q - 10.0).abs() < 1e-12);
905 }
906
907 #[test]
908 fn evaluate_two_norm_divides_by_sqrt_n() {
909 let q = evaluate_quality_function(
910 NormType::TwoNorm,
911 CentralityType::None,
912 BalancingTermType::None,
913 0.0,
914 0.0,
915 1.0,
916 aggr(2.0, 0.0, 4.0, 4, 0, 16),
917 );
918 assert!((q - 2.0).abs() < 1e-12, "got {}", q);
920 }
921
922 #[test]
923 fn evaluate_one_norm_handles_zero_pri_dim() {
924 let q = evaluate_quality_function(
926 NormType::OneNorm,
927 CentralityType::None,
928 BalancingTermType::None,
929 0.0,
930 0.0,
931 1.0,
932 aggr(0.0, 0.0, 1.0, 1, 0, 1),
933 );
934 assert!(q.is_finite() && (q - 1.0).abs() < 1e-12);
935 }
936
937 #[test]
938 fn evaluate_log_centrality_subtracts_compl_log_xi() {
939 let base = evaluate_quality_function(
940 NormType::MaxNorm,
941 CentralityType::None,
942 BalancingTermType::None,
943 0.0,
944 0.0,
945 std::f64::consts::E,
946 aggr(0.0, 0.0, 4.0, 1, 1, 1),
947 );
948 let logc = evaluate_quality_function(
949 NormType::MaxNorm,
950 CentralityType::LogCenter,
951 BalancingTermType::None,
952 0.0,
953 0.0,
954 std::f64::consts::E,
955 aggr(0.0, 0.0, 4.0, 1, 1, 1),
956 );
957 assert!((base - logc - 4.0).abs() < 1e-12, "base={base} logc={logc}");
959 }
960
961 #[test]
962 fn evaluate_reciprocal_centrality_adds_c_over_xi() {
963 let q = evaluate_quality_function(
964 NormType::MaxNorm,
965 CentralityType::ReciprocalCenter,
966 BalancingTermType::None,
967 0.0,
968 0.0,
969 0.5,
970 aggr(0.0, 0.0, 1.0, 1, 1, 1),
971 );
972 assert!((q - 3.0).abs() < 1e-12);
974 }
975
976 #[test]
977 fn evaluate_cubed_reciprocal_centrality_adds_c_over_xi3() {
978 let q = evaluate_quality_function(
979 NormType::MaxNorm,
980 CentralityType::CubedReciprocalCenter,
981 BalancingTermType::None,
982 0.0,
983 0.0,
984 0.5,
985 aggr(0.0, 0.0, 1.0, 1, 1, 1),
986 );
987 assert!((q - 9.0).abs() < 1e-12);
989 }
990
991 #[test]
992 fn evaluate_cubic_balancing_adds_when_dual_dominates() {
993 let q = evaluate_quality_function(
994 NormType::MaxNorm,
995 CentralityType::None,
996 BalancingTermType::CubicTerm,
997 0.0,
998 0.0,
999 1.0,
1000 aggr(5.0, 1.0, 2.0, 1, 1, 1),
1001 );
1002 assert!((q - 35.0).abs() < 1e-12, "got {}", q);
1004 }
1005
1006 #[test]
1007 fn evaluate_cubic_balancing_zero_when_compl_dominates() {
1008 let q = evaluate_quality_function(
1009 NormType::MaxNorm,
1010 CentralityType::None,
1011 BalancingTermType::CubicTerm,
1012 0.0,
1013 0.0,
1014 1.0,
1015 aggr(1.0, 1.0, 5.0, 1, 1, 1),
1016 );
1017 assert!((q - 7.0).abs() < 1e-12);
1019 }
1020
1021 #[test]
1022 fn pick_sigma_searches_below_one_for_decreasing_q() {
1023 let f = |s: f64| (s - 0.4).powi(2);
1025 let s = pick_sigma(1e-9, 100.0, 1e-11, 1e5, 1.0, 1e-6, 0.0, 50, f);
1026 assert!((s - 0.4).abs() < 1e-2, "got s = {}", s);
1027 }
1028
1029 #[test]
1030 fn pick_sigma_searches_above_one_for_q_decreasing_in_sigma() {
1031 let f = |s: f64| -s;
1033 let s = pick_sigma(1e-9, 10.0, 1e-11, 1e5, 1.0, 1e-6, 0.0, 50, f);
1034 assert!(s > 5.0, "got s = {}", s);
1036 }
1037
1038 #[test]
1039 fn pick_sigma_clamps_to_mu_max_over_avrg_in_up_search() {
1040 let f = |s: f64| -s;
1042 let s = pick_sigma(1e-9, 100.0, 1e-11, 2.0, 1.0, 1e-6, 0.0, 50, f);
1043 assert!(s <= 2.0 + 1e-9 && s >= 1.0, "got s = {}", s);
1044 }
1045
1046 #[test]
1047 fn pick_sigma_clamps_to_mu_min_over_avrg_in_down_search() {
1048 let f = |s: f64| s;
1051 let s = pick_sigma(1e-9, 100.0, 0.5, 1e5, 1.0, 1e-6, 0.0, 50, f);
1052 assert!(s >= 0.5 - 1e-9 && s <= 1.0, "got s = {}", s);
1053 }
1054}