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 let status = info.app.optimize_tnlp(bridge_for_solve);
756 let stats = info.app.statistics();
757 let narrate = pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
772 let ladder = run_second_opinion_ladder(
773 &mut info.app,
774 bridge.clone() as Rc<RefCell<dyn TNLP>>,
775 status,
776 stats,
777 &mut |line| {
778 if narrate {
779 eprintln!("{line}");
780 }
781 },
782 );
783 let status = ladder.status;
784 let bridge_ref = bridge.borrow();
785 info.last_solve = Some(LastSolve {
786 stats: ladder.statistics.clone(),
787 status,
788 linear_solver: info.app.linear_solver_summary(),
789 final_x: bridge_ref.final_x.clone(),
790 final_lambda: bridge_ref.final_lambda.clone(),
791 final_obj: bridge_ref.final_obj,
792 });
793 if !x.is_null() && n_us > 0 {
794 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
795 }
796 if !g.is_null() && m_us > 0 {
797 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
798 }
799 if !obj_val.is_null() {
800 *obj_val = bridge_ref.final_obj;
801 }
802 if !mult_g.is_null() && m_us > 0 {
803 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
804 }
805 if !mult_x_L.is_null() && n_us > 0 {
806 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
807 }
808 if !mult_x_U.is_null() && n_us > 0 {
809 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
810 }
811 status as Index
812 })
813 }
814}
815
816#[unsafe(no_mangle)]
822pub unsafe extern "C" fn SetIntermediateCallback(
823 ipopt_problem: IpoptProblem,
824 intermediate_cb: Option<Intermediate_CB>,
825) -> Bool {
826 unsafe {
827 if ipopt_problem.is_null() {
828 return FALSE;
829 }
830 let info = &mut *ipopt_problem;
831 info.intermediate_cb = intermediate_cb;
832 TRUE
833 }
834}
835
836#[allow(clippy::too_many_arguments)]
859#[unsafe(no_mangle)]
860pub unsafe extern "C" fn GetIpoptCurrentIterate(
861 ipopt_problem: IpoptProblem,
862 _scaled: Bool,
863 n: Index,
864 x: *mut Number,
865 z_l: *mut Number,
866 z_u: *mut Number,
867 m: Index,
868 g: *mut Number,
869 lambda: *mut Number,
870) -> Bool {
871 unsafe {
872 if ipopt_problem.is_null() {
873 return FALSE;
874 }
875 let info = &*ipopt_problem;
876 if n != info.n || m != info.m {
877 return FALSE;
878 }
879 let result = ip_intermediate::with_current(|ctx| {
880 let curr = {
889 let data = ctx.data.borrow();
890 match data.curr.as_ref() {
891 Some(curr) => curr.clone(),
892 None => return false,
893 }
894 };
895 let n_us = n as usize;
896 let m_us = m as usize;
897 if !x.is_null() && n_us > 0 {
898 let full_x = ctx.nlp.borrow().lift_x_to_full(&*curr.x);
899 if full_x.len() != n_us {
900 return false;
901 }
902 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
903 }
904 if !z_l.is_null() && n_us > 0 {
905 let full = ctx.nlp.borrow().pack_z_l_for_user(&*curr.z_l);
906 if full.len() != n_us {
907 return false;
908 }
909 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
910 }
911 if !z_u.is_null() && n_us > 0 {
912 let full = ctx.nlp.borrow().pack_z_u_for_user(&*curr.z_u);
913 if full.len() != n_us {
914 return false;
915 }
916 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
917 }
918 if !g.is_null() && m_us > 0 {
919 let (c, d) = {
922 let cq = ctx.cq.borrow();
923 (cq.curr_c(), cq.curr_d())
924 };
925 let full = ctx.nlp.borrow().pack_g_for_user(&*c, &*d);
926 if full.len() != m_us {
927 return false;
928 }
929 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
930 }
931 if !lambda.is_null() && m_us > 0 {
932 let full = ctx
933 .nlp
934 .borrow()
935 .pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
936 if full.len() != m_us {
937 return false;
938 }
939 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
940 }
941 true
942 });
943 if result.unwrap_or(false) { TRUE } else { FALSE }
944 }
945}
946
947#[allow(clippy::too_many_arguments)]
962#[unsafe(no_mangle)]
963pub unsafe extern "C" fn GetIpoptCurrentViolations(
964 ipopt_problem: IpoptProblem,
965 _scaled: Bool,
966 n: Index,
967 x_l_violation: *mut Number,
968 x_u_violation: *mut Number,
969 compl_x_l: *mut Number,
970 compl_x_u: *mut Number,
971 grad_lag_x: *mut Number,
972 m: Index,
973 nlp_constraint_violation: *mut Number,
974 compl_g: *mut Number,
975) -> Bool {
976 unsafe {
977 if ipopt_problem.is_null() {
978 return FALSE;
979 }
980 let info = &*ipopt_problem;
981 if n != info.n || m != info.m {
982 return FALSE;
983 }
984 let result = ip_intermediate::with_current(|ctx| {
985 let data = ctx.data.borrow();
986 let Some(_curr) = data.curr.as_ref() else {
987 return false;
988 };
989 drop(data);
990 let cq = ctx.cq.borrow();
991 let n_us = n as usize;
992 let m_us = m as usize;
993 if !x_l_violation.is_null() && n_us > 0 {
1004 let slack = cq.curr_slack_x_l();
1005 let z_l_full = ctx.nlp.borrow().pack_z_l_for_user(&*slack);
1006 if z_l_full.len() != n_us {
1011 return false;
1012 }
1013 let mut v = vec![0.0; n_us];
1018 for (i, s) in z_l_full.iter().enumerate() {
1019 v[i] = (-s).max(0.0);
1020 }
1021 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
1022 }
1023 if !x_u_violation.is_null() && n_us > 0 {
1024 let slack = cq.curr_slack_x_u();
1025 let s_full = ctx.nlp.borrow().pack_z_u_for_user(&*slack);
1026 if s_full.len() != n_us {
1027 return false;
1028 }
1029 let mut v = vec![0.0; n_us];
1030 for (i, s) in s_full.iter().enumerate() {
1031 v[i] = (-s).max(0.0);
1032 }
1033 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
1034 }
1035 if !compl_x_l.is_null() && n_us > 0 {
1036 let compl = cq.curr_compl_x_l();
1037 let v = ctx.nlp.borrow().pack_z_l_for_user(&*compl);
1038 if v.len() != n_us {
1039 return false;
1040 }
1041 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
1042 }
1043 if !compl_x_u.is_null() && n_us > 0 {
1044 let compl = cq.curr_compl_x_u();
1045 let v = ctx.nlp.borrow().pack_z_u_for_user(&*compl);
1046 if v.len() != n_us {
1047 return false;
1048 }
1049 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
1050 }
1051 if !grad_lag_x.is_null() && n_us > 0 {
1052 let glx = cq.curr_grad_lag_x();
1053 let full = ctx.nlp.borrow().lift_x_to_full(&*glx);
1057 if full.len() != n_us {
1058 return false;
1059 }
1060 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
1061 }
1062 if !nlp_constraint_violation.is_null() && m_us > 0 {
1063 let zero = vec![0.0; m_us];
1069 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
1070 }
1071 if !compl_g.is_null() && m_us > 0 {
1072 let zero = vec![0.0; m_us];
1075 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
1076 }
1077 true
1078 });
1079 if result.unwrap_or(false) { TRUE } else { FALSE }
1080 }
1081}
1082
1083#[unsafe(no_mangle)]
1091pub unsafe extern "C" fn GetIpoptVersion(
1092 major: *mut c_int,
1093 minor: *mut c_int,
1094 release: *mut c_int,
1095) {
1096 unsafe {
1097 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1102 if !major.is_null() {
1103 *major = mj;
1104 }
1105 if !minor.is_null() {
1106 *minor = mn;
1107 }
1108 if !release.is_null() {
1109 *release = pt;
1110 }
1111 }
1112}
1113
1114fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
1115 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
1116 (
1117 it.next().unwrap_or(0),
1118 it.next().unwrap_or(0),
1119 it.next().unwrap_or(0),
1120 )
1121}
1122
1123#[unsafe(no_mangle)]
1140pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
1141 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
1142}
1143
1144#[unsafe(no_mangle)]
1151pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
1152 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
1153}
1154
1155#[unsafe(no_mangle)]
1162pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
1163 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1164}
1165
1166#[unsafe(no_mangle)]
1173pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1174 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1175}
1176
1177#[unsafe(no_mangle)]
1183pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1184 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1185}
1186
1187unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1188where
1189 F: FnOnce(&SolveStatistics) -> T,
1190{
1191 unsafe {
1192 if ipopt_problem.is_null() {
1193 return None;
1194 }
1195 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1196 }
1197}
1198
1199#[unsafe(no_mangle)]
1214pub unsafe extern "C" fn GetPounceRestorationStats(
1215 ipopt_problem: IpoptProblem,
1216 calls: *mut Index,
1217 inner_iters: *mut Index,
1218 outer_iters: *mut Index,
1219 wall_secs: *mut Number,
1220) {
1221 unsafe {
1222 let stats = last_stat(ipopt_problem, |s| {
1223 (
1224 s.restoration_calls,
1225 s.restoration_inner_iters,
1226 s.restoration_outer_iters,
1227 s.restoration_wall_secs,
1228 )
1229 });
1230 let (c, i, o, w) = stats.unwrap_or((0, 0, 0, 0.0));
1231 if !calls.is_null() {
1232 *calls = c;
1233 }
1234 if !inner_iters.is_null() {
1235 *inner_iters = i;
1236 }
1237 if !outer_iters.is_null() {
1238 *outer_iters = o;
1239 }
1240 if !wall_secs.is_null() {
1241 *wall_secs = w;
1242 }
1243 }
1244}
1245
1246#[unsafe(no_mangle)]
1262pub unsafe extern "C" fn GetPounceFdHessianStats(
1263 ipopt_problem: IpoptProblem,
1264 pattern_used: *mut Index,
1265 nnz: *mut Index,
1266 n: *mut Index,
1267 groups: *mut Index,
1268 rho_max: *mut Index,
1269 coloring_fell_back: *mut Index,
1270 objective_clique_widened: *mut Index,
1271) {
1272 unsafe {
1273 let stats = last_stat(ipopt_problem, |s| {
1274 (
1275 s.fd_hessian_pattern_used,
1276 s.fd_hessian_nnz,
1277 s.fd_hessian_n,
1278 s.fd_hessian_groups,
1279 s.fd_hessian_rho_max,
1280 if s.fd_hessian_coloring_fell_back {
1281 1
1282 } else {
1283 0
1284 },
1285 if s.fd_hessian_objective_clique_widened {
1286 1
1287 } else {
1288 0
1289 },
1290 )
1291 });
1292 let (p, nz, cols, g, r, f, w) = stats.unwrap_or((-1, 0, 0, 0, 0, 0, 0));
1293 if !pattern_used.is_null() {
1294 *pattern_used = p;
1295 }
1296 if !nnz.is_null() {
1297 *nnz = nz;
1298 }
1299 if !n.is_null() {
1300 *n = cols;
1301 }
1302 if !groups.is_null() {
1303 *groups = g;
1304 }
1305 if !rho_max.is_null() {
1306 *rho_max = r;
1307 }
1308 if !coloring_fell_back.is_null() {
1309 *coloring_fell_back = f;
1310 }
1311 if !objective_clique_widened.is_null() {
1312 *objective_clique_widened = w;
1313 }
1314 }
1315}
1316
1317#[repr(C)]
1323#[derive(Debug, Clone, Copy)]
1324pub struct PounceLinearSolverStats {
1325 pub solver_name: [c_char; 32],
1326 pub n_factors: Index,
1327 pub n_pattern_reuse: Index,
1328 pub n_pattern_changes: Index,
1329 pub max_fill_ratio: Number,
1330 pub min_abs_pivot: Number,
1331 pub max_abs_pivot: Number,
1332 pub last_inertia_positive: Index,
1333 pub last_inertia_negative: Index,
1334 pub last_inertia_zero: Index,
1335 pub last_nnz_a: Index,
1336 pub last_nnz_l: Index,
1337}
1338
1339#[unsafe(no_mangle)]
1352pub unsafe extern "C" fn GetPounceLinearSolverStats(
1353 ipopt_problem: IpoptProblem,
1354 stats: *mut PounceLinearSolverStats,
1355) -> Bool {
1356 unsafe {
1357 if ipopt_problem.is_null() || stats.is_null() {
1358 return FALSE;
1359 }
1360 let Some(summary) = (*ipopt_problem)
1361 .last_solve
1362 .as_ref()
1363 .and_then(|ls| ls.linear_solver.as_ref())
1364 else {
1365 return FALSE;
1366 };
1367 let count = |v: u64| Index::try_from(v).unwrap_or(Index::MAX);
1371 let size = |x: usize| Index::try_from(x).unwrap_or(Index::MAX);
1372 let opt_size = |v: Option<usize>| v.map_or(-1, size);
1373 let inertia = summary.last_inertia;
1374 let mut out = PounceLinearSolverStats {
1375 solver_name: [0; 32],
1376 n_factors: count(summary.n_factors),
1377 n_pattern_reuse: count(summary.n_pattern_reuse),
1378 n_pattern_changes: count(summary.n_pattern_changes),
1379 max_fill_ratio: summary.max_fill_ratio.unwrap_or(Number::NAN),
1380 min_abs_pivot: summary.min_abs_pivot.unwrap_or(Number::NAN),
1381 max_abs_pivot: summary.max_abs_pivot.unwrap_or(Number::NAN),
1382 last_inertia_positive: inertia.map_or(-1, |(p, _, _)| size(p)),
1383 last_inertia_negative: inertia.map_or(-1, |(_, n, _)| size(n)),
1384 last_inertia_zero: inertia.map_or(-1, |(_, _, z)| size(z)),
1385 last_nnz_a: opt_size(summary.last_nnz_a),
1386 last_nnz_l: opt_size(summary.last_nnz_l),
1387 };
1388 let name = summary.solver_name.as_bytes();
1392 let keep = name.len().min(out.solver_name.len() - 1);
1393 for (slot, b) in out.solver_name.iter_mut().zip(&name[..keep]) {
1394 *slot = *b as c_char;
1395 }
1396 *stats = out;
1397 TRUE
1398 }
1399}
1400
1401thread_local! {
1402 static DEFAULT_REGISTRY: Rc<pounce_common::reg_options::RegisteredOptions> =
1408 Rc::clone(IpoptApplication::new().registered_options());
1409}
1410
1411#[unsafe(no_mangle)]
1430pub unsafe extern "C" fn GetPounceOptionType(
1431 ipopt_problem: IpoptProblem,
1432 keyword: *const c_char,
1433) -> c_int {
1434 unsafe {
1435 if keyword.is_null() {
1436 return 0;
1437 }
1438 let Ok(name) = CStr::from_ptr(keyword).to_str() else {
1439 return 0;
1440 };
1441 let registered = if ipopt_problem.is_null() {
1442 DEFAULT_REGISTRY.with(|r| r.get_option(name))
1443 } else {
1444 (*ipopt_problem).app.registered_options().get_option(name)
1445 };
1446 let Some(opt) = registered else {
1447 return 0;
1448 };
1449 match opt.option_type {
1450 OptionType::OT_Number => 1,
1451 OptionType::OT_Integer => 2,
1452 OptionType::OT_String => 3,
1453 OptionType::OT_Unknown => 0,
1454 }
1455 }
1456}
1457
1458fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1468 use pounce_qp::BoundStatus::*;
1469 match s {
1470 Inactive => POUNCE_WS_INACTIVE,
1471 AtLower => POUNCE_WS_AT_LOWER,
1472 AtUpper => POUNCE_WS_AT_UPPER,
1473 Fixed => POUNCE_WS_FIXED_OR_EQ,
1474 }
1475}
1476
1477fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1478 use pounce_qp::BoundStatus::*;
1479 match v {
1480 POUNCE_WS_INACTIVE => Some(Inactive),
1481 POUNCE_WS_AT_LOWER => Some(AtLower),
1482 POUNCE_WS_AT_UPPER => Some(AtUpper),
1483 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1484 _ => None,
1485 }
1486}
1487
1488fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1489 use pounce_qp::ConsStatus::*;
1490 match s {
1491 Inactive => POUNCE_WS_INACTIVE,
1492 AtLower => POUNCE_WS_AT_LOWER,
1493 AtUpper => POUNCE_WS_AT_UPPER,
1494 Equality => POUNCE_WS_FIXED_OR_EQ,
1495 }
1496}
1497
1498fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1499 use pounce_qp::ConsStatus::*;
1500 match v {
1501 POUNCE_WS_INACTIVE => Some(Inactive),
1502 POUNCE_WS_AT_LOWER => Some(AtLower),
1503 POUNCE_WS_AT_UPPER => Some(AtUpper),
1504 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1505 _ => None,
1506 }
1507}
1508
1509fn internal_to_user_rows(g_l: &[Number], g_u: &[Number]) -> Vec<usize> {
1525 let m = g_l.len();
1526 let is_eq =
1527 |i: usize| g_l[i] > NLP_LOWER_BOUND_INF && g_u[i] < NLP_UPPER_BOUND_INF && g_l[i] == g_u[i];
1528 let mut map: Vec<usize> = (0..m).filter(|&i| is_eq(i)).collect();
1529 map.extend((0..m).filter(|&i| !is_eq(i)));
1530 map
1531}
1532
1533fn internal_to_user_vars(x_l: &[Number], x_u: &[Number]) -> Vec<usize> {
1539 (0..x_l.len()).filter(|&i| x_l[i] != x_u[i]).collect()
1540}
1541
1542#[unsafe(no_mangle)]
1558pub unsafe extern "C" fn IpoptGetWorkingSet(
1559 ipopt_problem: IpoptProblem,
1560 bound_status_out: *mut IpoptBoundStatus,
1561 cons_status_out: *mut IpoptConsStatus,
1562) -> Bool {
1563 unsafe {
1564 if ipopt_problem.is_null() {
1565 return FALSE;
1566 }
1567 let info = &*ipopt_problem;
1568 let ws = match info.app.last_sqp_working_set() {
1569 Some(w) => w,
1570 None => return FALSE,
1571 };
1572 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1575 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1576 if ws.constraints.len() != row_map.len() || ws.bounds.len() != var_map.len() {
1577 return FALSE;
1580 }
1581 if !bound_status_out.is_null() {
1582 for i in 0..info.x_l.len() {
1585 *bound_status_out.add(i) = POUNCE_WS_FIXED_OR_EQ;
1586 }
1587 for (internal, &user) in var_map.iter().enumerate() {
1588 *bound_status_out.add(user) = bound_status_to_int(ws.bounds[internal]);
1589 }
1590 }
1591 if !cons_status_out.is_null() {
1592 for (internal, &user) in row_map.iter().enumerate() {
1593 *cons_status_out.add(user) = cons_status_to_int(ws.constraints[internal]);
1594 }
1595 }
1596 TRUE
1597 }
1598}
1599
1600#[unsafe(no_mangle)]
1616pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1617 ipopt_problem: IpoptProblem,
1618 bound_status_in: *const IpoptBoundStatus,
1619 cons_status_in: *const IpoptConsStatus,
1620) -> Bool {
1621 unsafe {
1622 if ipopt_problem.is_null() {
1623 return FALSE;
1624 }
1625 if bound_status_in.is_null() && cons_status_in.is_null() {
1626 return FALSE;
1627 }
1628 let info = &mut *ipopt_problem;
1629 let n = info.n.max(0) as usize;
1630 let m = info.m.max(0) as usize;
1631 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1636 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1637 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; var_map.len()];
1656 if !bound_status_in.is_null() {
1657 for i in 0..n {
1661 let v = *bound_status_in.add(i);
1662 let Some(s) = int_to_bound_status(v) else {
1663 return FALSE;
1664 };
1665 let lo_finite = info.x_l[i] > NLP_LOWER_BOUND_INF;
1666 let hi_finite = info.x_u[i] < NLP_UPPER_BOUND_INF;
1667 let consistent = match s {
1668 pounce_qp::BoundStatus::Fixed => info.x_l[i] == info.x_u[i],
1669 pounce_qp::BoundStatus::AtLower => lo_finite,
1670 pounce_qp::BoundStatus::AtUpper => hi_finite,
1671 pounce_qp::BoundStatus::Inactive => true,
1672 };
1673 if !consistent {
1674 return FALSE;
1675 }
1676 }
1677 for (internal, &user) in var_map.iter().enumerate() {
1678 if let Some(s) = int_to_bound_status(*bound_status_in.add(user)) {
1680 bounds[internal] = s;
1681 }
1682 }
1683 }
1684 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1685 if !cons_status_in.is_null() {
1686 for i in 0..m {
1687 let v = *cons_status_in.add(i);
1688 let Some(s) = int_to_cons_status(v) else {
1689 return FALSE;
1690 };
1691 let lo_finite = info.g_l[i] > NLP_LOWER_BOUND_INF;
1692 let hi_finite = info.g_u[i] < NLP_UPPER_BOUND_INF;
1693 let consistent = match s {
1694 pounce_qp::ConsStatus::Equality => {
1695 lo_finite && hi_finite && info.g_l[i] == info.g_u[i]
1696 }
1697 pounce_qp::ConsStatus::AtLower => lo_finite,
1698 pounce_qp::ConsStatus::AtUpper => hi_finite,
1699 pounce_qp::ConsStatus::Inactive => true,
1700 };
1701 if !consistent {
1702 return FALSE;
1703 }
1704 }
1705 for (internal, &user) in row_map.iter().enumerate() {
1706 if let Some(s) = int_to_cons_status(*cons_status_in.add(user)) {
1707 constraints[internal] = s;
1708 }
1709 }
1710 }
1711 info.pending_working_set = Some(pounce_qp::WorkingSet {
1723 bounds,
1724 constraints,
1725 });
1726 TRUE
1727 }
1728}
1729
1730#[unsafe(no_mangle)]
1770pub unsafe extern "C" fn IpoptSetNonlinearVariables(
1771 ipopt_problem: IpoptProblem,
1772 num_nonlin_vars: Index,
1773 pos_nonlin_vars: *const Index,
1774) -> Bool {
1775 unsafe {
1776 if ipopt_problem.is_null() {
1777 return FALSE;
1778 }
1779 let info = &mut *ipopt_problem;
1780 if num_nonlin_vars < 0 || num_nonlin_vars > info.n {
1781 return FALSE;
1782 }
1783 if num_nonlin_vars > 0 && pos_nonlin_vars.is_null() {
1784 return FALSE;
1785 }
1786 let offset = if info.index_style == 1 { 1 } else { 0 };
1787 let raw = if num_nonlin_vars == 0 {
1788 &[][..]
1789 } else {
1790 std::slice::from_raw_parts(pos_nonlin_vars, num_nonlin_vars as usize)
1791 };
1792 for &p in raw {
1795 let zero_based = p - offset;
1796 if zero_based < 0 || zero_based >= info.n {
1797 return FALSE;
1798 }
1799 }
1800 info.nonlinear_vars = Some(raw.to_vec());
1801 TRUE
1802 }
1803}
1804
1805#[unsafe(no_mangle)]
1812pub unsafe extern "C" fn IpoptClearNonlinearVariables(ipopt_problem: IpoptProblem) -> Bool {
1813 unsafe {
1814 if ipopt_problem.is_null() {
1815 return FALSE;
1816 }
1817 (*ipopt_problem).nonlinear_vars = None;
1818 TRUE
1819 }
1820}
1821
1822#[unsafe(no_mangle)]
1829pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1830 unsafe {
1831 if ipopt_problem.is_null() {
1832 return FALSE;
1833 }
1834 (*ipopt_problem).pending_working_set = None;
1835 (*ipopt_problem).app.clear_sqp_warm_start();
1836 TRUE
1837 }
1838}
1839
1840#[allow(clippy::too_many_arguments)]
1856#[unsafe(no_mangle)]
1857pub unsafe extern "C" fn IpoptSolveWarmStart(
1858 ipopt_problem: IpoptProblem,
1859 x: *mut Number,
1860 g: *mut Number,
1861 obj_val: *mut Number,
1862 mult_g: *mut Number,
1863 mult_x_L: *mut Number,
1864 mult_x_U: *mut Number,
1865 bound_status_in: *const IpoptBoundStatus,
1866 cons_status_in: *const IpoptConsStatus,
1867 bound_status_out: *mut IpoptBoundStatus,
1868 cons_status_out: *mut IpoptConsStatus,
1869 user_data: *mut c_void,
1870) -> Index {
1871 if ipopt_problem.is_null() {
1872 return ApplicationReturnStatus::InternalError as Index;
1873 }
1874 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1878 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1883 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1884 }
1885 let status = IpoptSolve(
1886 ipopt_problem,
1887 x,
1888 g,
1889 obj_val,
1890 mult_g,
1891 mult_x_L,
1892 mult_x_U,
1893 user_data,
1894 );
1895 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1896 status
1897 })
1898}
1899
1900pub(crate) struct CCallbackTnlp {
1911 pub(crate) n: Index,
1912 pub(crate) m: Index,
1913 pub(crate) nele_jac: Index,
1914 pub(crate) nele_hess: Index,
1915 pub(crate) index_style: Index,
1916 pub(crate) x_l: Vec<Number>,
1917 pub(crate) x_u: Vec<Number>,
1918 pub(crate) g_l: Vec<Number>,
1919 pub(crate) g_u: Vec<Number>,
1920 pub(crate) initial_x: Vec<Number>,
1921 pub(crate) eval_f: Option<Eval_F_CB>,
1922 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1923 pub(crate) eval_g: Option<Eval_G_CB>,
1924 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1925 pub(crate) eval_h: Option<Eval_H_CB>,
1926 pub(crate) user_data: *mut c_void,
1927 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1930 pub(crate) user_scaling: Option<UserScaling>,
1932 pub(crate) nonlinear_vars: Option<Vec<Index>>,
1935 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1936 pub(crate) final_x: Vec<Number>,
1937 pub(crate) final_z_l: Vec<Number>,
1938 pub(crate) final_z_u: Vec<Number>,
1939 pub(crate) final_g: Vec<Number>,
1940 pub(crate) final_lambda: Vec<Number>,
1941 pub(crate) final_obj: Number,
1942}
1943
1944impl TNLP for CCallbackTnlp {
1945 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1946 Some(NlpInfo {
1947 n: self.n as pounce_common::types::Index,
1948 m: self.m as pounce_common::types::Index,
1949 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1950 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1951 index_style: if self.index_style == 1 {
1952 IndexStyle::Fortran
1953 } else {
1954 IndexStyle::C
1955 },
1956 })
1957 }
1958
1959 fn get_number_of_nonlinear_variables(&mut self) -> pounce_common::types::Index {
1963 match &self.nonlinear_vars {
1964 Some(v) => v.len() as pounce_common::types::Index,
1965 None => -1,
1966 }
1967 }
1968
1969 fn get_list_of_nonlinear_variables(
1970 &mut self,
1971 pos_nonlin_vars: &mut [pounce_common::types::Index],
1972 ) -> bool {
1973 let Some(v) = self.nonlinear_vars.as_ref() else {
1974 return false;
1975 };
1976 if v.len() != pos_nonlin_vars.len() {
1977 return false;
1978 }
1979 pos_nonlin_vars.copy_from_slice(v);
1980 true
1981 }
1982
1983 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1984 if !self.x_l.is_empty() {
1985 b.x_l.copy_from_slice(&self.x_l);
1986 }
1987 if !self.x_u.is_empty() {
1988 b.x_u.copy_from_slice(&self.x_u);
1989 }
1990 if !self.g_l.is_empty() {
1991 b.g_l.copy_from_slice(&self.g_l);
1992 }
1993 if !self.g_u.is_empty() {
1994 b.g_u.copy_from_slice(&self.g_u);
1995 }
1996 true
1997 }
1998
1999 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2000 if !self.initial_x.is_empty() {
2001 sp.x.copy_from_slice(&self.initial_x);
2002 }
2003 true
2004 }
2005
2006 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
2007 let Some(s) = self.user_scaling.as_ref() else {
2008 return false;
2009 };
2010 *req.obj_scaling = s.obj_scaling;
2011 if let Some(x) = s.x_scaling.as_ref() {
2012 if x.len() == req.x_scaling.len() {
2013 req.x_scaling.copy_from_slice(x);
2014 *req.use_x_scaling = true;
2015 }
2016 } else {
2017 *req.use_x_scaling = false;
2018 }
2019 if let Some(g) = s.g_scaling.as_ref() {
2020 if g.len() == req.g_scaling.len() {
2021 req.g_scaling.copy_from_slice(g);
2022 *req.use_g_scaling = true;
2023 }
2024 } else {
2025 *req.use_g_scaling = false;
2026 }
2027 true
2028 }
2029
2030 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
2031 let cb = self.eval_f?;
2032 let mut obj = 0.0;
2033 let ok = unsafe {
2034 cb(
2035 self.n,
2036 x.as_ptr() as *mut Number,
2037 if new_x { TRUE } else { FALSE },
2038 &mut obj,
2039 self.user_data,
2040 )
2041 };
2042 if ok != FALSE { Some(obj) } else { None }
2043 }
2044
2045 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
2046 let Some(cb) = self.eval_grad_f else {
2047 return false;
2048 };
2049 let ok = unsafe {
2050 cb(
2051 self.n,
2052 x.as_ptr() as *mut Number,
2053 if new_x { TRUE } else { FALSE },
2054 grad_f.as_mut_ptr(),
2055 self.user_data,
2056 )
2057 };
2058 ok != FALSE
2059 }
2060
2061 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
2062 if self.m == 0 {
2063 return true;
2064 }
2065 let Some(cb) = self.eval_g else {
2066 return false;
2067 };
2068 let ok = unsafe {
2069 cb(
2070 self.n,
2071 x.as_ptr() as *mut Number,
2072 if new_x { TRUE } else { FALSE },
2073 self.m,
2074 g.as_mut_ptr(),
2075 self.user_data,
2076 )
2077 };
2078 ok != FALSE
2079 }
2080
2081 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
2082 if self.m == 0 || self.nele_jac == 0 {
2083 return true;
2084 }
2085 let Some(cb) = self.eval_jac_g else {
2086 return false;
2087 };
2088 let x_ptr = x
2089 .map(|s| s.as_ptr() as *mut Number)
2090 .unwrap_or(std::ptr::null_mut());
2091 let ok = match mode {
2092 SparsityRequest::Structure { irow, jcol } => unsafe {
2093 cb(
2094 self.n,
2095 x_ptr,
2096 if new_x { TRUE } else { FALSE },
2097 self.m,
2098 self.nele_jac,
2099 irow.as_mut_ptr(),
2100 jcol.as_mut_ptr(),
2101 std::ptr::null_mut(),
2102 self.user_data,
2103 )
2104 },
2105 SparsityRequest::Values { values } => unsafe {
2106 cb(
2107 self.n,
2108 x_ptr,
2109 if new_x { TRUE } else { FALSE },
2110 self.m,
2111 self.nele_jac,
2112 std::ptr::null_mut(),
2113 std::ptr::null_mut(),
2114 values.as_mut_ptr(),
2115 self.user_data,
2116 )
2117 },
2118 };
2119 ok != FALSE
2120 }
2121
2122 fn eval_h(
2123 &mut self,
2124 x: Option<&[Number]>,
2125 new_x: bool,
2126 obj_factor: Number,
2127 lambda: Option<&[Number]>,
2128 new_lambda: bool,
2129 mode: SparsityRequest<'_>,
2130 ) -> bool {
2131 let Some(cb) = self.eval_h else {
2132 return false;
2133 };
2134 if self.nele_hess == 0 {
2135 return true;
2136 }
2137 let x_ptr = x
2138 .map(|s| s.as_ptr() as *mut Number)
2139 .unwrap_or(std::ptr::null_mut());
2140 let lambda_ptr = lambda
2141 .map(|s| s.as_ptr() as *mut Number)
2142 .unwrap_or(std::ptr::null_mut());
2143 let ok = match mode {
2144 SparsityRequest::Structure { irow, jcol } => unsafe {
2145 cb(
2146 self.n,
2147 x_ptr,
2148 if new_x { TRUE } else { FALSE },
2149 obj_factor,
2150 self.m,
2151 lambda_ptr,
2152 if new_lambda { TRUE } else { FALSE },
2153 self.nele_hess,
2154 irow.as_mut_ptr(),
2155 jcol.as_mut_ptr(),
2156 std::ptr::null_mut(),
2157 self.user_data,
2158 )
2159 },
2160 SparsityRequest::Values { values } => unsafe {
2161 cb(
2162 self.n,
2163 x_ptr,
2164 if new_x { TRUE } else { FALSE },
2165 obj_factor,
2166 self.m,
2167 lambda_ptr,
2168 if new_lambda { TRUE } else { FALSE },
2169 self.nele_hess,
2170 std::ptr::null_mut(),
2171 std::ptr::null_mut(),
2172 values.as_mut_ptr(),
2173 self.user_data,
2174 )
2175 },
2176 };
2177 ok != FALSE
2178 }
2179
2180 fn intermediate_callback(
2181 &mut self,
2182 stats: pounce_nlp::tnlp::IterStats,
2183 _ip_data: &IpoptData,
2184 _ip_cq: &IpoptCq,
2185 ) -> bool {
2186 let Some(cb) = self.intermediate_cb else {
2187 return true;
2188 };
2189 let ok = unsafe {
2190 cb(
2191 stats.mode as Index,
2192 stats.iter as Index,
2193 stats.obj_value,
2194 stats.inf_pr,
2195 stats.inf_du,
2196 stats.mu,
2197 stats.d_norm,
2198 stats.regularization_size,
2199 stats.alpha_du,
2200 stats.alpha_pr,
2201 stats.ls_trials as Index,
2202 self.user_data,
2203 )
2204 };
2205 ok != FALSE
2206 }
2207
2208 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2209 self.final_status = Some(sol.status);
2210 if !sol.x.is_empty() {
2211 self.final_x.copy_from_slice(sol.x);
2212 }
2213 if !sol.z_l.is_empty() {
2214 self.final_z_l.copy_from_slice(sol.z_l);
2215 }
2216 if !sol.z_u.is_empty() {
2217 self.final_z_u.copy_from_slice(sol.z_u);
2218 }
2219 if !sol.g.is_empty() {
2220 self.final_g.copy_from_slice(sol.g);
2221 }
2222 if !sol.lambda.is_empty() {
2223 self.final_lambda.copy_from_slice(sol.lambda);
2224 }
2225 self.final_obj = sol.obj_value;
2226 }
2227}
2228
2229#[unsafe(no_mangle)]
2242pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
2243 if ipopt_problem.is_null() {
2244 return FALSE;
2245 }
2246 let info = unsafe { &mut *ipopt_problem };
2247 info.app.enable_iter_history();
2248 TRUE
2249}
2250
2251#[unsafe(no_mangle)]
2272pub unsafe extern "C" fn IpoptWriteSolveReport(
2273 ipopt_problem: IpoptProblem,
2274 path: *const c_char,
2275 detail: *const c_char,
2276) -> Bool {
2277 use pounce_solve_report::{
2278 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
2279 };
2280
2281 ffi_guard(FALSE, || unsafe {
2286 if ipopt_problem.is_null() || path.is_null() {
2287 return FALSE;
2288 }
2289 let info = &*ipopt_problem;
2290 let Some(last) = info.last_solve.as_ref() else {
2291 return FALSE;
2292 };
2293
2294 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
2295 return FALSE;
2296 };
2297
2298 let detail_choice = if detail.is_null() {
2299 ReportDetail::Summary
2300 } else {
2301 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
2302 return FALSE;
2303 };
2304 match ReportDetail::parse(detail_str) {
2305 Ok(d) => d,
2306 Err(_) => return FALSE,
2307 }
2308 };
2309
2310 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
2311 builder.problem.n_variables = info.n;
2312 builder.problem.n_constraints = info.m;
2313 builder.problem.n_objectives = 1;
2314 builder.problem.nnz_jac_g = Some(info.nele_jac);
2315 builder.problem.nnz_h_lag = Some(info.nele_hess);
2316
2317 builder.solution.status = last.status;
2318 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
2319 builder.solution.objective = last.final_obj;
2320 builder.solution.x = last.final_x.clone();
2321 builder.solution.lambda = last.final_lambda.clone();
2322
2323 builder.ingest_stats(&last.stats);
2324 if let Some(linsol) = last.linear_solver.clone() {
2325 builder.set_linear_solver_summary(linsol);
2326 }
2327
2328 let report = builder.finish();
2329 match write_report_file(std::path::Path::new(path_str), &report) {
2330 Ok(_) => TRUE,
2331 Err(_) => FALSE,
2332 }
2333 })
2334}
2335
2336#[cfg(test)]
2337mod tests {
2338 use super::*;
2339 use std::ffi::CString;
2340
2341 unsafe extern "C" fn dummy_eval_f(
2342 _n: Index,
2343 _x: *const Number,
2344 _new_x: Bool,
2345 _obj_value: *mut Number,
2346 _user_data: *mut c_void,
2347 ) -> Bool {
2348 TRUE
2349 }
2350 unsafe extern "C" fn dummy_eval_grad_f(
2351 _n: Index,
2352 _x: *const Number,
2353 _new_x: Bool,
2354 _grad_f: *mut Number,
2355 _user_data: *mut c_void,
2356 ) -> Bool {
2357 TRUE
2358 }
2359
2360 fn create_unconstrained() -> IpoptProblem {
2361 let xl = [-1.0; 4];
2362 let xu = [1.0; 4];
2363 unsafe {
2364 CreateIpoptProblem(
2365 4,
2366 xl.as_ptr(),
2367 xu.as_ptr(),
2368 0,
2369 std::ptr::null(),
2370 std::ptr::null(),
2371 0,
2372 10,
2373 0,
2374 Some(dummy_eval_f),
2375 None,
2376 Some(dummy_eval_grad_f),
2377 None,
2378 None,
2379 )
2380 }
2381 }
2382
2383 #[test]
2384 fn create_succeeds_for_unconstrained_problem() {
2385 let p = create_unconstrained();
2386 assert!(!p.is_null());
2387 unsafe { FreeIpoptProblem(p) };
2388 }
2389
2390 #[test]
2391 fn create_returns_null_on_missing_required_callbacks() {
2392 let xl = [-1.0; 4];
2393 let xu = [1.0; 4];
2394 let p = unsafe {
2395 CreateIpoptProblem(
2396 4,
2397 xl.as_ptr(),
2398 xu.as_ptr(),
2399 0,
2400 std::ptr::null(),
2401 std::ptr::null(),
2402 0,
2403 10,
2404 0,
2405 None, None,
2407 Some(dummy_eval_grad_f),
2408 None,
2409 None,
2410 )
2411 };
2412 assert!(p.is_null());
2413 }
2414
2415 #[test]
2416 fn create_returns_null_on_negative_n() {
2417 let p = unsafe {
2418 CreateIpoptProblem(
2419 -1,
2420 std::ptr::null(),
2421 std::ptr::null(),
2422 0,
2423 std::ptr::null(),
2424 std::ptr::null(),
2425 0,
2426 10,
2427 0,
2428 Some(dummy_eval_f),
2429 None,
2430 Some(dummy_eval_grad_f),
2431 None,
2432 None,
2433 )
2434 };
2435 assert!(p.is_null());
2436 }
2437
2438 #[test]
2439 fn create_returns_null_on_invalid_index_style() {
2440 let xl = [0.0; 1];
2441 let xu = [1.0; 1];
2442 let p = unsafe {
2443 CreateIpoptProblem(
2444 1,
2445 xl.as_ptr(),
2446 xu.as_ptr(),
2447 0,
2448 std::ptr::null(),
2449 std::ptr::null(),
2450 0,
2451 1,
2452 2, Some(dummy_eval_f),
2454 None,
2455 Some(dummy_eval_grad_f),
2456 None,
2457 None,
2458 )
2459 };
2460 assert!(p.is_null());
2461 }
2462
2463 #[test]
2464 fn add_int_option_forwards_to_application() {
2465 let p = create_unconstrained();
2466 let key = CString::new("print_level").unwrap();
2467 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
2468 assert_eq!(ok, TRUE);
2469 let info = unsafe { &*p };
2470 let (level, found) = info
2471 .app
2472 .options()
2473 .get_integer_value("print_level", "")
2474 .unwrap();
2475 assert!(found);
2476 assert_eq!(level, 5);
2477 unsafe { FreeIpoptProblem(p) };
2478 }
2479
2480 #[test]
2481 fn add_str_option_with_invalid_key_returns_false() {
2482 let p = create_unconstrained();
2483 let key = CString::new("totally_unknown_option").unwrap();
2484 let val = CString::new("yes").unwrap();
2485 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2486 assert_eq!(ok, FALSE);
2487 unsafe { FreeIpoptProblem(p) };
2488 }
2489
2490 #[test]
2491 fn add_options_on_null_problem_returns_false() {
2492 let key = CString::new("print_level").unwrap();
2493 let v = CString::new("yes").unwrap();
2494 unsafe {
2495 assert_eq!(
2496 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
2497 FALSE
2498 );
2499 assert_eq!(
2500 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
2501 FALSE
2502 );
2503 assert_eq!(
2504 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
2505 FALSE
2506 );
2507 }
2508 }
2509
2510 unsafe extern "C" fn dummy_intermediate(
2511 _alg_mod: Index,
2512 _iter_count: Index,
2513 _obj_value: Number,
2514 _inf_pr: Number,
2515 _inf_du: Number,
2516 _mu: Number,
2517 _d_norm: Number,
2518 _regularization_size: Number,
2519 _alpha_du: Number,
2520 _alpha_pr: Number,
2521 _ls_trials: Index,
2522 _user_data: *mut c_void,
2523 ) -> Bool {
2524 TRUE
2525 }
2526
2527 #[test]
2528 fn set_intermediate_callback_stores_pointer() {
2529 let p = create_unconstrained();
2530 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
2531 assert_eq!(ok, TRUE);
2532 let info = unsafe { &*p };
2533 assert!(info.intermediate_cb.is_some());
2534 unsafe { FreeIpoptProblem(p) };
2535 }
2536
2537 #[test]
2538 fn solve_returns_internal_error_on_null_problem() {
2539 let rc = unsafe {
2540 IpoptSolve(
2541 std::ptr::null_mut(),
2542 std::ptr::null_mut(),
2543 std::ptr::null_mut(),
2544 std::ptr::null_mut(),
2545 std::ptr::null_mut(),
2546 std::ptr::null_mut(),
2547 std::ptr::null_mut(),
2548 std::ptr::null_mut(),
2549 )
2550 };
2551 assert_eq!(rc, -199);
2552 }
2553
2554 #[test]
2555 fn free_null_is_safe() {
2556 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
2557 }
2558
2559 unsafe extern "C" fn quad_eval_f(
2565 _n: Index,
2566 x: *const Number,
2567 _new_x: Bool,
2568 obj_value: *mut Number,
2569 _user_data: *mut c_void,
2570 ) -> Bool {
2571 unsafe {
2572 let v = *x.offset(0);
2573 *obj_value = (v - 2.0) * (v - 2.0);
2574 TRUE
2575 }
2576 }
2577 unsafe extern "C" fn quad_eval_grad_f(
2578 _n: Index,
2579 x: *const Number,
2580 _new_x: Bool,
2581 grad: *mut Number,
2582 _user_data: *mut c_void,
2583 ) -> Bool {
2584 unsafe {
2585 let v = *x.offset(0);
2586 *grad.offset(0) = 2.0 * (v - 2.0);
2587 TRUE
2588 }
2589 }
2590 unsafe extern "C" fn quad_eval_h(
2591 _n: Index,
2592 _x: *const Number,
2593 _new_x: Bool,
2594 obj_factor: Number,
2595 _m: Index,
2596 _lambda: *const Number,
2597 _new_lambda: Bool,
2598 _nele_hess: Index,
2599 irow: *mut Index,
2600 jcol: *mut Index,
2601 values: *mut Number,
2602 _user_data: *mut c_void,
2603 ) -> Bool {
2604 unsafe {
2605 if !irow.is_null() && !jcol.is_null() && values.is_null() {
2606 *irow.offset(0) = 0;
2607 *jcol.offset(0) = 0;
2608 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
2609 *values.offset(0) = 2.0 * obj_factor;
2610 } else {
2611 return FALSE;
2612 }
2613 TRUE
2614 }
2615 }
2616
2617 #[test]
2618 fn solve_drives_unconstrained_quadratic_through_bridge() {
2619 let xl = [-1.0e20];
2622 let xu = [1.0e20];
2623 let p = unsafe {
2624 CreateIpoptProblem(
2625 1,
2626 xl.as_ptr(),
2627 xu.as_ptr(),
2628 0,
2629 std::ptr::null(),
2630 std::ptr::null(),
2631 0,
2632 1,
2633 0,
2634 Some(quad_eval_f),
2635 None,
2636 Some(quad_eval_grad_f),
2637 None,
2638 Some(quad_eval_h),
2639 )
2640 };
2641 assert!(!p.is_null());
2642 let mut x = [0.0_f64];
2643 let mut obj = 0.0_f64;
2644 let rc = unsafe {
2645 IpoptSolve(
2646 p,
2647 x.as_mut_ptr(),
2648 std::ptr::null_mut(),
2649 &mut obj,
2650 std::ptr::null_mut(),
2651 std::ptr::null_mut(),
2652 std::ptr::null_mut(),
2653 std::ptr::null_mut(),
2654 )
2655 };
2656 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2657 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
2658 assert!(obj.abs() < 1e-10, "obj = {}", obj);
2659 unsafe { FreeIpoptProblem(p) };
2660 }
2661
2662 #[test]
2677 fn stale_stats_cleared_when_resolve_bails() {
2678 let xl = [-1.0e20];
2679 let xu = [1.0e20];
2680 let p = unsafe {
2681 CreateIpoptProblem(
2682 1,
2683 xl.as_ptr(),
2684 xu.as_ptr(),
2685 0,
2686 std::ptr::null(),
2687 std::ptr::null(),
2688 0,
2689 1,
2690 0,
2691 Some(quad_eval_f),
2692 None,
2693 Some(quad_eval_grad_f),
2694 None,
2695 Some(quad_eval_h),
2696 )
2697 };
2698 assert!(!p.is_null());
2699
2700 let mut x = [0.0_f64];
2701 let mut obj = 0.0_f64;
2702 let rc = unsafe {
2703 IpoptSolve(
2704 p,
2705 x.as_mut_ptr(),
2706 std::ptr::null_mut(),
2707 &mut obj,
2708 std::ptr::null_mut(),
2709 std::ptr::null_mut(),
2710 std::ptr::null_mut(),
2711 std::ptr::null_mut(),
2712 )
2713 };
2714 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2715 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2717 assert!(
2718 iters_after_success >= 1,
2719 "a converged solve should record >=1 iteration, got {iters_after_success}"
2720 );
2721 assert!(unsafe { (*p).last_solve.is_some() });
2722
2723 unsafe { (*p).n = -1 };
2726 let mut x2 = [0.0_f64];
2727 let rc2 = unsafe {
2728 IpoptSolve(
2729 p,
2730 x2.as_mut_ptr(),
2731 std::ptr::null_mut(),
2732 std::ptr::null_mut(),
2733 std::ptr::null_mut(),
2734 std::ptr::null_mut(),
2735 std::ptr::null_mut(),
2736 std::ptr::null_mut(),
2737 )
2738 };
2739 assert_eq!(
2740 rc2,
2741 ApplicationReturnStatus::InvalidProblemDefinition as Index
2742 );
2743
2744 assert!(
2748 unsafe { (*p).last_solve.is_none() },
2749 "a bailed re-solve must clear stale last_solve (F5)"
2750 );
2751 assert_eq!(
2752 unsafe { GetIpoptIterCount(p) },
2753 0,
2754 "stale iteration count must not survive a bailed re-solve (F5)"
2755 );
2756
2757 unsafe { FreeIpoptProblem(p) };
2758 }
2759
2760 #[test]
2761 fn solve_invalid_problem_definition_when_x_null() {
2762 let p = create_unconstrained();
2763 let rc = unsafe {
2764 IpoptSolve(
2765 p,
2766 std::ptr::null_mut(), std::ptr::null_mut(),
2768 std::ptr::null_mut(),
2769 std::ptr::null_mut(),
2770 std::ptr::null_mut(),
2771 std::ptr::null_mut(),
2772 std::ptr::null_mut(),
2773 )
2774 };
2775 assert_eq!(
2776 rc,
2777 ApplicationReturnStatus::InvalidProblemDefinition as Index
2778 );
2779 unsafe { FreeIpoptProblem(p) };
2780 }
2781
2782 #[test]
2785 fn get_version_writes_pkg_version() {
2786 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
2787 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
2788 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
2789 assert_eq!((mj, mn, pt), expected);
2790 }
2791
2792 #[test]
2793 fn get_version_tolerates_null_buffers() {
2794 unsafe {
2796 GetIpoptVersion(
2797 std::ptr::null_mut(),
2798 std::ptr::null_mut(),
2799 std::ptr::null_mut(),
2800 )
2801 };
2802 }
2803
2804 #[test]
2805 fn set_scaling_stores_user_supplied_arrays() {
2806 let p = create_unconstrained();
2807 let xs = [2.0, 3.0, 4.0, 5.0];
2808 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
2809 assert_eq!(ok, TRUE);
2810 let info = unsafe { &*p };
2811 let s = info.user_scaling.as_ref().unwrap();
2812 assert_eq!(s.obj_scaling, 7.0);
2813 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
2814 assert!(s.g_scaling.is_none());
2815 unsafe { FreeIpoptProblem(p) };
2816 }
2817
2818 #[test]
2819 fn set_scaling_on_null_problem_returns_false() {
2820 let ok = unsafe {
2821 SetIpoptProblemScaling(
2822 std::ptr::null_mut(),
2823 1.0,
2824 std::ptr::null(),
2825 std::ptr::null(),
2826 )
2827 };
2828 assert_eq!(ok, FALSE);
2829 }
2830
2831 #[test]
2832 fn open_output_file_writes_and_attaches_journal() {
2833 let p = create_unconstrained();
2834 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2835 let _ = std::fs::create_dir_all(&dir);
2836 let path = dir.join("output.log");
2837 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2838 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2839 assert_eq!(ok, TRUE);
2840 let info = unsafe { &*p };
2842 let (level, found) = info
2843 .app
2844 .options()
2845 .get_integer_value("file_print_level", "")
2846 .unwrap();
2847 assert!(found);
2848 assert_eq!(level, 5);
2849 unsafe { FreeIpoptProblem(p) };
2850 let _ = std::fs::remove_file(&path);
2851 }
2852
2853 #[test]
2854 fn open_output_file_with_null_inputs_returns_false() {
2855 let key = CString::new("nope").unwrap();
2856 unsafe {
2857 assert_eq!(
2858 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2859 FALSE
2860 );
2861 }
2862 let p = create_unconstrained();
2863 unsafe {
2864 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2865 FreeIpoptProblem(p);
2866 }
2867 }
2868
2869 #[test]
2870 fn get_current_iterate_returns_false_outside_callback() {
2871 let p = create_unconstrained();
2872 let rc = unsafe {
2873 GetIpoptCurrentIterate(
2874 p,
2875 FALSE,
2876 0,
2877 std::ptr::null_mut(),
2878 std::ptr::null_mut(),
2879 std::ptr::null_mut(),
2880 0,
2881 std::ptr::null_mut(),
2882 std::ptr::null_mut(),
2883 )
2884 };
2885 assert_eq!(rc, FALSE);
2886 unsafe { FreeIpoptProblem(p) };
2887 }
2888
2889 #[test]
2890 fn get_current_violations_returns_false_outside_callback() {
2891 let p = create_unconstrained();
2892 let rc = unsafe {
2893 GetIpoptCurrentViolations(
2894 p,
2895 FALSE,
2896 0,
2897 std::ptr::null_mut(),
2898 std::ptr::null_mut(),
2899 std::ptr::null_mut(),
2900 std::ptr::null_mut(),
2901 std::ptr::null_mut(),
2902 0,
2903 std::ptr::null_mut(),
2904 std::ptr::null_mut(),
2905 )
2906 };
2907 assert_eq!(rc, FALSE);
2908 unsafe { FreeIpoptProblem(p) };
2909 }
2910
2911 #[test]
2912 fn post_solve_stats_zero_before_solve() {
2913 let p = create_unconstrained();
2914 unsafe {
2915 assert_eq!(GetIpoptIterCount(p), 0);
2916 assert_eq!(GetIpoptSolveTime(p), 0.0);
2917 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2918 assert_eq!(GetIpoptDualInf(p), 0.0);
2919 assert_eq!(GetIpoptComplInf(p), 0.0);
2920 FreeIpoptProblem(p);
2921 }
2922 }
2923
2924 #[test]
2925 fn post_solve_stats_populated_after_solve() {
2926 let xl = [-1.0e20];
2928 let xu = [1.0e20];
2929 let p = unsafe {
2930 CreateIpoptProblem(
2931 1,
2932 xl.as_ptr(),
2933 xu.as_ptr(),
2934 0,
2935 std::ptr::null(),
2936 std::ptr::null(),
2937 0,
2938 1,
2939 0,
2940 Some(quad_eval_f),
2941 None,
2942 Some(quad_eval_grad_f),
2943 None,
2944 Some(quad_eval_h),
2945 )
2946 };
2947 let mut x = [0.0_f64];
2948 let mut obj = 0.0_f64;
2949 let rc = unsafe {
2950 IpoptSolve(
2951 p,
2952 x.as_mut_ptr(),
2953 std::ptr::null_mut(),
2954 &mut obj,
2955 std::ptr::null_mut(),
2956 std::ptr::null_mut(),
2957 std::ptr::null_mut(),
2958 std::ptr::null_mut(),
2959 )
2960 };
2961 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2962 unsafe {
2965 assert!(GetIpoptIterCount(p) >= 0);
2966 assert!(GetIpoptSolveTime(p) >= 0.0);
2967 assert!(GetIpoptPrimalInf(p).is_finite());
2968 assert!(GetIpoptDualInf(p).is_finite());
2969 assert!(GetIpoptComplInf(p).is_finite());
2970 FreeIpoptProblem(p);
2971 }
2972 }
2973
2974 #[test]
2979 fn linear_solver_stats_populated_after_solve() {
2980 let xl = [-1.0e20];
2981 let xu = [1.0e20];
2982 let p = unsafe {
2983 CreateIpoptProblem(
2984 1,
2985 xl.as_ptr(),
2986 xu.as_ptr(),
2987 0,
2988 std::ptr::null(),
2989 std::ptr::null(),
2990 0,
2991 1,
2992 0,
2993 Some(quad_eval_f),
2994 None,
2995 Some(quad_eval_grad_f),
2996 None,
2997 Some(quad_eval_h),
2998 )
2999 };
3000 let mut stats = unsafe { std::mem::zeroed::<PounceLinearSolverStats>() };
3001 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, FALSE);
3003
3004 let mut x = [0.0_f64];
3005 let mut obj = 0.0_f64;
3006 let rc = unsafe {
3007 IpoptSolve(
3008 p,
3009 x.as_mut_ptr(),
3010 std::ptr::null_mut(),
3011 &mut obj,
3012 std::ptr::null_mut(),
3013 std::ptr::null_mut(),
3014 std::ptr::null_mut(),
3015 std::ptr::null_mut(),
3016 )
3017 };
3018 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3019 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, TRUE);
3020
3021 let name = unsafe { CStr::from_ptr(stats.solver_name.as_ptr()) }
3022 .to_str()
3023 .expect("solver name is ASCII");
3024 assert_eq!(name, "feral", "default backend should report itself");
3025 assert!(stats.n_factors > 0, "n_factors = {}", stats.n_factors);
3026 assert_eq!(
3027 stats.n_pattern_reuse + stats.n_pattern_changes,
3028 stats.n_factors,
3029 "every factor is either a pattern reuse or a pattern change"
3030 );
3031 assert!(stats.max_fill_ratio.is_nan() || stats.max_fill_ratio > 0.0);
3034 assert!(stats.last_nnz_l == -1 || stats.last_nnz_l > 0);
3035 unsafe { FreeIpoptProblem(p) };
3036 }
3037
3038 #[test]
3039 fn option_type_reports_the_setter_a_keyword_expects() {
3040 let p = create_unconstrained();
3041 let ty = |s: &str| {
3042 let c = std::ffi::CString::new(s).unwrap();
3043 unsafe { GetPounceOptionType(p, c.as_ptr()) }
3044 };
3045 assert_eq!(ty("tol"), 1, "tol is a number");
3046 assert_eq!(ty("max_iter"), 2, "max_iter is an integer");
3047 assert_eq!(ty("linear_solver"), 3, "linear_solver is a string");
3048 assert_eq!(ty("hessian_approximation"), 3);
3051 assert_eq!(ty("no_such_option_at_all"), 0);
3054 assert_eq!(unsafe { GetPounceOptionType(p, std::ptr::null()) }, 0);
3055 unsafe { FreeIpoptProblem(p) };
3056 }
3057
3058 #[test]
3061 fn option_type_answers_without_a_problem_handle() {
3062 let ty = |s: &str| {
3063 let c = std::ffi::CString::new(s).unwrap();
3064 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) }
3065 };
3066 assert_eq!(ty("tol"), 1);
3067 assert_eq!(ty("max_iter"), 2);
3068 assert_eq!(ty("linear_solver"), 3);
3069 assert_eq!(ty("no_such_option_at_all"), 0);
3070
3071 let p = create_unconstrained();
3074 for name in [
3075 "tol",
3076 "max_iter",
3077 "linear_solver",
3078 "mu_strategy",
3079 "print_level",
3080 ] {
3081 let c = std::ffi::CString::new(name).unwrap();
3082 assert_eq!(
3083 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) },
3084 unsafe { GetPounceOptionType(p, c.as_ptr()) },
3085 "handle-free and problem-bound disagree on {name}"
3086 );
3087 }
3088 unsafe { FreeIpoptProblem(p) };
3089 }
3090
3091 #[test]
3092 fn write_solve_report_emits_v1_json_with_iter_history() {
3093 let xl = [-1.0e20];
3096 let xu = [1.0e20];
3097 let p = unsafe {
3098 CreateIpoptProblem(
3099 1,
3100 xl.as_ptr(),
3101 xu.as_ptr(),
3102 0,
3103 std::ptr::null(),
3104 std::ptr::null(),
3105 0,
3106 1,
3107 0,
3108 Some(quad_eval_f),
3109 None,
3110 Some(quad_eval_grad_f),
3111 None,
3112 Some(quad_eval_h),
3113 )
3114 };
3115
3116 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
3118 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
3119 assert_eq!(bad, FALSE);
3120
3121 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
3123 let mut x = [0.0_f64];
3124 let mut obj = 0.0_f64;
3125 let rc = unsafe {
3126 IpoptSolve(
3127 p,
3128 x.as_mut_ptr(),
3129 std::ptr::null_mut(),
3130 &mut obj,
3131 std::ptr::null_mut(),
3132 std::ptr::null_mut(),
3133 std::ptr::null_mut(),
3134 std::ptr::null_mut(),
3135 )
3136 };
3137 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3138
3139 let dir = std::env::temp_dir();
3140 let path = dir.join("pounce_cinterface_report.json");
3141 let cpath = CString::new(path.to_str().unwrap()).unwrap();
3142 let cdetail = CString::new("full").unwrap();
3143 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
3144 assert_eq!(ok, TRUE);
3145
3146 let txt = std::fs::read_to_string(&path).unwrap();
3149 assert!(
3150 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
3151 "{txt}"
3152 );
3153 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
3154 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
3155 assert_eq!(parsed.problem.n_variables, 1);
3156 assert_eq!(parsed.problem.n_constraints, 0);
3157
3158 let bad_detail = CString::new("verbose").unwrap();
3160 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
3161 assert_eq!(bad, FALSE);
3162
3163 let _ = std::fs::remove_file(&path);
3164 unsafe { FreeIpoptProblem(p) };
3165 }
3166
3167 unsafe extern "C" fn cb_quad_eval_g(
3174 _n: Index,
3175 x: *const Number,
3176 _new_x: Bool,
3177 _m: Index,
3178 g: *mut Number,
3179 _user_data: *mut c_void,
3180 ) -> Bool {
3181 unsafe {
3182 *g.offset(0) = *x.offset(0);
3183 TRUE
3184 }
3185 }
3186 unsafe extern "C" fn cb_quad_eval_jac_g(
3187 _n: Index,
3188 _x: *const Number,
3189 _new_x: Bool,
3190 _m: Index,
3191 nele_jac: Index,
3192 irow: *mut Index,
3193 jcol: *mut Index,
3194 values: *mut Number,
3195 _user_data: *mut c_void,
3196 ) -> Bool {
3197 unsafe {
3198 assert_eq!(nele_jac, 1);
3199 if !irow.is_null() {
3200 *irow.offset(0) = 0;
3201 *jcol.offset(0) = 0;
3202 }
3203 if !values.is_null() {
3204 *values.offset(0) = 1.0;
3205 }
3206 TRUE
3207 }
3208 }
3209 unsafe extern "C" fn cb_quad_eval_h(
3210 _n: Index,
3211 _x: *const Number,
3212 _new_x: Bool,
3213 obj_factor: Number,
3214 _m: Index,
3215 _lambda: *const Number,
3216 _new_lambda: Bool,
3217 _nele_hess: Index,
3218 irow: *mut Index,
3219 jcol: *mut Index,
3220 values: *mut Number,
3221 _user_data: *mut c_void,
3222 ) -> Bool {
3223 unsafe {
3224 if !irow.is_null() {
3225 *irow.offset(0) = 0;
3226 *jcol.offset(0) = 0;
3227 }
3228 if !values.is_null() {
3229 *values.offset(0) = 2.0 * obj_factor;
3230 }
3231 TRUE
3232 }
3233 }
3234
3235 fn create_callback_test_problem() -> IpoptProblem {
3236 let xl = [-1.0e20];
3238 let xu = [1.0e20];
3239 let gl = [-10.0];
3240 let gu = [10.0];
3241 unsafe {
3242 CreateIpoptProblem(
3243 1,
3244 xl.as_ptr(),
3245 xu.as_ptr(),
3246 1,
3247 gl.as_ptr(),
3248 gu.as_ptr(),
3249 1,
3250 1,
3251 0,
3252 Some(quad_eval_f),
3253 Some(cb_quad_eval_g),
3254 Some(quad_eval_grad_f),
3255 Some(cb_quad_eval_jac_g),
3256 Some(cb_quad_eval_h),
3257 )
3258 }
3259 }
3260
3261 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
3262 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
3263 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
3264 std::sync::atomic::AtomicBool::new(false);
3265
3266 unsafe extern "C" fn counting_cb(
3267 _alg_mod: Index,
3268 iter_count: Index,
3269 _obj_value: Number,
3270 _inf_pr: Number,
3271 _inf_du: Number,
3272 _mu: Number,
3273 _d_norm: Number,
3274 _regularization_size: Number,
3275 _alpha_du: Number,
3276 _alpha_pr: Number,
3277 _ls_trials: Index,
3278 user_data: *mut c_void,
3279 ) -> Bool {
3280 unsafe {
3281 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3282 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
3283 let problem = user_data as IpoptProblem;
3286 let mut x = [0.0_f64];
3287 let rc = GetIpoptCurrentIterate(
3288 problem,
3289 FALSE,
3290 1,
3291 x.as_mut_ptr(),
3292 std::ptr::null_mut(),
3293 std::ptr::null_mut(),
3294 1,
3295 std::ptr::null_mut(),
3296 std::ptr::null_mut(),
3297 );
3298 if rc == TRUE && x[0].is_finite() {
3299 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3300 }
3301 TRUE
3302 }
3303 }
3304
3305 #[test]
3306 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
3307 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
3308 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
3309 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3310
3311 let p = create_callback_test_problem();
3312 assert!(!p.is_null());
3313 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
3314 assert_eq!(ok, TRUE);
3315 let mut x = [0.0_f64];
3316 let mut obj = 0.0_f64;
3317 let rc = unsafe {
3318 IpoptSolve(
3319 p,
3320 x.as_mut_ptr(),
3321 std::ptr::null_mut(),
3322 &mut obj,
3323 std::ptr::null_mut(),
3324 std::ptr::null_mut(),
3325 std::ptr::null_mut(),
3326 p as *mut c_void,
3327 )
3328 };
3329 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3330 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
3332 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
3333 assert!(
3334 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
3335 "last iter should be >= 1 after at least one accepted step"
3336 );
3337 assert!(
3338 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
3339 "GetIpoptCurrentIterate did not return a usable x"
3340 );
3341 unsafe { FreeIpoptProblem(p) };
3342 }
3343
3344 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
3345
3346 fn create_bounded_callback_test_problem() -> IpoptProblem {
3351 let xl = [0.0];
3353 let xu = [10.0];
3354 let gl = [-10.0];
3355 let gu = [10.0];
3356 unsafe {
3357 CreateIpoptProblem(
3358 1,
3359 xl.as_ptr(),
3360 xu.as_ptr(),
3361 1,
3362 gl.as_ptr(),
3363 gu.as_ptr(),
3364 1,
3365 1,
3366 0,
3367 Some(quad_eval_f),
3368 Some(cb_quad_eval_g),
3369 Some(quad_eval_grad_f),
3370 Some(cb_quad_eval_jac_g),
3371 Some(cb_quad_eval_h),
3372 )
3373 }
3374 }
3375
3376 unsafe extern "C" fn violations_inspecting_cb(
3377 _alg_mod: Index,
3378 _iter_count: Index,
3379 _obj_value: Number,
3380 _inf_pr: Number,
3381 _inf_du: Number,
3382 _mu: Number,
3383 _d_norm: Number,
3384 _regularization_size: Number,
3385 _alpha_du: Number,
3386 _alpha_pr: Number,
3387 _ls_trials: Index,
3388 user_data: *mut c_void,
3389 ) -> Bool {
3390 unsafe {
3391 let problem = user_data as IpoptProblem;
3392 let mut x_l_viol = [f64::NAN];
3397 let mut x_u_viol = [f64::NAN];
3398 let rc = GetIpoptCurrentViolations(
3399 problem,
3400 FALSE,
3401 1,
3402 x_l_viol.as_mut_ptr(),
3403 x_u_viol.as_mut_ptr(),
3404 std::ptr::null_mut(),
3405 std::ptr::null_mut(),
3406 std::ptr::null_mut(),
3407 1,
3408 std::ptr::null_mut(),
3409 std::ptr::null_mut(),
3410 );
3411 if rc == TRUE
3412 && x_l_viol[0].is_finite()
3413 && x_l_viol[0] >= 0.0
3414 && x_u_viol[0].is_finite()
3415 && x_u_viol[0] >= 0.0
3416 {
3417 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3418 }
3419 TRUE
3420 }
3421 }
3422
3423 #[test]
3424 fn get_current_violations_inside_callback_reports_finite_bounds() {
3425 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3426 let p = create_bounded_callback_test_problem();
3427 assert!(!p.is_null());
3428 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
3429 assert_eq!(ok, TRUE);
3430 let mut x = [5.0_f64];
3431 let mut obj = 0.0_f64;
3432 let rc = unsafe {
3433 IpoptSolve(
3434 p,
3435 x.as_mut_ptr(),
3436 std::ptr::null_mut(),
3437 &mut obj,
3438 std::ptr::null_mut(),
3439 std::ptr::null_mut(),
3440 std::ptr::null_mut(),
3441 p as *mut c_void,
3442 )
3443 };
3444 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3445 assert!(
3446 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
3447 "GetIpoptCurrentViolations did not return finite, non-negative \
3448 bound violations from inside the callback"
3449 );
3450 unsafe { FreeIpoptProblem(p) };
3451 }
3452
3453 #[test]
3454 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
3455 let n_us = 1usize;
3465 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
3469 let mut v = vec![0.0; n_us];
3470 for (i, s) in packed.iter().enumerate() {
3471 v[i] = (-s).max(0.0);
3472 }
3473 v
3474 });
3475 assert!(
3476 unguarded.is_err(),
3477 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
3478 );
3479
3480 let guarded: Result<Vec<f64>, ()> = (|| {
3482 if packed.len() != n_us {
3483 return Err(());
3484 }
3485 let mut v = vec![0.0; n_us];
3486 for (i, s) in packed.iter().enumerate() {
3487 v[i] = (-s).max(0.0);
3488 }
3489 Ok(v)
3490 })();
3491 assert!(
3492 guarded.is_err(),
3493 "guarded scatter should reject the length mismatch (return FALSE), not panic"
3494 );
3495 }
3496
3497 unsafe extern "C" fn user_stop_cb(
3498 _alg_mod: Index,
3499 _iter_count: Index,
3500 _obj_value: Number,
3501 _inf_pr: Number,
3502 _inf_du: Number,
3503 _mu: Number,
3504 _d_norm: Number,
3505 _regularization_size: Number,
3506 _alpha_du: Number,
3507 _alpha_pr: Number,
3508 _ls_trials: Index,
3509 _user_data: *mut c_void,
3510 ) -> Bool {
3511 FALSE
3512 }
3513
3514 #[test]
3515 fn intermediate_callback_false_surfaces_user_requested_stop() {
3516 let p = create_callback_test_problem();
3517 assert!(!p.is_null());
3518 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
3519 assert_eq!(ok, TRUE);
3520 let mut x = [0.0_f64];
3521 let rc = unsafe {
3522 IpoptSolve(
3523 p,
3524 x.as_mut_ptr(),
3525 std::ptr::null_mut(),
3526 std::ptr::null_mut(),
3527 std::ptr::null_mut(),
3528 std::ptr::null_mut(),
3529 std::ptr::null_mut(),
3530 std::ptr::null_mut(),
3531 )
3532 };
3533 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
3534 unsafe { FreeIpoptProblem(p) };
3535 }
3536
3537 #[test]
3538 fn ffi_guard_converts_panic_to_fallback() {
3539 let fallback = ApplicationReturnStatus::InternalError as Index;
3546 let got = ffi_guard(fallback, || -> Index {
3547 panic!("boom inside solver core");
3548 });
3549 assert_eq!(got, fallback);
3550 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
3551 }
3552
3553 #[test]
3554 fn ffi_guard_is_transparent_on_success() {
3555 let got = ffi_guard(-99, || 7);
3559 assert_eq!(got, 7);
3560 }
3561
3562 #[test]
3563 fn parse_pkg_version_handles_missing_components() {
3564 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
3565 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
3566 assert_eq!(parse_pkg_version(""), (0, 0, 0));
3567 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
3568 }
3569
3570 use crate::solver::{
3573 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
3574 IpoptSolverSolve,
3575 };
3576
3577 #[test]
3578 fn solver_create_consumes_problem_handle() {
3579 let mut p = create_unconstrained();
3580 assert!(!p.is_null());
3581 let s = unsafe { IpoptCreateSolver(&mut p) };
3582 assert!(!s.is_null());
3583 assert!(
3584 p.is_null(),
3585 "IpoptCreateSolver should NULL out the caller's handle"
3586 );
3587 unsafe { IpoptFreeSolver(s) };
3588 }
3589
3590 #[test]
3591 fn solver_create_null_inputs_return_null() {
3592 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
3594 assert!(s.is_null());
3595 let mut p: IpoptProblem = std::ptr::null_mut();
3597 let s = unsafe { IpoptCreateSolver(&mut p) };
3598 assert!(s.is_null());
3599 }
3600
3601 #[test]
3602 fn solver_free_null_is_safe() {
3603 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
3604 }
3605
3606 #[test]
3607 fn solver_solve_drives_quadratic_and_retains_factor() {
3608 let xl = [-1.0e20];
3609 let xu = [1.0e20];
3610 let mut p = unsafe {
3611 CreateIpoptProblem(
3612 1,
3613 xl.as_ptr(),
3614 xu.as_ptr(),
3615 0,
3616 std::ptr::null(),
3617 std::ptr::null(),
3618 0,
3619 1,
3620 0,
3621 Some(quad_eval_f),
3622 None,
3623 Some(quad_eval_grad_f),
3624 None,
3625 Some(quad_eval_h),
3626 )
3627 };
3628 assert!(!p.is_null());
3629 let s = unsafe { IpoptCreateSolver(&mut p) };
3630 assert!(!s.is_null());
3631 let mut x = [0.0_f64];
3632 let mut obj = 0.0_f64;
3633 let rc = unsafe {
3634 IpoptSolverSolve(
3635 s,
3636 x.as_mut_ptr(),
3637 std::ptr::null_mut(),
3638 &mut obj,
3639 std::ptr::null_mut(),
3640 std::ptr::null_mut(),
3641 std::ptr::null_mut(),
3642 std::ptr::null_mut(),
3643 )
3644 };
3645 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3646 assert!((x[0] - 2.0).abs() < 1e-6);
3647 assert!(obj.abs() < 1e-10);
3648
3649 let dim = unsafe { IpoptSolverGetKktDim(s) };
3652 assert!(dim > 0, "expected positive KKT dim, got {dim}");
3653 let rhs = vec![0.0_f64; dim as usize];
3654 let mut lhs = vec![1.0_f64; dim as usize];
3655 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
3656 assert_eq!(ok, TRUE);
3657 for (i, v) in lhs.iter().enumerate() {
3658 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
3659 }
3660 unsafe { IpoptFreeSolver(s) };
3661 }
3662
3663 #[test]
3664 fn solver_kkt_dim_minus_one_before_solve() {
3665 let mut p = create_unconstrained();
3666 let s = unsafe { IpoptCreateSolver(&mut p) };
3667 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
3668 unsafe { IpoptFreeSolver(s) };
3669 }
3670
3671 #[test]
3676 fn c_get_working_set_returns_false_before_any_solve() {
3677 let p = create_unconstrained();
3678 let mut bound_buf = [0; 4];
3679 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
3680 assert_eq!(rc, FALSE);
3681 unsafe { FreeIpoptProblem(p) };
3682 }
3683
3684 #[test]
3685 fn c_set_warm_start_with_both_null_returns_false() {
3686 let p = create_unconstrained();
3687 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
3688 assert_eq!(rc, FALSE);
3689 unsafe { FreeIpoptProblem(p) };
3690 }
3691
3692 #[test]
3693 fn c_set_warm_start_with_bad_status_code_returns_false() {
3694 let p = create_unconstrained();
3695 let bogus = [
3697 POUNCE_WS_INACTIVE,
3698 7,
3699 POUNCE_WS_AT_LOWER,
3700 POUNCE_WS_INACTIVE,
3701 ];
3702 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
3703 assert_eq!(rc, FALSE);
3704 unsafe { FreeIpoptProblem(p) };
3705 }
3706
3707 #[test]
3708 fn c_set_warm_start_then_clear_succeeds() {
3709 let p = create_unconstrained();
3710 let in_buf = [POUNCE_WS_INACTIVE; 4];
3711 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
3712 assert_eq!(set_rc, TRUE);
3713 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
3714 assert_eq!(clr_rc, TRUE);
3715 unsafe { FreeIpoptProblem(p) };
3716 }
3717
3718 #[test]
3719 fn c_set_warm_start_on_null_problem_returns_false() {
3720 let in_buf = [POUNCE_WS_INACTIVE; 1];
3721 let rc = unsafe {
3722 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
3723 };
3724 assert_eq!(rc, FALSE);
3725 }
3726
3727 #[test]
3728 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
3729 let p = create_callback_test_problem();
3735 let key = CString::new("algorithm").unwrap();
3736 let val = CString::new("active-set-sqp").unwrap();
3737 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
3738 assert_eq!(ok, TRUE);
3739
3740 let mut x = [0.0_f64];
3741 let mut obj = 0.0_f64;
3742 let rc1 = unsafe {
3743 IpoptSolve(
3744 p,
3745 x.as_mut_ptr(),
3746 std::ptr::null_mut(),
3747 &mut obj,
3748 std::ptr::null_mut(),
3749 std::ptr::null_mut(),
3750 std::ptr::null_mut(),
3751 std::ptr::null_mut(),
3752 )
3753 };
3754 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
3755
3756 let mut bound_buf = [-1; 1];
3757 let mut cons_buf = [-1; 1];
3758 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
3759 assert_eq!(got, TRUE);
3760 assert!((0..=3).contains(&bound_buf[0]));
3762 assert!((0..=3).contains(&cons_buf[0]));
3763
3764 x[0] = 0.0;
3769 let mut obj2 = 0.0_f64;
3770 let mut bound_out = [-1; 1];
3771 let mut cons_out = [-1; 1];
3772 let rc2 = unsafe {
3773 IpoptSolveWarmStart(
3774 p,
3775 x.as_mut_ptr(),
3776 std::ptr::null_mut(),
3777 &mut obj2,
3778 std::ptr::null_mut(),
3779 std::ptr::null_mut(),
3780 std::ptr::null_mut(),
3781 bound_buf.as_ptr(),
3782 cons_buf.as_ptr(),
3783 bound_out.as_mut_ptr(),
3784 cons_out.as_mut_ptr(),
3785 std::ptr::null_mut(),
3786 )
3787 };
3788 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
3789 assert!((0..=3).contains(&bound_out[0]));
3790 assert!((0..=3).contains(&cons_out[0]));
3791
3792 unsafe { FreeIpoptProblem(p) };
3793 }
3794}