1use crate::constant_derivatives::{
61 ConstantDerivatives, DerivativeProof, DerivativeProofs, subsystem_proof,
62};
63use crate::ipopt_nlp::{IpoptNlp, Nlp, SplitNames};
64use crate::tnlp::{IDX_NAMES, MetaData, NlpInfo, ScalingRequest, SparsityRequest, StartingPoint};
65use crate::tnlp_adapter::{BoundClassification, TNLPAdapter};
66use pounce_common::cached::Cache;
67use pounce_common::timing::TimingStatistics;
68use pounce_common::types::{Index, Number};
69use pounce_linalg::{
70 DenseVector, DenseVectorSpace, ExpansionMatrix, ExpansionMatrixSpace, GenTMatrix,
71 GenTMatrixSpace, Matrix, SymMatrix, SymTMatrix, SymTMatrixSpace, Vector,
72};
73use std::cell::{Cell, RefCell};
74use std::rc::Rc;
75
76pub trait NlpScaling {
88 fn obj_scaling(&self) -> Number {
93 1.0
94 }
95}
96
97#[derive(Debug, Default, Clone, Copy)]
100pub struct NoScaling;
101impl NlpScaling for NoScaling {}
102
103#[derive(Debug, Clone, Copy)]
108pub struct ConstObjScaling(pub Number);
109impl NlpScaling for ConstObjScaling {
110 fn obj_scaling(&self) -> Number {
111 self.0
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum ScalingMethod {
119 None,
121 GradientBased,
123 UserScaling,
130}
131
132pub fn gradient_obj_scale(
146 max_grad_f: Number,
147 max_gradient: Number,
148 min_value: Number,
149 obj_target_gradient: Number,
150) -> Number {
151 let mut df = 1.0;
152 if obj_target_gradient > 0.0 && max_grad_f > 0.0 {
153 df = obj_target_gradient / max_grad_f;
156 } else if max_grad_f > max_gradient {
157 df = max_gradient / max_grad_f;
158 }
159 if df < min_value {
160 df = min_value;
161 }
162 df
163}
164
165pub fn gradient_row_scale(
183 row_max: Number,
184 max_gradient: Number,
185 min_value: Number,
186 constr_target_gradient: Number,
187) -> Number {
188 let mut s = if constr_target_gradient > 0.0 {
189 constr_target_gradient / row_max
190 } else {
191 let raw = max_gradient / row_max;
192 if raw > 1.0 { 1.0 } else { raw }
193 };
194 if s < min_value {
195 s = min_value;
196 }
197 s
198}
199
200pub fn gradient_scaling_fires(
206 row_max: &[Number],
207 max_gradient: Number,
208 constr_target_gradient: Number,
209) -> bool {
210 constr_target_gradient > 0.0 || row_max.iter().any(|&v| v > max_gradient)
211}
212
213pub struct OrigIpoptNlp {
216 adapter: Rc<RefCell<TNLPAdapter>>,
218 scaling: Rc<dyn NlpScaling>,
223
224 obj_scale_factor: Cell<Number>,
227 computed_obj_scale: Cell<Number>,
230 c_scale: RefCell<Option<Vec<Number>>>,
234 d_scale: RefCell<Option<Vec<Number>>>,
236 declared_d_l: RefCell<Option<Vec<Number>>>,
245 declared_d_u: RefCell<Option<Vec<Number>>>,
246 declared_x_l: RefCell<Option<Vec<Number>>>,
252 declared_x_u: RefCell<Option<Vec<Number>>>,
253 honor_original_bounds: Cell<bool>,
261 x_scaling_rejected: Cell<bool>,
270
271 x_space: Rc<DenseVectorSpace>,
273 c_space: Rc<DenseVectorSpace>,
274 d_space: Rc<DenseVectorSpace>,
275 x_l_space: Rc<DenseVectorSpace>,
276 x_u_space: Rc<DenseVectorSpace>,
277 d_l_space: Rc<DenseVectorSpace>,
278 d_u_space: Rc<DenseVectorSpace>,
279 px_l_space: Rc<ExpansionMatrixSpace>,
280 px_u_space: Rc<ExpansionMatrixSpace>,
281 pd_l_space: Rc<ExpansionMatrixSpace>,
282 pd_u_space: Rc<ExpansionMatrixSpace>,
283 jac_c_space: Rc<GenTMatrixSpace>,
284 jac_d_space: Rc<GenTMatrixSpace>,
285 h_space: Option<Rc<SymTMatrixSpace>>,
288
289 x_l: Rc<DenseVector>,
291 x_u: Rc<DenseVector>,
292 d_l: Rc<DenseVector>,
293 d_u: Rc<DenseVector>,
294 c_rhs: Vec<Number>,
301 warm_start_snapshot: RefCell<Option<StartingPointSnapshot>>,
304
305 px_l: Rc<dyn Matrix>,
307 px_u: Rc<dyn Matrix>,
308 pd_l: Rc<dyn Matrix>,
309 pd_u: Rc<dyn Matrix>,
310
311 jac_c_entry_in_g: Vec<Index>,
315 jac_d_entry_in_g: Vec<Index>,
317 nnz_jac_g_full: Index,
319
320 nnz_h_lag_full: Index,
324 h_entry_in_full: Vec<Index>,
329
330 f_cache: RefCell<Cache<Number>>,
332 grad_f_cache: RefCell<Cache<Rc<dyn Vector>>>,
333 c_cache: RefCell<Cache<Rc<dyn Vector>>>,
334 d_cache: RefCell<Cache<Rc<dyn Vector>>>,
335 jac_c_cache: RefCell<Cache<Rc<dyn Matrix>>>,
336 jac_d_cache: RefCell<Cache<Rc<dyn Matrix>>>,
337 h_cache: RefCell<Cache<Rc<dyn SymMatrix>>>,
338 full_g_cache: RefCell<Cache<Rc<Vec<Number>>>>,
346 full_jac_g_cache: RefCell<Cache<Rc<Vec<Number>>>>,
347
348 f_evals: RefCell<Index>,
350 grad_f_evals: RefCell<Index>,
351 c_evals: RefCell<Index>,
352 d_evals: RefCell<Index>,
353 jac_c_evals: RefCell<Index>,
354 jac_d_evals: RefCell<Index>,
355 h_evals: RefCell<Index>,
356
357 info: NlpInfo,
360
361 timing: RefCell<Option<Rc<TimingStatistics>>>,
366
367 const_deriv: ConstantDerivatives,
379
380 fixed_reach: FixedVarReach,
384}
385
386#[derive(Clone, Debug, Default)]
402struct FixedVarReach {
403 g_row_touches_fixed: Vec<bool>,
409 second_order_touches_fixed: bool,
421}
422
423#[derive(Clone)]
424struct StartingPointSnapshot {
425 x: Vec<Number>,
426 z_l: Vec<Number>,
427 z_u: Vec<Number>,
428 lambda: Vec<Number>,
429}
430
431impl std::fmt::Debug for OrigIpoptNlp {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 f.debug_struct("OrigIpoptNlp")
434 .field("info", &self.info)
435 .field("f_evals", &*self.f_evals.borrow())
436 .field("grad_f_evals", &*self.grad_f_evals.borrow())
437 .field("c_evals", &*self.c_evals.borrow())
438 .field("d_evals", &*self.d_evals.borrow())
439 .field("jac_c_evals", &*self.jac_c_evals.borrow())
440 .field("jac_d_evals", &*self.jac_d_evals.borrow())
441 .field("h_evals", &*self.h_evals.borrow())
442 .finish_non_exhaustive()
443 }
444}
445
446impl OrigIpoptNlp {
447 pub fn new(
453 adapter: Rc<RefCell<TNLPAdapter>>,
454 scaling: Rc<dyn NlpScaling>,
455 ) -> Result<Self, String> {
456 let (info, classification) = {
458 let a = adapter.borrow();
459 (*a.nlp_info(), a.classification().clone())
460 };
461
462 let n_x_var = classification.n_x_var();
464 let x_space = DenseVectorSpace::new(n_x_var);
465 let c_space = DenseVectorSpace::new(classification.n_c);
466 let d_space = DenseVectorSpace::new(classification.n_d);
467 let x_l_space = DenseVectorSpace::new(classification.n_x_l());
468 let x_u_space = DenseVectorSpace::new(classification.n_x_u());
469 let d_l_space = DenseVectorSpace::new(classification.n_d_l());
470 let d_u_space = DenseVectorSpace::new(classification.n_d_u());
471
472 let px_l_space =
474 ExpansionMatrixSpace::new(n_x_var, classification.n_x_l(), &classification.x_l_map, 0);
475 let px_u_space =
476 ExpansionMatrixSpace::new(n_x_var, classification.n_x_u(), &classification.x_u_map, 0);
477 let pd_l_space = ExpansionMatrixSpace::new(
478 classification.n_d,
479 classification.n_d_l(),
480 &classification.d_l_map,
481 0,
482 );
483 let pd_u_space = ExpansionMatrixSpace::new(
484 classification.n_d,
485 classification.n_d_u(),
486 &classification.d_u_map,
487 0,
488 );
489 let px_l: Rc<dyn Matrix> = Rc::new(ExpansionMatrix::new(Rc::clone(&px_l_space)));
490 let px_u: Rc<dyn Matrix> = Rc::new(ExpansionMatrix::new(Rc::clone(&px_u_space)));
491 let pd_l: Rc<dyn Matrix> = Rc::new(ExpansionMatrix::new(Rc::clone(&pd_l_space)));
492 let pd_u: Rc<dyn Matrix> = Rc::new(ExpansionMatrix::new(Rc::clone(&pd_u_space)));
493
494 let n_full_x = classification.n_full_x as usize;
498 let n_full_g = classification.n_full_g as usize;
499 let mut full_x_l = vec![0.0; n_full_x];
500 let mut full_x_u = vec![0.0; n_full_x];
501 let mut full_g_l = vec![0.0; n_full_g];
502 let mut full_g_u = vec![0.0; n_full_g];
503 {
504 let a = adapter.borrow();
505 let mut t = a.tnlp().borrow_mut();
506 let ok = t.get_bounds_info(crate::tnlp::BoundsInfo {
507 x_l: &mut full_x_l,
508 x_u: &mut full_x_u,
509 g_l: &mut full_g_l,
510 g_u: &mut full_g_u,
511 });
512 if !ok {
513 return Err("TNLP::get_bounds_info returned false on second call".into());
514 }
515 }
516
517 let x_l = make_dense_from(&x_l_space, |i| {
518 let var_idx = classification.x_l_map[i] as usize;
520 let full_idx = classification.x_not_fixed_map[var_idx] as usize;
521 full_x_l[full_idx]
522 });
523 let x_u = make_dense_from(&x_u_space, |i| {
524 let var_idx = classification.x_u_map[i] as usize;
525 let full_idx = classification.x_not_fixed_map[var_idx] as usize;
526 full_x_u[full_idx]
527 });
528 let d_l = make_dense_from(&d_l_space, |i| {
529 let d_idx = classification.d_l_map[i] as usize;
531 let full_g_idx = classification.d_map[d_idx] as usize;
532 full_g_l[full_g_idx]
533 });
534 let d_u = make_dense_from(&d_u_space, |i| {
535 let d_idx = classification.d_u_map[i] as usize;
536 let full_g_idx = classification.d_map[d_idx] as usize;
537 full_g_u[full_g_idx]
538 });
539
540 let c_rhs: Vec<Number> = classification
546 .c_map
547 .iter()
548 .map(|&g_idx| full_g_l[g_idx as usize])
549 .collect();
550
551 let mut full_irow = vec![0 as Index; info.nnz_jac_g as usize];
561 let mut full_jcol = vec![0 as Index; info.nnz_jac_g as usize];
562 {
563 let a = adapter.borrow();
564 let mut t = a.tnlp().borrow_mut();
565 let ok = t.eval_jac_g(
566 None,
567 false,
568 SparsityRequest::Structure {
569 irow: &mut full_irow,
570 jcol: &mut full_jcol,
571 },
572 );
573 if !ok {
574 return Err("TNLP::eval_jac_g(Structure) returned false".into());
575 }
576 }
577
578 let mut g_to_c = vec![-1 as Index; n_full_g];
580 for (c_idx, &g_idx) in classification.c_map.iter().enumerate() {
581 g_to_c[g_idx as usize] = c_idx as Index;
582 }
583 let mut g_to_d = vec![-1 as Index; n_full_g];
584 for (d_idx, &g_idx) in classification.d_map.iter().enumerate() {
585 g_to_d[g_idx as usize] = d_idx as Index;
586 }
587
588 let style_offset = match info.index_style {
589 crate::tnlp::IndexStyle::C => 0 as Index,
590 crate::tnlp::IndexStyle::Fortran => 1 as Index,
591 };
592
593 let mut jac_c_irow_1based = Vec::new();
594 let mut jac_c_jcol_1based = Vec::new();
595 let mut jac_c_entry_in_g = Vec::new();
596 let mut jac_d_irow_1based = Vec::new();
597 let mut jac_d_jcol_1based = Vec::new();
598 let mut jac_d_entry_in_g = Vec::new();
599
600 let full_to_var = &classification.full_to_var;
604 let mut g_row_touches_fixed = vec![false; info.m as usize];
608 for k in 0..info.nnz_jac_g as usize {
609 let g_row_0 = (full_irow[k] - style_offset) as usize;
610 let x_col_0 = (full_jcol[k] - style_offset) as usize;
611 let var_col = full_to_var[x_col_0];
612 if var_col < 0 {
613 g_row_touches_fixed[g_row_0] = true;
614 continue;
615 }
616 let col_1based = var_col + 1;
618 let c_row = g_to_c[g_row_0];
619 if c_row >= 0 {
620 jac_c_irow_1based.push(c_row + 1);
621 jac_c_jcol_1based.push(col_1based);
622 jac_c_entry_in_g.push(k as Index);
623 } else {
624 let d_row = g_to_d[g_row_0];
625 debug_assert!(d_row >= 0, "g row {g_row_0} is neither in c_map nor d_map");
626 jac_d_irow_1based.push(d_row + 1);
627 jac_d_jcol_1based.push(col_1based);
628 jac_d_entry_in_g.push(k as Index);
629 }
630 }
631
632 let jac_c_space = GenTMatrixSpace::new(
633 classification.n_c,
634 n_x_var,
635 jac_c_irow_1based,
636 jac_c_jcol_1based,
637 );
638 let jac_d_space = GenTMatrixSpace::new(
639 classification.n_d,
640 n_x_var,
641 jac_d_irow_1based,
642 jac_d_jcol_1based,
643 );
644
645 let nnz_h_lag_full = info.nnz_h_lag;
650 let mut h_entry_in_full: Vec<Index> = Vec::new();
651 let mut second_order_touches_fixed = info.nnz_h_lag > 0;
656 let h_space = if info.nnz_h_lag > 0 {
657 let mut h_irow = vec![0 as Index; info.nnz_h_lag as usize];
658 let mut h_jcol = vec![0 as Index; info.nnz_h_lag as usize];
659 let supports_h = {
660 let a = adapter.borrow();
661 let mut t = a.tnlp().borrow_mut();
662 t.eval_h(
663 None,
664 false,
665 1.0,
666 None,
667 false,
668 SparsityRequest::Structure {
669 irow: &mut h_irow,
670 jcol: &mut h_jcol,
671 },
672 )
673 };
674 if supports_h {
675 second_order_touches_fixed = false;
676 let mut h_irow_1: Vec<Index> = Vec::with_capacity(h_irow.len());
682 let mut h_jcol_1: Vec<Index> = Vec::with_capacity(h_jcol.len());
683 for k in 0..h_irow.len() {
684 let i_full = (h_irow[k] - style_offset) as usize;
685 let j_full = (h_jcol[k] - style_offset) as usize;
686 let i_var = full_to_var[i_full];
687 let j_var = full_to_var[j_full];
688 if i_var < 0 || j_var < 0 {
689 second_order_touches_fixed = true;
690 continue;
691 }
692 h_irow_1.push(i_var + 1);
693 h_jcol_1.push(j_var + 1);
694 h_entry_in_full.push(k as Index);
695 }
696 Some(SymTMatrixSpace::new(n_x_var, h_irow_1, h_jcol_1))
697 } else {
698 None
700 }
701 } else {
702 Some(SymTMatrixSpace::new(n_x_var, Vec::new(), Vec::new()))
706 };
707
708 let initial_obj_scal = scaling.obj_scaling();
714 Ok(Self {
715 adapter,
716 scaling,
717 obj_scale_factor: Cell::new(initial_obj_scal),
718 computed_obj_scale: Cell::new(1.0),
719 c_scale: RefCell::new(None),
720 d_scale: RefCell::new(None),
721 declared_d_l: RefCell::new(None),
722 declared_d_u: RefCell::new(None),
723 declared_x_l: RefCell::new(None),
724 declared_x_u: RefCell::new(None),
725 honor_original_bounds: Cell::new(false),
726 x_scaling_rejected: Cell::new(false),
727 x_space,
728 c_space,
729 d_space,
730 x_l_space,
731 x_u_space,
732 d_l_space,
733 d_u_space,
734 px_l_space,
735 px_u_space,
736 pd_l_space,
737 pd_u_space,
738 jac_c_space,
739 jac_d_space,
740 h_space,
741 x_l: Rc::new(x_l),
742 x_u: Rc::new(x_u),
743 d_l: Rc::new(d_l),
744 d_u: Rc::new(d_u),
745 c_rhs,
746 warm_start_snapshot: RefCell::new(None),
747 px_l,
748 px_u,
749 pd_l,
750 pd_u,
751 jac_c_entry_in_g,
752 jac_d_entry_in_g,
753 nnz_jac_g_full: info.nnz_jac_g,
754 nnz_h_lag_full,
755 h_entry_in_full,
756 f_cache: RefCell::new(Cache::new(1)),
757 grad_f_cache: RefCell::new(Cache::new(1)),
758 c_cache: RefCell::new(Cache::new(1)),
759 d_cache: RefCell::new(Cache::new(1)),
760 jac_c_cache: RefCell::new(Cache::new(1)),
761 jac_d_cache: RefCell::new(Cache::new(1)),
762 h_cache: RefCell::new(Cache::new(1)),
763 full_g_cache: RefCell::new(Cache::new(1)),
764 full_jac_g_cache: RefCell::new(Cache::new(1)),
765 f_evals: RefCell::new(0),
766 grad_f_evals: RefCell::new(0),
767 c_evals: RefCell::new(0),
768 d_evals: RefCell::new(0),
769 jac_c_evals: RefCell::new(0),
770 jac_d_evals: RefCell::new(0),
771 h_evals: RefCell::new(0),
772 info,
773 timing: RefCell::new(None),
774 const_deriv: ConstantDerivatives::default(),
775 fixed_reach: FixedVarReach {
776 g_row_touches_fixed,
777 second_order_touches_fixed,
778 },
779 })
780 }
781
782 pub fn set_timing_stats(&self, t: Rc<TimingStatistics>) {
788 *self.timing.borrow_mut() = Some(t);
789 }
790
791 fn timed_eval<R, F>(&self, pick: fn(&TimingStatistics) -> &pounce_common::TimedTask, f: F) -> R
796 where
797 F: FnOnce() -> R,
798 {
799 let guard = self.timing.borrow();
800 match guard.as_deref() {
801 Some(t) => {
802 let task = pick(t);
803 task.start();
804 t.total_function_evaluation_time.start();
805 let r = f();
806 t.total_function_evaluation_time.end();
807 task.end();
808 r
809 }
810 None => {
811 drop(guard);
812 f()
813 }
814 }
815 }
816
817 pub fn derivative_proofs(&self) -> [DerivativeProof; 4] {
844 let mut proofs: DerivativeProofs = {
845 let a = self.adapter.borrow();
846 let mut t = a.tnlp().borrow_mut();
847 t.derivative_proofs()
848 };
849 let cls = self.adapter.borrow().classification().clone();
850 if cls.n_x_fixed > 0 {
851 let reach = &self.fixed_reach;
865 for (row, proof) in proofs.jac.iter_mut().enumerate() {
866 if reach.g_row_touches_fixed.get(row).copied().unwrap_or(true) {
867 *proof = proof.forget_variation();
868 }
869 }
870 if reach.second_order_touches_fixed {
871 proofs.grad_f = proofs.grad_f.forget_variation();
872 proofs.hessian = proofs.hessian.forget_variation();
873 }
874 }
875 [
876 proofs.grad_f,
877 proofs.hessian,
878 subsystem_proof(&proofs, &cls.c_map),
879 subsystem_proof(&proofs, &cls.d_map),
880 ]
881 }
882
883 pub fn set_constant_derivatives(&mut self, cd: ConstantDerivatives) {
889 self.const_deriv = cd;
890 self.invalidate_eval_caches();
893 }
894
895 pub fn constant_derivatives(&self) -> ConstantDerivatives {
897 self.const_deriv
898 }
899
900 pub fn nlp_info(&self) -> &NlpInfo {
903 &self.info
904 }
905 pub fn classification_n_x_var(&self) -> Index {
906 self.x_space.dim()
907 }
908 pub fn x_space(&self) -> &Rc<DenseVectorSpace> {
909 &self.x_space
910 }
911 pub fn c_space(&self) -> &Rc<DenseVectorSpace> {
912 &self.c_space
913 }
914 pub fn d_space(&self) -> &Rc<DenseVectorSpace> {
915 &self.d_space
916 }
917 pub fn x_l_space(&self) -> &Rc<DenseVectorSpace> {
918 &self.x_l_space
919 }
920 pub fn x_u_space(&self) -> &Rc<DenseVectorSpace> {
921 &self.x_u_space
922 }
923 pub fn d_l_space(&self) -> &Rc<DenseVectorSpace> {
924 &self.d_l_space
925 }
926 pub fn d_u_space(&self) -> &Rc<DenseVectorSpace> {
927 &self.d_u_space
928 }
929 pub fn px_l_space(&self) -> &Rc<ExpansionMatrixSpace> {
930 &self.px_l_space
931 }
932 pub fn px_u_space(&self) -> &Rc<ExpansionMatrixSpace> {
933 &self.px_u_space
934 }
935 pub fn pd_l_space(&self) -> &Rc<ExpansionMatrixSpace> {
936 &self.pd_l_space
937 }
938 pub fn pd_u_space(&self) -> &Rc<ExpansionMatrixSpace> {
939 &self.pd_u_space
940 }
941 pub fn jac_c_space(&self) -> &Rc<GenTMatrixSpace> {
942 &self.jac_c_space
943 }
944 pub fn jac_d_space(&self) -> &Rc<GenTMatrixSpace> {
945 &self.jac_d_space
946 }
947 pub fn h_space(&self) -> Option<&Rc<SymTMatrixSpace>> {
948 self.h_space.as_ref()
949 }
950
951 pub fn obj_scale_factor(&self) -> Number {
954 self.obj_scale_factor.get()
955 }
956
957 pub fn snapshot_declared_bounds(&mut self) {
984 *self.declared_d_l.borrow_mut() = Some(self.d_l.expanded_values());
985 *self.declared_d_u.borrow_mut() = Some(self.d_u.expanded_values());
986 *self.declared_x_l.borrow_mut() = Some(self.x_l.expanded_values());
987 *self.declared_x_u.borrow_mut() = Some(self.x_u.expanded_values());
988 }
989
990 pub fn relax_bounds(&mut self, bound_relax_factor: Number, constr_viol_tol: Number) {
991 self.snapshot_declared_bounds();
992 if bound_relax_factor <= 0.0 {
993 return;
994 }
995 let relax = bound_relax_factor.abs();
996 let cap = constr_viol_tol;
997 let apply = |v: &mut DenseVector, sign: Number| {
998 let xs = v.values_mut();
999 for x in xs.iter_mut() {
1000 let delta = (relax * x.abs().max(1.0)).min(cap);
1001 *x += sign * delta;
1002 }
1003 };
1004 let apply_d = |v: &mut DenseVector, sign: Number| {
1022 let rel_width = relax.min(cap);
1023 let xs = v.values_mut();
1024 for x in xs.iter_mut() {
1025 let scale = if *x == 0.0 { 1.0 } else { x.abs() };
1026 *x += sign * rel_width * scale;
1027 }
1028 };
1029 apply(
1036 Rc::get_mut(&mut self.x_l).expect("relax_bounds: x_l is uniquely owned"),
1037 -1.0,
1038 );
1039 apply(
1040 Rc::get_mut(&mut self.x_u).expect("relax_bounds: x_u is uniquely owned"),
1041 1.0,
1042 );
1043 apply_d(
1044 Rc::get_mut(&mut self.d_l).expect("relax_bounds: d_l is uniquely owned"),
1045 -1.0,
1046 );
1047 apply_d(
1048 Rc::get_mut(&mut self.d_u).expect("relax_bounds: d_u is uniquely owned"),
1049 1.0,
1050 );
1051 }
1052
1053 pub fn determine_scaling_from_starting_point(
1075 &mut self,
1076 method: ScalingMethod,
1077 max_gradient: Number,
1078 min_value: Number,
1079 obj_target_gradient: Number,
1080 constr_target_gradient: Number,
1081 ) {
1082 let user_obj_factor = self.scaling.obj_scaling();
1085 if matches!(method, ScalingMethod::None) {
1086 self.obj_scale_factor.set(user_obj_factor);
1087 *self.c_scale.borrow_mut() = None;
1088 *self.d_scale.borrow_mut() = None;
1089 self.invalidate_eval_caches();
1090 return;
1091 }
1092
1093 let cls = self.adapter.borrow().classification().clone();
1095 let n_full_x = cls.n_full_x as usize;
1096 let n_full_g = cls.n_full_g as usize;
1097 let mut full_x = vec![0.0; n_full_x];
1098 let mut full_z_l = vec![0.0; n_full_x];
1099 let mut full_z_u = vec![0.0; n_full_x];
1100 let mut full_lambda = vec![0.0; n_full_g];
1101 let starting_ok = {
1102 let a = self.adapter.borrow();
1103 let mut t = a.tnlp().borrow_mut();
1104 t.get_starting_point(StartingPoint {
1105 init_x: true,
1106 x: &mut full_x,
1107 init_z: false,
1108 z_l: &mut full_z_l,
1109 z_u: &mut full_z_u,
1110 init_lambda: false,
1111 lambda: &mut full_lambda,
1112 })
1113 };
1114 if !starting_ok {
1115 self.obj_scale_factor.set(user_obj_factor);
1117 *self.c_scale.borrow_mut() = None;
1118 *self.d_scale.borrow_mut() = None;
1119 self.invalidate_eval_caches();
1120 return;
1121 }
1122
1123 for (i, &full_idx) in cls.x_fixed_map.iter().enumerate() {
1136 full_x[full_idx as usize] = cls.x_fixed_vals[i];
1137 }
1138
1139 match method {
1140 ScalingMethod::None => unreachable!("handled above"),
1141 ScalingMethod::GradientBased => {
1142 self.scale_gradient_based(
1143 &cls,
1144 &full_x,
1145 user_obj_factor,
1146 max_gradient,
1147 min_value,
1148 obj_target_gradient,
1149 constr_target_gradient,
1150 );
1151 }
1152 ScalingMethod::UserScaling => {
1153 let applied = self.scale_user_supplied(&cls, user_obj_factor, min_value);
1154 if !applied {
1155 self.obj_scale_factor.set(user_obj_factor);
1159 *self.c_scale.borrow_mut() = None;
1160 *self.d_scale.borrow_mut() = None;
1161 }
1162 }
1163 }
1164
1165 self.apply_d_scale_to_bounds();
1168
1169 self.invalidate_eval_caches();
1172 }
1173
1174 fn scale_gradient_based(
1177 &self,
1178 cls: &BoundClassification,
1179 full_x: &[Number],
1180 user_obj_factor: Number,
1181 max_gradient: Number,
1182 min_value: Number,
1183 obj_target_gradient: Number,
1184 constr_target_gradient: Number,
1185 ) {
1186 let n_full_x = cls.n_full_x as usize;
1187 let n_full_g = cls.n_full_g as usize;
1188
1189 let mut full_grad_f = vec![0.0; n_full_x];
1191 let grad_ok = {
1192 let a = self.adapter.borrow();
1193 let mut t = a.tnlp().borrow_mut();
1194 t.eval_grad_f(full_x, true, &mut full_grad_f)
1195 };
1196 let mut df = 1.0;
1197 if grad_ok {
1198 let mut max_grad_f: Number = 0.0;
1201 for &full_idx in cls.x_not_fixed_map.iter() {
1202 let v = full_grad_f[full_idx as usize].abs();
1203 if v > max_grad_f {
1204 max_grad_f = v;
1205 }
1206 }
1207 df = gradient_obj_scale(max_grad_f, max_gradient, min_value, obj_target_gradient);
1208 }
1209 self.computed_obj_scale.set(df);
1210 self.obj_scale_factor.set(df * user_obj_factor);
1211
1212 if cls.n_full_g == 0 {
1214 *self.c_scale.borrow_mut() = None;
1215 *self.d_scale.borrow_mut() = None;
1216 return;
1217 }
1218 let mut full_jac_vals = vec![0.0; self.nnz_jac_g_full as usize];
1220 let jac_ok = {
1221 let a = self.adapter.borrow();
1222 let mut t = a.tnlp().borrow_mut();
1223 t.eval_jac_g(
1224 Some(full_x),
1225 true,
1226 SparsityRequest::Values {
1227 values: &mut full_jac_vals,
1228 },
1229 )
1230 };
1231 if !jac_ok {
1232 *self.c_scale.borrow_mut() = None;
1233 *self.d_scale.borrow_mut() = None;
1234 return;
1235 }
1236 let mut full_irow = vec![0 as Index; self.nnz_jac_g_full as usize];
1238 let mut full_jcol = vec![0 as Index; self.nnz_jac_g_full as usize];
1239 let _ = {
1240 let a = self.adapter.borrow();
1241 let mut t = a.tnlp().borrow_mut();
1242 t.eval_jac_g(
1243 None,
1244 false,
1245 SparsityRequest::Structure {
1246 irow: &mut full_irow,
1247 jcol: &mut full_jcol,
1248 },
1249 )
1250 };
1251 let style_offset: Index = match self.info.index_style {
1252 crate::tnlp::IndexStyle::C => 0,
1253 crate::tnlp::IndexStyle::Fortran => 1,
1254 };
1255 let mut g_to_c = vec![-1 as Index; n_full_g];
1257 for (c_idx, &g_idx) in cls.c_map.iter().enumerate() {
1258 g_to_c[g_idx as usize] = c_idx as Index;
1259 }
1260 let mut g_to_d = vec![-1 as Index; n_full_g];
1261 for (d_idx, &g_idx) in cls.d_map.iter().enumerate() {
1262 g_to_d[g_idx as usize] = d_idx as Index;
1263 }
1264 let n_c = cls.n_c as usize;
1265 let n_d = cls.n_d as usize;
1266 let dbl_min = Number::MIN_POSITIVE;
1268 let mut c_row_max: Vec<Number> = vec![dbl_min; n_c];
1269 let mut d_row_max: Vec<Number> = vec![dbl_min; n_d];
1270 for k in 0..self.nnz_jac_g_full as usize {
1271 let g_row_0 = (full_irow[k] - style_offset) as usize;
1272 let v = full_jac_vals[k].abs();
1273 let cr = g_to_c[g_row_0];
1274 if cr >= 0 {
1275 let row = cr as usize;
1276 if v > c_row_max[row] {
1277 c_row_max[row] = v;
1278 }
1279 } else {
1280 let dr = g_to_d[g_row_0];
1281 if dr >= 0 {
1282 let row = dr as usize;
1283 if v > d_row_max[row] {
1284 d_row_max[row] = v;
1285 }
1286 }
1287 }
1288 }
1289
1290 let row_max_to_scale = |row_max: Number| -> Number {
1291 gradient_row_scale(row_max, max_gradient, min_value, constr_target_gradient)
1292 };
1293 let any_row_above = |rows: &[Number]| -> bool {
1294 gradient_scaling_fires(rows, max_gradient, constr_target_gradient)
1295 };
1296
1297 if n_c > 0 && any_row_above(&c_row_max) {
1298 let dc: Vec<Number> = c_row_max.iter().map(|&v| row_max_to_scale(v)).collect();
1299 *self.c_scale.borrow_mut() = Some(dc);
1300 } else {
1301 *self.c_scale.borrow_mut() = None;
1302 }
1303
1304 if n_d > 0 && any_row_above(&d_row_max) {
1305 let dd: Vec<Number> = d_row_max.iter().map(|&v| row_max_to_scale(v)).collect();
1306 *self.d_scale.borrow_mut() = Some(dd);
1307 } else {
1308 *self.d_scale.borrow_mut() = None;
1309 }
1310 }
1311
1312 fn scale_user_supplied(
1329 &self,
1330 cls: &BoundClassification,
1331 user_obj_factor: Number,
1332 min_value: Number,
1333 ) -> bool {
1334 let n_full_x = cls.n_full_x as usize;
1335 let n_full_g = cls.n_full_g as usize;
1336 let mut obj_scaling: Number = 1.0;
1337 let mut use_x_scaling = false;
1338 let mut x_scaling = vec![1.0; n_full_x];
1339 let mut use_g_scaling = false;
1340 let mut g_scaling = vec![1.0; n_full_g];
1341 let ok = {
1342 let a = self.adapter.borrow();
1343 let mut t = a.tnlp().borrow_mut();
1344 t.get_scaling_parameters(ScalingRequest {
1345 obj_scaling: &mut obj_scaling,
1346 use_x_scaling: &mut use_x_scaling,
1347 x_scaling: &mut x_scaling,
1348 use_g_scaling: &mut use_g_scaling,
1349 g_scaling: &mut g_scaling,
1350 })
1351 };
1352 if !ok {
1353 return false;
1354 }
1355
1356 let mut df = obj_scaling;
1360 if df.abs() < min_value {
1361 df = df.signum().max(0.0).max(1.0) * min_value;
1364 }
1365 self.obj_scale_factor.set(df * user_obj_factor);
1366
1367 if use_g_scaling && g_scaling.len() == n_full_g {
1369 let n_c = cls.n_c as usize;
1370 let n_d = cls.n_d as usize;
1371 let mut dc = vec![1.0; n_c];
1372 for (c_idx, &g_idx) in cls.c_map.iter().enumerate() {
1373 let s = g_scaling[g_idx as usize];
1374 dc[c_idx] = if s < min_value { min_value } else { s };
1375 }
1376 let mut dd = vec![1.0; n_d];
1377 for (d_idx, &g_idx) in cls.d_map.iter().enumerate() {
1378 let s = g_scaling[g_idx as usize];
1379 dd[d_idx] = if s < min_value { min_value } else { s };
1380 }
1381 let nontrivial_c = dc.iter().any(|&s| s != 1.0);
1384 *self.c_scale.borrow_mut() = if nontrivial_c && n_c > 0 {
1385 Some(dc)
1386 } else {
1387 None
1388 };
1389 let nontrivial_d = dd.iter().any(|&s| s != 1.0);
1390 *self.d_scale.borrow_mut() = if nontrivial_d && n_d > 0 {
1391 Some(dd)
1392 } else {
1393 None
1394 };
1395 } else {
1396 *self.c_scale.borrow_mut() = None;
1397 *self.d_scale.borrow_mut() = None;
1398 }
1399 if use_x_scaling && x_scaling.iter().any(|&s| s != 1.0) {
1403 self.x_scaling_rejected.set(true);
1404 }
1405 true
1406 }
1407
1408 pub fn user_x_scaling_rejected(&self) -> bool {
1414 self.x_scaling_rejected.get()
1415 }
1416
1417 fn apply_d_scale_to_bounds(&mut self) {
1422 let cls = self.adapter.borrow().classification().clone();
1423 if let Some(dd) = self.d_scale.borrow().as_ref() {
1424 if let Some(d_l) = Rc::get_mut(&mut self.d_l) {
1425 let xs = d_l.values_mut();
1426 for (i, slot) in xs.iter_mut().enumerate() {
1427 let d_idx = cls.d_l_map[i] as usize;
1428 *slot *= dd[d_idx];
1429 }
1430 }
1431 if let Some(d_u) = Rc::get_mut(&mut self.d_u) {
1432 let xs = d_u.values_mut();
1433 for (i, slot) in xs.iter_mut().enumerate() {
1434 let d_idx = cls.d_u_map[i] as usize;
1435 *slot *= dd[d_idx];
1436 }
1437 }
1438 }
1439 }
1440
1441 fn invalidate_eval_caches(&self) {
1442 self.f_cache.borrow_mut().clear();
1443 self.grad_f_cache.borrow_mut().clear();
1444 self.c_cache.borrow_mut().clear();
1445 self.d_cache.borrow_mut().clear();
1446 self.jac_c_cache.borrow_mut().clear();
1447 self.jac_d_cache.borrow_mut().clear();
1448 self.h_cache.borrow_mut().clear();
1449 }
1450
1451 pub fn f_evals(&self) -> Index {
1452 *self.f_evals.borrow()
1453 }
1454 pub fn grad_f_evals(&self) -> Index {
1455 *self.grad_f_evals.borrow()
1456 }
1457 pub fn c_evals(&self) -> Index {
1458 *self.c_evals.borrow()
1459 }
1460 pub fn d_evals(&self) -> Index {
1461 *self.d_evals.borrow()
1462 }
1463 pub fn jac_c_evals(&self) -> Index {
1464 *self.jac_c_evals.borrow()
1465 }
1466 pub fn jac_d_evals(&self) -> Index {
1467 *self.jac_d_evals.borrow()
1468 }
1469 pub fn h_evals(&self) -> Index {
1470 *self.h_evals.borrow()
1471 }
1472
1473 pub fn lift_x_to_full(&self, x: &dyn Vector) -> Vec<Number> {
1479 let Some(dx) = x.as_any().downcast_ref::<DenseVector>() else {
1480 panic!("OrigIpoptNlp expects DenseVector for x");
1481 };
1482 let a = self.adapter.borrow();
1483 let cls = a.classification();
1484 let mut full = vec![0.0; cls.n_full_x as usize];
1485 let vals = dx.expanded_values();
1486 for (var_idx, &full_idx) in cls.x_not_fixed_map.iter().enumerate() {
1487 full[full_idx as usize] = vals[var_idx];
1488 }
1489 for (i, &full_idx) in cls.x_fixed_map.iter().enumerate() {
1490 full[full_idx as usize] = cls.x_fixed_vals[i];
1491 }
1492 full
1493 }
1494
1495 pub fn set_honor_original_bounds(&self, on: bool) {
1499 self.honor_original_bounds.set(on);
1500 }
1501
1502 pub fn finalize_solution_x(&self, x: &dyn Vector) -> Vec<Number> {
1520 let mut full = self.lift_x_to_full(x);
1521 if !self.honor_original_bounds.get() {
1522 return full;
1523 }
1524 let cls = self.adapter.borrow().classification().clone();
1525 if let Some(x_l) = self.declared_x_l.borrow().as_ref() {
1528 for (i, &var_idx) in cls.x_l_map.iter().enumerate() {
1529 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
1530 if full[full_idx] < x_l[i] {
1531 full[full_idx] = x_l[i];
1532 }
1533 }
1534 }
1535 if let Some(x_u) = self.declared_x_u.borrow().as_ref() {
1536 for (i, &var_idx) in cls.x_u_map.iter().enumerate() {
1537 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
1538 if full[full_idx] > x_u[i] {
1539 full[full_idx] = x_u[i];
1540 }
1541 }
1542 }
1543 full
1544 }
1545
1546 pub fn finalize_solution_lambda(&self, y_c: &dyn Vector, y_d: &dyn Vector) -> Vec<Number> {
1558 let cls = self.adapter.borrow().classification().clone();
1559 let mut lambda = self.pack_lambda_for_user(y_c, y_d, &cls);
1560 let obj_scal = self.obj_scale_factor.get();
1561 if obj_scal != 0.0 && obj_scal != 1.0 {
1562 let inv = 1.0 / obj_scal;
1563 for v in lambda.iter_mut() {
1564 *v *= inv;
1565 }
1566 }
1567 lambda
1568 }
1569
1570 pub fn finalize_solution_z_l(&self, z_l: &dyn Vector) -> Vec<Number> {
1578 let cls = self.adapter.borrow().classification().clone();
1579 let n_full_x = cls.n_full_x as usize;
1580 let mut full_z_l = vec![0.0; n_full_x];
1581 let n_x_l = self.x_l.dim() as usize;
1582 if n_x_l == 0 {
1583 return full_z_l;
1584 }
1585 let Some(dz) = z_l.as_any().downcast_ref::<DenseVector>() else {
1586 panic!("OrigIpoptNlp::finalize_solution_z_l expects DenseVector");
1587 };
1588 let vals = dz.expanded_values();
1589 let obj_scal = self.obj_scale_factor.get();
1590 let inv = if obj_scal == 0.0 { 1.0 } else { 1.0 / obj_scal };
1591 for i in 0..n_x_l {
1592 let var_idx = cls.x_l_map[i] as usize;
1593 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
1594 full_z_l[full_idx] = vals[i] * inv;
1595 }
1596 full_z_l
1597 }
1598
1599 pub fn finalize_solution_z_u(&self, z_u: &dyn Vector) -> Vec<Number> {
1602 let cls = self.adapter.borrow().classification().clone();
1603 let n_full_x = cls.n_full_x as usize;
1604 let mut full_z_u = vec![0.0; n_full_x];
1605 let n_x_u = self.x_u.dim() as usize;
1606 if n_x_u == 0 {
1607 return full_z_u;
1608 }
1609 let Some(dz) = z_u.as_any().downcast_ref::<DenseVector>() else {
1610 panic!("OrigIpoptNlp::finalize_solution_z_u expects DenseVector");
1611 };
1612 let vals = dz.expanded_values();
1613 let obj_scal = self.obj_scale_factor.get();
1614 let inv = if obj_scal == 0.0 { 1.0 } else { 1.0 / obj_scal };
1615 for i in 0..n_x_u {
1616 let var_idx = cls.x_u_map[i] as usize;
1617 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
1618 full_z_u[full_idx] = vals[i] * inv;
1619 }
1620 full_z_u
1621 }
1622
1623 pub fn pack_lambda_for_user(
1633 &self,
1634 y_c: &dyn Vector,
1635 y_d: &dyn Vector,
1636 cls: &BoundClassification,
1637 ) -> Vec<Number> {
1638 let mut lambda = vec![0.0; cls.n_full_g as usize];
1639 if cls.n_c > 0 {
1640 let Some(dy) = y_c.as_any().downcast_ref::<DenseVector>() else {
1641 panic!("OrigIpoptNlp expects DenseVector for y_c");
1642 };
1643 let vals = dy.expanded_values();
1644 let cs = self.c_scale.borrow();
1645 for (i, &g_idx) in cls.c_map.iter().enumerate() {
1646 lambda[g_idx as usize] = match cs.as_ref() {
1647 Some(v) => vals[i] * v[i],
1648 None => vals[i],
1649 };
1650 }
1651 }
1652 if cls.n_d > 0 {
1653 let Some(dy) = y_d.as_any().downcast_ref::<DenseVector>() else {
1654 panic!("OrigIpoptNlp expects DenseVector for y_d");
1655 };
1656 let vals = dy.expanded_values();
1657 let ds = self.d_scale.borrow();
1658 for (i, &g_idx) in cls.d_map.iter().enumerate() {
1659 lambda[g_idx as usize] = match ds.as_ref() {
1660 Some(v) => vals[i] * v[i],
1661 None => vals[i],
1662 };
1663 }
1664 }
1665 lambda
1666 }
1667
1668 fn fetch_warm_start_snapshot(&self) -> Option<StartingPointSnapshot> {
1671 let cls = self.adapter.borrow().classification().clone();
1672 let mut snapshot = StartingPointSnapshot {
1694 x: vec![0.0; cls.n_full_x as usize],
1695 z_l: vec![Number::NAN; cls.n_full_x as usize],
1696 z_u: vec![Number::NAN; cls.n_full_x as usize],
1697 lambda: vec![0.0; cls.n_full_g as usize],
1698 };
1699 let ok = {
1700 let a = self.adapter.borrow();
1701 let mut t = a.tnlp().borrow_mut();
1702 t.get_starting_point(StartingPoint {
1703 init_x: true,
1704 x: &mut snapshot.x,
1705 init_z: true,
1706 z_l: &mut snapshot.z_l,
1707 z_u: &mut snapshot.z_u,
1708 init_lambda: true,
1709 lambda: &mut snapshot.lambda,
1710 })
1711 };
1712 ok.then_some(snapshot)
1713 }
1714
1715 #[allow(clippy::too_many_arguments)]
1723 pub fn initialize_starting_point(
1724 &mut self,
1725 x: &mut DenseVector,
1726 init_x: bool,
1727 y_c: &mut DenseVector,
1728 init_y_c: bool,
1729 y_d: &mut DenseVector,
1730 init_y_d: bool,
1731 z_l: &mut DenseVector,
1732 init_z_l: bool,
1733 z_u: &mut DenseVector,
1734 init_z_u: bool,
1735 ) -> bool {
1736 let n_full_x = self.adapter.borrow().classification().n_full_x as usize;
1737 let n_full_g = self.adapter.borrow().classification().n_full_g as usize;
1738 let n_x_l = self.x_l.dim() as usize;
1739 let n_x_u = self.x_u.dim() as usize;
1740
1741 let mut full_x = vec![0.0; n_full_x];
1742 let mut full_z_l = vec![Number::NAN; n_full_x];
1747 let mut full_z_u = vec![Number::NAN; n_full_x];
1748 let mut full_lambda = vec![0.0; n_full_g];
1749
1750 let ok = {
1751 let a = self.adapter.borrow();
1752 let mut t = a.tnlp().borrow_mut();
1753 t.get_starting_point(StartingPoint {
1754 init_x,
1755 x: &mut full_x,
1756 init_z: init_z_l || init_z_u,
1757 z_l: &mut full_z_l,
1758 z_u: &mut full_z_u,
1759 init_lambda: init_y_c || init_y_d,
1760 lambda: &mut full_lambda,
1761 })
1762 };
1763 if !ok {
1764 return false;
1765 }
1766
1767 let cls = self.adapter.borrow().classification().clone();
1768 let obj_scal = self.obj_scale_factor.get();
1769 let c_scale = self.c_scale.borrow();
1770 let d_scale = self.d_scale.borrow();
1771
1772 if init_x {
1774 let xs = x.values_mut();
1775 for (var_idx, &full_idx) in cls.x_not_fixed_map.iter().enumerate() {
1776 xs[var_idx] = full_x[full_idx as usize];
1777 }
1778 }
1779 if init_y_c && cls.n_c > 0 {
1785 let yc = y_c.values_mut();
1786 for (i, &g_idx) in cls.c_map.iter().enumerate() {
1787 let cs = c_scale.as_ref().map(|v| v[i]).unwrap_or(1.0);
1788 yc[i] = full_lambda[g_idx as usize] / cs * obj_scal;
1789 }
1790 }
1791 if init_y_d && cls.n_d > 0 {
1792 let yd = y_d.values_mut();
1793 for (i, &g_idx) in cls.d_map.iter().enumerate() {
1794 let ds = d_scale.as_ref().map(|v| v[i]).unwrap_or(1.0);
1795 yd[i] = full_lambda[g_idx as usize] / ds * obj_scal;
1796 }
1797 }
1798 if init_z_l && n_x_l > 0 {
1800 let zl = z_l.values_mut();
1801 for (i, slot) in zl.iter_mut().enumerate().take(n_x_l) {
1802 let var_idx = cls.x_l_map[i] as usize;
1803 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
1804 *slot = full_z_l[full_idx] * obj_scal;
1805 }
1806 }
1807 if init_z_u && n_x_u > 0 {
1808 let zu = z_u.values_mut();
1809 for (i, slot) in zu.iter_mut().enumerate().take(n_x_u) {
1810 let var_idx = cls.x_u_map[i] as usize;
1811 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
1812 *slot = full_z_u[full_idx] * obj_scal;
1813 }
1814 }
1815 true
1816 }
1817
1818 fn eval_f_internal(&self, x: &dyn Vector) -> Number {
1821 if let Some(v) = self.f_cache.borrow().get_1dep(x.as_tagged()) {
1822 return v;
1823 }
1824 *self.f_evals.borrow_mut() += 1;
1825 let full_x = self.lift_x_to_full(x);
1826 let unscaled = {
1827 let a = self.adapter.borrow();
1828 let mut t = a.tnlp().borrow_mut();
1829 t.eval_f(&full_x, true).unwrap_or(f64::NAN)
1834 };
1835 let scaled = unscaled * self.obj_scale_factor.get();
1836 self.f_cache.borrow_mut().add_1dep(scaled, x.as_tagged());
1837 scaled
1838 }
1839
1840 fn eval_grad_f_internal(&self, x: &dyn Vector) -> Rc<dyn Vector> {
1841 if self.const_deriv.grad_f
1844 && let Some(v) = self.grad_f_cache.borrow().get(&[], &[])
1845 {
1846 return v;
1847 }
1848 if let Some(v) = self.grad_f_cache.borrow().get_1dep(x.as_tagged()) {
1849 return v;
1850 }
1851 *self.grad_f_evals.borrow_mut() += 1;
1852 let full_x = self.lift_x_to_full(x);
1853 let mut full_g = vec![0.0; full_x.len()];
1854 let ok = {
1855 let a = self.adapter.borrow();
1856 let mut t = a.tnlp().borrow_mut();
1857 t.eval_grad_f(&full_x, true, &mut full_g)
1858 };
1859 if !ok {
1862 full_g.fill(f64::NAN);
1863 }
1864 let cls = self.adapter.borrow().classification().clone();
1866 let mut g_compressed = self.x_space.make_new_dense();
1867 let obj_scal = self.obj_scale_factor.get();
1868 {
1869 let gv = g_compressed.values_mut();
1870 for (var_idx, &full_idx) in cls.x_not_fixed_map.iter().enumerate() {
1871 gv[var_idx] = full_g[full_idx as usize] * obj_scal;
1872 }
1873 }
1874 let reuse = self.const_deriv.grad_f && all_finite(g_compressed.values());
1879 let result: Rc<dyn Vector> = Rc::new(g_compressed);
1880 if reuse {
1881 self.grad_f_cache
1882 .borrow_mut()
1883 .add(Rc::clone(&result), &[], &[]);
1884 } else {
1885 self.grad_f_cache
1886 .borrow_mut()
1887 .add_1dep(Rc::clone(&result), x.as_tagged());
1888 }
1889 result
1890 }
1891
1892 fn full_g(&self, x: &dyn Vector) -> Rc<Vec<Number>> {
1898 if let Some(v) = self.full_g_cache.borrow().get_1dep(x.as_tagged()) {
1899 return v;
1900 }
1901 let n_full_g = self.adapter.borrow().classification().n_full_g as usize;
1902 let full_x = self.lift_x_to_full(x);
1903 let mut full_g = vec![0.0; n_full_g];
1904 let ok = {
1905 let a = self.adapter.borrow();
1906 let mut t = a.tnlp().borrow_mut();
1907 t.eval_g(&full_x, true, &mut full_g)
1908 };
1909 if !ok {
1910 full_g.fill(f64::NAN);
1911 }
1912 let result = Rc::new(full_g);
1913 self.full_g_cache
1914 .borrow_mut()
1915 .add_1dep(Rc::clone(&result), x.as_tagged());
1916 result
1917 }
1918
1919 fn full_jac_g(&self, x: &dyn Vector) -> Rc<Vec<Number>> {
1924 if let Some(v) = self.full_jac_g_cache.borrow().get_1dep(x.as_tagged()) {
1925 return v;
1926 }
1927 let mut full_vals = vec![0.0; self.nnz_jac_g_full as usize];
1928 let full_x = self.lift_x_to_full(x);
1929 let ok = {
1930 let a = self.adapter.borrow();
1931 let mut t = a.tnlp().borrow_mut();
1932 t.eval_jac_g(
1933 Some(&full_x),
1934 true,
1935 SparsityRequest::Values {
1936 values: &mut full_vals,
1937 },
1938 )
1939 };
1940 if !ok {
1941 full_vals.fill(f64::NAN);
1942 }
1943 let result = Rc::new(full_vals);
1944 self.full_jac_g_cache
1945 .borrow_mut()
1946 .add_1dep(Rc::clone(&result), x.as_tagged());
1947 result
1948 }
1949
1950 fn eval_c_internal(&self, x: &dyn Vector) -> Rc<dyn Vector> {
1951 let cls = self.adapter.borrow().classification().clone();
1952 if cls.n_c == 0 {
1953 if let Some(v) = self.c_cache.borrow().get_1dep(x.as_tagged()) {
1955 return v;
1956 }
1957 let v = self.c_space.make_new_dense();
1958 let result: Rc<dyn Vector> = Rc::new(v);
1959 self.c_cache
1960 .borrow_mut()
1961 .add_1dep(Rc::clone(&result), x.as_tagged());
1962 return result;
1963 }
1964 if let Some(v) = self.c_cache.borrow().get_1dep(x.as_tagged()) {
1965 return v;
1966 }
1967 *self.c_evals.borrow_mut() += 1;
1968 let full_g = self.full_g(x);
1973 let mut c = self.c_space.make_new_dense();
1974 {
1982 let cv = c.values_mut();
1983 let cs = self.c_scale.borrow();
1984 for (i, &g_idx) in cls.c_map.iter().enumerate() {
1985 let raw = full_g[g_idx as usize] - self.c_rhs[i];
1986 cv[i] = match cs.as_ref() {
1987 Some(v) => raw * v[i],
1988 None => raw,
1989 };
1990 }
1991 }
1992 let result: Rc<dyn Vector> = Rc::new(c);
1993 self.c_cache
1994 .borrow_mut()
1995 .add_1dep(Rc::clone(&result), x.as_tagged());
1996 result
1997 }
1998
1999 fn eval_d_internal(&self, x: &dyn Vector) -> Rc<dyn Vector> {
2000 let cls = self.adapter.borrow().classification().clone();
2001 if cls.n_d == 0 {
2002 if let Some(v) = self.d_cache.borrow().get_1dep(x.as_tagged()) {
2003 return v;
2004 }
2005 let v = self.d_space.make_new_dense();
2006 let result: Rc<dyn Vector> = Rc::new(v);
2007 self.d_cache
2008 .borrow_mut()
2009 .add_1dep(Rc::clone(&result), x.as_tagged());
2010 return result;
2011 }
2012 if let Some(v) = self.d_cache.borrow().get_1dep(x.as_tagged()) {
2013 return v;
2014 }
2015 *self.d_evals.borrow_mut() += 1;
2016 let full_g = self.full_g(x);
2018 let mut d = self.d_space.make_new_dense();
2019 {
2020 let dv = d.values_mut();
2021 let ds = self.d_scale.borrow();
2022 for (i, &g_idx) in cls.d_map.iter().enumerate() {
2023 let raw = full_g[g_idx as usize];
2024 dv[i] = match ds.as_ref() {
2025 Some(v) => raw * v[i],
2026 None => raw,
2027 };
2028 }
2029 }
2030 let result: Rc<dyn Vector> = Rc::new(d);
2031 self.d_cache
2032 .borrow_mut()
2033 .add_1dep(Rc::clone(&result), x.as_tagged());
2034 result
2035 }
2036
2037 fn eval_jac_c_internal(&self, x: &dyn Vector) -> Rc<dyn Matrix> {
2038 if self.const_deriv.jac_c
2039 && let Some(m) = self.jac_c_cache.borrow().get(&[], &[])
2040 {
2041 return m;
2042 }
2043 if let Some(m) = self.jac_c_cache.borrow().get_1dep(x.as_tagged()) {
2044 return m;
2045 }
2046 *self.jac_c_evals.borrow_mut() += 1;
2047 let full_vals = self.full_jac_g(x);
2051 let mut jac_c = GenTMatrix::new(Rc::clone(&self.jac_c_space));
2052 {
2053 let cs = self.c_scale.borrow();
2054 let irows = self.jac_c_space.irows().to_vec();
2055 let vs = jac_c.values_mut();
2056 for (k, &src) in self.jac_c_entry_in_g.iter().enumerate() {
2057 let raw = full_vals[src as usize];
2058 vs[k] = match cs.as_ref() {
2059 Some(v) => raw * v[(irows[k] - 1) as usize],
2061 None => raw,
2062 };
2063 }
2064 }
2065 let reuse = self.const_deriv.jac_c && all_finite(jac_c.values());
2066 let result: Rc<dyn Matrix> = Rc::new(jac_c);
2067 if reuse {
2068 self.jac_c_cache
2069 .borrow_mut()
2070 .add(Rc::clone(&result), &[], &[]);
2071 } else {
2072 self.jac_c_cache
2073 .borrow_mut()
2074 .add_1dep(Rc::clone(&result), x.as_tagged());
2075 }
2076 result
2077 }
2078
2079 fn eval_jac_d_internal(&self, x: &dyn Vector) -> Rc<dyn Matrix> {
2080 if self.const_deriv.jac_d
2081 && let Some(m) = self.jac_d_cache.borrow().get(&[], &[])
2082 {
2083 return m;
2084 }
2085 if let Some(m) = self.jac_d_cache.borrow().get_1dep(x.as_tagged()) {
2086 return m;
2087 }
2088 *self.jac_d_evals.borrow_mut() += 1;
2089 let full_vals = self.full_jac_g(x);
2092 let mut jac_d = GenTMatrix::new(Rc::clone(&self.jac_d_space));
2093 {
2094 let ds = self.d_scale.borrow();
2095 let irows = self.jac_d_space.irows().to_vec();
2096 let vs = jac_d.values_mut();
2097 for (k, &src) in self.jac_d_entry_in_g.iter().enumerate() {
2098 let raw = full_vals[src as usize];
2099 vs[k] = match ds.as_ref() {
2100 Some(v) => raw * v[(irows[k] - 1) as usize],
2101 None => raw,
2102 };
2103 }
2104 }
2105 let reuse = self.const_deriv.jac_d && all_finite(jac_d.values());
2106 let result: Rc<dyn Matrix> = Rc::new(jac_d);
2107 if reuse {
2108 self.jac_d_cache
2109 .borrow_mut()
2110 .add(Rc::clone(&result), &[], &[]);
2111 } else {
2112 self.jac_d_cache
2113 .borrow_mut()
2114 .add_1dep(Rc::clone(&result), x.as_tagged());
2115 }
2116 result
2117 }
2118
2119 fn eval_h_internal(
2120 &self,
2121 x: &dyn Vector,
2122 obj_factor: Number,
2123 y_c: &dyn Vector,
2124 y_d: &dyn Vector,
2125 ) -> Rc<dyn SymMatrix> {
2126 if self.const_deriv.hessian
2138 && let Some(m) = self.h_cache.borrow().get(&[], &[obj_factor])
2139 {
2140 return m;
2141 }
2142 if let Some(m) = self.h_cache.borrow().get(
2143 &[x.as_tagged(), y_c.as_tagged(), y_d.as_tagged()],
2144 &[obj_factor],
2145 ) {
2146 return m;
2147 }
2148 *self.h_evals.borrow_mut() += 1;
2149 let Some(h_space) = self.h_space.as_ref() else {
2150 panic!(
2151 "OrigIpoptNlp::eval_h called but the TNLP did not provide \
2152 eval_h sparsity. The L-BFGS path lands in Phase 8."
2153 );
2154 };
2155 let cls = self.adapter.borrow().classification().clone();
2156 let full_x = self.lift_x_to_full(x);
2157 let full_lambda = self.pack_lambda_for_user(y_c, y_d, &cls);
2165 let scaled_obj_factor = obj_factor * self.obj_scale_factor.get();
2166
2167 let mut full_vals = vec![0.0; self.nnz_h_lag_full as usize];
2172 let ok = {
2173 let a = self.adapter.borrow();
2174 let mut t = a.tnlp().borrow_mut();
2175 t.eval_h(
2176 Some(&full_x),
2177 true,
2178 scaled_obj_factor,
2179 Some(&full_lambda),
2180 true,
2181 SparsityRequest::Values {
2182 values: &mut full_vals,
2183 },
2184 )
2185 };
2186 if !ok {
2187 full_vals.fill(f64::NAN);
2188 }
2189 let mut h = SymTMatrix::new(Rc::clone(h_space));
2190 let kept = h_space.nonzeros() as usize;
2191 let h_vals = h.values_mut();
2192 debug_assert_eq!(kept, self.h_entry_in_full.len());
2195 for (k, &src) in self.h_entry_in_full.iter().enumerate() {
2196 h_vals[k] = full_vals[src as usize];
2197 }
2198 let reuse = self.const_deriv.hessian && all_finite(h.values());
2199 let result: Rc<dyn SymMatrix> = Rc::new(h);
2200 if reuse {
2201 self.h_cache
2202 .borrow_mut()
2203 .add(Rc::clone(&result), &[], &[obj_factor]);
2204 } else {
2205 self.h_cache.borrow_mut().add(
2206 Rc::clone(&result),
2207 &[x.as_tagged(), y_c.as_tagged(), y_d.as_tagged()],
2208 &[obj_factor],
2209 );
2210 }
2211 result
2212 }
2213}
2214
2215fn all_finite(v: &[Number]) -> bool {
2222 v.iter().all(|x| x.is_finite())
2223}
2224
2225fn make_dense_from(
2226 space: &Rc<DenseVectorSpace>,
2227 mut f: impl FnMut(usize) -> Number,
2228) -> DenseVector {
2229 let mut v = space.make_new_dense();
2230 let dim = space.dim() as usize;
2231 if dim > 0 {
2232 let vs = v.values_mut();
2233 for (i, slot) in vs.iter_mut().enumerate().take(dim) {
2234 *slot = f(i);
2235 }
2236 }
2237 v
2238}
2239
2240impl Nlp for OrigIpoptNlp {
2243 fn n(&self) -> Index {
2244 self.x_space.dim()
2245 }
2246 fn m_eq(&self) -> Index {
2247 self.c_space.dim()
2248 }
2249 fn m_ineq(&self) -> Index {
2250 self.d_space.dim()
2251 }
2252
2253 fn eval_f(&mut self, x: &dyn Vector) -> Number {
2254 self.timed_eval(|t| &t.eval_obj, || self.eval_f_internal(x))
2255 }
2256 fn eval_grad_f(&mut self, x: &dyn Vector, g: &mut dyn Vector) {
2257 let result = self.timed_eval(|t| &t.eval_grad_obj, || self.eval_grad_f_internal(x));
2258 g.copy(&*result);
2259 }
2260 fn eval_c(&mut self, x: &dyn Vector, c: &mut dyn Vector) {
2261 let result = self.timed_eval(|t| &t.eval_constr, || self.eval_c_internal(x));
2262 c.copy(&*result);
2263 }
2264 fn eval_d(&mut self, x: &dyn Vector, d: &mut dyn Vector) {
2265 let result = self.timed_eval(|t| &t.eval_constr, || self.eval_d_internal(x));
2266 d.copy(&*result);
2267 }
2268 fn eval_jac_c(&mut self, x: &dyn Vector) -> Rc<dyn Matrix> {
2269 self.timed_eval(|t| &t.eval_constr_jac, || self.eval_jac_c_internal(x))
2270 }
2271 fn eval_jac_d(&mut self, x: &dyn Vector) -> Rc<dyn Matrix> {
2272 self.timed_eval(|t| &t.eval_constr_jac, || self.eval_jac_d_internal(x))
2273 }
2274 fn eval_h(
2275 &mut self,
2276 x: &dyn Vector,
2277 obj_factor: Number,
2278 y_c: &dyn Vector,
2279 y_d: &dyn Vector,
2280 ) -> Rc<dyn SymMatrix> {
2281 self.timed_eval(
2282 |t| &t.eval_lag_hess,
2283 || self.eval_h_internal(x, obj_factor, y_c, y_d),
2284 )
2285 }
2286}
2287
2288impl IpoptNlp for OrigIpoptNlp {
2289 fn uninitialized_h(&self) -> Rc<dyn SymMatrix> {
2295 match self.h_space.as_ref() {
2296 Some(space) => Rc::new(crate::ipopt_nlp::zeroed_sym_t(Rc::clone(space))),
2297 None => Rc::new(crate::ipopt_nlp::zeroed_sym_t(SymTMatrixSpace::new(
2298 self.x_space.dim(),
2299 Vec::new(),
2300 Vec::new(),
2301 ))),
2302 }
2303 }
2304
2305 fn eval_counts(&self) -> [Index; 7] {
2306 [
2307 self.f_evals(),
2308 self.grad_f_evals(),
2309 self.c_evals(),
2310 self.d_evals(),
2311 self.jac_c_evals(),
2312 self.jac_d_evals(),
2313 self.h_evals(),
2314 ]
2315 }
2316 fn x_l(&self) -> &dyn Vector {
2317 &*self.x_l
2318 }
2319 fn x_u(&self) -> &dyn Vector {
2320 &*self.x_u
2321 }
2322 fn d_l(&self) -> &dyn Vector {
2323 &*self.d_l
2324 }
2325 fn d_u(&self) -> &dyn Vector {
2326 &*self.d_u
2327 }
2328
2329 fn declared_d_bounds(&self) -> Option<(Vec<Number>, Vec<Number>)> {
2330 let mut dl = self.declared_d_l.borrow().clone()?;
2331 let mut du = self.declared_d_u.borrow().clone()?;
2332 if let Some(dd) = self.d_scale.borrow().as_ref() {
2336 let cls = self.adapter.borrow().classification().clone();
2337 for (i, slot) in dl.iter_mut().enumerate() {
2338 *slot *= dd[cls.d_l_map[i] as usize];
2339 }
2340 for (i, slot) in du.iter_mut().enumerate() {
2341 *slot *= dd[cls.d_u_map[i] as usize];
2342 }
2343 }
2344 Some((dl, du))
2345 }
2346
2347 fn declared_box_violation(&self, x: &dyn Vector) -> Option<Number> {
2348 let x_l = self.declared_x_l.borrow();
2354 let x_u = self.declared_x_u.borrow();
2355 if x_l.is_none() && x_u.is_none() {
2356 return None;
2357 }
2358 let full = self.lift_x_to_full(x);
2359 let cls = self.adapter.borrow().classification().clone();
2360 let mut worst = 0.0_f64;
2361 if let Some(x_l) = x_l.as_ref() {
2362 for (i, &var_idx) in cls.x_l_map.iter().enumerate() {
2363 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
2364 let viol = x_l[i] - full[full_idx];
2365 if viol.is_finite() && viol > worst {
2366 worst = viol;
2367 }
2368 }
2369 }
2370 if let Some(x_u) = x_u.as_ref() {
2371 for (i, &var_idx) in cls.x_u_map.iter().enumerate() {
2372 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
2373 let viol = full[full_idx] - x_u[i];
2374 if viol.is_finite() && viol > worst {
2375 worst = viol;
2376 }
2377 }
2378 }
2379 Some(worst)
2380 }
2381
2382 fn declared_x_bounds(&self) -> Option<(Vec<Number>, Vec<Number>)> {
2383 let xl = self.declared_x_l.borrow().clone()?;
2388 let xu = self.declared_x_u.borrow().clone()?;
2389 Some((xl, xu))
2390 }
2391
2392 fn declared_c_rhs(&self) -> Option<Vec<Number>> {
2393 let mut b = self.c_rhs.clone();
2399 if let Some(dc) = self.c_scale.borrow().as_ref() {
2400 for (i, slot) in b.iter_mut().enumerate() {
2401 *slot *= dc[i];
2402 }
2403 }
2404 Some(b)
2405 }
2406
2407 fn px_l(&self) -> Rc<dyn Matrix> {
2408 Rc::clone(&self.px_l)
2409 }
2410 fn px_u(&self) -> Rc<dyn Matrix> {
2411 Rc::clone(&self.px_u)
2412 }
2413 fn pd_l(&self) -> Rc<dyn Matrix> {
2414 Rc::clone(&self.pd_l)
2415 }
2416 fn pd_u(&self) -> Rc<dyn Matrix> {
2417 Rc::clone(&self.pd_u)
2418 }
2419
2420 fn adjust_variable_bounds(
2428 &mut self,
2429 new_x_l: &dyn Vector,
2430 new_x_u: &dyn Vector,
2431 new_d_l: &dyn Vector,
2432 new_d_u: &dyn Vector,
2433 ) {
2434 fn install(slot: &mut Rc<DenseVector>, new: &dyn Vector) {
2438 Rc::get_mut(slot)
2439 .expect("adjust_variable_bounds: bound vector is uniquely owned")
2440 .copy(new);
2441 }
2442 install(&mut self.x_l, new_x_l);
2443 install(&mut self.x_u, new_x_u);
2444 install(&mut self.d_l, new_d_l);
2445 install(&mut self.d_u, new_d_u);
2446 }
2447
2448 fn obj_scaling_factor(&self) -> Number {
2449 self.obj_scale_factor.get()
2450 }
2451
2452 fn computed_obj_scaling_factor(&self) -> Number {
2453 self.computed_obj_scale.get()
2454 }
2455
2456 fn c_scale_vec(&self) -> Option<Vec<Number>> {
2457 self.c_scale.borrow().clone()
2458 }
2459
2460 fn d_scale_vec(&self) -> Option<Vec<Number>> {
2461 self.d_scale.borrow().clone()
2462 }
2463
2464 fn split_space_names(&self) -> Option<SplitNames> {
2478 let a = self.adapter.borrow();
2479 let cls = a.classification();
2480
2481 let mut var_meta = MetaData::default();
2482 let mut con_meta = MetaData::default();
2483 if !a
2484 .tnlp()
2485 .borrow_mut()
2486 .get_var_con_metadata(&mut var_meta, &mut con_meta)
2487 {
2488 return None;
2489 }
2490
2491 let var_full = var_meta.strings.get(IDX_NAMES);
2494 let con_full = con_meta.strings.get(IDX_NAMES);
2495 if var_full.is_none() && con_full.is_none() {
2496 return None;
2497 }
2498
2499 let pick = |pool: Option<&Vec<String>>, full_idx: Index| -> Option<String> {
2502 pool.and_then(|v| v.get(full_idx as usize))
2503 .filter(|s| !s.is_empty())
2504 .cloned()
2505 };
2506
2507 let x_var = cls
2508 .x_not_fixed_map
2509 .iter()
2510 .map(|&full_idx| pick(var_full, full_idx))
2511 .collect();
2512 let eq = cls
2513 .c_map
2514 .iter()
2515 .map(|&full_idx| pick(con_full, full_idx))
2516 .collect();
2517 let ineq = cls
2518 .d_map
2519 .iter()
2520 .map(|&full_idx| pick(con_full, full_idx))
2521 .collect();
2522
2523 let names = SplitNames { x_var, eq, ineq };
2524 names.any_present().then_some(names)
2525 }
2526
2527 fn prepare_warm_start(&mut self) -> bool {
2528 let Some(snapshot) = self.fetch_warm_start_snapshot() else {
2529 return false;
2530 };
2531 *self.warm_start_snapshot.borrow_mut() = Some(snapshot);
2532 true
2533 }
2534
2535 fn finish_warm_start(&mut self) {
2536 self.warm_start_snapshot.borrow_mut().take();
2537 }
2538
2539 fn get_starting_x(&mut self, x: &mut dyn Vector) -> bool {
2543 let cls = self.adapter.borrow().classification().clone();
2544 if let Some(snapshot) = self.warm_start_snapshot.borrow().as_ref() {
2545 let Some(dx) = x.as_any_mut().downcast_mut::<DenseVector>() else {
2546 return false;
2547 };
2548 for (var_idx, &full_idx) in cls.x_not_fixed_map.iter().enumerate() {
2549 dx.values_mut()[var_idx] = snapshot.x[full_idx as usize];
2550 }
2551 return true;
2552 }
2553 let n_full_x = cls.n_full_x as usize;
2554 let n_full_g = cls.n_full_g as usize;
2555 let mut full_x = vec![0.0; n_full_x];
2556 let mut full_z_l = vec![0.0; n_full_x];
2557 let mut full_z_u = vec![0.0; n_full_x];
2558 let mut full_lambda = vec![0.0; n_full_g];
2559 let ok = {
2560 let a = self.adapter.borrow();
2561 let mut t = a.tnlp().borrow_mut();
2562 t.get_starting_point(StartingPoint {
2563 init_x: true,
2564 x: &mut full_x,
2565 init_z: false,
2566 z_l: &mut full_z_l,
2567 z_u: &mut full_z_u,
2568 init_lambda: false,
2569 lambda: &mut full_lambda,
2570 })
2571 };
2572 if !ok {
2573 return false;
2574 }
2575 let Some(dx) = x.as_any_mut().downcast_mut::<DenseVector>() else {
2576 return false;
2577 };
2578 let xs = dx.values_mut();
2579 for (var_idx, &full_idx) in cls.x_not_fixed_map.iter().enumerate() {
2580 xs[var_idx] = full_x[full_idx as usize];
2581 }
2582 true
2583 }
2584
2585 fn get_starting_y(&mut self, y_c: &mut dyn Vector, y_d: &mut dyn Vector) -> bool {
2586 let Some(y_c) = y_c.as_any_mut().downcast_mut::<DenseVector>() else {
2587 return false;
2588 };
2589 let Some(y_d) = y_d.as_any_mut().downcast_mut::<DenseVector>() else {
2590 return false;
2591 };
2592 if let Some(snapshot) = self.warm_start_snapshot.borrow().as_ref() {
2593 let cls = self.adapter.borrow().classification().clone();
2594 let obj_scal = self.obj_scale_factor.get();
2595 let c_scale = self.c_scale.borrow();
2596 for (i, &g_idx) in cls.c_map.iter().enumerate() {
2597 let cs = c_scale.as_ref().map(|v| v[i]).unwrap_or(1.0);
2598 y_c.values_mut()[i] = snapshot.lambda[g_idx as usize] / cs * obj_scal;
2599 }
2600 let d_scale = self.d_scale.borrow();
2601 for (i, &g_idx) in cls.d_map.iter().enumerate() {
2602 let ds = d_scale.as_ref().map(|v| v[i]).unwrap_or(1.0);
2603 y_d.values_mut()[i] = snapshot.lambda[g_idx as usize] / ds * obj_scal;
2604 }
2605 return true;
2606 }
2607 let mut x = DenseVectorSpace::new(self.n()).make_new_dense();
2608 let mut z_l = DenseVectorSpace::new(self.x_l.dim()).make_new_dense();
2609 let mut z_u = DenseVectorSpace::new(self.x_u.dim()).make_new_dense();
2610 self.initialize_starting_point(
2611 &mut x, false, y_c, true, y_d, true, &mut z_l, false, &mut z_u, false,
2612 )
2613 }
2614
2615 fn get_starting_z(
2616 &mut self,
2617 z_l: &mut dyn Vector,
2618 z_u: &mut dyn Vector,
2619 _v_l: &mut dyn Vector,
2620 _v_u: &mut dyn Vector,
2621 ) -> bool {
2622 let Some(z_l) = z_l.as_any_mut().downcast_mut::<DenseVector>() else {
2625 return false;
2626 };
2627 let Some(z_u) = z_u.as_any_mut().downcast_mut::<DenseVector>() else {
2628 return false;
2629 };
2630 if let Some(snapshot) = self.warm_start_snapshot.borrow().as_ref() {
2631 let cls = self.adapter.borrow().classification().clone();
2632 let obj_scal = self.obj_scale_factor.get();
2633 for (i, slot) in z_l.values_mut().iter_mut().enumerate() {
2634 let var_idx = cls.x_l_map[i] as usize;
2635 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
2636 *slot = snapshot.z_l[full_idx] * obj_scal;
2637 }
2638 for (i, slot) in z_u.values_mut().iter_mut().enumerate() {
2639 let var_idx = cls.x_u_map[i] as usize;
2640 let full_idx = cls.x_not_fixed_map[var_idx] as usize;
2641 *slot = snapshot.z_u[full_idx] * obj_scal;
2642 }
2643 return true;
2644 }
2645 let mut x = DenseVectorSpace::new(self.n()).make_new_dense();
2646 let mut y_c = DenseVectorSpace::new(self.m_eq()).make_new_dense();
2647 let mut y_d = DenseVectorSpace::new(self.m_ineq()).make_new_dense();
2648 self.initialize_starting_point(
2649 &mut x, false, &mut y_c, false, &mut y_d, false, z_l, true, z_u, true,
2650 )
2651 }
2652
2653 fn lift_x_to_full(&self, x: &dyn Vector) -> Vec<Number> {
2654 OrigIpoptNlp::lift_x_to_full(self, x)
2655 }
2656
2657 fn finalize_solution_x(&self, x: &dyn Vector) -> Vec<Number> {
2658 OrigIpoptNlp::finalize_solution_x(self, x)
2659 }
2660
2661 fn n_full_x(&self) -> Index {
2662 self.adapter.borrow().classification().n_full_x
2663 }
2664
2665 fn n_full_g(&self) -> Index {
2666 self.adapter.borrow().classification().n_full_g
2667 }
2668
2669 fn pack_lambda_for_user(&self, y_c: &dyn Vector, y_d: &dyn Vector) -> Vec<Number> {
2670 let cls = self.adapter.borrow().classification().clone();
2671 OrigIpoptNlp::pack_lambda_for_user(self, y_c, y_d, &cls)
2672 }
2673
2674 fn pack_g_for_user(&self, c: &dyn Vector, d: &dyn Vector) -> Vec<Number> {
2675 let cls = self.adapter.borrow().classification().clone();
2676 let mut g = vec![0.0; cls.n_full_g as usize];
2677 if cls.n_c > 0 {
2678 let Some(dc) = c.as_any().downcast_ref::<DenseVector>() else {
2679 panic!("OrigIpoptNlp expects DenseVector for c");
2680 };
2681 let cs = self.c_scale.borrow();
2682 let c_vals = dc.expanded_values();
2692 for (i, &g_idx) in cls.c_map.iter().enumerate() {
2693 let v = c_vals[i];
2694 g[g_idx as usize] = match cs.as_ref() {
2695 Some(s) => v / s[i],
2696 None => v,
2697 };
2698 }
2699 }
2700 if cls.n_d > 0 {
2701 let Some(dd) = d.as_any().downcast_ref::<DenseVector>() else {
2702 panic!("OrigIpoptNlp expects DenseVector for d");
2703 };
2704 let ds = self.d_scale.borrow();
2705 let d_vals = dd.expanded_values();
2707 for (i, &g_idx) in cls.d_map.iter().enumerate() {
2708 let v = d_vals[i];
2709 g[g_idx as usize] = match ds.as_ref() {
2710 Some(s) => v / s[i],
2711 None => v,
2712 };
2713 }
2714 }
2715 g
2716 }
2717
2718 fn pack_z_l_for_user(&self, z_l: &dyn Vector) -> Vec<Number> {
2719 let cls = self.adapter.borrow().classification().clone();
2720 let mut full = vec![0.0; cls.n_full_x as usize];
2721 if z_l.dim() == 0 {
2722 return full;
2723 }
2724 let Some(dz) = z_l.as_any().downcast_ref::<DenseVector>() else {
2725 panic!("OrigIpoptNlp expects DenseVector for z_l");
2726 };
2727 let vals = dz.expanded_values();
2728 for (k, &var_idx) in cls.x_l_map.iter().enumerate() {
2729 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
2730 full[full_idx] = vals[k];
2731 }
2732 full
2733 }
2734
2735 fn pack_z_u_for_user(&self, z_u: &dyn Vector) -> Vec<Number> {
2736 let cls = self.adapter.borrow().classification().clone();
2737 let mut full = vec![0.0; cls.n_full_x as usize];
2738 if z_u.dim() == 0 {
2739 return full;
2740 }
2741 let Some(dz) = z_u.as_any().downcast_ref::<DenseVector>() else {
2742 panic!("OrigIpoptNlp expects DenseVector for z_u");
2743 };
2744 let vals = dz.expanded_values();
2745 for (k, &var_idx) in cls.x_u_map.iter().enumerate() {
2746 let full_idx = cls.x_not_fixed_map[var_idx as usize] as usize;
2747 full[full_idx] = vals[k];
2748 }
2749 full
2750 }
2751
2752 fn finalize_solution_lambda(&self, y_c: &dyn Vector, y_d: &dyn Vector) -> Vec<Number> {
2753 OrigIpoptNlp::finalize_solution_lambda(self, y_c, y_d)
2754 }
2755
2756 fn finalize_solution_z_l(&self, z_l: &dyn Vector) -> Vec<Number> {
2757 OrigIpoptNlp::finalize_solution_z_l(self, z_l)
2758 }
2759
2760 fn finalize_solution_z_u(&self, z_u: &dyn Vector) -> Vec<Number> {
2761 OrigIpoptNlp::finalize_solution_z_u(self, z_u)
2762 }
2763
2764 fn variable_scaling(&self) -> Option<Vec<Number>> {
2765 self.adapter.borrow().tnlp().borrow().scaling_factors()
2771 }
2772
2773 fn full_x_to_var_x(&self, full_idx: Index) -> Option<Index> {
2774 let cls = self.adapter.borrow();
2775 let cls = cls.classification();
2776 let f = full_idx as usize;
2777 if f >= cls.full_to_var.len() {
2778 return None;
2779 }
2780 let v = cls.full_to_var[f];
2781 if v < 0 { None } else { Some(v) }
2782 }
2783
2784 fn full_g_to_c_block(&self, full_idx: Index) -> Option<Index> {
2785 let cls = self.adapter.borrow();
2786 let cls = cls.classification();
2787 let f = full_idx as usize;
2788 if f >= cls.full_to_c.len() {
2789 return None;
2790 }
2791 let c = cls.full_to_c[f];
2792 if c < 0 { None } else { Some(c) }
2793 }
2794
2795 fn full_g_to_d_block(&self, full_idx: Index) -> Option<Index> {
2796 let cls = self.adapter.borrow();
2797 let cls = cls.classification();
2798 let f = full_idx as usize;
2799 if f >= cls.full_to_d.len() {
2800 return None;
2801 }
2802 let d = cls.full_to_d[f];
2803 if d < 0 { None } else { Some(d) }
2804 }
2805
2806 fn var_x_to_full_x(&self, var_idx: Index) -> Index {
2807 let cls = self.adapter.borrow();
2808 let cls = cls.classification();
2809 cls.x_not_fixed_map[var_idx as usize]
2810 }
2811}
2812
2813#[cfg(test)]
2816mod tests {
2817 use super::*;
2818 use crate::tnlp::{
2819 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest,
2820 StartingPoint, TNLP,
2821 };
2822
2823 #[derive(Default)]
2828 struct Hs071 {
2829 eval_f_calls: usize,
2830 eval_grad_f_calls: usize,
2831 eval_g_calls: usize,
2832 eval_jac_g_value_calls: usize,
2833 eval_h_value_calls: usize,
2834 get_bounds_info_calls: usize,
2835 get_starting_point_calls: usize,
2836 }
2837
2838 impl TNLP for Hs071 {
2839 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2840 Some(NlpInfo {
2841 n: 4,
2842 m: 2,
2843 nnz_jac_g: 8,
2844 nnz_h_lag: 10,
2845 index_style: IndexStyle::C,
2846 })
2847 }
2848 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2849 self.get_bounds_info_calls += 1;
2850 b.x_l.copy_from_slice(&[1.0; 4]);
2851 b.x_u.copy_from_slice(&[5.0; 4]);
2852 b.g_l.copy_from_slice(&[25.0, 40.0]);
2855 b.g_u.copy_from_slice(&[2.0e19, 40.0]);
2856 true
2857 }
2858 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2859 self.get_starting_point_calls += 1;
2860 sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
2861 if sp.init_z {
2862 sp.z_l.copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
2863 sp.z_u.copy_from_slice(&[5.0, 6.0, 7.0, 8.0]);
2864 }
2865 if sp.init_lambda {
2866 sp.lambda.copy_from_slice(&[11.0, 13.0]);
2867 }
2868 true
2869 }
2870 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2871 self.eval_f_calls += 1;
2872 Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
2873 }
2874 fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2875 self.eval_grad_f_calls += 1;
2876 g[0] = x[3] * (2.0 * x[0] + x[1] + x[2]);
2881 g[1] = x[0] * x[3];
2882 g[2] = x[0] * x[3] + 1.0;
2883 g[3] = x[0] * (x[0] + x[1] + x[2]);
2884 true
2885 }
2886 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2887 self.eval_g_calls += 1;
2888 g[0] = x[0] * x[1] * x[2] * x[3];
2891 g[1] = x[0] * x[0] + x[1] * x[1] + x[2] * x[2] + x[3] * x[3];
2892 true
2893 }
2894 fn eval_jac_g(
2895 &mut self,
2896 x: Option<&[Number]>,
2897 _new_x: bool,
2898 mode: SparsityRequest<'_>,
2899 ) -> bool {
2900 match mode {
2901 SparsityRequest::Structure { irow, jcol } => {
2902 irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
2904 jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
2905 }
2906 SparsityRequest::Values { values } => {
2907 self.eval_jac_g_value_calls += 1;
2908 let x = x.expect("eval_jac_g(Values) without x");
2909 values[0] = x[1] * x[2] * x[3];
2911 values[1] = x[0] * x[2] * x[3];
2912 values[2] = x[0] * x[1] * x[3];
2913 values[3] = x[0] * x[1] * x[2];
2914 values[4] = 2.0 * x[0];
2916 values[5] = 2.0 * x[1];
2917 values[6] = 2.0 * x[2];
2918 values[7] = 2.0 * x[3];
2919 }
2920 }
2921 true
2922 }
2923 fn eval_h(
2924 &mut self,
2925 x: Option<&[Number]>,
2926 _new_x: bool,
2927 obj_factor: Number,
2928 lambda: Option<&[Number]>,
2929 _new_lambda: bool,
2930 mode: SparsityRequest<'_>,
2931 ) -> bool {
2932 match mode {
2935 SparsityRequest::Structure { irow, jcol } => {
2936 irow.copy_from_slice(&[0, 1, 1, 2, 2, 2, 3, 3, 3, 3]);
2937 jcol.copy_from_slice(&[0, 0, 1, 0, 1, 2, 0, 1, 2, 3]);
2938 }
2939 SparsityRequest::Values { values } => {
2940 self.eval_h_value_calls += 1;
2941 let x = x.expect("eval_h(Values) without x");
2942 let lam = lambda.expect("eval_h(Values) without lambda");
2943 let of = obj_factor;
2944 let l0 = lam[0];
2954 let l1 = lam[1];
2955 values[0] = of * (2.0 * x[3]) + l1 * 2.0; values[1] = of * x[3] + l0 * (x[2] * x[3]); values[2] = l1 * 2.0; values[3] = of * x[3] + l0 * (x[1] * x[3]); values[4] = l0 * (x[0] * x[3]); values[5] = l1 * 2.0; values[6] = of * (2.0 * x[0] + x[1] + x[2]) + l0 * (x[1] * x[2]); values[7] = of * x[0] + l0 * (x[0] * x[2]); values[8] = of * x[0] + l0 * (x[0] * x[1]); values[9] = l1 * 2.0; }
2966 }
2967 true
2968 }
2969 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
2970 }
2971
2972 fn build_orig_nlp() -> (Rc<RefCell<TNLPAdapter>>, OrigIpoptNlp) {
2973 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Hs071::default()));
2974 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
2975 let nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
2976 (adapter, nlp)
2977 }
2978
2979 fn dense_x(values: &[Number], space: &Rc<DenseVectorSpace>) -> DenseVector {
2980 let mut v = space.make_new_dense();
2981 v.values_mut().copy_from_slice(values);
2982 v
2983 }
2984
2985 #[test]
2986 fn dimensions_match_classification() {
2987 let (_, nlp) = build_orig_nlp();
2988 assert_eq!(nlp.n(), 4);
2990 assert_eq!(nlp.m_eq(), 1);
2991 assert_eq!(nlp.m_ineq(), 1);
2992 assert_eq!(nlp.jac_c_space().nonzeros(), 4);
2994 assert_eq!(nlp.jac_d_space().nonzeros(), 4);
2995 assert_eq!(nlp.h_space().unwrap().nonzeros(), 10);
2997 assert_eq!(nlp.x_l().dim(), 4);
2999 assert_eq!(nlp.x_u().dim(), 4);
3000 assert_eq!(nlp.d_l().dim(), 1);
3001 assert_eq!(nlp.d_u().dim(), 0);
3002 }
3003
3004 #[test]
3005 fn eval_f_at_starting_point() {
3006 let (_, mut nlp) = build_orig_nlp();
3007 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3008 assert_eq!(nlp.eval_f(&x), 16.0);
3010 assert_eq!(nlp.f_evals(), 1);
3011 }
3012
3013 #[test]
3014 fn eval_grad_f_at_starting_point() {
3015 let (_, mut nlp) = build_orig_nlp();
3016 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3017 let mut g = nlp.x_space().make_new_dense();
3018 nlp.eval_grad_f(&x, &mut g);
3019 assert_eq!(g.values(), &[12.0, 1.0, 2.0, 11.0]);
3024 assert_eq!(nlp.grad_f_evals(), 1);
3025 }
3026
3027 #[test]
3028 fn eval_c_returns_equality_residual() {
3029 let (_, mut nlp) = build_orig_nlp();
3030 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3031 let mut c = nlp.c_space().make_new_dense();
3032 nlp.eval_c(&x, &mut c);
3033 assert_eq!(c.values(), &[12.0]);
3035 assert_eq!(nlp.c_evals(), 1);
3036 }
3037
3038 #[test]
3039 fn eval_d_returns_inequality_value_unshifted() {
3040 let (_, mut nlp) = build_orig_nlp();
3041 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3042 let mut d = nlp.d_space().make_new_dense();
3043 nlp.eval_d(&x, &mut d);
3044 assert_eq!(d.values(), &[25.0]);
3046 assert_eq!(nlp.d_evals(), 1);
3047 }
3048
3049 #[test]
3050 fn cache_returns_without_re_eval() {
3051 let (_, mut nlp) = build_orig_nlp();
3052 let mut x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3053 let f1 = nlp.eval_f(&x);
3054 let f2 = nlp.eval_f(&x);
3055 assert_eq!(f1, f2);
3056 assert_eq!(nlp.f_evals(), 1, "second call must be served from cache");
3057 x.values_mut()[0] = 1.0; let _ = nlp.eval_f(&x);
3060 assert_eq!(nlp.f_evals(), 2);
3061 }
3062
3063 #[test]
3064 fn jac_c_picks_only_equality_rows() {
3065 let (_, mut nlp) = build_orig_nlp();
3066 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3067 let m = nlp.eval_jac_c(&x);
3068 let g = m
3069 .as_any()
3070 .downcast_ref::<GenTMatrix>()
3071 .expect("jac_c is a GenTMatrix");
3072 assert_eq!(g.values(), &[2.0, 10.0, 10.0, 2.0]);
3074 assert_eq!(g.irows(), &[1, 1, 1, 1]);
3076 assert_eq!(g.jcols(), &[1, 2, 3, 4]);
3077 }
3078
3079 #[test]
3080 fn jac_d_picks_only_inequality_rows() {
3081 let (_, mut nlp) = build_orig_nlp();
3082 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3083 let m = nlp.eval_jac_d(&x);
3084 let g = m
3085 .as_any()
3086 .downcast_ref::<GenTMatrix>()
3087 .expect("jac_d is a GenTMatrix");
3088 assert_eq!(g.values(), &[25.0, 5.0, 5.0, 25.0]);
3091 }
3092
3093 fn build_orig_nlp_counting() -> (Rc<RefCell<Hs071>>, OrigIpoptNlp) {
3098 let concrete = Rc::new(RefCell::new(Hs071::default()));
3099 let tnlp: Rc<RefCell<dyn TNLP>> = concrete.clone();
3100 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3101 let nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3102 (concrete, nlp)
3103 }
3104
3105 #[test]
3106 fn eval_c_and_eval_d_share_one_eval_g_per_iterate() {
3107 let (tnlp, mut nlp) = build_orig_nlp_counting();
3111 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3112 let mut c = nlp.c_space().make_new_dense();
3113 let mut d = nlp.d_space().make_new_dense();
3114 nlp.eval_c(&x, &mut c);
3115 nlp.eval_d(&x, &mut d);
3116 assert_eq!(
3117 tnlp.borrow().eval_g_calls,
3118 1,
3119 "eval_c + eval_d at one iterate must share a single user eval_g"
3120 );
3121 assert_eq!(nlp.c_evals(), 1);
3123 assert_eq!(nlp.d_evals(), 1);
3124 assert_eq!(c.values(), &[12.0]);
3126 assert_eq!(d.values(), &[25.0]);
3127
3128 let mut x2 = x;
3131 x2.values_mut()[0] = 2.0;
3132 nlp.eval_c(&x2, &mut c);
3133 nlp.eval_d(&x2, &mut d);
3134 assert_eq!(
3135 tnlp.borrow().eval_g_calls,
3136 2,
3137 "a new iterate triggers exactly one more shared eval_g"
3138 );
3139 }
3140
3141 #[test]
3142 fn eval_c_does_not_refetch_bounds_per_iterate() {
3143 let (tnlp, mut nlp) = build_orig_nlp_counting();
3151 let baseline = tnlp.borrow().get_bounds_info_calls;
3154
3155 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3156 let mut c = nlp.c_space().make_new_dense();
3157 nlp.eval_c(&x, &mut c);
3158 assert_eq!(c.values(), &[12.0]);
3160
3161 let mut x2 = x;
3163 for k in 0..5 {
3164 x2.values_mut()[0] = 2.0 + k as Number;
3165 nlp.eval_c(&x2, &mut c);
3166 }
3167
3168 assert_eq!(
3169 tnlp.borrow().get_bounds_info_calls,
3170 baseline,
3171 "eval_c must reuse the captured c_rhs, not re-fetch bounds per iterate"
3172 );
3173 }
3174
3175 #[test]
3176 fn eval_jac_c_and_eval_jac_d_share_one_eval_jac_g_per_iterate() {
3177 let (tnlp, mut nlp) = build_orig_nlp_counting();
3181 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
3182 let _ = nlp.eval_jac_c(&x);
3183 let _ = nlp.eval_jac_d(&x);
3184 assert_eq!(
3185 tnlp.borrow().eval_jac_g_value_calls,
3186 1,
3187 "eval_jac_c + eval_jac_d at one iterate must share a single eval_jac_g"
3188 );
3189 assert_eq!(nlp.jac_c_evals(), 1);
3190 assert_eq!(nlp.jac_d_evals(), 1);
3191 }
3192
3193 #[test]
3194 fn starting_point_is_compressed_into_x_var() {
3195 let (_, mut nlp) = build_orig_nlp();
3196 let mut x = nlp.x_space().make_new_dense();
3197 let mut yc = nlp.c_space().make_new_dense();
3198 let mut yd = nlp.d_space().make_new_dense();
3199 let mut zl = nlp.x_l_space().make_new_dense();
3200 let mut zu = nlp.x_u_space().make_new_dense();
3201 let ok = nlp.initialize_starting_point(
3202 &mut x, true, &mut yc, false, &mut yd, false, &mut zl, false, &mut zu, false,
3203 );
3204 assert!(ok);
3205 assert_eq!(x.values(), &[1.0, 5.0, 5.0, 1.0]);
3206 }
3207
3208 #[test]
3209 fn warm_start_duals_are_forwarded_into_algorithm_vectors() {
3210 let (_, mut nlp) = build_orig_nlp();
3211 let mut y_c = nlp.c_space().make_new_dense();
3212 let mut y_d = nlp.d_space().make_new_dense();
3213 assert!(nlp.get_starting_y(&mut y_c, &mut y_d));
3214 assert_eq!(y_c.values(), &[13.0], "equality multiplier g1");
3215 assert_eq!(y_d.values(), &[11.0], "inequality multiplier g0");
3216
3217 let mut z_l = nlp.x_l_space().make_new_dense();
3218 let mut z_u = nlp.x_u_space().make_new_dense();
3219 let mut v_l = nlp.d_l_space().make_new_dense();
3220 let mut v_u = nlp.d_u_space().make_new_dense();
3221 assert!(nlp.get_starting_z(&mut z_l, &mut z_u, &mut v_l, &mut v_u));
3222 assert_eq!(z_l.values(), &[1.0, 2.0, 3.0, 4.0]);
3223 assert_eq!(z_u.values(), &[5.0, 6.0, 7.0, 8.0]);
3224 }
3225
3226 #[test]
3227 fn warm_start_prefetches_one_tnlp_snapshot_for_x_y_and_z() {
3228 let (tnlp, mut nlp) = build_orig_nlp_counting();
3229 assert!(nlp.prepare_warm_start());
3230
3231 let mut x = nlp.x_space().make_new_dense();
3232 let mut y_c = nlp.c_space().make_new_dense();
3233 let mut y_d = nlp.d_space().make_new_dense();
3234 let mut z_l = nlp.x_l_space().make_new_dense();
3235 let mut z_u = nlp.x_u_space().make_new_dense();
3236 let mut v_l = nlp.d_l_space().make_new_dense();
3237 let mut v_u = nlp.d_u_space().make_new_dense();
3238 assert!(nlp.get_starting_x(&mut x));
3239 assert!(nlp.get_starting_y(&mut y_c, &mut y_d));
3240 assert!(nlp.get_starting_z(&mut z_l, &mut z_u, &mut v_l, &mut v_u));
3241
3242 assert_eq!(tnlp.borrow().get_starting_point_calls, 1);
3243 assert_eq!(x.values(), &[1.0, 5.0, 5.0, 1.0]);
3244 assert_eq!(y_c.values(), &[13.0]);
3245 assert_eq!(y_d.values(), &[11.0]);
3246 assert_eq!(z_l.values(), &[1.0, 2.0, 3.0, 4.0]);
3247 assert_eq!(z_u.values(), &[5.0, 6.0, 7.0, 8.0]);
3248
3249 nlp.finish_warm_start();
3250 let mut x_after_init = nlp.x_space().make_new_dense();
3251 assert!(nlp.get_starting_x(&mut x_after_init));
3252 assert_eq!(
3253 tnlp.borrow().get_starting_point_calls,
3254 2,
3255 "the snapshot must not affect later starting-point requests"
3256 );
3257 }
3258
3259 struct OneFixedOneFree;
3264 impl TNLP for OneFixedOneFree {
3265 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3266 Some(NlpInfo {
3267 n: 2,
3268 m: 1,
3269 nnz_jac_g: 1,
3270 nnz_h_lag: 0,
3271 index_style: IndexStyle::C,
3272 })
3273 }
3274 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3275 b.x_l[0] = 7.0;
3276 b.x_u[0] = 7.0; b.x_l[1] = -1.0e19;
3278 b.x_u[1] = 1.0e19;
3279 b.g_l[0] = 0.0;
3280 b.g_u[0] = 0.0; true
3282 }
3283 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3284 sp.x[0] = 7.0;
3285 sp.x[1] = 0.5;
3286 true
3287 }
3288 fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
3289 Some(x[1])
3290 }
3291 fn eval_grad_f(&mut self, _: &[Number], _: bool, g: &mut [Number]) -> bool {
3292 g[0] = 0.0;
3293 g[1] = 1.0;
3294 true
3295 }
3296 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3297 g[0] = x[1];
3298 true
3299 }
3300 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3301 match m {
3302 SparsityRequest::Structure { irow, jcol } => {
3303 irow[0] = 0;
3304 jcol[0] = 1;
3305 }
3306 SparsityRequest::Values { values } => values[0] = 1.0,
3307 }
3308 true
3309 }
3310 fn eval_h(
3311 &mut self,
3312 _: Option<&[Number]>,
3313 _: bool,
3314 _: Number,
3315 _: Option<&[Number]>,
3316 _: bool,
3317 _: SparsityRequest<'_>,
3318 ) -> bool {
3319 true
3320 }
3321 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3322 }
3323
3324 struct FixedVarReachModel {
3339 second_order: bool,
3340 }
3341 impl TNLP for FixedVarReachModel {
3342 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3343 Some(NlpInfo {
3344 n: 3,
3345 m: 2,
3346 nnz_jac_g: 4,
3347 nnz_h_lag: 3,
3348 index_style: IndexStyle::C,
3349 })
3350 }
3351 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3352 b.x_l.copy_from_slice(&[-1.0e19, -1.0e19, 2.0]);
3353 b.x_u.copy_from_slice(&[1.0e19, 1.0e19, 2.0]); b.g_l.copy_from_slice(&[0.0, 0.0]);
3355 b.g_u.copy_from_slice(&[0.0, 1.0e19]); true
3357 }
3358 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3359 sp.x.copy_from_slice(&[1.0, 1.0, 2.0]);
3360 true
3361 }
3362 fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
3363 Some(x[0] * x[0] * x[0])
3364 }
3365 fn eval_grad_f(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3366 g.copy_from_slice(&[3.0 * x[0] * x[0], 0.0, 0.0]);
3367 true
3368 }
3369 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3370 g[0] = x[0] * x[1] - 1.0;
3371 g[1] = if self.second_order {
3372 x[1] * x[2]
3373 } else {
3374 x[1] * x[1] + x[2]
3375 };
3376 true
3377 }
3378 fn eval_jac_g(&mut self, x: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3379 match m {
3380 SparsityRequest::Structure { irow, jcol } => {
3381 irow.copy_from_slice(&[0, 0, 1, 1]);
3384 jcol.copy_from_slice(&[0, 1, 1, 2]);
3385 }
3386 SparsityRequest::Values { values } => {
3387 let x = x.expect("values need x");
3388 values[0] = x[1];
3389 values[1] = x[0];
3390 let (d1, d2) = if self.second_order {
3391 (x[2], x[1])
3392 } else {
3393 (2.0 * x[1], 1.0)
3394 };
3395 values[2] = d1;
3396 values[3] = d2;
3397 }
3398 }
3399 true
3400 }
3401 fn eval_h(
3402 &mut self,
3403 x: Option<&[Number]>,
3404 _: bool,
3405 sigma: Number,
3406 lambda: Option<&[Number]>,
3407 _: bool,
3408 m: SparsityRequest<'_>,
3409 ) -> bool {
3410 match m {
3411 SparsityRequest::Structure { irow, jcol } => {
3412 irow.copy_from_slice(&[0, 1, if self.second_order { 2 } else { 1 }]);
3416 jcol.copy_from_slice(&[0, 0, 1]);
3417 }
3418 SparsityRequest::Values { values } => {
3419 let (x, l) = (x.expect("values need x"), lambda.expect("values need λ"));
3420 values[0] = sigma * 6.0 * x[0];
3421 values[1] = l[0];
3422 values[2] = if self.second_order { l[1] } else { 2.0 * l[1] };
3423 }
3424 }
3425 true
3426 }
3427 fn derivative_proofs(&mut self) -> DerivativeProofs {
3428 DerivativeProofs {
3429 grad_f: DerivativeProof::Varying,
3430 hessian: DerivativeProof::Varying,
3431 jac: vec![DerivativeProof::Varying; 2],
3432 }
3433 }
3434 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3435 }
3436
3437 fn reach_proofs(second_order: bool) -> [DerivativeProof; 4] {
3438 let tnlp: Rc<RefCell<dyn TNLP>> =
3439 Rc::new(RefCell::new(FixedVarReachModel { second_order }));
3440 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3441 let nlp = OrigIpoptNlp::new(adapter, Rc::new(NoScaling)).unwrap();
3442 nlp.derivative_proofs()
3443 }
3444
3445 #[test]
3454 fn only_the_forms_a_fixed_variable_reaches_lose_their_refusal() {
3455 use DerivativeProof::*;
3456 let [grad_f, hessian, jac_c, jac_d] = reach_proofs(false);
3457 assert_eq!(grad_f, Varying, "x[2] is nowhere in ∇f");
3458 assert_eq!(hessian, Varying, "x[2] appears linearly; ∇²L cannot see it");
3459 assert_eq!(jac_c, Varying, "row 0 has no nonzero in the fixed column");
3460 assert_eq!(jac_d, Unknown, "row 1 does, so fixing x[2] may flatten it");
3461 }
3462
3463 #[test]
3468 fn a_second_order_coupling_to_a_fixed_variable_weakens_the_hessian() {
3469 use DerivativeProof::*;
3470 let [grad_f, hessian, jac_c, jac_d] = reach_proofs(true);
3471 assert_eq!(hessian, Unknown, "x1·x2 puts a fixed index in ∇²L");
3472 assert_eq!(grad_f, Unknown, "same test, and ∇f is reached the same way");
3473 assert_eq!(jac_c, Varying, "row 0 is still untouched by x[2]");
3474 assert_eq!(jac_d, Unknown);
3475 }
3476
3477 #[test]
3478 fn ipopt_nlp_index_mapping_methods_handle_fixed_var() {
3479 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedOneFree));
3480 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3481 let nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3482
3483 assert_eq!(nlp.n_full_x(), 2);
3485 assert_eq!(nlp.n(), 1);
3486
3487 let nlp_dyn: &dyn crate::ipopt_nlp::IpoptNlp = &nlp;
3489 assert_eq!(nlp_dyn.full_x_to_var_x(0), None);
3490 assert_eq!(nlp_dyn.full_x_to_var_x(1), Some(0));
3491
3492 assert_eq!(nlp_dyn.var_x_to_full_x(0), 1);
3494
3495 assert_eq!(nlp_dyn.full_g_to_c_block(0), Some(0));
3497
3498 assert_eq!(nlp_dyn.full_g_to_d_block(0), None);
3502 assert_eq!(nlp_dyn.full_g_to_d_block(1), None);
3503
3504 let mut x_var = nlp.x_space().make_new_dense();
3506 x_var.values_mut()[0] = 0.5;
3507 let lifted = nlp_dyn.lift_x_to_full(&x_var);
3508 assert_eq!(lifted, vec![7.0, 0.5]);
3509 }
3510
3511 struct NamedFixedOneFree;
3515 impl TNLP for NamedFixedOneFree {
3516 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3517 OneFixedOneFree.get_nlp_info()
3518 }
3519 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3520 OneFixedOneFree.get_bounds_info(b)
3521 }
3522 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3523 OneFixedOneFree.get_starting_point(sp)
3524 }
3525 fn eval_f(&mut self, x: &[Number], n: bool) -> Option<Number> {
3526 OneFixedOneFree.eval_f(x, n)
3527 }
3528 fn eval_grad_f(&mut self, x: &[Number], n: bool, g: &mut [Number]) -> bool {
3529 OneFixedOneFree.eval_grad_f(x, n, g)
3530 }
3531 fn eval_g(&mut self, x: &[Number], n: bool, g: &mut [Number]) -> bool {
3532 OneFixedOneFree.eval_g(x, n, g)
3533 }
3534 fn eval_jac_g(&mut self, x: Option<&[Number]>, n: bool, m: SparsityRequest<'_>) -> bool {
3535 OneFixedOneFree.eval_jac_g(x, n, m)
3536 }
3537 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3538 fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
3539 var.strings.insert(
3540 IDX_NAMES.to_string(),
3541 vec!["fixed_x".to_string(), "free_x".to_string()],
3542 );
3543 con.strings
3544 .insert(IDX_NAMES.to_string(), vec!["balance".to_string()]);
3545 true
3546 }
3547 }
3548
3549 #[test]
3550 fn split_space_names_threads_through_fixed_var_and_cd_split() {
3551 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(NamedFixedOneFree));
3552 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3553 let nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3554
3555 let names = nlp.split_space_names().expect("names present");
3556 assert_eq!(names.x_var, vec![Some("free_x".to_string())]);
3558 assert_eq!(names.eq, vec![Some("balance".to_string())]);
3560 assert!(names.ineq.is_empty());
3562 assert!(names.any_present());
3563 }
3564
3565 #[test]
3566 fn split_space_names_none_when_tnlp_declines() {
3567 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedOneFree));
3569 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3570 let nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3571 assert!(nlp.split_space_names().is_none());
3572 }
3573
3574 struct FixedOnlyHess;
3580 impl TNLP for FixedOnlyHess {
3581 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3582 Some(NlpInfo {
3583 n: 2,
3584 m: 1,
3585 nnz_jac_g: 1,
3586 nnz_h_lag: 1,
3587 index_style: IndexStyle::C,
3588 })
3589 }
3590 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3591 b.x_l[0] = 7.0;
3592 b.x_u[0] = 7.0; b.x_l[1] = -1.0e19;
3594 b.x_u[1] = 1.0e19;
3595 b.g_l[0] = 0.0;
3596 b.g_u[0] = 0.0;
3597 true
3598 }
3599 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3600 sp.x[0] = 7.0;
3601 sp.x[1] = 0.5;
3602 true
3603 }
3604 fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
3605 Some(0.5 * x[0] * x[0] + x[1])
3606 }
3607 fn eval_grad_f(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3608 g[0] = x[0];
3609 g[1] = 1.0;
3610 true
3611 }
3612 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3613 g[0] = x[1];
3614 true
3615 }
3616 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3617 match m {
3618 SparsityRequest::Structure { irow, jcol } => {
3619 irow[0] = 0;
3620 jcol[0] = 1;
3621 }
3622 SparsityRequest::Values { values } => values[0] = 1.0,
3623 }
3624 true
3625 }
3626 fn eval_h(
3627 &mut self,
3628 _: Option<&[Number]>,
3629 _: bool,
3630 obj_factor: Number,
3631 _: Option<&[Number]>,
3632 _: bool,
3633 m: SparsityRequest<'_>,
3634 ) -> bool {
3635 match m {
3636 SparsityRequest::Structure { irow, jcol } => {
3637 irow[0] = 0;
3638 jcol[0] = 0;
3639 }
3640 SparsityRequest::Values { values } => values[0] = obj_factor,
3641 }
3642 true
3643 }
3644 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3645 }
3646
3647 struct OneIneqLargeOffset;
3656 impl TNLP for OneIneqLargeOffset {
3657 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3658 Some(NlpInfo {
3659 n: 1,
3660 m: 1,
3661 nnz_jac_g: 1,
3662 nnz_h_lag: 0,
3663 index_style: IndexStyle::C,
3664 })
3665 }
3666 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3667 b.x_l[0] = -1.0e19;
3668 b.x_u[0] = 1.0e19;
3669 b.g_l[0] = 4.0e6;
3670 b.g_u[0] = 2.0e19;
3671 true
3672 }
3673 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3674 sp.x[0] = 5000.0;
3675 true
3676 }
3677 fn eval_f(&mut self, _: &[Number], _: bool) -> Option<Number> {
3678 Some(0.0)
3679 }
3680 fn eval_grad_f(&mut self, _: &[Number], _: bool, g: &mut [Number]) -> bool {
3681 g[0] = 0.0;
3682 true
3683 }
3684 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3685 g[0] = 1000.0 * x[0];
3686 true
3687 }
3688 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3689 match m {
3690 SparsityRequest::Structure { irow, jcol } => {
3691 irow[0] = 0;
3692 jcol[0] = 0;
3693 }
3694 SparsityRequest::Values { values } => values[0] = 1000.0,
3695 }
3696 true
3697 }
3698 fn eval_h(
3699 &mut self,
3700 _: Option<&[Number]>,
3701 _: bool,
3702 _: Number,
3703 _: Option<&[Number]>,
3704 _: bool,
3705 _: SparsityRequest<'_>,
3706 ) -> bool {
3707 true
3708 }
3709 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3710 }
3711
3712 struct OneEqLargeOffset;
3717 impl TNLP for OneEqLargeOffset {
3718 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3719 Some(NlpInfo {
3720 n: 1,
3721 m: 1,
3722 nnz_jac_g: 1,
3723 nnz_h_lag: 0,
3724 index_style: IndexStyle::C,
3725 })
3726 }
3727 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3728 b.x_l[0] = -1.0e19;
3729 b.x_u[0] = 1.0e19;
3730 b.g_l[0] = 4.0e6;
3731 b.g_u[0] = 4.0e6;
3732 true
3733 }
3734 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3735 sp.x[0] = 5000.0;
3736 true
3737 }
3738 fn eval_f(&mut self, _: &[Number], _: bool) -> Option<Number> {
3739 Some(0.0)
3740 }
3741 fn eval_grad_f(&mut self, _: &[Number], _: bool, g: &mut [Number]) -> bool {
3742 g[0] = 0.0;
3743 true
3744 }
3745 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3746 g[0] = 1000.0 * x[0];
3747 true
3748 }
3749 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3750 match m {
3751 SparsityRequest::Structure { irow, jcol } => {
3752 irow[0] = 0;
3753 jcol[0] = 0;
3754 }
3755 SparsityRequest::Values { values } => values[0] = 1000.0,
3756 }
3757 true
3758 }
3759 fn eval_h(
3760 &mut self,
3761 _: Option<&[Number]>,
3762 _: bool,
3763 _: Number,
3764 _: Option<&[Number]>,
3765 _: bool,
3766 _: SparsityRequest<'_>,
3767 ) -> bool {
3768 true
3769 }
3770 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3771 }
3772
3773 #[test]
3774 fn gradient_based_scaling_scales_d_l_and_d_u() {
3775 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneIneqLargeOffset));
3776 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3777 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3778
3779 assert_eq!(nlp.d_l().dim(), 1);
3781 let pre = nlp
3782 .d_l()
3783 .as_any()
3784 .downcast_ref::<DenseVector>()
3785 .unwrap()
3786 .values()[0];
3787 assert_eq!(pre, 4.0e6);
3788
3789 nlp.determine_scaling_from_starting_point(
3790 ScalingMethod::GradientBased,
3791 100.0,
3792 1e-8,
3793 0.0,
3794 0.0,
3795 );
3796
3797 let post = nlp
3799 .d_l()
3800 .as_any()
3801 .downcast_ref::<DenseVector>()
3802 .unwrap()
3803 .values()[0];
3804 assert!(
3805 (post - 4.0e5).abs() < 1e-9,
3806 "d_l should be scaled by d_scale=0.1; got {}",
3807 post
3808 );
3809
3810 let x = dense_x(&[5000.0], nlp.x_space());
3813 let mut d = nlp.d_space().make_new_dense();
3814 nlp.eval_d(&x, &mut d);
3815 assert!(
3816 (d.values()[0] - 5.0e5).abs() < 1e-6,
3817 "scaled d(x) mismatch; got {}",
3818 d.values()[0]
3819 );
3820 assert!(
3821 d.values()[0] >= post,
3822 "starting point must be feasible in scaled space"
3823 );
3824 }
3825
3826 struct OneIneqWithObj;
3832 impl TNLP for OneIneqWithObj {
3833 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3834 Some(NlpInfo {
3835 n: 1,
3836 m: 1,
3837 nnz_jac_g: 1,
3838 nnz_h_lag: 0,
3839 index_style: IndexStyle::C,
3840 })
3841 }
3842 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3843 b.x_l[0] = -1.0e19;
3844 b.x_u[0] = 1.0e19;
3845 b.g_l[0] = 4.0e6;
3846 b.g_u[0] = 2.0e19;
3847 true
3848 }
3849 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3850 sp.x[0] = 5000.0;
3851 true
3852 }
3853 fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
3854 Some(10.0 * x[0])
3855 }
3856 fn eval_grad_f(&mut self, _: &[Number], _: bool, g: &mut [Number]) -> bool {
3857 g[0] = 10.0;
3858 true
3859 }
3860 fn eval_g(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3861 g[0] = 1000.0 * x[0];
3862 true
3863 }
3864 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, m: SparsityRequest<'_>) -> bool {
3865 match m {
3866 SparsityRequest::Structure { irow, jcol } => {
3867 irow[0] = 0;
3868 jcol[0] = 0;
3869 }
3870 SparsityRequest::Values { values } => values[0] = 1000.0,
3871 }
3872 true
3873 }
3874 fn eval_h(
3875 &mut self,
3876 _: Option<&[Number]>,
3877 _: bool,
3878 _: Number,
3879 _: Option<&[Number]>,
3880 _: bool,
3881 _: SparsityRequest<'_>,
3882 ) -> bool {
3883 true
3884 }
3885 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3886 }
3887
3888 #[test]
3889 fn obj_target_gradient_pins_obj_scale() {
3890 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneIneqWithObj));
3893 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
3894 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
3895 nlp.determine_scaling_from_starting_point(
3896 ScalingMethod::GradientBased,
3897 100.0,
3898 1e-8,
3899 0.0, 0.0,
3901 );
3902 assert!(
3903 (nlp.obj_scale_factor() - 1.0).abs() < 1e-12,
3904 "no-target path leaves df=1 when grad < cutoff; got {}",
3905 nlp.obj_scale_factor()
3906 );
3907
3908 let tnlp2: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneIneqWithObj));
3911 let adapter2 = Rc::new(RefCell::new(TNLPAdapter::new(tnlp2).unwrap()));
3912 let mut nlp2 = OrigIpoptNlp::new(Rc::clone(&adapter2), Rc::new(NoScaling)).unwrap();
3913 nlp2.determine_scaling_from_starting_point(
3914 ScalingMethod::GradientBased,
3915 100.0,
3916 1e-8,
3917 1.0,
3918 0.0,
3919 );
3920 assert!(
3921 (nlp2.obj_scale_factor() - 0.1).abs() < 1e-12,
3922 "target_gradient=1, max_grad_f=10 → df=0.1; got {}",
3923 nlp2.obj_scale_factor()
3924 );
3925 }
3926
3927 struct FixedVarShiftsObjGrad;
3937 impl TNLP for FixedVarShiftsObjGrad {
3938 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3939 Some(NlpInfo {
3940 n: 2,
3941 m: 0,
3942 nnz_jac_g: 0,
3943 nnz_h_lag: 0,
3944 index_style: IndexStyle::C,
3945 })
3946 }
3947 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3948 b.x_l[0] = -1.0e19;
3949 b.x_u[0] = 1.0e19;
3950 b.x_l[1] = 1000.0;
3951 b.x_u[1] = 1000.0; true
3953 }
3954 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3955 sp.x[0] = 1.0;
3956 sp.x[1] = 0.0; true
3958 }
3959 fn eval_f(&mut self, x: &[Number], _: bool) -> Option<Number> {
3960 Some(x[0] * x[1])
3961 }
3962 fn eval_grad_f(&mut self, x: &[Number], _: bool, g: &mut [Number]) -> bool {
3963 g[0] = x[1];
3964 g[1] = x[0];
3965 true
3966 }
3967 fn eval_g(&mut self, _: &[Number], _: bool, _: &mut [Number]) -> bool {
3968 true
3969 }
3970 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, _: SparsityRequest<'_>) -> bool {
3971 true
3972 }
3973 fn eval_h(
3974 &mut self,
3975 _: Option<&[Number]>,
3976 _: bool,
3977 _: Number,
3978 _: Option<&[Number]>,
3979 _: bool,
3980 _: SparsityRequest<'_>,
3981 ) -> bool {
3982 true
3983 }
3984 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
3985 }
3986
3987 struct BoxedVar;
3991 impl TNLP for BoxedVar {
3992 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3993 Some(NlpInfo {
3994 n: 1,
3995 m: 0,
3996 nnz_jac_g: 0,
3997 nnz_h_lag: 0,
3998 index_style: IndexStyle::C,
3999 })
4000 }
4001 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
4002 b.x_l[0] = -1.0;
4003 b.x_u[0] = 2.0;
4004 true
4005 }
4006 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
4007 sp.x[0] = 0.0;
4008 true
4009 }
4010 fn eval_f(&mut self, _: &[Number], _: bool) -> Option<Number> {
4011 Some(0.0)
4012 }
4013 fn eval_grad_f(&mut self, _: &[Number], _: bool, g: &mut [Number]) -> bool {
4014 g[0] = 0.0;
4015 true
4016 }
4017 fn eval_g(&mut self, _: &[Number], _: bool, _: &mut [Number]) -> bool {
4018 true
4019 }
4020 fn eval_jac_g(&mut self, _: Option<&[Number]>, _: bool, _: SparsityRequest<'_>) -> bool {
4021 true
4022 }
4023 fn eval_h(
4024 &mut self,
4025 _: Option<&[Number]>,
4026 _: bool,
4027 _: Number,
4028 _: Option<&[Number]>,
4029 _: bool,
4030 _: SparsityRequest<'_>,
4031 ) -> bool {
4032 true
4033 }
4034 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
4035 }
4036
4037 fn boxed_var_nlp() -> OrigIpoptNlp {
4038 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(BoxedVar));
4039 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4040 OrigIpoptNlp::new(adapter, Rc::new(NoScaling)).unwrap()
4041 }
4042
4043 fn at(x: Number) -> DenseVector {
4044 let space = DenseVectorSpace::new(1);
4045 let mut v = space.make_new_dense();
4046 v.set_values(&[x]);
4047 v
4048 }
4049
4050 #[test]
4061 fn the_box_violation_abstains_until_the_declared_bounds_are_snapshotted() {
4062 let nlp = boxed_var_nlp();
4063 assert!(
4064 nlp.declared_box_violation(&at(5.0)).is_none(),
4065 "no snapshot means no answer, not a fabricated zero"
4066 );
4067 }
4068
4069 #[test]
4082 fn the_box_violation_measures_the_distance_outside_the_declared_box() {
4083 let mut nlp = boxed_var_nlp();
4084 nlp.snapshot_declared_bounds();
4085 assert_eq!(nlp.declared_box_violation(&at(0.5)), Some(0.0));
4087 assert_eq!(nlp.declared_box_violation(&at(-1.0)), Some(0.0));
4089 assert_eq!(nlp.declared_box_violation(&at(2.0)), Some(0.0));
4090 let over = nlp.declared_box_violation(&at(2.25)).unwrap();
4092 assert!((over - 0.25).abs() < 1e-15, "expected 0.25, got {over}");
4093 let under = nlp.declared_box_violation(&at(-1.5)).unwrap();
4094 assert!((under - 0.5).abs() < 1e-15, "expected 0.5, got {under}");
4095 }
4096
4097 #[test]
4101 fn relaxing_the_bounds_still_snapshots_the_declared_ones_first() {
4102 let mut nlp = boxed_var_nlp();
4103 nlp.relax_bounds(1e-2, 1.0);
4104 let v = nlp.declared_box_violation(&at(2.02)).unwrap();
4109 assert!((v - 0.02).abs() < 1e-12, "expected 0.02, got {v}");
4110 }
4111
4112 #[test]
4113 fn gradient_scaling_lifts_fixed_vars_to_their_value() {
4114 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(FixedVarShiftsObjGrad));
4115 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4116 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4117
4118 assert_eq!(nlp.n_full_x(), 2);
4120 assert_eq!(nlp.n(), 1);
4121
4122 nlp.determine_scaling_from_starting_point(
4123 ScalingMethod::GradientBased,
4124 100.0,
4125 1e-8,
4126 0.0,
4127 0.0,
4128 );
4129
4130 assert!(
4134 (nlp.obj_scale_factor() - 0.1).abs() < 1e-12,
4135 "fixed var must be lifted before scaling; expected df=0.1, got {}",
4136 nlp.obj_scale_factor()
4137 );
4138 }
4139
4140 #[test]
4141 fn constr_target_gradient_overrides_cutoff_and_clamp() {
4142 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneIneqLargeOffset));
4147 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4148 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4149 nlp.determine_scaling_from_starting_point(
4150 ScalingMethod::GradientBased,
4151 100.0,
4152 1e-8,
4153 0.0,
4154 50.0,
4155 );
4156 let x = dense_x(&[5000.0], nlp.x_space());
4157 let mut d = nlp.d_space().make_new_dense();
4158 nlp.eval_d(&x, &mut d);
4159 assert!(
4161 (d.values()[0] - 2.5e5).abs() < 1e-6,
4162 "constr target=50 → dd=0.05; scaled d(5000)=2.5e5, got {}",
4163 d.values()[0]
4164 );
4165 }
4166
4167 struct Hs071UserScaled;
4172 impl TNLP for Hs071UserScaled {
4173 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
4174 Hs071::default().get_nlp_info()
4175 }
4176 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
4177 Hs071::default().get_bounds_info(b)
4178 }
4179 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
4180 Hs071::default().get_starting_point(sp)
4181 }
4182 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
4183 Hs071::default().eval_f(x, new_x)
4184 }
4185 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4186 Hs071::default().eval_grad_f(x, new_x, g)
4187 }
4188 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4189 Hs071::default().eval_g(x, new_x, g)
4190 }
4191 fn eval_jac_g(
4192 &mut self,
4193 x: Option<&[Number]>,
4194 new_x: bool,
4195 mode: SparsityRequest<'_>,
4196 ) -> bool {
4197 Hs071::default().eval_jac_g(x, new_x, mode)
4198 }
4199 fn eval_h(
4200 &mut self,
4201 x: Option<&[Number]>,
4202 new_x: bool,
4203 obj_factor: Number,
4204 lambda: Option<&[Number]>,
4205 new_lambda: bool,
4206 mode: SparsityRequest<'_>,
4207 ) -> bool {
4208 Hs071::default().eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
4209 }
4210 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
4211 *req.obj_scaling = 2.0;
4212 *req.use_x_scaling = false;
4213 *req.use_g_scaling = true;
4214 req.g_scaling[0] = 0.5;
4216 req.g_scaling[1] = 0.25;
4217 true
4218 }
4219 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
4220 }
4221
4222 #[test]
4223 fn user_scaling_dispatch_applies_obj_and_g_scaling() {
4224 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Hs071UserScaled));
4225 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4226 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4227 nlp.determine_scaling_from_starting_point(
4228 ScalingMethod::UserScaling,
4229 100.0,
4230 1e-8,
4231 0.0,
4232 0.0,
4233 );
4234
4235 assert!(
4238 (nlp.obj_scale_factor() - 2.0).abs() < 1e-12,
4239 "user obj_scaling=2.0 should be installed; got {}",
4240 nlp.obj_scale_factor()
4241 );
4242
4243 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
4247 let mut c = nlp.c_space().make_new_dense();
4248 nlp.eval_c(&x, &mut c);
4249 assert!(
4252 (c.values()[0] - 3.0).abs() < 1e-9,
4253 "user g_scaling=0.25 on equality → c=3.0; got {}",
4254 c.values()[0]
4255 );
4256
4257 let mut d = nlp.d_space().make_new_dense();
4260 nlp.eval_d(&x, &mut d);
4261 assert!(
4262 (d.values()[0] - 12.5).abs() < 1e-9,
4263 "user g_scaling=0.5 on inequality → d=12.5; got {}",
4264 d.values()[0]
4265 );
4266
4267 let post_d_l = nlp
4270 .d_l()
4271 .as_any()
4272 .downcast_ref::<DenseVector>()
4273 .unwrap()
4274 .values()[0];
4275 assert!(
4276 (post_d_l - 12.5).abs() < 1e-9,
4277 "d_l scaled in step: got {}",
4278 post_d_l
4279 );
4280 }
4281
4282 struct Hs071DeclinesScaling;
4286 impl TNLP for Hs071DeclinesScaling {
4287 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
4288 Hs071::default().get_nlp_info()
4289 }
4290 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
4291 Hs071::default().get_bounds_info(b)
4292 }
4293 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
4294 Hs071::default().get_starting_point(sp)
4295 }
4296 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
4297 Hs071::default().eval_f(x, new_x)
4298 }
4299 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4300 Hs071::default().eval_grad_f(x, new_x, g)
4301 }
4302 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4303 Hs071::default().eval_g(x, new_x, g)
4304 }
4305 fn eval_jac_g(
4306 &mut self,
4307 x: Option<&[Number]>,
4308 new_x: bool,
4309 mode: SparsityRequest<'_>,
4310 ) -> bool {
4311 Hs071::default().eval_jac_g(x, new_x, mode)
4312 }
4313 fn eval_h(
4314 &mut self,
4315 x: Option<&[Number]>,
4316 new_x: bool,
4317 obj_factor: Number,
4318 lambda: Option<&[Number]>,
4319 new_lambda: bool,
4320 mode: SparsityRequest<'_>,
4321 ) -> bool {
4322 Hs071::default().eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
4323 }
4324 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
4325 }
4326
4327 #[test]
4328 fn user_scaling_falls_back_when_tnlp_declines() {
4329 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Hs071DeclinesScaling));
4330 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4331 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4332 nlp.determine_scaling_from_starting_point(
4333 ScalingMethod::UserScaling,
4334 100.0,
4335 1e-8,
4336 0.0,
4337 0.0,
4338 );
4339 assert!((nlp.obj_scale_factor() - 1.0).abs() < 1e-12);
4342 let x = dense_x(&[1.0, 5.0, 5.0, 1.0], nlp.x_space());
4343 let mut c = nlp.c_space().make_new_dense();
4344 nlp.eval_c(&x, &mut c);
4345 assert_eq!(c.values(), &[12.0], "unscaled equality residual");
4346 }
4347
4348 struct Hs071XScaled(Vec<Number>);
4351 impl TNLP for Hs071XScaled {
4352 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
4353 Hs071::default().get_nlp_info()
4354 }
4355 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
4356 Hs071::default().get_bounds_info(b)
4357 }
4358 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
4359 Hs071::default().get_starting_point(sp)
4360 }
4361 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
4362 Hs071::default().eval_f(x, new_x)
4363 }
4364 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4365 Hs071::default().eval_grad_f(x, new_x, g)
4366 }
4367 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
4368 Hs071::default().eval_g(x, new_x, g)
4369 }
4370 fn eval_jac_g(
4371 &mut self,
4372 x: Option<&[Number]>,
4373 new_x: bool,
4374 mode: SparsityRequest<'_>,
4375 ) -> bool {
4376 Hs071::default().eval_jac_g(x, new_x, mode)
4377 }
4378 fn eval_h(
4379 &mut self,
4380 x: Option<&[Number]>,
4381 new_x: bool,
4382 obj_factor: Number,
4383 lambda: Option<&[Number]>,
4384 new_lambda: bool,
4385 mode: SparsityRequest<'_>,
4386 ) -> bool {
4387 Hs071::default().eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
4388 }
4389 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
4390 *req.obj_scaling = 2.0;
4391 *req.use_x_scaling = true;
4392 req.x_scaling.copy_from_slice(&self.0);
4393 *req.use_g_scaling = false;
4394 true
4395 }
4396 fn finalize_solution(&mut self, _: Solution<'_>, _: &IpoptData, _: &IpoptCq) {}
4397 }
4398
4399 fn user_x_scaling_run(factors: &[Number]) -> OrigIpoptNlp {
4400 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Hs071XScaled(factors.to_vec())));
4401 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4402 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4403 nlp.determine_scaling_from_starting_point(
4404 ScalingMethod::UserScaling,
4405 100.0,
4406 1e-8,
4407 0.0,
4408 0.0,
4409 );
4410 nlp
4411 }
4412
4413 #[test]
4418 fn user_x_scaling_request_is_flagged_not_discarded() {
4419 let nlp = user_x_scaling_run(&[1.0, 1e3, 1.0, 1.0]);
4420 assert!(
4421 nlp.user_x_scaling_rejected(),
4422 "a non-unit x_scaling must be refused, not dropped"
4423 );
4424 assert!((nlp.obj_scale_factor() - 2.0).abs() < 1e-12);
4427 }
4428
4429 #[test]
4432 fn unit_x_scaling_request_is_not_rejected() {
4433 let nlp = user_x_scaling_run(&[1.0, 1.0, 1.0, 1.0]);
4434 assert!(!nlp.user_x_scaling_rejected());
4435 }
4436
4437 #[test]
4438 fn eval_h_with_all_entries_on_fixed_var_does_not_panic() {
4439 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(FixedOnlyHess));
4440 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4441 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4442
4443 assert_eq!(nlp.h_space().unwrap().nonzeros(), 0);
4446
4447 let x = dense_x(&[0.5], &nlp.x_space().clone());
4448 let yc = dense_x(&[0.0], &nlp.c_space().clone());
4449 let yd = nlp.d_space().make_new_dense();
4450 let h = nlp.eval_h(&x, 1.0, &yc, &yd);
4451 assert_eq!(h.n_rows(), 1);
4452 }
4453
4454 #[test]
4455 fn relax_bounds_widens_uniquely_owned_bounds() {
4456 let (_adapter, mut nlp) = build_orig_nlp();
4459 let x_l_before = nlp.x_l.values().to_vec();
4460 let x_u_before = nlp.x_u.values().to_vec();
4461 nlp.relax_bounds(1e-2, 1.0);
4462 for (b, a) in x_l_before.iter().zip(nlp.x_l.values()) {
4463 assert!(a < b, "x_l should relax downward: {a} !< {b}");
4464 }
4465 for (b, a) in x_u_before.iter().zip(nlp.x_u.values()) {
4466 assert!(a > b, "x_u should relax upward: {a} !> {b}");
4467 }
4468 }
4469
4470 #[test]
4475 fn relax_bounds_is_scale_relative_on_d_and_snapshots_declared() {
4476 let (_adapter, mut nlp) = build_orig_nlp();
4478 assert_eq!(nlp.d_l.values(), &[25.0]);
4479 assert_eq!(nlp.declared_d_bounds(), None, "no snapshot before relax");
4480 nlp.relax_bounds(1e-2, 1.0);
4481 assert_eq!(nlp.d_l.values(), &[25.0 - 0.25]);
4487 let (dl, du) = nlp.declared_d_bounds().expect("snapshotted at relax");
4488 assert_eq!(dl, vec![25.0], "declared bound is the pre-relax value");
4489 assert!(du.is_empty() || du[0] >= 25.0); }
4491
4492 #[test]
4500 fn declared_x_bounds_are_the_pre_relax_box() {
4501 let (_adapter, mut nlp) = build_orig_nlp();
4502 assert_eq!(nlp.declared_x_bounds(), None, "no snapshot before relax");
4503 let x_l_before = nlp.x_l.values().to_vec();
4504 let x_u_before = nlp.x_u.values().to_vec();
4505 nlp.relax_bounds(1e-2, 1.0);
4506 let (xl, xu) = nlp.declared_x_bounds().expect("snapshotted at relax");
4507 assert_eq!(xl, x_l_before, "declared lower box is the pre-relax value");
4508 assert_eq!(xu, x_u_before, "declared upper box is the pre-relax value");
4509 assert!(nlp.x_l.values()[0] < xl[0]);
4512 assert!(nlp.x_u.values()[0] > xu[0]);
4513 }
4514
4515 #[test]
4521 fn declared_c_rhs_is_the_pre_fold_right_hand_side() {
4522 let (_adapter, mut nlp) = build_orig_nlp();
4524 assert_eq!(nlp.declared_c_rhs(), Some(vec![40.0]));
4525 nlp.relax_bounds(1e-2, 1.0);
4526 assert_eq!(
4527 nlp.declared_c_rhs(),
4528 Some(vec![40.0]),
4529 "bound relaxation must not reach the equality RHS"
4530 );
4531 }
4532
4533 #[test]
4538 fn declared_c_rhs_carries_the_row_scaling() {
4539 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneEqLargeOffset));
4540 let adapter = Rc::new(RefCell::new(TNLPAdapter::new(tnlp).unwrap()));
4541 let mut nlp = OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)).unwrap();
4542 assert_eq!(nlp.declared_c_rhs(), Some(vec![4.0e6]));
4543
4544 nlp.determine_scaling_from_starting_point(
4545 ScalingMethod::GradientBased,
4546 100.0,
4547 1e-8,
4548 0.0,
4549 0.0,
4550 );
4551 let rhs = nlp.declared_c_rhs().unwrap();
4553 assert!(
4554 (rhs[0] - 4.0e5).abs() < 1e-9,
4555 "declared RHS should carry c_scale=0.1; got {}",
4556 rhs[0]
4557 );
4558
4559 let x = dense_x(&[5000.0], nlp.x_space());
4562 let mut c = nlp.c_space().make_new_dense();
4563 nlp.eval_c(&x, &mut c);
4564 assert!((c.values()[0] / rhs[0] - 0.25).abs() < 1e-12);
4565 }
4566
4567 #[test]
4568 #[should_panic(expected = "x_l is uniquely owned")]
4569 fn relax_bounds_panics_on_shared_bound_rc() {
4570 let (_adapter, mut nlp) = build_orig_nlp();
4575 let _shared = Rc::clone(&nlp.x_l); nlp.relax_bounds(1e-2, 1.0);
4577 }
4578}