1#![allow(non_camel_case_types, non_snake_case)]
31#![allow(unsafe_op_in_unsafe_fn, dead_code)]
32#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
33
34pub mod fortran;
35pub mod solver;
36
37use pounce_algorithm::application::{
38 IpoptApplication, default_backend_factory, feral_config_from_options, ma57_config_from_options,
39};
40use pounce_algorithm::intermediate as ip_intermediate;
41use pounce_common::reg_options::OptionType;
42use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
43use pounce_nlp::return_codes::ApplicationReturnStatus;
44use pounce_nlp::solve_statistics::SolveStatistics;
45use pounce_nlp::tnlp::{
46 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
47 StartingPoint, TNLP,
48};
49use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
50use pounce_restoration::resto_inner_solver::{
51 InnerBackendFactoryFactory, make_default_restoration_factory_provider,
52};
53use pounce_restoration::second_opinion_driver::run_second_opinion_ladder;
54use std::cell::RefCell;
55use std::ffi::{CStr, c_char, c_int, c_void};
56use std::rc::Rc;
57
58pub type Number = f64;
60pub type Index = c_int;
62pub type Bool = u8;
88
89const TRUE: Bool = 1;
90const FALSE: Bool = 0;
91
92const _: () = assert!(
96 core::mem::size_of::<Bool>() == 1,
97 "Bool must be one byte to match `typedef bool Bool` in pounce.h"
98);
99
100pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
113 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
114 Ok(r) => r,
115 Err(_) => fallback,
116 }
117}
118
119pub type IpoptBoundStatus = c_int;
123pub type IpoptConsStatus = c_int;
126
127const POUNCE_WS_INACTIVE: c_int = 0;
128const POUNCE_WS_AT_LOWER: c_int = 1;
129const POUNCE_WS_AT_UPPER: c_int = 2;
130const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
131
132pub struct IpoptProblemInfo {
135 pub(crate) app: IpoptApplication,
136 pub(crate) n: Index,
137 pub(crate) m: Index,
138 pub(crate) nele_jac: Index,
139 pub(crate) nele_hess: Index,
140 pub(crate) index_style: Index,
141 pub(crate) x_l: Vec<Number>,
142 pub(crate) x_u: Vec<Number>,
143 pub(crate) g_l: Vec<Number>,
144 pub(crate) g_u: Vec<Number>,
145 pub(crate) eval_f: Option<Eval_F_CB>,
146 pub(crate) eval_g: Option<Eval_G_CB>,
147 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
148 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
149 pub(crate) eval_h: Option<Eval_H_CB>,
150 pub(crate) intermediate_cb: Option<Intermediate_CB>,
151 pub(crate) user_scaling: Option<UserScaling>,
155 pub(crate) last_solve: Option<LastSolve>,
159 pub(crate) pending_working_set: Option<pounce_qp::WorkingSet>,
173 pub(crate) nonlinear_vars: Option<Vec<Index>>,
182}
183
184#[derive(Clone)]
187pub(crate) struct UserScaling {
188 obj_scaling: Number,
189 x_scaling: Option<Vec<Number>>,
190 g_scaling: Option<Vec<Number>>,
191}
192
193#[derive(Clone)]
199pub(crate) struct LastSolve {
200 pub(crate) stats: SolveStatistics,
201 pub(crate) status: ApplicationReturnStatus,
202 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
203 pub(crate) final_x: Vec<Number>,
204 pub(crate) final_lambda: Vec<Number>,
205 pub(crate) final_obj: Number,
206}
207
208impl Default for LastSolve {
209 fn default() -> Self {
210 Self {
211 stats: SolveStatistics::default(),
212 status: ApplicationReturnStatus::InternalError,
213 linear_solver: None,
214 final_x: Vec::new(),
215 final_lambda: Vec::new(),
216 final_obj: 0.0,
217 }
218 }
219}
220
221pub type IpoptProblem = *mut IpoptProblemInfo;
222
223pub type Eval_F_CB = unsafe extern "C" fn(
227 n: Index,
228 x: *const Number,
229 new_x: Bool,
230 obj_value: *mut Number,
231 user_data: *mut c_void,
232) -> Bool;
233
234pub type Eval_Grad_F_CB = unsafe extern "C" fn(
235 n: Index,
236 x: *const Number,
237 new_x: Bool,
238 grad_f: *mut Number,
239 user_data: *mut c_void,
240) -> Bool;
241
242pub type Eval_G_CB = unsafe extern "C" fn(
243 n: Index,
244 x: *const Number,
245 new_x: Bool,
246 m: Index,
247 g: *mut Number,
248 user_data: *mut c_void,
249) -> Bool;
250
251pub type Eval_Jac_G_CB = unsafe extern "C" fn(
252 n: Index,
253 x: *const Number,
254 new_x: Bool,
255 m: Index,
256 nele_jac: Index,
257 iRow: *mut Index,
258 jCol: *mut Index,
259 values: *mut Number,
260 user_data: *mut c_void,
261) -> Bool;
262
263pub type Eval_H_CB = unsafe extern "C" fn(
264 n: Index,
265 x: *const Number,
266 new_x: Bool,
267 obj_factor: Number,
268 m: Index,
269 lambda: *const Number,
270 new_lambda: Bool,
271 nele_hess: Index,
272 iRow: *mut Index,
273 jCol: *mut Index,
274 values: *mut Number,
275 user_data: *mut c_void,
276) -> Bool;
277
278pub type Intermediate_CB = unsafe extern "C" fn(
279 alg_mod: Index,
280 iter_count: Index,
281 obj_value: Number,
282 inf_pr: Number,
283 inf_du: Number,
284 mu: Number,
285 d_norm: Number,
286 regularization_size: Number,
287 alpha_du: Number,
288 alpha_pr: Number,
289 ls_trials: Index,
290 user_data: *mut c_void,
291) -> Bool;
292
293#[unsafe(no_mangle)]
304pub unsafe extern "C" fn CreateIpoptProblem(
305 n: Index,
306 x_L: *const Number,
307 x_U: *const Number,
308 m: Index,
309 g_L: *const Number,
310 g_U: *const Number,
311 nele_jac: Index,
312 nele_hess: Index,
313 index_style: Index,
314 eval_f: Option<Eval_F_CB>,
315 eval_g: Option<Eval_G_CB>,
316 eval_grad_f: Option<Eval_Grad_F_CB>,
317 eval_jac_g: Option<Eval_Jac_G_CB>,
318 eval_h: Option<Eval_H_CB>,
319) -> IpoptProblem {
320 unsafe {
321 pounce_observability::init_subscriber();
325
326 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
327 return std::ptr::null_mut();
328 }
329 if !(0..=1).contains(&index_style) {
330 return std::ptr::null_mut();
331 }
332 if eval_f.is_none() || eval_grad_f.is_none() {
333 return std::ptr::null_mut();
334 }
335 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
336 return std::ptr::null_mut();
337 }
338 if n > 0 && (x_L.is_null() || x_U.is_null()) {
339 return std::ptr::null_mut();
340 }
341 if m > 0 && (g_L.is_null() || g_U.is_null()) {
342 return std::ptr::null_mut();
343 }
344
345 let x_l = if n > 0 {
346 std::slice::from_raw_parts(x_L, n as usize).to_vec()
347 } else {
348 Vec::new()
349 };
350 let x_u = if n > 0 {
351 std::slice::from_raw_parts(x_U, n as usize).to_vec()
352 } else {
353 Vec::new()
354 };
355 let g_l_vec = if m > 0 {
356 std::slice::from_raw_parts(g_L, m as usize).to_vec()
357 } else {
358 Vec::new()
359 };
360 let g_u_vec = if m > 0 {
361 std::slice::from_raw_parts(g_U, m as usize).to_vec()
362 } else {
363 Vec::new()
364 };
365
366 let info = Box::new(IpoptProblemInfo {
367 app: IpoptApplication::new(),
368 n,
369 m,
370 nele_jac,
371 nele_hess,
372 index_style,
373 x_l,
374 x_u,
375 g_l: g_l_vec,
376 g_u: g_u_vec,
377 eval_f,
378 eval_g,
379 eval_grad_f,
380 eval_jac_g,
381 eval_h,
382 intermediate_cb: None,
383 user_scaling: None,
384 nonlinear_vars: None,
385 last_solve: None,
386 pending_working_set: None,
387 });
388 Box::into_raw(info)
389 }
390}
391
392#[unsafe(no_mangle)]
399pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
400 unsafe {
401 if ipopt_problem.is_null() {
402 return;
403 }
404 drop(Box::from_raw(ipopt_problem));
405 }
406}
407
408unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
409 unsafe {
410 if keyword.is_null() {
411 return None;
412 }
413 CStr::from_ptr(keyword).to_str().ok()
414 }
415}
416
417#[unsafe(no_mangle)]
424pub unsafe extern "C" fn AddIpoptStrOption(
425 ipopt_problem: IpoptProblem,
426 keyword: *const c_char,
427 val: *const c_char,
428) -> Bool {
429 unsafe {
430 if ipopt_problem.is_null() {
431 return FALSE;
432 }
433 let info = &mut *ipopt_problem;
434 let Some(k) = keyword_str(keyword) else {
435 return FALSE;
436 };
437 if val.is_null() {
438 return FALSE;
439 }
440 let Ok(v) = CStr::from_ptr(val).to_str() else {
441 return FALSE;
442 };
443 match info.app.options_mut().set_string_value(k, v, true, false) {
444 Ok(_) => TRUE,
445 Err(_) => FALSE,
446 }
447 }
448}
449
450#[unsafe(no_mangle)]
457pub unsafe extern "C" fn AddIpoptNumOption(
458 ipopt_problem: IpoptProblem,
459 keyword: *const c_char,
460 val: Number,
461) -> Bool {
462 unsafe {
463 if ipopt_problem.is_null() {
464 return FALSE;
465 }
466 let info = &mut *ipopt_problem;
467 let Some(k) = keyword_str(keyword) else {
468 return FALSE;
469 };
470 match info
471 .app
472 .options_mut()
473 .set_numeric_value(k, val, true, false)
474 {
475 Ok(_) => TRUE,
476 Err(_) => FALSE,
477 }
478 }
479}
480
481#[unsafe(no_mangle)]
488pub unsafe extern "C" fn AddIpoptIntOption(
489 ipopt_problem: IpoptProblem,
490 keyword: *const c_char,
491 val: Index,
492) -> Bool {
493 unsafe {
494 if ipopt_problem.is_null() {
495 return FALSE;
496 }
497 let info = &mut *ipopt_problem;
498 let Some(k) = keyword_str(keyword) else {
499 return FALSE;
500 };
501 match info.app.options_mut().set_integer_value(
502 k,
503 val as pounce_common::types::Index,
504 true,
505 false,
506 ) {
507 Ok(_) => TRUE,
508 Err(_) => FALSE,
509 }
510 }
511}
512
513#[unsafe(no_mangle)]
527pub unsafe extern "C" fn OpenIpoptOutputFile(
528 ipopt_problem: IpoptProblem,
529 file_name: *const c_char,
530 print_level: c_int,
531) -> Bool {
532 unsafe {
533 if ipopt_problem.is_null() || file_name.is_null() {
534 return FALSE;
535 }
536 let info = &mut *ipopt_problem;
537 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
538 return FALSE;
539 };
540 if info.app.open_output_file(fname, print_level) {
541 TRUE
542 } else {
543 FALSE
544 }
545 }
546}
547
548#[unsafe(no_mangle)]
568pub unsafe extern "C" fn SetIpoptProblemScaling(
569 ipopt_problem: IpoptProblem,
570 obj_scaling: Number,
571 x_scaling: *const Number,
572 g_scaling: *const Number,
573) -> Bool {
574 unsafe {
575 if ipopt_problem.is_null() {
576 return FALSE;
577 }
578 let info = &mut *ipopt_problem;
579 let n = info.n as usize;
580 let m = info.m as usize;
581 let x_vec = if !x_scaling.is_null() && n > 0 {
582 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
583 } else {
584 None
585 };
586 let g_vec = if !g_scaling.is_null() && m > 0 {
587 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
588 } else {
589 None
590 };
591 info.user_scaling = Some(UserScaling {
592 obj_scaling,
593 x_scaling: x_vec,
594 g_scaling: g_vec,
595 });
596 TRUE
597 }
598}
599
600#[allow(clippy::too_many_arguments)]
614#[unsafe(no_mangle)]
615pub unsafe extern "C" fn IpoptSolve(
616 ipopt_problem: IpoptProblem,
617 x: *mut Number,
618 g: *mut Number,
619 obj_val: *mut Number,
620 mult_g: *mut Number,
621 mult_x_L: *mut Number,
622 mult_x_U: *mut Number,
623 user_data: *mut c_void,
624) -> Index {
625 unsafe {
626 if ipopt_problem.is_null() {
627 return ApplicationReturnStatus::InternalError as Index;
628 }
629 (*ipopt_problem).last_solve = None;
637 ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
643 let info = &mut *ipopt_problem;
644 if info.n < 0 || info.m < 0 {
645 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
646 }
647 if info.n > 0 && x.is_null() {
648 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
649 }
650
651 let n_us = info.n as usize;
652 let m_us = info.m as usize;
653 let initial_x = if n_us > 0 {
654 std::slice::from_raw_parts(x, n_us).to_vec()
655 } else {
656 Vec::new()
657 };
658
659 if let Some(working) = info.pending_working_set.take() {
672 let seed_duals = matches!(
673 info.app
674 .options()
675 .get_bool_value("warm_start_init_point", ""),
676 Ok((true, true))
677 );
678 let read_in = |p: *const Number, len: usize| -> Vec<Number> {
679 if seed_duals && !p.is_null() && len > 0 {
680 std::slice::from_raw_parts(p, len).to_vec()
681 } else {
682 vec![0.0; len]
683 }
684 };
685 let lambda_g = read_in(mult_g as *const Number, m_us);
686 let z_l = read_in(mult_x_L as *const Number, n_us);
687 let z_u = read_in(mult_x_U as *const Number, n_us);
688 let lambda_x = z_l.iter().zip(&z_u).map(|(l, u)| l - u).collect();
691 info.app
692 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
693 x: initial_x.clone(),
694 lambda_g,
695 lambda_x,
696 working: Some(working),
697 });
698 }
699
700 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
701 n: info.n,
702 m: info.m,
703 nele_jac: info.nele_jac,
704 nele_hess: info.nele_hess,
705 index_style: info.index_style,
706 x_l: info.x_l.clone(),
707 x_u: info.x_u.clone(),
708 g_l: info.g_l.clone(),
709 g_u: info.g_u.clone(),
710 initial_x,
711 eval_f: info.eval_f,
712 eval_grad_f: info.eval_grad_f,
713 eval_g: info.eval_g,
714 eval_jac_g: info.eval_jac_g,
715 eval_h: info.eval_h,
716 user_data,
717 intermediate_cb: info.intermediate_cb,
718 user_scaling: info.user_scaling.clone(),
719 nonlinear_vars: info.nonlinear_vars.clone(),
720 final_status: None,
721 final_x: vec![0.0; n_us],
722 final_z_l: vec![0.0; n_us],
723 final_z_u: vec![0.0; n_us],
724 final_g: vec![0.0; m_us],
725 final_lambda: vec![0.0; m_us],
726 final_obj: 0.0,
727 }));
728
729 let feral_cfg = feral_config_from_options(info.app.options());
739 let ma57_cfg = ma57_config_from_options(info.app.options(), "resto.");
742 let bff_mint = move || -> InnerBackendFactoryFactory {
743 let feral_cfg = feral_cfg.clone();
744 let ma57_cfg = ma57_cfg.clone();
745 Box::new(move || default_backend_factory(feral_cfg.clone(), ma57_cfg.clone()))
746 };
747 let resto_provider = make_default_restoration_factory_provider(
748 RestoAlgorithmBuilder::new(),
749 info.app.algorithm_builder_from_options(),
750 bff_mint,
751 );
752 info.app.set_restoration_factory_provider(resto_provider);
753
754 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
755 info.app.defer_end_verdict();
762 let status = info.app.optimize_tnlp(bridge_for_solve);
763 let stats = info.app.statistics();
764 let narrate = pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
779 let ladder = run_second_opinion_ladder(
780 &mut info.app,
781 bridge.clone() as Rc<RefCell<dyn TNLP>>,
782 status,
783 stats,
784 &mut |line| {
785 if narrate {
786 eprintln!("{line}");
787 }
788 },
789 );
790 let status = ladder.status;
791 let bridge_ref = bridge.borrow();
792 info.last_solve = Some(LastSolve {
793 stats: ladder.statistics.clone(),
794 status,
795 linear_solver: info.app.linear_solver_summary(),
796 final_x: bridge_ref.final_x.clone(),
797 final_lambda: bridge_ref.final_lambda.clone(),
798 final_obj: bridge_ref.final_obj,
799 });
800 if !x.is_null() && n_us > 0 {
801 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
802 }
803 if !g.is_null() && m_us > 0 {
804 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
805 }
806 if !obj_val.is_null() {
807 *obj_val = bridge_ref.final_obj;
808 }
809 if !mult_g.is_null() && m_us > 0 {
810 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
811 }
812 if !mult_x_L.is_null() && n_us > 0 {
813 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
814 }
815 if !mult_x_U.is_null() && n_us > 0 {
816 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
817 }
818 status as Index
819 })
820 }
821}
822
823#[unsafe(no_mangle)]
829pub unsafe extern "C" fn SetIntermediateCallback(
830 ipopt_problem: IpoptProblem,
831 intermediate_cb: Option<Intermediate_CB>,
832) -> Bool {
833 unsafe {
834 if ipopt_problem.is_null() {
835 return FALSE;
836 }
837 let info = &mut *ipopt_problem;
838 info.intermediate_cb = intermediate_cb;
839 TRUE
840 }
841}
842
843#[allow(clippy::too_many_arguments)]
866#[unsafe(no_mangle)]
867pub unsafe extern "C" fn GetIpoptCurrentIterate(
868 ipopt_problem: IpoptProblem,
869 _scaled: Bool,
870 n: Index,
871 x: *mut Number,
872 z_l: *mut Number,
873 z_u: *mut Number,
874 m: Index,
875 g: *mut Number,
876 lambda: *mut Number,
877) -> Bool {
878 unsafe {
879 if ipopt_problem.is_null() {
880 return FALSE;
881 }
882 let info = &*ipopt_problem;
883 if n != info.n || m != info.m {
884 return FALSE;
885 }
886 let result = ip_intermediate::with_current(|ctx| {
887 let curr = {
896 let data = ctx.data.borrow();
897 match data.curr.as_ref() {
898 Some(curr) => curr.clone(),
899 None => return false,
900 }
901 };
902 let n_us = n as usize;
903 let m_us = m as usize;
904 if !x.is_null() && n_us > 0 {
905 let full_x = ctx.nlp.borrow().lift_x_to_full(&*curr.x);
906 if full_x.len() != n_us {
907 return false;
908 }
909 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
910 }
911 if !z_l.is_null() && n_us > 0 {
912 let full = ctx.nlp.borrow().pack_z_l_for_user(&*curr.z_l);
913 if full.len() != n_us {
914 return false;
915 }
916 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
917 }
918 if !z_u.is_null() && n_us > 0 {
919 let full = ctx.nlp.borrow().pack_z_u_for_user(&*curr.z_u);
920 if full.len() != n_us {
921 return false;
922 }
923 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
924 }
925 if !g.is_null() && m_us > 0 {
926 let (c, d) = {
929 let cq = ctx.cq.borrow();
930 (cq.curr_c(), cq.curr_d())
931 };
932 let full = ctx.nlp.borrow().pack_g_for_user(&*c, &*d);
933 if full.len() != m_us {
934 return false;
935 }
936 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
937 }
938 if !lambda.is_null() && m_us > 0 {
939 let full = ctx
940 .nlp
941 .borrow()
942 .pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
943 if full.len() != m_us {
944 return false;
945 }
946 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
947 }
948 true
949 });
950 if result.unwrap_or(false) { TRUE } else { FALSE }
951 }
952}
953
954#[allow(clippy::too_many_arguments)]
969#[unsafe(no_mangle)]
970pub unsafe extern "C" fn GetIpoptCurrentViolations(
971 ipopt_problem: IpoptProblem,
972 _scaled: Bool,
973 n: Index,
974 x_l_violation: *mut Number,
975 x_u_violation: *mut Number,
976 compl_x_l: *mut Number,
977 compl_x_u: *mut Number,
978 grad_lag_x: *mut Number,
979 m: Index,
980 nlp_constraint_violation: *mut Number,
981 compl_g: *mut Number,
982) -> Bool {
983 unsafe {
984 if ipopt_problem.is_null() {
985 return FALSE;
986 }
987 let info = &*ipopt_problem;
988 if n != info.n || m != info.m {
989 return FALSE;
990 }
991 let result = ip_intermediate::with_current(|ctx| {
992 let data = ctx.data.borrow();
993 let Some(_curr) = data.curr.as_ref() else {
994 return false;
995 };
996 drop(data);
997 let cq = ctx.cq.borrow();
998 let n_us = n as usize;
999 let m_us = m as usize;
1000 if !x_l_violation.is_null() && n_us > 0 {
1011 let slack = cq.curr_slack_x_l();
1012 let z_l_full = ctx.nlp.borrow().pack_z_l_for_user(&*slack);
1013 if z_l_full.len() != n_us {
1018 return false;
1019 }
1020 let mut v = vec![0.0; n_us];
1025 for (i, s) in z_l_full.iter().enumerate() {
1026 v[i] = (-s).max(0.0);
1027 }
1028 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
1029 }
1030 if !x_u_violation.is_null() && n_us > 0 {
1031 let slack = cq.curr_slack_x_u();
1032 let s_full = ctx.nlp.borrow().pack_z_u_for_user(&*slack);
1033 if s_full.len() != n_us {
1034 return false;
1035 }
1036 let mut v = vec![0.0; n_us];
1037 for (i, s) in s_full.iter().enumerate() {
1038 v[i] = (-s).max(0.0);
1039 }
1040 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
1041 }
1042 if !compl_x_l.is_null() && n_us > 0 {
1043 let compl = cq.curr_compl_x_l();
1044 let v = ctx.nlp.borrow().pack_z_l_for_user(&*compl);
1045 if v.len() != n_us {
1046 return false;
1047 }
1048 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
1049 }
1050 if !compl_x_u.is_null() && n_us > 0 {
1051 let compl = cq.curr_compl_x_u();
1052 let v = ctx.nlp.borrow().pack_z_u_for_user(&*compl);
1053 if v.len() != n_us {
1054 return false;
1055 }
1056 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
1057 }
1058 if !grad_lag_x.is_null() && n_us > 0 {
1059 let glx = cq.curr_grad_lag_x();
1060 let full = ctx.nlp.borrow().lift_x_to_full(&*glx);
1064 if full.len() != n_us {
1065 return false;
1066 }
1067 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
1068 }
1069 if !nlp_constraint_violation.is_null() && m_us > 0 {
1070 let zero = vec![0.0; m_us];
1076 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
1077 }
1078 if !compl_g.is_null() && m_us > 0 {
1079 let zero = vec![0.0; m_us];
1082 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
1083 }
1084 true
1085 });
1086 if result.unwrap_or(false) { TRUE } else { FALSE }
1087 }
1088}
1089
1090#[unsafe(no_mangle)]
1098pub unsafe extern "C" fn GetIpoptVersion(
1099 major: *mut c_int,
1100 minor: *mut c_int,
1101 release: *mut c_int,
1102) {
1103 unsafe {
1104 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1109 if !major.is_null() {
1110 *major = mj;
1111 }
1112 if !minor.is_null() {
1113 *minor = mn;
1114 }
1115 if !release.is_null() {
1116 *release = pt;
1117 }
1118 }
1119}
1120
1121fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
1122 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
1123 (
1124 it.next().unwrap_or(0),
1125 it.next().unwrap_or(0),
1126 it.next().unwrap_or(0),
1127 )
1128}
1129
1130#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
1148 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
1149}
1150
1151#[unsafe(no_mangle)]
1158pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
1159 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
1160}
1161
1162#[unsafe(no_mangle)]
1169pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
1170 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1171}
1172
1173#[unsafe(no_mangle)]
1180pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1181 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1182}
1183
1184#[unsafe(no_mangle)]
1190pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1191 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1192}
1193
1194unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1195where
1196 F: FnOnce(&SolveStatistics) -> T,
1197{
1198 unsafe {
1199 if ipopt_problem.is_null() {
1200 return None;
1201 }
1202 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1203 }
1204}
1205
1206#[unsafe(no_mangle)]
1221pub unsafe extern "C" fn GetPounceRestorationStats(
1222 ipopt_problem: IpoptProblem,
1223 calls: *mut Index,
1224 inner_iters: *mut Index,
1225 outer_iters: *mut Index,
1226 wall_secs: *mut Number,
1227) {
1228 unsafe {
1229 let stats = last_stat(ipopt_problem, |s| {
1230 (
1231 s.restoration_calls,
1232 s.restoration_inner_iters,
1233 s.restoration_outer_iters,
1234 s.restoration_wall_secs,
1235 )
1236 });
1237 let (c, i, o, w) = stats.unwrap_or((0, 0, 0, 0.0));
1238 if !calls.is_null() {
1239 *calls = c;
1240 }
1241 if !inner_iters.is_null() {
1242 *inner_iters = i;
1243 }
1244 if !outer_iters.is_null() {
1245 *outer_iters = o;
1246 }
1247 if !wall_secs.is_null() {
1248 *wall_secs = w;
1249 }
1250 }
1251}
1252
1253#[unsafe(no_mangle)]
1269pub unsafe extern "C" fn GetPounceFdHessianStats(
1270 ipopt_problem: IpoptProblem,
1271 pattern_used: *mut Index,
1272 nnz: *mut Index,
1273 n: *mut Index,
1274 groups: *mut Index,
1275 rho_max: *mut Index,
1276 coloring_fell_back: *mut Index,
1277 objective_clique_widened: *mut Index,
1278) {
1279 unsafe {
1280 let stats = last_stat(ipopt_problem, |s| {
1281 (
1282 s.fd_hessian_pattern_used,
1283 s.fd_hessian_nnz,
1284 s.fd_hessian_n,
1285 s.fd_hessian_groups,
1286 s.fd_hessian_rho_max,
1287 if s.fd_hessian_coloring_fell_back {
1288 1
1289 } else {
1290 0
1291 },
1292 if s.fd_hessian_objective_clique_widened {
1293 1
1294 } else {
1295 0
1296 },
1297 )
1298 });
1299 let (p, nz, cols, g, r, f, w) = stats.unwrap_or((-1, 0, 0, 0, 0, 0, 0));
1300 if !pattern_used.is_null() {
1301 *pattern_used = p;
1302 }
1303 if !nnz.is_null() {
1304 *nnz = nz;
1305 }
1306 if !n.is_null() {
1307 *n = cols;
1308 }
1309 if !groups.is_null() {
1310 *groups = g;
1311 }
1312 if !rho_max.is_null() {
1313 *rho_max = r;
1314 }
1315 if !coloring_fell_back.is_null() {
1316 *coloring_fell_back = f;
1317 }
1318 if !objective_clique_widened.is_null() {
1319 *objective_clique_widened = w;
1320 }
1321 }
1322}
1323
1324#[repr(C)]
1330#[derive(Debug, Clone, Copy)]
1331pub struct PounceLinearSolverStats {
1332 pub solver_name: [c_char; 32],
1333 pub n_factors: Index,
1334 pub n_pattern_reuse: Index,
1335 pub n_pattern_changes: Index,
1336 pub max_fill_ratio: Number,
1337 pub min_abs_pivot: Number,
1338 pub max_abs_pivot: Number,
1339 pub last_inertia_positive: Index,
1340 pub last_inertia_negative: Index,
1341 pub last_inertia_zero: Index,
1342 pub last_nnz_a: Index,
1343 pub last_nnz_l: Index,
1344}
1345
1346#[unsafe(no_mangle)]
1359pub unsafe extern "C" fn GetPounceLinearSolverStats(
1360 ipopt_problem: IpoptProblem,
1361 stats: *mut PounceLinearSolverStats,
1362) -> Bool {
1363 unsafe {
1364 if ipopt_problem.is_null() || stats.is_null() {
1365 return FALSE;
1366 }
1367 let Some(summary) = (*ipopt_problem)
1368 .last_solve
1369 .as_ref()
1370 .and_then(|ls| ls.linear_solver.as_ref())
1371 else {
1372 return FALSE;
1373 };
1374 let count = |v: u64| Index::try_from(v).unwrap_or(Index::MAX);
1378 let size = |x: usize| Index::try_from(x).unwrap_or(Index::MAX);
1379 let opt_size = |v: Option<usize>| v.map_or(-1, size);
1380 let inertia = summary.last_inertia;
1381 let mut out = PounceLinearSolverStats {
1382 solver_name: [0; 32],
1383 n_factors: count(summary.n_factors),
1384 n_pattern_reuse: count(summary.n_pattern_reuse),
1385 n_pattern_changes: count(summary.n_pattern_changes),
1386 max_fill_ratio: summary.max_fill_ratio.unwrap_or(Number::NAN),
1387 min_abs_pivot: summary.min_abs_pivot.unwrap_or(Number::NAN),
1388 max_abs_pivot: summary.max_abs_pivot.unwrap_or(Number::NAN),
1389 last_inertia_positive: inertia.map_or(-1, |(p, _, _)| size(p)),
1390 last_inertia_negative: inertia.map_or(-1, |(_, n, _)| size(n)),
1391 last_inertia_zero: inertia.map_or(-1, |(_, _, z)| size(z)),
1392 last_nnz_a: opt_size(summary.last_nnz_a),
1393 last_nnz_l: opt_size(summary.last_nnz_l),
1394 };
1395 let name = summary.solver_name.as_bytes();
1399 let keep = name.len().min(out.solver_name.len() - 1);
1400 for (slot, b) in out.solver_name.iter_mut().zip(&name[..keep]) {
1401 *slot = *b as c_char;
1402 }
1403 *stats = out;
1404 TRUE
1405 }
1406}
1407
1408thread_local! {
1409 static DEFAULT_REGISTRY: Rc<pounce_common::reg_options::RegisteredOptions> =
1415 Rc::clone(IpoptApplication::new().registered_options());
1416}
1417
1418#[unsafe(no_mangle)]
1437pub unsafe extern "C" fn GetPounceOptionType(
1438 ipopt_problem: IpoptProblem,
1439 keyword: *const c_char,
1440) -> c_int {
1441 unsafe {
1442 if keyword.is_null() {
1443 return 0;
1444 }
1445 let Ok(name) = CStr::from_ptr(keyword).to_str() else {
1446 return 0;
1447 };
1448 let registered = if ipopt_problem.is_null() {
1449 DEFAULT_REGISTRY.with(|r| r.get_option(name))
1450 } else {
1451 (*ipopt_problem).app.registered_options().get_option(name)
1452 };
1453 let Some(opt) = registered else {
1454 return 0;
1455 };
1456 match opt.option_type {
1457 OptionType::OT_Number => 1,
1458 OptionType::OT_Integer => 2,
1459 OptionType::OT_String => 3,
1460 OptionType::OT_Unknown => 0,
1461 }
1462 }
1463}
1464
1465fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1475 use pounce_qp::BoundStatus::*;
1476 match s {
1477 Inactive => POUNCE_WS_INACTIVE,
1478 AtLower => POUNCE_WS_AT_LOWER,
1479 AtUpper => POUNCE_WS_AT_UPPER,
1480 Fixed => POUNCE_WS_FIXED_OR_EQ,
1481 }
1482}
1483
1484fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1485 use pounce_qp::BoundStatus::*;
1486 match v {
1487 POUNCE_WS_INACTIVE => Some(Inactive),
1488 POUNCE_WS_AT_LOWER => Some(AtLower),
1489 POUNCE_WS_AT_UPPER => Some(AtUpper),
1490 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1491 _ => None,
1492 }
1493}
1494
1495fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1496 use pounce_qp::ConsStatus::*;
1497 match s {
1498 Inactive => POUNCE_WS_INACTIVE,
1499 AtLower => POUNCE_WS_AT_LOWER,
1500 AtUpper => POUNCE_WS_AT_UPPER,
1501 Equality => POUNCE_WS_FIXED_OR_EQ,
1502 }
1503}
1504
1505fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1506 use pounce_qp::ConsStatus::*;
1507 match v {
1508 POUNCE_WS_INACTIVE => Some(Inactive),
1509 POUNCE_WS_AT_LOWER => Some(AtLower),
1510 POUNCE_WS_AT_UPPER => Some(AtUpper),
1511 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1512 _ => None,
1513 }
1514}
1515
1516fn internal_to_user_rows(g_l: &[Number], g_u: &[Number]) -> Vec<usize> {
1532 let m = g_l.len();
1533 let is_eq =
1534 |i: usize| g_l[i] > NLP_LOWER_BOUND_INF && g_u[i] < NLP_UPPER_BOUND_INF && g_l[i] == g_u[i];
1535 let mut map: Vec<usize> = (0..m).filter(|&i| is_eq(i)).collect();
1536 map.extend((0..m).filter(|&i| !is_eq(i)));
1537 map
1538}
1539
1540fn internal_to_user_vars(x_l: &[Number], x_u: &[Number]) -> Vec<usize> {
1546 (0..x_l.len()).filter(|&i| x_l[i] != x_u[i]).collect()
1547}
1548
1549#[unsafe(no_mangle)]
1565pub unsafe extern "C" fn IpoptGetWorkingSet(
1566 ipopt_problem: IpoptProblem,
1567 bound_status_out: *mut IpoptBoundStatus,
1568 cons_status_out: *mut IpoptConsStatus,
1569) -> Bool {
1570 unsafe {
1571 if ipopt_problem.is_null() {
1572 return FALSE;
1573 }
1574 let info = &*ipopt_problem;
1575 let ws = match info.app.last_sqp_working_set() {
1576 Some(w) => w,
1577 None => return FALSE,
1578 };
1579 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1582 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1583 if ws.constraints.len() != row_map.len() || ws.bounds.len() != var_map.len() {
1584 return FALSE;
1587 }
1588 if !bound_status_out.is_null() {
1589 for i in 0..info.x_l.len() {
1592 *bound_status_out.add(i) = POUNCE_WS_FIXED_OR_EQ;
1593 }
1594 for (internal, &user) in var_map.iter().enumerate() {
1595 *bound_status_out.add(user) = bound_status_to_int(ws.bounds[internal]);
1596 }
1597 }
1598 if !cons_status_out.is_null() {
1599 for (internal, &user) in row_map.iter().enumerate() {
1600 *cons_status_out.add(user) = cons_status_to_int(ws.constraints[internal]);
1601 }
1602 }
1603 TRUE
1604 }
1605}
1606
1607#[unsafe(no_mangle)]
1623pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1624 ipopt_problem: IpoptProblem,
1625 bound_status_in: *const IpoptBoundStatus,
1626 cons_status_in: *const IpoptConsStatus,
1627) -> Bool {
1628 unsafe {
1629 if ipopt_problem.is_null() {
1630 return FALSE;
1631 }
1632 if bound_status_in.is_null() && cons_status_in.is_null() {
1633 return FALSE;
1634 }
1635 let info = &mut *ipopt_problem;
1636 let n = info.n.max(0) as usize;
1637 let m = info.m.max(0) as usize;
1638 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1643 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1644 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; var_map.len()];
1663 if !bound_status_in.is_null() {
1664 for i in 0..n {
1668 let v = *bound_status_in.add(i);
1669 let Some(s) = int_to_bound_status(v) else {
1670 return FALSE;
1671 };
1672 let lo_finite = info.x_l[i] > NLP_LOWER_BOUND_INF;
1673 let hi_finite = info.x_u[i] < NLP_UPPER_BOUND_INF;
1674 let consistent = match s {
1675 pounce_qp::BoundStatus::Fixed => info.x_l[i] == info.x_u[i],
1676 pounce_qp::BoundStatus::AtLower => lo_finite,
1677 pounce_qp::BoundStatus::AtUpper => hi_finite,
1678 pounce_qp::BoundStatus::Inactive => true,
1679 };
1680 if !consistent {
1681 return FALSE;
1682 }
1683 }
1684 for (internal, &user) in var_map.iter().enumerate() {
1685 if let Some(s) = int_to_bound_status(*bound_status_in.add(user)) {
1687 bounds[internal] = s;
1688 }
1689 }
1690 }
1691 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1692 if !cons_status_in.is_null() {
1693 for i in 0..m {
1694 let v = *cons_status_in.add(i);
1695 let Some(s) = int_to_cons_status(v) else {
1696 return FALSE;
1697 };
1698 let lo_finite = info.g_l[i] > NLP_LOWER_BOUND_INF;
1699 let hi_finite = info.g_u[i] < NLP_UPPER_BOUND_INF;
1700 let consistent = match s {
1701 pounce_qp::ConsStatus::Equality => {
1702 lo_finite && hi_finite && info.g_l[i] == info.g_u[i]
1703 }
1704 pounce_qp::ConsStatus::AtLower => lo_finite,
1705 pounce_qp::ConsStatus::AtUpper => hi_finite,
1706 pounce_qp::ConsStatus::Inactive => true,
1707 };
1708 if !consistent {
1709 return FALSE;
1710 }
1711 }
1712 for (internal, &user) in row_map.iter().enumerate() {
1713 if let Some(s) = int_to_cons_status(*cons_status_in.add(user)) {
1714 constraints[internal] = s;
1715 }
1716 }
1717 }
1718 info.pending_working_set = Some(pounce_qp::WorkingSet {
1730 bounds,
1731 constraints,
1732 });
1733 TRUE
1734 }
1735}
1736
1737#[unsafe(no_mangle)]
1777pub unsafe extern "C" fn IpoptSetNonlinearVariables(
1778 ipopt_problem: IpoptProblem,
1779 num_nonlin_vars: Index,
1780 pos_nonlin_vars: *const Index,
1781) -> Bool {
1782 unsafe {
1783 if ipopt_problem.is_null() {
1784 return FALSE;
1785 }
1786 let info = &mut *ipopt_problem;
1787 if num_nonlin_vars < 0 || num_nonlin_vars > info.n {
1788 return FALSE;
1789 }
1790 if num_nonlin_vars > 0 && pos_nonlin_vars.is_null() {
1791 return FALSE;
1792 }
1793 let offset = if info.index_style == 1 { 1 } else { 0 };
1794 let raw = if num_nonlin_vars == 0 {
1795 &[][..]
1796 } else {
1797 std::slice::from_raw_parts(pos_nonlin_vars, num_nonlin_vars as usize)
1798 };
1799 for &p in raw {
1802 let zero_based = p - offset;
1803 if zero_based < 0 || zero_based >= info.n {
1804 return FALSE;
1805 }
1806 }
1807 info.nonlinear_vars = Some(raw.to_vec());
1808 TRUE
1809 }
1810}
1811
1812#[unsafe(no_mangle)]
1819pub unsafe extern "C" fn IpoptClearNonlinearVariables(ipopt_problem: IpoptProblem) -> Bool {
1820 unsafe {
1821 if ipopt_problem.is_null() {
1822 return FALSE;
1823 }
1824 (*ipopt_problem).nonlinear_vars = None;
1825 TRUE
1826 }
1827}
1828
1829#[unsafe(no_mangle)]
1836pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1837 unsafe {
1838 if ipopt_problem.is_null() {
1839 return FALSE;
1840 }
1841 (*ipopt_problem).pending_working_set = None;
1842 (*ipopt_problem).app.clear_sqp_warm_start();
1843 TRUE
1844 }
1845}
1846
1847#[allow(clippy::too_many_arguments)]
1863#[unsafe(no_mangle)]
1864pub unsafe extern "C" fn IpoptSolveWarmStart(
1865 ipopt_problem: IpoptProblem,
1866 x: *mut Number,
1867 g: *mut Number,
1868 obj_val: *mut Number,
1869 mult_g: *mut Number,
1870 mult_x_L: *mut Number,
1871 mult_x_U: *mut Number,
1872 bound_status_in: *const IpoptBoundStatus,
1873 cons_status_in: *const IpoptConsStatus,
1874 bound_status_out: *mut IpoptBoundStatus,
1875 cons_status_out: *mut IpoptConsStatus,
1876 user_data: *mut c_void,
1877) -> Index {
1878 if ipopt_problem.is_null() {
1879 return ApplicationReturnStatus::InternalError as Index;
1880 }
1881 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1885 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1890 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1891 }
1892 let status = IpoptSolve(
1893 ipopt_problem,
1894 x,
1895 g,
1896 obj_val,
1897 mult_g,
1898 mult_x_L,
1899 mult_x_U,
1900 user_data,
1901 );
1902 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1903 status
1904 })
1905}
1906
1907pub(crate) struct CCallbackTnlp {
1918 pub(crate) n: Index,
1919 pub(crate) m: Index,
1920 pub(crate) nele_jac: Index,
1921 pub(crate) nele_hess: Index,
1922 pub(crate) index_style: Index,
1923 pub(crate) x_l: Vec<Number>,
1924 pub(crate) x_u: Vec<Number>,
1925 pub(crate) g_l: Vec<Number>,
1926 pub(crate) g_u: Vec<Number>,
1927 pub(crate) initial_x: Vec<Number>,
1928 pub(crate) eval_f: Option<Eval_F_CB>,
1929 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1930 pub(crate) eval_g: Option<Eval_G_CB>,
1931 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1932 pub(crate) eval_h: Option<Eval_H_CB>,
1933 pub(crate) user_data: *mut c_void,
1934 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1937 pub(crate) user_scaling: Option<UserScaling>,
1939 pub(crate) nonlinear_vars: Option<Vec<Index>>,
1942 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1943 pub(crate) final_x: Vec<Number>,
1944 pub(crate) final_z_l: Vec<Number>,
1945 pub(crate) final_z_u: Vec<Number>,
1946 pub(crate) final_g: Vec<Number>,
1947 pub(crate) final_lambda: Vec<Number>,
1948 pub(crate) final_obj: Number,
1949}
1950
1951impl TNLP for CCallbackTnlp {
1952 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1953 Some(NlpInfo {
1954 n: self.n as pounce_common::types::Index,
1955 m: self.m as pounce_common::types::Index,
1956 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1957 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1958 index_style: if self.index_style == 1 {
1959 IndexStyle::Fortran
1960 } else {
1961 IndexStyle::C
1962 },
1963 })
1964 }
1965
1966 fn get_number_of_nonlinear_variables(&mut self) -> pounce_common::types::Index {
1970 match &self.nonlinear_vars {
1971 Some(v) => v.len() as pounce_common::types::Index,
1972 None => -1,
1973 }
1974 }
1975
1976 fn get_list_of_nonlinear_variables(
1977 &mut self,
1978 pos_nonlin_vars: &mut [pounce_common::types::Index],
1979 ) -> bool {
1980 let Some(v) = self.nonlinear_vars.as_ref() else {
1981 return false;
1982 };
1983 if v.len() != pos_nonlin_vars.len() {
1984 return false;
1985 }
1986 pos_nonlin_vars.copy_from_slice(v);
1987 true
1988 }
1989
1990 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1991 if !self.x_l.is_empty() {
1992 b.x_l.copy_from_slice(&self.x_l);
1993 }
1994 if !self.x_u.is_empty() {
1995 b.x_u.copy_from_slice(&self.x_u);
1996 }
1997 if !self.g_l.is_empty() {
1998 b.g_l.copy_from_slice(&self.g_l);
1999 }
2000 if !self.g_u.is_empty() {
2001 b.g_u.copy_from_slice(&self.g_u);
2002 }
2003 true
2004 }
2005
2006 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2007 if !self.initial_x.is_empty() {
2008 sp.x.copy_from_slice(&self.initial_x);
2009 }
2010 true
2011 }
2012
2013 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
2014 let Some(s) = self.user_scaling.as_ref() else {
2015 return false;
2016 };
2017 *req.obj_scaling = s.obj_scaling;
2018 if let Some(x) = s.x_scaling.as_ref() {
2019 if x.len() == req.x_scaling.len() {
2020 req.x_scaling.copy_from_slice(x);
2021 *req.use_x_scaling = true;
2022 }
2023 } else {
2024 *req.use_x_scaling = false;
2025 }
2026 if let Some(g) = s.g_scaling.as_ref() {
2027 if g.len() == req.g_scaling.len() {
2028 req.g_scaling.copy_from_slice(g);
2029 *req.use_g_scaling = true;
2030 }
2031 } else {
2032 *req.use_g_scaling = false;
2033 }
2034 true
2035 }
2036
2037 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
2038 let cb = self.eval_f?;
2039 let mut obj = 0.0;
2040 let ok = unsafe {
2041 cb(
2042 self.n,
2043 x.as_ptr() as *mut Number,
2044 if new_x { TRUE } else { FALSE },
2045 &mut obj,
2046 self.user_data,
2047 )
2048 };
2049 if ok != FALSE { Some(obj) } else { None }
2050 }
2051
2052 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
2053 let Some(cb) = self.eval_grad_f else {
2054 return false;
2055 };
2056 let ok = unsafe {
2057 cb(
2058 self.n,
2059 x.as_ptr() as *mut Number,
2060 if new_x { TRUE } else { FALSE },
2061 grad_f.as_mut_ptr(),
2062 self.user_data,
2063 )
2064 };
2065 ok != FALSE
2066 }
2067
2068 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
2069 if self.m == 0 {
2070 return true;
2071 }
2072 let Some(cb) = self.eval_g else {
2073 return false;
2074 };
2075 let ok = unsafe {
2076 cb(
2077 self.n,
2078 x.as_ptr() as *mut Number,
2079 if new_x { TRUE } else { FALSE },
2080 self.m,
2081 g.as_mut_ptr(),
2082 self.user_data,
2083 )
2084 };
2085 ok != FALSE
2086 }
2087
2088 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
2089 if self.m == 0 || self.nele_jac == 0 {
2090 return true;
2091 }
2092 let Some(cb) = self.eval_jac_g else {
2093 return false;
2094 };
2095 let x_ptr = x
2096 .map(|s| s.as_ptr() as *mut Number)
2097 .unwrap_or(std::ptr::null_mut());
2098 let ok = match mode {
2099 SparsityRequest::Structure { irow, jcol } => unsafe {
2100 cb(
2101 self.n,
2102 x_ptr,
2103 if new_x { TRUE } else { FALSE },
2104 self.m,
2105 self.nele_jac,
2106 irow.as_mut_ptr(),
2107 jcol.as_mut_ptr(),
2108 std::ptr::null_mut(),
2109 self.user_data,
2110 )
2111 },
2112 SparsityRequest::Values { values } => unsafe {
2113 cb(
2114 self.n,
2115 x_ptr,
2116 if new_x { TRUE } else { FALSE },
2117 self.m,
2118 self.nele_jac,
2119 std::ptr::null_mut(),
2120 std::ptr::null_mut(),
2121 values.as_mut_ptr(),
2122 self.user_data,
2123 )
2124 },
2125 };
2126 ok != FALSE
2127 }
2128
2129 fn eval_h(
2130 &mut self,
2131 x: Option<&[Number]>,
2132 new_x: bool,
2133 obj_factor: Number,
2134 lambda: Option<&[Number]>,
2135 new_lambda: bool,
2136 mode: SparsityRequest<'_>,
2137 ) -> bool {
2138 let Some(cb) = self.eval_h else {
2139 return false;
2140 };
2141 if self.nele_hess == 0 {
2142 return true;
2143 }
2144 let x_ptr = x
2145 .map(|s| s.as_ptr() as *mut Number)
2146 .unwrap_or(std::ptr::null_mut());
2147 let lambda_ptr = lambda
2148 .map(|s| s.as_ptr() as *mut Number)
2149 .unwrap_or(std::ptr::null_mut());
2150 let ok = match mode {
2151 SparsityRequest::Structure { irow, jcol } => unsafe {
2152 cb(
2153 self.n,
2154 x_ptr,
2155 if new_x { TRUE } else { FALSE },
2156 obj_factor,
2157 self.m,
2158 lambda_ptr,
2159 if new_lambda { TRUE } else { FALSE },
2160 self.nele_hess,
2161 irow.as_mut_ptr(),
2162 jcol.as_mut_ptr(),
2163 std::ptr::null_mut(),
2164 self.user_data,
2165 )
2166 },
2167 SparsityRequest::Values { values } => unsafe {
2168 cb(
2169 self.n,
2170 x_ptr,
2171 if new_x { TRUE } else { FALSE },
2172 obj_factor,
2173 self.m,
2174 lambda_ptr,
2175 if new_lambda { TRUE } else { FALSE },
2176 self.nele_hess,
2177 std::ptr::null_mut(),
2178 std::ptr::null_mut(),
2179 values.as_mut_ptr(),
2180 self.user_data,
2181 )
2182 },
2183 };
2184 ok != FALSE
2185 }
2186
2187 fn intermediate_callback(
2188 &mut self,
2189 stats: pounce_nlp::tnlp::IterStats,
2190 _ip_data: &IpoptData,
2191 _ip_cq: &IpoptCq,
2192 ) -> bool {
2193 let Some(cb) = self.intermediate_cb else {
2194 return true;
2195 };
2196 let ok = unsafe {
2197 cb(
2198 stats.mode as Index,
2199 stats.iter as Index,
2200 stats.obj_value,
2201 stats.inf_pr,
2202 stats.inf_du,
2203 stats.mu,
2204 stats.d_norm,
2205 stats.regularization_size,
2206 stats.alpha_du,
2207 stats.alpha_pr,
2208 stats.ls_trials as Index,
2209 self.user_data,
2210 )
2211 };
2212 ok != FALSE
2213 }
2214
2215 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2216 self.final_status = Some(sol.status);
2217 if !sol.x.is_empty() {
2218 self.final_x.copy_from_slice(sol.x);
2219 }
2220 if !sol.z_l.is_empty() {
2221 self.final_z_l.copy_from_slice(sol.z_l);
2222 }
2223 if !sol.z_u.is_empty() {
2224 self.final_z_u.copy_from_slice(sol.z_u);
2225 }
2226 if !sol.g.is_empty() {
2227 self.final_g.copy_from_slice(sol.g);
2228 }
2229 if !sol.lambda.is_empty() {
2230 self.final_lambda.copy_from_slice(sol.lambda);
2231 }
2232 self.final_obj = sol.obj_value;
2233 }
2234}
2235
2236#[unsafe(no_mangle)]
2249pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
2250 if ipopt_problem.is_null() {
2251 return FALSE;
2252 }
2253 let info = unsafe { &mut *ipopt_problem };
2254 info.app.enable_iter_history();
2255 TRUE
2256}
2257
2258#[unsafe(no_mangle)]
2279pub unsafe extern "C" fn IpoptWriteSolveReport(
2280 ipopt_problem: IpoptProblem,
2281 path: *const c_char,
2282 detail: *const c_char,
2283) -> Bool {
2284 use pounce_solve_report::{
2285 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
2286 };
2287
2288 ffi_guard(FALSE, || unsafe {
2293 if ipopt_problem.is_null() || path.is_null() {
2294 return FALSE;
2295 }
2296 let info = &*ipopt_problem;
2297 let Some(last) = info.last_solve.as_ref() else {
2298 return FALSE;
2299 };
2300
2301 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
2302 return FALSE;
2303 };
2304
2305 let detail_choice = if detail.is_null() {
2306 ReportDetail::Summary
2307 } else {
2308 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
2309 return FALSE;
2310 };
2311 match ReportDetail::parse(detail_str) {
2312 Ok(d) => d,
2313 Err(_) => return FALSE,
2314 }
2315 };
2316
2317 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
2318 builder.problem.n_variables = info.n;
2319 builder.problem.n_constraints = info.m;
2320 builder.problem.n_objectives = 1;
2321 builder.problem.nnz_jac_g = Some(info.nele_jac);
2322 builder.problem.nnz_h_lag = Some(info.nele_hess);
2323
2324 builder.solution.status = last.status;
2325 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
2326 builder.solution.objective = last.final_obj;
2327 builder.solution.x = last.final_x.clone();
2328 builder.solution.lambda = last.final_lambda.clone();
2329
2330 builder.ingest_stats(&last.stats);
2331 if let Some(linsol) = last.linear_solver.clone() {
2332 builder.set_linear_solver_summary(linsol);
2333 }
2334
2335 let report = builder.finish();
2336 match write_report_file(std::path::Path::new(path_str), &report) {
2337 Ok(_) => TRUE,
2338 Err(_) => FALSE,
2339 }
2340 })
2341}
2342
2343#[cfg(test)]
2344mod tests {
2345 use super::*;
2346 use std::ffi::CString;
2347
2348 unsafe extern "C" fn dummy_eval_f(
2349 _n: Index,
2350 _x: *const Number,
2351 _new_x: Bool,
2352 _obj_value: *mut Number,
2353 _user_data: *mut c_void,
2354 ) -> Bool {
2355 TRUE
2356 }
2357 unsafe extern "C" fn dummy_eval_grad_f(
2358 _n: Index,
2359 _x: *const Number,
2360 _new_x: Bool,
2361 _grad_f: *mut Number,
2362 _user_data: *mut c_void,
2363 ) -> Bool {
2364 TRUE
2365 }
2366
2367 fn create_unconstrained() -> IpoptProblem {
2368 let xl = [-1.0; 4];
2369 let xu = [1.0; 4];
2370 unsafe {
2371 CreateIpoptProblem(
2372 4,
2373 xl.as_ptr(),
2374 xu.as_ptr(),
2375 0,
2376 std::ptr::null(),
2377 std::ptr::null(),
2378 0,
2379 10,
2380 0,
2381 Some(dummy_eval_f),
2382 None,
2383 Some(dummy_eval_grad_f),
2384 None,
2385 None,
2386 )
2387 }
2388 }
2389
2390 #[test]
2391 fn create_succeeds_for_unconstrained_problem() {
2392 let p = create_unconstrained();
2393 assert!(!p.is_null());
2394 unsafe { FreeIpoptProblem(p) };
2395 }
2396
2397 #[test]
2398 fn create_returns_null_on_missing_required_callbacks() {
2399 let xl = [-1.0; 4];
2400 let xu = [1.0; 4];
2401 let p = unsafe {
2402 CreateIpoptProblem(
2403 4,
2404 xl.as_ptr(),
2405 xu.as_ptr(),
2406 0,
2407 std::ptr::null(),
2408 std::ptr::null(),
2409 0,
2410 10,
2411 0,
2412 None, None,
2414 Some(dummy_eval_grad_f),
2415 None,
2416 None,
2417 )
2418 };
2419 assert!(p.is_null());
2420 }
2421
2422 #[test]
2423 fn create_returns_null_on_negative_n() {
2424 let p = unsafe {
2425 CreateIpoptProblem(
2426 -1,
2427 std::ptr::null(),
2428 std::ptr::null(),
2429 0,
2430 std::ptr::null(),
2431 std::ptr::null(),
2432 0,
2433 10,
2434 0,
2435 Some(dummy_eval_f),
2436 None,
2437 Some(dummy_eval_grad_f),
2438 None,
2439 None,
2440 )
2441 };
2442 assert!(p.is_null());
2443 }
2444
2445 #[test]
2446 fn create_returns_null_on_invalid_index_style() {
2447 let xl = [0.0; 1];
2448 let xu = [1.0; 1];
2449 let p = unsafe {
2450 CreateIpoptProblem(
2451 1,
2452 xl.as_ptr(),
2453 xu.as_ptr(),
2454 0,
2455 std::ptr::null(),
2456 std::ptr::null(),
2457 0,
2458 1,
2459 2, Some(dummy_eval_f),
2461 None,
2462 Some(dummy_eval_grad_f),
2463 None,
2464 None,
2465 )
2466 };
2467 assert!(p.is_null());
2468 }
2469
2470 #[test]
2471 fn add_int_option_forwards_to_application() {
2472 let p = create_unconstrained();
2473 let key = CString::new("print_level").unwrap();
2474 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
2475 assert_eq!(ok, TRUE);
2476 let info = unsafe { &*p };
2477 let (level, found) = info
2478 .app
2479 .options()
2480 .get_integer_value("print_level", "")
2481 .unwrap();
2482 assert!(found);
2483 assert_eq!(level, 5);
2484 unsafe { FreeIpoptProblem(p) };
2485 }
2486
2487 #[test]
2488 fn add_str_option_with_invalid_key_returns_false() {
2489 let p = create_unconstrained();
2490 let key = CString::new("totally_unknown_option").unwrap();
2491 let val = CString::new("yes").unwrap();
2492 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2493 assert_eq!(ok, FALSE);
2494 unsafe { FreeIpoptProblem(p) };
2495 }
2496
2497 #[test]
2498 fn add_options_on_null_problem_returns_false() {
2499 let key = CString::new("print_level").unwrap();
2500 let v = CString::new("yes").unwrap();
2501 unsafe {
2502 assert_eq!(
2503 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
2504 FALSE
2505 );
2506 assert_eq!(
2507 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
2508 FALSE
2509 );
2510 assert_eq!(
2511 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
2512 FALSE
2513 );
2514 }
2515 }
2516
2517 unsafe extern "C" fn dummy_intermediate(
2518 _alg_mod: Index,
2519 _iter_count: Index,
2520 _obj_value: Number,
2521 _inf_pr: Number,
2522 _inf_du: Number,
2523 _mu: Number,
2524 _d_norm: Number,
2525 _regularization_size: Number,
2526 _alpha_du: Number,
2527 _alpha_pr: Number,
2528 _ls_trials: Index,
2529 _user_data: *mut c_void,
2530 ) -> Bool {
2531 TRUE
2532 }
2533
2534 #[test]
2535 fn set_intermediate_callback_stores_pointer() {
2536 let p = create_unconstrained();
2537 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
2538 assert_eq!(ok, TRUE);
2539 let info = unsafe { &*p };
2540 assert!(info.intermediate_cb.is_some());
2541 unsafe { FreeIpoptProblem(p) };
2542 }
2543
2544 #[test]
2545 fn solve_returns_internal_error_on_null_problem() {
2546 let rc = unsafe {
2547 IpoptSolve(
2548 std::ptr::null_mut(),
2549 std::ptr::null_mut(),
2550 std::ptr::null_mut(),
2551 std::ptr::null_mut(),
2552 std::ptr::null_mut(),
2553 std::ptr::null_mut(),
2554 std::ptr::null_mut(),
2555 std::ptr::null_mut(),
2556 )
2557 };
2558 assert_eq!(rc, -199);
2559 }
2560
2561 #[test]
2562 fn free_null_is_safe() {
2563 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
2564 }
2565
2566 unsafe extern "C" fn quad_eval_f(
2572 _n: Index,
2573 x: *const Number,
2574 _new_x: Bool,
2575 obj_value: *mut Number,
2576 _user_data: *mut c_void,
2577 ) -> Bool {
2578 unsafe {
2579 let v = *x.offset(0);
2580 *obj_value = (v - 2.0) * (v - 2.0);
2581 TRUE
2582 }
2583 }
2584 unsafe extern "C" fn quad_eval_grad_f(
2585 _n: Index,
2586 x: *const Number,
2587 _new_x: Bool,
2588 grad: *mut Number,
2589 _user_data: *mut c_void,
2590 ) -> Bool {
2591 unsafe {
2592 let v = *x.offset(0);
2593 *grad.offset(0) = 2.0 * (v - 2.0);
2594 TRUE
2595 }
2596 }
2597 unsafe extern "C" fn quad_eval_h(
2598 _n: Index,
2599 _x: *const Number,
2600 _new_x: Bool,
2601 obj_factor: Number,
2602 _m: Index,
2603 _lambda: *const Number,
2604 _new_lambda: Bool,
2605 _nele_hess: Index,
2606 irow: *mut Index,
2607 jcol: *mut Index,
2608 values: *mut Number,
2609 _user_data: *mut c_void,
2610 ) -> Bool {
2611 unsafe {
2612 if !irow.is_null() && !jcol.is_null() && values.is_null() {
2613 *irow.offset(0) = 0;
2614 *jcol.offset(0) = 0;
2615 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
2616 *values.offset(0) = 2.0 * obj_factor;
2617 } else {
2618 return FALSE;
2619 }
2620 TRUE
2621 }
2622 }
2623
2624 #[test]
2625 fn solve_drives_unconstrained_quadratic_through_bridge() {
2626 let xl = [-1.0e20];
2629 let xu = [1.0e20];
2630 let p = unsafe {
2631 CreateIpoptProblem(
2632 1,
2633 xl.as_ptr(),
2634 xu.as_ptr(),
2635 0,
2636 std::ptr::null(),
2637 std::ptr::null(),
2638 0,
2639 1,
2640 0,
2641 Some(quad_eval_f),
2642 None,
2643 Some(quad_eval_grad_f),
2644 None,
2645 Some(quad_eval_h),
2646 )
2647 };
2648 assert!(!p.is_null());
2649 let mut x = [0.0_f64];
2650 let mut obj = 0.0_f64;
2651 let rc = unsafe {
2652 IpoptSolve(
2653 p,
2654 x.as_mut_ptr(),
2655 std::ptr::null_mut(),
2656 &mut obj,
2657 std::ptr::null_mut(),
2658 std::ptr::null_mut(),
2659 std::ptr::null_mut(),
2660 std::ptr::null_mut(),
2661 )
2662 };
2663 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2664 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
2665 assert!(obj.abs() < 1e-10, "obj = {}", obj);
2666 unsafe { FreeIpoptProblem(p) };
2667 }
2668
2669 #[test]
2684 fn stale_stats_cleared_when_resolve_bails() {
2685 let xl = [-1.0e20];
2686 let xu = [1.0e20];
2687 let p = unsafe {
2688 CreateIpoptProblem(
2689 1,
2690 xl.as_ptr(),
2691 xu.as_ptr(),
2692 0,
2693 std::ptr::null(),
2694 std::ptr::null(),
2695 0,
2696 1,
2697 0,
2698 Some(quad_eval_f),
2699 None,
2700 Some(quad_eval_grad_f),
2701 None,
2702 Some(quad_eval_h),
2703 )
2704 };
2705 assert!(!p.is_null());
2706
2707 let mut x = [0.0_f64];
2708 let mut obj = 0.0_f64;
2709 let rc = unsafe {
2710 IpoptSolve(
2711 p,
2712 x.as_mut_ptr(),
2713 std::ptr::null_mut(),
2714 &mut obj,
2715 std::ptr::null_mut(),
2716 std::ptr::null_mut(),
2717 std::ptr::null_mut(),
2718 std::ptr::null_mut(),
2719 )
2720 };
2721 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2722 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2724 assert!(
2725 iters_after_success >= 1,
2726 "a converged solve should record >=1 iteration, got {iters_after_success}"
2727 );
2728 assert!(unsafe { (*p).last_solve.is_some() });
2729
2730 unsafe { (*p).n = -1 };
2733 let mut x2 = [0.0_f64];
2734 let rc2 = unsafe {
2735 IpoptSolve(
2736 p,
2737 x2.as_mut_ptr(),
2738 std::ptr::null_mut(),
2739 std::ptr::null_mut(),
2740 std::ptr::null_mut(),
2741 std::ptr::null_mut(),
2742 std::ptr::null_mut(),
2743 std::ptr::null_mut(),
2744 )
2745 };
2746 assert_eq!(
2747 rc2,
2748 ApplicationReturnStatus::InvalidProblemDefinition as Index
2749 );
2750
2751 assert!(
2755 unsafe { (*p).last_solve.is_none() },
2756 "a bailed re-solve must clear stale last_solve (F5)"
2757 );
2758 assert_eq!(
2759 unsafe { GetIpoptIterCount(p) },
2760 0,
2761 "stale iteration count must not survive a bailed re-solve (F5)"
2762 );
2763
2764 unsafe { FreeIpoptProblem(p) };
2765 }
2766
2767 #[test]
2768 fn solve_invalid_problem_definition_when_x_null() {
2769 let p = create_unconstrained();
2770 let rc = unsafe {
2771 IpoptSolve(
2772 p,
2773 std::ptr::null_mut(), std::ptr::null_mut(),
2775 std::ptr::null_mut(),
2776 std::ptr::null_mut(),
2777 std::ptr::null_mut(),
2778 std::ptr::null_mut(),
2779 std::ptr::null_mut(),
2780 )
2781 };
2782 assert_eq!(
2783 rc,
2784 ApplicationReturnStatus::InvalidProblemDefinition as Index
2785 );
2786 unsafe { FreeIpoptProblem(p) };
2787 }
2788
2789 #[test]
2792 fn get_version_writes_pkg_version() {
2793 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
2794 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
2795 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
2796 assert_eq!((mj, mn, pt), expected);
2797 }
2798
2799 #[test]
2800 fn get_version_tolerates_null_buffers() {
2801 unsafe {
2803 GetIpoptVersion(
2804 std::ptr::null_mut(),
2805 std::ptr::null_mut(),
2806 std::ptr::null_mut(),
2807 )
2808 };
2809 }
2810
2811 #[test]
2812 fn set_scaling_stores_user_supplied_arrays() {
2813 let p = create_unconstrained();
2814 let xs = [2.0, 3.0, 4.0, 5.0];
2815 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
2816 assert_eq!(ok, TRUE);
2817 let info = unsafe { &*p };
2818 let s = info.user_scaling.as_ref().unwrap();
2819 assert_eq!(s.obj_scaling, 7.0);
2820 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
2821 assert!(s.g_scaling.is_none());
2822 unsafe { FreeIpoptProblem(p) };
2823 }
2824
2825 #[test]
2826 fn set_scaling_on_null_problem_returns_false() {
2827 let ok = unsafe {
2828 SetIpoptProblemScaling(
2829 std::ptr::null_mut(),
2830 1.0,
2831 std::ptr::null(),
2832 std::ptr::null(),
2833 )
2834 };
2835 assert_eq!(ok, FALSE);
2836 }
2837
2838 #[test]
2839 fn open_output_file_writes_and_attaches_journal() {
2840 let p = create_unconstrained();
2841 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2842 let _ = std::fs::create_dir_all(&dir);
2843 let path = dir.join("output.log");
2844 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2845 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2846 assert_eq!(ok, TRUE);
2847 let info = unsafe { &*p };
2849 let (level, found) = info
2850 .app
2851 .options()
2852 .get_integer_value("file_print_level", "")
2853 .unwrap();
2854 assert!(found);
2855 assert_eq!(level, 5);
2856 unsafe { FreeIpoptProblem(p) };
2857 let _ = std::fs::remove_file(&path);
2858 }
2859
2860 #[test]
2861 fn open_output_file_with_null_inputs_returns_false() {
2862 let key = CString::new("nope").unwrap();
2863 unsafe {
2864 assert_eq!(
2865 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2866 FALSE
2867 );
2868 }
2869 let p = create_unconstrained();
2870 unsafe {
2871 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2872 FreeIpoptProblem(p);
2873 }
2874 }
2875
2876 #[test]
2877 fn get_current_iterate_returns_false_outside_callback() {
2878 let p = create_unconstrained();
2879 let rc = unsafe {
2880 GetIpoptCurrentIterate(
2881 p,
2882 FALSE,
2883 0,
2884 std::ptr::null_mut(),
2885 std::ptr::null_mut(),
2886 std::ptr::null_mut(),
2887 0,
2888 std::ptr::null_mut(),
2889 std::ptr::null_mut(),
2890 )
2891 };
2892 assert_eq!(rc, FALSE);
2893 unsafe { FreeIpoptProblem(p) };
2894 }
2895
2896 #[test]
2897 fn get_current_violations_returns_false_outside_callback() {
2898 let p = create_unconstrained();
2899 let rc = unsafe {
2900 GetIpoptCurrentViolations(
2901 p,
2902 FALSE,
2903 0,
2904 std::ptr::null_mut(),
2905 std::ptr::null_mut(),
2906 std::ptr::null_mut(),
2907 std::ptr::null_mut(),
2908 std::ptr::null_mut(),
2909 0,
2910 std::ptr::null_mut(),
2911 std::ptr::null_mut(),
2912 )
2913 };
2914 assert_eq!(rc, FALSE);
2915 unsafe { FreeIpoptProblem(p) };
2916 }
2917
2918 #[test]
2919 fn post_solve_stats_zero_before_solve() {
2920 let p = create_unconstrained();
2921 unsafe {
2922 assert_eq!(GetIpoptIterCount(p), 0);
2923 assert_eq!(GetIpoptSolveTime(p), 0.0);
2924 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2925 assert_eq!(GetIpoptDualInf(p), 0.0);
2926 assert_eq!(GetIpoptComplInf(p), 0.0);
2927 FreeIpoptProblem(p);
2928 }
2929 }
2930
2931 #[test]
2932 fn post_solve_stats_populated_after_solve() {
2933 let xl = [-1.0e20];
2935 let xu = [1.0e20];
2936 let p = unsafe {
2937 CreateIpoptProblem(
2938 1,
2939 xl.as_ptr(),
2940 xu.as_ptr(),
2941 0,
2942 std::ptr::null(),
2943 std::ptr::null(),
2944 0,
2945 1,
2946 0,
2947 Some(quad_eval_f),
2948 None,
2949 Some(quad_eval_grad_f),
2950 None,
2951 Some(quad_eval_h),
2952 )
2953 };
2954 let mut x = [0.0_f64];
2955 let mut obj = 0.0_f64;
2956 let rc = unsafe {
2957 IpoptSolve(
2958 p,
2959 x.as_mut_ptr(),
2960 std::ptr::null_mut(),
2961 &mut obj,
2962 std::ptr::null_mut(),
2963 std::ptr::null_mut(),
2964 std::ptr::null_mut(),
2965 std::ptr::null_mut(),
2966 )
2967 };
2968 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2969 unsafe {
2972 assert!(GetIpoptIterCount(p) >= 0);
2973 assert!(GetIpoptSolveTime(p) >= 0.0);
2974 assert!(GetIpoptPrimalInf(p).is_finite());
2975 assert!(GetIpoptDualInf(p).is_finite());
2976 assert!(GetIpoptComplInf(p).is_finite());
2977 FreeIpoptProblem(p);
2978 }
2979 }
2980
2981 #[test]
2986 fn linear_solver_stats_populated_after_solve() {
2987 let xl = [-1.0e20];
2988 let xu = [1.0e20];
2989 let p = unsafe {
2990 CreateIpoptProblem(
2991 1,
2992 xl.as_ptr(),
2993 xu.as_ptr(),
2994 0,
2995 std::ptr::null(),
2996 std::ptr::null(),
2997 0,
2998 1,
2999 0,
3000 Some(quad_eval_f),
3001 None,
3002 Some(quad_eval_grad_f),
3003 None,
3004 Some(quad_eval_h),
3005 )
3006 };
3007 let mut stats = unsafe { std::mem::zeroed::<PounceLinearSolverStats>() };
3008 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, FALSE);
3010
3011 let mut x = [0.0_f64];
3012 let mut obj = 0.0_f64;
3013 let rc = unsafe {
3014 IpoptSolve(
3015 p,
3016 x.as_mut_ptr(),
3017 std::ptr::null_mut(),
3018 &mut obj,
3019 std::ptr::null_mut(),
3020 std::ptr::null_mut(),
3021 std::ptr::null_mut(),
3022 std::ptr::null_mut(),
3023 )
3024 };
3025 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3026 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, TRUE);
3027
3028 let name = unsafe { CStr::from_ptr(stats.solver_name.as_ptr()) }
3029 .to_str()
3030 .expect("solver name is ASCII");
3031 assert_eq!(name, "feral", "default backend should report itself");
3032 assert!(stats.n_factors > 0, "n_factors = {}", stats.n_factors);
3033 assert_eq!(
3034 stats.n_pattern_reuse + stats.n_pattern_changes,
3035 stats.n_factors,
3036 "every factor is either a pattern reuse or a pattern change"
3037 );
3038 assert!(stats.max_fill_ratio.is_nan() || stats.max_fill_ratio > 0.0);
3041 assert!(stats.last_nnz_l == -1 || stats.last_nnz_l > 0);
3042 unsafe { FreeIpoptProblem(p) };
3043 }
3044
3045 #[test]
3046 fn option_type_reports_the_setter_a_keyword_expects() {
3047 let p = create_unconstrained();
3048 let ty = |s: &str| {
3049 let c = std::ffi::CString::new(s).unwrap();
3050 unsafe { GetPounceOptionType(p, c.as_ptr()) }
3051 };
3052 assert_eq!(ty("tol"), 1, "tol is a number");
3053 assert_eq!(ty("max_iter"), 2, "max_iter is an integer");
3054 assert_eq!(ty("linear_solver"), 3, "linear_solver is a string");
3055 assert_eq!(ty("hessian_approximation"), 3);
3058 assert_eq!(ty("no_such_option_at_all"), 0);
3061 assert_eq!(unsafe { GetPounceOptionType(p, std::ptr::null()) }, 0);
3062 unsafe { FreeIpoptProblem(p) };
3063 }
3064
3065 #[test]
3068 fn option_type_answers_without_a_problem_handle() {
3069 let ty = |s: &str| {
3070 let c = std::ffi::CString::new(s).unwrap();
3071 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) }
3072 };
3073 assert_eq!(ty("tol"), 1);
3074 assert_eq!(ty("max_iter"), 2);
3075 assert_eq!(ty("linear_solver"), 3);
3076 assert_eq!(ty("no_such_option_at_all"), 0);
3077
3078 let p = create_unconstrained();
3081 for name in [
3082 "tol",
3083 "max_iter",
3084 "linear_solver",
3085 "mu_strategy",
3086 "print_level",
3087 ] {
3088 let c = std::ffi::CString::new(name).unwrap();
3089 assert_eq!(
3090 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) },
3091 unsafe { GetPounceOptionType(p, c.as_ptr()) },
3092 "handle-free and problem-bound disagree on {name}"
3093 );
3094 }
3095 unsafe { FreeIpoptProblem(p) };
3096 }
3097
3098 #[test]
3099 fn write_solve_report_emits_v1_json_with_iter_history() {
3100 let xl = [-1.0e20];
3103 let xu = [1.0e20];
3104 let p = unsafe {
3105 CreateIpoptProblem(
3106 1,
3107 xl.as_ptr(),
3108 xu.as_ptr(),
3109 0,
3110 std::ptr::null(),
3111 std::ptr::null(),
3112 0,
3113 1,
3114 0,
3115 Some(quad_eval_f),
3116 None,
3117 Some(quad_eval_grad_f),
3118 None,
3119 Some(quad_eval_h),
3120 )
3121 };
3122
3123 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
3125 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
3126 assert_eq!(bad, FALSE);
3127
3128 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
3130 let mut x = [0.0_f64];
3131 let mut obj = 0.0_f64;
3132 let rc = unsafe {
3133 IpoptSolve(
3134 p,
3135 x.as_mut_ptr(),
3136 std::ptr::null_mut(),
3137 &mut obj,
3138 std::ptr::null_mut(),
3139 std::ptr::null_mut(),
3140 std::ptr::null_mut(),
3141 std::ptr::null_mut(),
3142 )
3143 };
3144 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3145
3146 let dir = std::env::temp_dir();
3147 let path = dir.join("pounce_cinterface_report.json");
3148 let cpath = CString::new(path.to_str().unwrap()).unwrap();
3149 let cdetail = CString::new("full").unwrap();
3150 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
3151 assert_eq!(ok, TRUE);
3152
3153 let txt = std::fs::read_to_string(&path).unwrap();
3156 assert!(
3157 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
3158 "{txt}"
3159 );
3160 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
3161 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
3162 assert_eq!(parsed.problem.n_variables, 1);
3163 assert_eq!(parsed.problem.n_constraints, 0);
3164
3165 let bad_detail = CString::new("verbose").unwrap();
3167 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
3168 assert_eq!(bad, FALSE);
3169
3170 let _ = std::fs::remove_file(&path);
3171 unsafe { FreeIpoptProblem(p) };
3172 }
3173
3174 unsafe extern "C" fn cb_quad_eval_g(
3181 _n: Index,
3182 x: *const Number,
3183 _new_x: Bool,
3184 _m: Index,
3185 g: *mut Number,
3186 _user_data: *mut c_void,
3187 ) -> Bool {
3188 unsafe {
3189 *g.offset(0) = *x.offset(0);
3190 TRUE
3191 }
3192 }
3193 unsafe extern "C" fn cb_quad_eval_jac_g(
3194 _n: Index,
3195 _x: *const Number,
3196 _new_x: Bool,
3197 _m: Index,
3198 nele_jac: Index,
3199 irow: *mut Index,
3200 jcol: *mut Index,
3201 values: *mut Number,
3202 _user_data: *mut c_void,
3203 ) -> Bool {
3204 unsafe {
3205 assert_eq!(nele_jac, 1);
3206 if !irow.is_null() {
3207 *irow.offset(0) = 0;
3208 *jcol.offset(0) = 0;
3209 }
3210 if !values.is_null() {
3211 *values.offset(0) = 1.0;
3212 }
3213 TRUE
3214 }
3215 }
3216 unsafe extern "C" fn cb_quad_eval_h(
3217 _n: Index,
3218 _x: *const Number,
3219 _new_x: Bool,
3220 obj_factor: Number,
3221 _m: Index,
3222 _lambda: *const Number,
3223 _new_lambda: Bool,
3224 _nele_hess: Index,
3225 irow: *mut Index,
3226 jcol: *mut Index,
3227 values: *mut Number,
3228 _user_data: *mut c_void,
3229 ) -> Bool {
3230 unsafe {
3231 if !irow.is_null() {
3232 *irow.offset(0) = 0;
3233 *jcol.offset(0) = 0;
3234 }
3235 if !values.is_null() {
3236 *values.offset(0) = 2.0 * obj_factor;
3237 }
3238 TRUE
3239 }
3240 }
3241
3242 fn create_callback_test_problem() -> IpoptProblem {
3243 let xl = [-1.0e20];
3245 let xu = [1.0e20];
3246 let gl = [-10.0];
3247 let gu = [10.0];
3248 unsafe {
3249 CreateIpoptProblem(
3250 1,
3251 xl.as_ptr(),
3252 xu.as_ptr(),
3253 1,
3254 gl.as_ptr(),
3255 gu.as_ptr(),
3256 1,
3257 1,
3258 0,
3259 Some(quad_eval_f),
3260 Some(cb_quad_eval_g),
3261 Some(quad_eval_grad_f),
3262 Some(cb_quad_eval_jac_g),
3263 Some(cb_quad_eval_h),
3264 )
3265 }
3266 }
3267
3268 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
3269 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
3270 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
3271 std::sync::atomic::AtomicBool::new(false);
3272
3273 unsafe extern "C" fn counting_cb(
3274 _alg_mod: Index,
3275 iter_count: Index,
3276 _obj_value: Number,
3277 _inf_pr: Number,
3278 _inf_du: Number,
3279 _mu: Number,
3280 _d_norm: Number,
3281 _regularization_size: Number,
3282 _alpha_du: Number,
3283 _alpha_pr: Number,
3284 _ls_trials: Index,
3285 user_data: *mut c_void,
3286 ) -> Bool {
3287 unsafe {
3288 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3289 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
3290 let problem = user_data as IpoptProblem;
3293 let mut x = [0.0_f64];
3294 let rc = GetIpoptCurrentIterate(
3295 problem,
3296 FALSE,
3297 1,
3298 x.as_mut_ptr(),
3299 std::ptr::null_mut(),
3300 std::ptr::null_mut(),
3301 1,
3302 std::ptr::null_mut(),
3303 std::ptr::null_mut(),
3304 );
3305 if rc == TRUE && x[0].is_finite() {
3306 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3307 }
3308 TRUE
3309 }
3310 }
3311
3312 #[test]
3313 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
3314 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
3315 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
3316 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3317
3318 let p = create_callback_test_problem();
3319 assert!(!p.is_null());
3320 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
3321 assert_eq!(ok, TRUE);
3322 let mut x = [0.0_f64];
3323 let mut obj = 0.0_f64;
3324 let rc = unsafe {
3325 IpoptSolve(
3326 p,
3327 x.as_mut_ptr(),
3328 std::ptr::null_mut(),
3329 &mut obj,
3330 std::ptr::null_mut(),
3331 std::ptr::null_mut(),
3332 std::ptr::null_mut(),
3333 p as *mut c_void,
3334 )
3335 };
3336 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3337 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
3339 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
3340 assert!(
3341 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
3342 "last iter should be >= 1 after at least one accepted step"
3343 );
3344 assert!(
3345 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
3346 "GetIpoptCurrentIterate did not return a usable x"
3347 );
3348 unsafe { FreeIpoptProblem(p) };
3349 }
3350
3351 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
3352
3353 fn create_bounded_callback_test_problem() -> IpoptProblem {
3358 let xl = [0.0];
3360 let xu = [10.0];
3361 let gl = [-10.0];
3362 let gu = [10.0];
3363 unsafe {
3364 CreateIpoptProblem(
3365 1,
3366 xl.as_ptr(),
3367 xu.as_ptr(),
3368 1,
3369 gl.as_ptr(),
3370 gu.as_ptr(),
3371 1,
3372 1,
3373 0,
3374 Some(quad_eval_f),
3375 Some(cb_quad_eval_g),
3376 Some(quad_eval_grad_f),
3377 Some(cb_quad_eval_jac_g),
3378 Some(cb_quad_eval_h),
3379 )
3380 }
3381 }
3382
3383 unsafe extern "C" fn violations_inspecting_cb(
3384 _alg_mod: Index,
3385 _iter_count: Index,
3386 _obj_value: Number,
3387 _inf_pr: Number,
3388 _inf_du: Number,
3389 _mu: Number,
3390 _d_norm: Number,
3391 _regularization_size: Number,
3392 _alpha_du: Number,
3393 _alpha_pr: Number,
3394 _ls_trials: Index,
3395 user_data: *mut c_void,
3396 ) -> Bool {
3397 unsafe {
3398 let problem = user_data as IpoptProblem;
3399 let mut x_l_viol = [f64::NAN];
3404 let mut x_u_viol = [f64::NAN];
3405 let rc = GetIpoptCurrentViolations(
3406 problem,
3407 FALSE,
3408 1,
3409 x_l_viol.as_mut_ptr(),
3410 x_u_viol.as_mut_ptr(),
3411 std::ptr::null_mut(),
3412 std::ptr::null_mut(),
3413 std::ptr::null_mut(),
3414 1,
3415 std::ptr::null_mut(),
3416 std::ptr::null_mut(),
3417 );
3418 if rc == TRUE
3419 && x_l_viol[0].is_finite()
3420 && x_l_viol[0] >= 0.0
3421 && x_u_viol[0].is_finite()
3422 && x_u_viol[0] >= 0.0
3423 {
3424 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3425 }
3426 TRUE
3427 }
3428 }
3429
3430 #[test]
3431 fn get_current_violations_inside_callback_reports_finite_bounds() {
3432 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3433 let p = create_bounded_callback_test_problem();
3434 assert!(!p.is_null());
3435 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
3436 assert_eq!(ok, TRUE);
3437 let mut x = [5.0_f64];
3438 let mut obj = 0.0_f64;
3439 let rc = unsafe {
3440 IpoptSolve(
3441 p,
3442 x.as_mut_ptr(),
3443 std::ptr::null_mut(),
3444 &mut obj,
3445 std::ptr::null_mut(),
3446 std::ptr::null_mut(),
3447 std::ptr::null_mut(),
3448 p as *mut c_void,
3449 )
3450 };
3451 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3452 assert!(
3453 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
3454 "GetIpoptCurrentViolations did not return finite, non-negative \
3455 bound violations from inside the callback"
3456 );
3457 unsafe { FreeIpoptProblem(p) };
3458 }
3459
3460 #[test]
3461 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
3462 let n_us = 1usize;
3472 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
3476 let mut v = vec![0.0; n_us];
3477 for (i, s) in packed.iter().enumerate() {
3478 v[i] = (-s).max(0.0);
3479 }
3480 v
3481 });
3482 assert!(
3483 unguarded.is_err(),
3484 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
3485 );
3486
3487 let guarded: Result<Vec<f64>, ()> = (|| {
3489 if packed.len() != n_us {
3490 return Err(());
3491 }
3492 let mut v = vec![0.0; n_us];
3493 for (i, s) in packed.iter().enumerate() {
3494 v[i] = (-s).max(0.0);
3495 }
3496 Ok(v)
3497 })();
3498 assert!(
3499 guarded.is_err(),
3500 "guarded scatter should reject the length mismatch (return FALSE), not panic"
3501 );
3502 }
3503
3504 unsafe extern "C" fn user_stop_cb(
3505 _alg_mod: Index,
3506 _iter_count: Index,
3507 _obj_value: Number,
3508 _inf_pr: Number,
3509 _inf_du: Number,
3510 _mu: Number,
3511 _d_norm: Number,
3512 _regularization_size: Number,
3513 _alpha_du: Number,
3514 _alpha_pr: Number,
3515 _ls_trials: Index,
3516 _user_data: *mut c_void,
3517 ) -> Bool {
3518 FALSE
3519 }
3520
3521 #[test]
3522 fn intermediate_callback_false_surfaces_user_requested_stop() {
3523 let p = create_callback_test_problem();
3524 assert!(!p.is_null());
3525 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
3526 assert_eq!(ok, TRUE);
3527 let mut x = [0.0_f64];
3528 let rc = unsafe {
3529 IpoptSolve(
3530 p,
3531 x.as_mut_ptr(),
3532 std::ptr::null_mut(),
3533 std::ptr::null_mut(),
3534 std::ptr::null_mut(),
3535 std::ptr::null_mut(),
3536 std::ptr::null_mut(),
3537 std::ptr::null_mut(),
3538 )
3539 };
3540 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
3541 unsafe { FreeIpoptProblem(p) };
3542 }
3543
3544 #[test]
3545 fn ffi_guard_converts_panic_to_fallback() {
3546 let fallback = ApplicationReturnStatus::InternalError as Index;
3553 let got = ffi_guard(fallback, || -> Index {
3554 panic!("boom inside solver core");
3555 });
3556 assert_eq!(got, fallback);
3557 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
3558 }
3559
3560 #[test]
3561 fn ffi_guard_is_transparent_on_success() {
3562 let got = ffi_guard(-99, || 7);
3566 assert_eq!(got, 7);
3567 }
3568
3569 #[test]
3570 fn parse_pkg_version_handles_missing_components() {
3571 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
3572 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
3573 assert_eq!(parse_pkg_version(""), (0, 0, 0));
3574 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
3575 }
3576
3577 use crate::solver::{
3580 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
3581 IpoptSolverSolve,
3582 };
3583
3584 #[test]
3585 fn solver_create_consumes_problem_handle() {
3586 let mut p = create_unconstrained();
3587 assert!(!p.is_null());
3588 let s = unsafe { IpoptCreateSolver(&mut p) };
3589 assert!(!s.is_null());
3590 assert!(
3591 p.is_null(),
3592 "IpoptCreateSolver should NULL out the caller's handle"
3593 );
3594 unsafe { IpoptFreeSolver(s) };
3595 }
3596
3597 #[test]
3598 fn solver_create_null_inputs_return_null() {
3599 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
3601 assert!(s.is_null());
3602 let mut p: IpoptProblem = std::ptr::null_mut();
3604 let s = unsafe { IpoptCreateSolver(&mut p) };
3605 assert!(s.is_null());
3606 }
3607
3608 #[test]
3609 fn solver_free_null_is_safe() {
3610 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
3611 }
3612
3613 #[test]
3614 fn solver_solve_drives_quadratic_and_retains_factor() {
3615 let xl = [-1.0e20];
3616 let xu = [1.0e20];
3617 let mut p = unsafe {
3618 CreateIpoptProblem(
3619 1,
3620 xl.as_ptr(),
3621 xu.as_ptr(),
3622 0,
3623 std::ptr::null(),
3624 std::ptr::null(),
3625 0,
3626 1,
3627 0,
3628 Some(quad_eval_f),
3629 None,
3630 Some(quad_eval_grad_f),
3631 None,
3632 Some(quad_eval_h),
3633 )
3634 };
3635 assert!(!p.is_null());
3636 let s = unsafe { IpoptCreateSolver(&mut p) };
3637 assert!(!s.is_null());
3638 let mut x = [0.0_f64];
3639 let mut obj = 0.0_f64;
3640 let rc = unsafe {
3641 IpoptSolverSolve(
3642 s,
3643 x.as_mut_ptr(),
3644 std::ptr::null_mut(),
3645 &mut obj,
3646 std::ptr::null_mut(),
3647 std::ptr::null_mut(),
3648 std::ptr::null_mut(),
3649 std::ptr::null_mut(),
3650 )
3651 };
3652 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3653 assert!((x[0] - 2.0).abs() < 1e-6);
3654 assert!(obj.abs() < 1e-10);
3655
3656 let dim = unsafe { IpoptSolverGetKktDim(s) };
3659 assert!(dim > 0, "expected positive KKT dim, got {dim}");
3660 let rhs = vec![0.0_f64; dim as usize];
3661 let mut lhs = vec![1.0_f64; dim as usize];
3662 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
3663 assert_eq!(ok, TRUE);
3664 for (i, v) in lhs.iter().enumerate() {
3665 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
3666 }
3667 unsafe { IpoptFreeSolver(s) };
3668 }
3669
3670 #[test]
3671 fn solver_kkt_dim_minus_one_before_solve() {
3672 let mut p = create_unconstrained();
3673 let s = unsafe { IpoptCreateSolver(&mut p) };
3674 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
3675 unsafe { IpoptFreeSolver(s) };
3676 }
3677
3678 #[test]
3683 fn c_get_working_set_returns_false_before_any_solve() {
3684 let p = create_unconstrained();
3685 let mut bound_buf = [0; 4];
3686 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
3687 assert_eq!(rc, FALSE);
3688 unsafe { FreeIpoptProblem(p) };
3689 }
3690
3691 #[test]
3692 fn c_set_warm_start_with_both_null_returns_false() {
3693 let p = create_unconstrained();
3694 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
3695 assert_eq!(rc, FALSE);
3696 unsafe { FreeIpoptProblem(p) };
3697 }
3698
3699 #[test]
3700 fn c_set_warm_start_with_bad_status_code_returns_false() {
3701 let p = create_unconstrained();
3702 let bogus = [
3704 POUNCE_WS_INACTIVE,
3705 7,
3706 POUNCE_WS_AT_LOWER,
3707 POUNCE_WS_INACTIVE,
3708 ];
3709 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
3710 assert_eq!(rc, FALSE);
3711 unsafe { FreeIpoptProblem(p) };
3712 }
3713
3714 #[test]
3715 fn c_set_warm_start_then_clear_succeeds() {
3716 let p = create_unconstrained();
3717 let in_buf = [POUNCE_WS_INACTIVE; 4];
3718 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
3719 assert_eq!(set_rc, TRUE);
3720 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
3721 assert_eq!(clr_rc, TRUE);
3722 unsafe { FreeIpoptProblem(p) };
3723 }
3724
3725 #[test]
3726 fn c_set_warm_start_on_null_problem_returns_false() {
3727 let in_buf = [POUNCE_WS_INACTIVE; 1];
3728 let rc = unsafe {
3729 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
3730 };
3731 assert_eq!(rc, FALSE);
3732 }
3733
3734 #[test]
3735 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
3736 let p = create_callback_test_problem();
3742 let key = CString::new("algorithm").unwrap();
3743 let val = CString::new("active-set-sqp").unwrap();
3744 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
3745 assert_eq!(ok, TRUE);
3746
3747 let mut x = [0.0_f64];
3748 let mut obj = 0.0_f64;
3749 let rc1 = unsafe {
3750 IpoptSolve(
3751 p,
3752 x.as_mut_ptr(),
3753 std::ptr::null_mut(),
3754 &mut obj,
3755 std::ptr::null_mut(),
3756 std::ptr::null_mut(),
3757 std::ptr::null_mut(),
3758 std::ptr::null_mut(),
3759 )
3760 };
3761 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
3762
3763 let mut bound_buf = [-1; 1];
3764 let mut cons_buf = [-1; 1];
3765 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
3766 assert_eq!(got, TRUE);
3767 assert!((0..=3).contains(&bound_buf[0]));
3769 assert!((0..=3).contains(&cons_buf[0]));
3770
3771 x[0] = 0.0;
3776 let mut obj2 = 0.0_f64;
3777 let mut bound_out = [-1; 1];
3778 let mut cons_out = [-1; 1];
3779 let rc2 = unsafe {
3780 IpoptSolveWarmStart(
3781 p,
3782 x.as_mut_ptr(),
3783 std::ptr::null_mut(),
3784 &mut obj2,
3785 std::ptr::null_mut(),
3786 std::ptr::null_mut(),
3787 std::ptr::null_mut(),
3788 bound_buf.as_ptr(),
3789 cons_buf.as_ptr(),
3790 bound_out.as_mut_ptr(),
3791 cons_out.as_mut_ptr(),
3792 std::ptr::null_mut(),
3793 )
3794 };
3795 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
3796 assert!((0..=3).contains(&bound_out[0]));
3797 assert!((0..=3).contains(&cons_out[0]));
3798
3799 unsafe { FreeIpoptProblem(p) };
3800 }
3801}