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 default_backend_factory, feral_config_from_options, IpoptApplication,
39};
40use pounce_algorithm::intermediate as ip_intermediate;
41use pounce_nlp::return_codes::ApplicationReturnStatus;
42use pounce_nlp::solve_statistics::SolveStatistics;
43use pounce_nlp::tnlp::{
44 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
45 StartingPoint, TNLP,
46};
47use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
48use pounce_restoration::resto_inner_solver::{
49 make_default_restoration_factory_provider, InnerBackendFactoryFactory,
50};
51use std::cell::RefCell;
52use std::ffi::{c_char, c_int, c_void, CStr};
53use std::rc::Rc;
54
55pub type Number = f64;
57pub type Index = c_int;
59pub type Bool = c_int;
61
62const TRUE: Bool = 1;
63const FALSE: Bool = 0;
64
65pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
78 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
79 Ok(r) => r,
80 Err(_) => fallback,
81 }
82}
83
84pub type IpoptBoundStatus = c_int;
88pub type IpoptConsStatus = c_int;
91
92const POUNCE_WS_INACTIVE: c_int = 0;
93const POUNCE_WS_AT_LOWER: c_int = 1;
94const POUNCE_WS_AT_UPPER: c_int = 2;
95const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
96
97pub struct IpoptProblemInfo {
100 pub(crate) app: IpoptApplication,
101 pub(crate) n: Index,
102 pub(crate) m: Index,
103 pub(crate) nele_jac: Index,
104 pub(crate) nele_hess: Index,
105 pub(crate) index_style: Index,
106 pub(crate) x_l: Vec<Number>,
107 pub(crate) x_u: Vec<Number>,
108 pub(crate) g_l: Vec<Number>,
109 pub(crate) g_u: Vec<Number>,
110 pub(crate) eval_f: Option<Eval_F_CB>,
111 pub(crate) eval_g: Option<Eval_G_CB>,
112 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
113 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
114 pub(crate) eval_h: Option<Eval_H_CB>,
115 pub(crate) intermediate_cb: Option<Intermediate_CB>,
116 pub(crate) user_scaling: Option<UserScaling>,
120 pub(crate) last_solve: Option<LastSolve>,
124}
125
126#[derive(Clone)]
129pub(crate) struct UserScaling {
130 obj_scaling: Number,
131 x_scaling: Option<Vec<Number>>,
132 g_scaling: Option<Vec<Number>>,
133}
134
135#[derive(Clone)]
141pub(crate) struct LastSolve {
142 pub(crate) stats: SolveStatistics,
143 pub(crate) status: ApplicationReturnStatus,
144 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
145 pub(crate) final_x: Vec<Number>,
146 pub(crate) final_lambda: Vec<Number>,
147 pub(crate) final_obj: Number,
148}
149
150impl Default for LastSolve {
151 fn default() -> Self {
152 Self {
153 stats: SolveStatistics::default(),
154 status: ApplicationReturnStatus::InternalError,
155 linear_solver: None,
156 final_x: Vec::new(),
157 final_lambda: Vec::new(),
158 final_obj: 0.0,
159 }
160 }
161}
162
163pub type IpoptProblem = *mut IpoptProblemInfo;
164
165pub type Eval_F_CB = unsafe extern "C" fn(
169 n: Index,
170 x: *const Number,
171 new_x: Bool,
172 obj_value: *mut Number,
173 user_data: *mut c_void,
174) -> Bool;
175
176pub type Eval_Grad_F_CB = unsafe extern "C" fn(
177 n: Index,
178 x: *const Number,
179 new_x: Bool,
180 grad_f: *mut Number,
181 user_data: *mut c_void,
182) -> Bool;
183
184pub type Eval_G_CB = unsafe extern "C" fn(
185 n: Index,
186 x: *const Number,
187 new_x: Bool,
188 m: Index,
189 g: *mut Number,
190 user_data: *mut c_void,
191) -> Bool;
192
193pub type Eval_Jac_G_CB = unsafe extern "C" fn(
194 n: Index,
195 x: *const Number,
196 new_x: Bool,
197 m: Index,
198 nele_jac: Index,
199 iRow: *mut Index,
200 jCol: *mut Index,
201 values: *mut Number,
202 user_data: *mut c_void,
203) -> Bool;
204
205pub type Eval_H_CB = unsafe extern "C" fn(
206 n: Index,
207 x: *const Number,
208 new_x: Bool,
209 obj_factor: Number,
210 m: Index,
211 lambda: *const Number,
212 new_lambda: Bool,
213 nele_hess: Index,
214 iRow: *mut Index,
215 jCol: *mut Index,
216 values: *mut Number,
217 user_data: *mut c_void,
218) -> Bool;
219
220pub type Intermediate_CB = unsafe extern "C" fn(
221 alg_mod: Index,
222 iter_count: Index,
223 obj_value: Number,
224 inf_pr: Number,
225 inf_du: Number,
226 mu: Number,
227 d_norm: Number,
228 regularization_size: Number,
229 alpha_du: Number,
230 alpha_pr: Number,
231 ls_trials: Index,
232 user_data: *mut c_void,
233) -> Bool;
234
235#[no_mangle]
246pub unsafe extern "C" fn CreateIpoptProblem(
247 n: Index,
248 x_L: *const Number,
249 x_U: *const Number,
250 m: Index,
251 g_L: *const Number,
252 g_U: *const Number,
253 nele_jac: Index,
254 nele_hess: Index,
255 index_style: Index,
256 eval_f: Option<Eval_F_CB>,
257 eval_g: Option<Eval_G_CB>,
258 eval_grad_f: Option<Eval_Grad_F_CB>,
259 eval_jac_g: Option<Eval_Jac_G_CB>,
260 eval_h: Option<Eval_H_CB>,
261) -> IpoptProblem {
262 pounce_observability::init_subscriber();
266
267 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
268 return std::ptr::null_mut();
269 }
270 if !(0..=1).contains(&index_style) {
271 return std::ptr::null_mut();
272 }
273 if eval_f.is_none() || eval_grad_f.is_none() {
274 return std::ptr::null_mut();
275 }
276 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
277 return std::ptr::null_mut();
278 }
279 if n > 0 && (x_L.is_null() || x_U.is_null()) {
280 return std::ptr::null_mut();
281 }
282 if m > 0 && (g_L.is_null() || g_U.is_null()) {
283 return std::ptr::null_mut();
284 }
285
286 let x_l = if n > 0 {
287 std::slice::from_raw_parts(x_L, n as usize).to_vec()
288 } else {
289 Vec::new()
290 };
291 let x_u = if n > 0 {
292 std::slice::from_raw_parts(x_U, n as usize).to_vec()
293 } else {
294 Vec::new()
295 };
296 let g_l_vec = if m > 0 {
297 std::slice::from_raw_parts(g_L, m as usize).to_vec()
298 } else {
299 Vec::new()
300 };
301 let g_u_vec = if m > 0 {
302 std::slice::from_raw_parts(g_U, m as usize).to_vec()
303 } else {
304 Vec::new()
305 };
306
307 let info = Box::new(IpoptProblemInfo {
308 app: IpoptApplication::new(),
309 n,
310 m,
311 nele_jac,
312 nele_hess,
313 index_style,
314 x_l,
315 x_u,
316 g_l: g_l_vec,
317 g_u: g_u_vec,
318 eval_f,
319 eval_g,
320 eval_grad_f,
321 eval_jac_g,
322 eval_h,
323 intermediate_cb: None,
324 user_scaling: None,
325 last_solve: None,
326 });
327 Box::into_raw(info)
328}
329
330#[no_mangle]
337pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
338 if ipopt_problem.is_null() {
339 return;
340 }
341 drop(Box::from_raw(ipopt_problem));
342}
343
344unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
345 if keyword.is_null() {
346 return None;
347 }
348 CStr::from_ptr(keyword).to_str().ok()
349}
350
351#[no_mangle]
358pub unsafe extern "C" fn AddIpoptStrOption(
359 ipopt_problem: IpoptProblem,
360 keyword: *const c_char,
361 val: *const c_char,
362) -> Bool {
363 if ipopt_problem.is_null() {
364 return FALSE;
365 }
366 let info = &mut *ipopt_problem;
367 let Some(k) = keyword_str(keyword) else {
368 return FALSE;
369 };
370 if val.is_null() {
371 return FALSE;
372 }
373 let Ok(v) = CStr::from_ptr(val).to_str() else {
374 return FALSE;
375 };
376 match info.app.options_mut().set_string_value(k, v, true, false) {
377 Ok(_) => TRUE,
378 Err(_) => FALSE,
379 }
380}
381
382#[no_mangle]
389pub unsafe extern "C" fn AddIpoptNumOption(
390 ipopt_problem: IpoptProblem,
391 keyword: *const c_char,
392 val: Number,
393) -> Bool {
394 if ipopt_problem.is_null() {
395 return FALSE;
396 }
397 let info = &mut *ipopt_problem;
398 let Some(k) = keyword_str(keyword) else {
399 return FALSE;
400 };
401 match info
402 .app
403 .options_mut()
404 .set_numeric_value(k, val, true, false)
405 {
406 Ok(_) => TRUE,
407 Err(_) => FALSE,
408 }
409}
410
411#[no_mangle]
418pub unsafe extern "C" fn AddIpoptIntOption(
419 ipopt_problem: IpoptProblem,
420 keyword: *const c_char,
421 val: Index,
422) -> Bool {
423 if ipopt_problem.is_null() {
424 return FALSE;
425 }
426 let info = &mut *ipopt_problem;
427 let Some(k) = keyword_str(keyword) else {
428 return FALSE;
429 };
430 match info.app.options_mut().set_integer_value(
431 k,
432 val as pounce_common::types::Index,
433 true,
434 false,
435 ) {
436 Ok(_) => TRUE,
437 Err(_) => FALSE,
438 }
439}
440
441#[no_mangle]
455pub unsafe extern "C" fn OpenIpoptOutputFile(
456 ipopt_problem: IpoptProblem,
457 file_name: *const c_char,
458 print_level: c_int,
459) -> Bool {
460 if ipopt_problem.is_null() || file_name.is_null() {
461 return FALSE;
462 }
463 let info = &mut *ipopt_problem;
464 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
465 return FALSE;
466 };
467 if info.app.open_output_file(fname, print_level) {
468 TRUE
469 } else {
470 FALSE
471 }
472}
473
474#[no_mangle]
488pub unsafe extern "C" fn SetIpoptProblemScaling(
489 ipopt_problem: IpoptProblem,
490 obj_scaling: Number,
491 x_scaling: *const Number,
492 g_scaling: *const Number,
493) -> Bool {
494 if ipopt_problem.is_null() {
495 return FALSE;
496 }
497 let info = &mut *ipopt_problem;
498 let n = info.n as usize;
499 let m = info.m as usize;
500 let x_vec = if !x_scaling.is_null() && n > 0 {
501 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
502 } else {
503 None
504 };
505 let g_vec = if !g_scaling.is_null() && m > 0 {
506 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
507 } else {
508 None
509 };
510 info.user_scaling = Some(UserScaling {
511 obj_scaling,
512 x_scaling: x_vec,
513 g_scaling: g_vec,
514 });
515 TRUE
516}
517
518#[allow(clippy::too_many_arguments)]
532#[no_mangle]
533pub unsafe extern "C" fn IpoptSolve(
534 ipopt_problem: IpoptProblem,
535 x: *mut Number,
536 g: *mut Number,
537 obj_val: *mut Number,
538 mult_g: *mut Number,
539 mult_x_L: *mut Number,
540 mult_x_U: *mut Number,
541 user_data: *mut c_void,
542) -> Index {
543 if ipopt_problem.is_null() {
544 return ApplicationReturnStatus::InternalError as Index;
545 }
546 (*ipopt_problem).last_solve = None;
554 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
560 let info = &mut *ipopt_problem;
561 if info.n < 0 || info.m < 0 {
562 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
563 }
564 if info.n > 0 && x.is_null() {
565 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
566 }
567
568 let n_us = info.n as usize;
569 let m_us = info.m as usize;
570 let initial_x = if n_us > 0 {
571 std::slice::from_raw_parts(x, n_us).to_vec()
572 } else {
573 Vec::new()
574 };
575
576 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
577 n: info.n,
578 m: info.m,
579 nele_jac: info.nele_jac,
580 nele_hess: info.nele_hess,
581 index_style: info.index_style,
582 x_l: info.x_l.clone(),
583 x_u: info.x_u.clone(),
584 g_l: info.g_l.clone(),
585 g_u: info.g_u.clone(),
586 initial_x,
587 eval_f: info.eval_f,
588 eval_grad_f: info.eval_grad_f,
589 eval_g: info.eval_g,
590 eval_jac_g: info.eval_jac_g,
591 eval_h: info.eval_h,
592 user_data,
593 intermediate_cb: info.intermediate_cb,
594 user_scaling: info.user_scaling.clone(),
595 final_status: None,
596 final_x: vec![0.0; n_us],
597 final_z_l: vec![0.0; n_us],
598 final_z_u: vec![0.0; n_us],
599 final_g: vec![0.0; m_us],
600 final_lambda: vec![0.0; m_us],
601 final_obj: 0.0,
602 }));
603
604 let feral_cfg = feral_config_from_options(info.app.options());
614 let bff_mint = move || -> InnerBackendFactoryFactory {
615 let feral_cfg = feral_cfg.clone();
616 Box::new(move || default_backend_factory(feral_cfg.clone()))
617 };
618 let resto_provider = make_default_restoration_factory_provider(
619 RestoAlgorithmBuilder::new(),
620 info.app.algorithm_builder_from_options(),
621 bff_mint,
622 );
623 info.app.set_restoration_factory_provider(resto_provider);
624
625 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
626 let status = info.app.optimize_tnlp(bridge_for_solve);
627 let bridge_ref = bridge.borrow();
628 info.last_solve = Some(LastSolve {
629 stats: info.app.statistics(),
630 status,
631 linear_solver: info.app.linear_solver_summary(),
632 final_x: bridge_ref.final_x.clone(),
633 final_lambda: bridge_ref.final_lambda.clone(),
634 final_obj: bridge_ref.final_obj,
635 });
636 if !x.is_null() && n_us > 0 {
637 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
638 }
639 if !g.is_null() && m_us > 0 {
640 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
641 }
642 if !obj_val.is_null() {
643 *obj_val = bridge_ref.final_obj;
644 }
645 if !mult_g.is_null() && m_us > 0 {
646 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
647 }
648 if !mult_x_L.is_null() && n_us > 0 {
649 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
650 }
651 if !mult_x_U.is_null() && n_us > 0 {
652 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
653 }
654 status as Index
655 })
656}
657
658#[no_mangle]
664pub unsafe extern "C" fn SetIntermediateCallback(
665 ipopt_problem: IpoptProblem,
666 intermediate_cb: Option<Intermediate_CB>,
667) -> Bool {
668 if ipopt_problem.is_null() {
669 return FALSE;
670 }
671 let info = &mut *ipopt_problem;
672 info.intermediate_cb = intermediate_cb;
673 TRUE
674}
675
676#[allow(clippy::too_many_arguments)]
699#[no_mangle]
700pub unsafe extern "C" fn GetIpoptCurrentIterate(
701 ipopt_problem: IpoptProblem,
702 _scaled: Bool,
703 n: Index,
704 x: *mut Number,
705 z_l: *mut Number,
706 z_u: *mut Number,
707 m: Index,
708 g: *mut Number,
709 lambda: *mut Number,
710) -> Bool {
711 if ipopt_problem.is_null() {
712 return FALSE;
713 }
714 let info = &*ipopt_problem;
715 if n != info.n || m != info.m {
716 return FALSE;
717 }
718 let result = ip_intermediate::with_current(|ctx| {
719 let data = ctx.data.borrow();
720 let Some(curr) = data.curr.as_ref() else {
721 return false;
722 };
723 let nlp = ctx.nlp.borrow();
724 let n_us = n as usize;
725 let m_us = m as usize;
726 if !x.is_null() && n_us > 0 {
727 let full_x = nlp.lift_x_to_full(&*curr.x);
728 if full_x.len() != n_us {
729 return false;
730 }
731 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
732 }
733 if !z_l.is_null() && n_us > 0 {
734 let full = nlp.pack_z_l_for_user(&*curr.z_l);
735 if full.len() != n_us {
736 return false;
737 }
738 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
739 }
740 if !z_u.is_null() && n_us > 0 {
741 let full = nlp.pack_z_u_for_user(&*curr.z_u);
742 if full.len() != n_us {
743 return false;
744 }
745 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
746 }
747 if !g.is_null() && m_us > 0 {
748 let cq = ctx.cq.borrow();
749 let full = nlp.pack_g_for_user(&*cq.curr_c(), &*cq.curr_d());
750 if full.len() != m_us {
751 return false;
752 }
753 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
754 }
755 if !lambda.is_null() && m_us > 0 {
756 let full = nlp.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
757 if full.len() != m_us {
758 return false;
759 }
760 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
761 }
762 true
763 });
764 if result.unwrap_or(false) {
765 TRUE
766 } else {
767 FALSE
768 }
769}
770
771#[allow(clippy::too_many_arguments)]
786#[no_mangle]
787pub unsafe extern "C" fn GetIpoptCurrentViolations(
788 ipopt_problem: IpoptProblem,
789 _scaled: Bool,
790 n: Index,
791 x_l_violation: *mut Number,
792 x_u_violation: *mut Number,
793 compl_x_l: *mut Number,
794 compl_x_u: *mut Number,
795 grad_lag_x: *mut Number,
796 m: Index,
797 nlp_constraint_violation: *mut Number,
798 compl_g: *mut Number,
799) -> Bool {
800 if ipopt_problem.is_null() {
801 return FALSE;
802 }
803 let info = &*ipopt_problem;
804 if n != info.n || m != info.m {
805 return FALSE;
806 }
807 let result = ip_intermediate::with_current(|ctx| {
808 let data = ctx.data.borrow();
809 let Some(_curr) = data.curr.as_ref() else {
810 return false;
811 };
812 drop(data);
813 let nlp = ctx.nlp.borrow();
814 let cq = ctx.cq.borrow();
815 let n_us = n as usize;
816 let m_us = m as usize;
817 if !x_l_violation.is_null() && n_us > 0 {
823 let slack = cq.curr_slack_x_l();
824 let z_l_full = nlp.pack_z_l_for_user(&*slack);
825 if z_l_full.len() != n_us {
830 return false;
831 }
832 let mut v = vec![0.0; n_us];
837 for (i, s) in z_l_full.iter().enumerate() {
838 v[i] = (-s).max(0.0);
839 }
840 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
841 }
842 if !x_u_violation.is_null() && n_us > 0 {
843 let slack = cq.curr_slack_x_u();
844 let s_full = nlp.pack_z_u_for_user(&*slack);
845 if s_full.len() != n_us {
846 return false;
847 }
848 let mut v = vec![0.0; n_us];
849 for (i, s) in s_full.iter().enumerate() {
850 v[i] = (-s).max(0.0);
851 }
852 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
853 }
854 if !compl_x_l.is_null() && n_us > 0 {
855 let v = nlp.pack_z_l_for_user(&*cq.curr_compl_x_l());
856 if v.len() != n_us {
857 return false;
858 }
859 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
860 }
861 if !compl_x_u.is_null() && n_us > 0 {
862 let v = nlp.pack_z_u_for_user(&*cq.curr_compl_x_u());
863 if v.len() != n_us {
864 return false;
865 }
866 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
867 }
868 if !grad_lag_x.is_null() && n_us > 0 {
869 let glx = cq.curr_grad_lag_x();
870 let full = nlp.lift_x_to_full(&*glx);
874 if full.len() != n_us {
875 return false;
876 }
877 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
878 }
879 if !nlp_constraint_violation.is_null() && m_us > 0 {
880 let zero = vec![0.0; m_us];
886 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
887 }
888 if !compl_g.is_null() && m_us > 0 {
889 let zero = vec![0.0; m_us];
892 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
893 }
894 true
895 });
896 if result.unwrap_or(false) {
897 TRUE
898 } else {
899 FALSE
900 }
901}
902
903#[no_mangle]
911pub unsafe extern "C" fn GetIpoptVersion(
912 major: *mut c_int,
913 minor: *mut c_int,
914 release: *mut c_int,
915) {
916 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
921 if !major.is_null() {
922 *major = mj;
923 }
924 if !minor.is_null() {
925 *minor = mn;
926 }
927 if !release.is_null() {
928 *release = pt;
929 }
930}
931
932fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
933 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
934 (
935 it.next().unwrap_or(0),
936 it.next().unwrap_or(0),
937 it.next().unwrap_or(0),
938 )
939}
940
941#[no_mangle]
958pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
959 last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0)
960}
961
962#[no_mangle]
969pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
970 last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0)
971}
972
973#[no_mangle]
980pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
981 last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0)
982}
983
984#[no_mangle]
991pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
992 last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0)
993}
994
995#[no_mangle]
1001pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1002 last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0)
1003}
1004
1005unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1006where
1007 F: FnOnce(&SolveStatistics) -> T,
1008{
1009 if ipopt_problem.is_null() {
1010 return None;
1011 }
1012 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1013}
1014
1015fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1025 use pounce_qp::BoundStatus::*;
1026 match s {
1027 Inactive => POUNCE_WS_INACTIVE,
1028 AtLower => POUNCE_WS_AT_LOWER,
1029 AtUpper => POUNCE_WS_AT_UPPER,
1030 Fixed => POUNCE_WS_FIXED_OR_EQ,
1031 }
1032}
1033
1034fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1035 use pounce_qp::BoundStatus::*;
1036 match v {
1037 POUNCE_WS_INACTIVE => Some(Inactive),
1038 POUNCE_WS_AT_LOWER => Some(AtLower),
1039 POUNCE_WS_AT_UPPER => Some(AtUpper),
1040 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1041 _ => None,
1042 }
1043}
1044
1045fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1046 use pounce_qp::ConsStatus::*;
1047 match s {
1048 Inactive => POUNCE_WS_INACTIVE,
1049 AtLower => POUNCE_WS_AT_LOWER,
1050 AtUpper => POUNCE_WS_AT_UPPER,
1051 Equality => POUNCE_WS_FIXED_OR_EQ,
1052 }
1053}
1054
1055fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1056 use pounce_qp::ConsStatus::*;
1057 match v {
1058 POUNCE_WS_INACTIVE => Some(Inactive),
1059 POUNCE_WS_AT_LOWER => Some(AtLower),
1060 POUNCE_WS_AT_UPPER => Some(AtUpper),
1061 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1062 _ => None,
1063 }
1064}
1065
1066#[no_mangle]
1082pub unsafe extern "C" fn IpoptGetWorkingSet(
1083 ipopt_problem: IpoptProblem,
1084 bound_status_out: *mut IpoptBoundStatus,
1085 cons_status_out: *mut IpoptConsStatus,
1086) -> Bool {
1087 if ipopt_problem.is_null() {
1088 return FALSE;
1089 }
1090 let info = &*ipopt_problem;
1091 let ws = match info.app.last_sqp_working_set() {
1092 Some(w) => w,
1093 None => return FALSE,
1094 };
1095 if !bound_status_out.is_null() {
1096 for (i, &s) in ws.bounds.iter().enumerate() {
1097 *bound_status_out.add(i) = bound_status_to_int(s);
1098 }
1099 }
1100 if !cons_status_out.is_null() {
1101 for (i, &s) in ws.constraints.iter().enumerate() {
1102 *cons_status_out.add(i) = cons_status_to_int(s);
1103 }
1104 }
1105 TRUE
1106}
1107
1108#[no_mangle]
1124pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1125 ipopt_problem: IpoptProblem,
1126 bound_status_in: *const IpoptBoundStatus,
1127 cons_status_in: *const IpoptConsStatus,
1128) -> Bool {
1129 if ipopt_problem.is_null() {
1130 return FALSE;
1131 }
1132 if bound_status_in.is_null() && cons_status_in.is_null() {
1133 return FALSE;
1134 }
1135 let info = &mut *ipopt_problem;
1136 let n = info.n.max(0) as usize;
1137 let m = info.m.max(0) as usize;
1138 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1139 if !bound_status_in.is_null() {
1140 for i in 0..n {
1141 let v = *bound_status_in.add(i);
1142 match int_to_bound_status(v) {
1143 Some(s) => bounds[i] = s,
1144 None => return FALSE,
1145 }
1146 }
1147 }
1148 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1149 if !cons_status_in.is_null() {
1150 for i in 0..m {
1151 let v = *cons_status_in.add(i);
1152 match int_to_cons_status(v) {
1153 Some(s) => constraints[i] = s,
1154 None => return FALSE,
1155 }
1156 }
1157 }
1158 info.app
1166 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
1167 x: vec![0.0; n],
1168 lambda_g: vec![0.0; m],
1169 lambda_x: vec![0.0; n],
1170 working: Some(pounce_qp::WorkingSet {
1171 bounds,
1172 constraints,
1173 }),
1174 });
1175 TRUE
1176}
1177
1178#[no_mangle]
1185pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1186 if ipopt_problem.is_null() {
1187 return FALSE;
1188 }
1189 (*ipopt_problem).app.clear_sqp_warm_start();
1190 TRUE
1191}
1192
1193#[allow(clippy::too_many_arguments)]
1209#[no_mangle]
1210pub unsafe extern "C" fn IpoptSolveWarmStart(
1211 ipopt_problem: IpoptProblem,
1212 x: *mut Number,
1213 g: *mut Number,
1214 obj_val: *mut Number,
1215 mult_g: *mut Number,
1216 mult_x_L: *mut Number,
1217 mult_x_U: *mut Number,
1218 bound_status_in: *const IpoptBoundStatus,
1219 cons_status_in: *const IpoptConsStatus,
1220 bound_status_out: *mut IpoptBoundStatus,
1221 cons_status_out: *mut IpoptConsStatus,
1222 user_data: *mut c_void,
1223) -> Index {
1224 if ipopt_problem.is_null() {
1225 return ApplicationReturnStatus::InternalError as Index;
1226 }
1227 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1231 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1236 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1237 }
1238 let status = IpoptSolve(
1239 ipopt_problem,
1240 x,
1241 g,
1242 obj_val,
1243 mult_g,
1244 mult_x_L,
1245 mult_x_U,
1246 user_data,
1247 );
1248 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1249 status
1250 })
1251}
1252
1253pub(crate) struct CCallbackTnlp {
1264 pub(crate) n: Index,
1265 pub(crate) m: Index,
1266 pub(crate) nele_jac: Index,
1267 pub(crate) nele_hess: Index,
1268 pub(crate) index_style: Index,
1269 pub(crate) x_l: Vec<Number>,
1270 pub(crate) x_u: Vec<Number>,
1271 pub(crate) g_l: Vec<Number>,
1272 pub(crate) g_u: Vec<Number>,
1273 pub(crate) initial_x: Vec<Number>,
1274 pub(crate) eval_f: Option<Eval_F_CB>,
1275 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1276 pub(crate) eval_g: Option<Eval_G_CB>,
1277 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1278 pub(crate) eval_h: Option<Eval_H_CB>,
1279 pub(crate) user_data: *mut c_void,
1280 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1283 pub(crate) user_scaling: Option<UserScaling>,
1285 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1286 pub(crate) final_x: Vec<Number>,
1287 pub(crate) final_z_l: Vec<Number>,
1288 pub(crate) final_z_u: Vec<Number>,
1289 pub(crate) final_g: Vec<Number>,
1290 pub(crate) final_lambda: Vec<Number>,
1291 pub(crate) final_obj: Number,
1292}
1293
1294impl TNLP for CCallbackTnlp {
1295 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1296 Some(NlpInfo {
1297 n: self.n as pounce_common::types::Index,
1298 m: self.m as pounce_common::types::Index,
1299 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1300 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1301 index_style: if self.index_style == 1 {
1302 IndexStyle::Fortran
1303 } else {
1304 IndexStyle::C
1305 },
1306 })
1307 }
1308
1309 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1310 if !self.x_l.is_empty() {
1311 b.x_l.copy_from_slice(&self.x_l);
1312 }
1313 if !self.x_u.is_empty() {
1314 b.x_u.copy_from_slice(&self.x_u);
1315 }
1316 if !self.g_l.is_empty() {
1317 b.g_l.copy_from_slice(&self.g_l);
1318 }
1319 if !self.g_u.is_empty() {
1320 b.g_u.copy_from_slice(&self.g_u);
1321 }
1322 true
1323 }
1324
1325 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1326 if !self.initial_x.is_empty() {
1327 sp.x.copy_from_slice(&self.initial_x);
1328 }
1329 true
1330 }
1331
1332 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1333 let Some(s) = self.user_scaling.as_ref() else {
1334 return false;
1335 };
1336 *req.obj_scaling = s.obj_scaling;
1337 if let Some(x) = s.x_scaling.as_ref() {
1338 if x.len() == req.x_scaling.len() {
1339 req.x_scaling.copy_from_slice(x);
1340 *req.use_x_scaling = true;
1341 }
1342 } else {
1343 *req.use_x_scaling = false;
1344 }
1345 if let Some(g) = s.g_scaling.as_ref() {
1346 if g.len() == req.g_scaling.len() {
1347 req.g_scaling.copy_from_slice(g);
1348 *req.use_g_scaling = true;
1349 }
1350 } else {
1351 *req.use_g_scaling = false;
1352 }
1353 true
1354 }
1355
1356 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
1357 let cb = self.eval_f?;
1358 let mut obj = 0.0;
1359 let ok = unsafe {
1360 cb(
1361 self.n,
1362 x.as_ptr() as *mut Number,
1363 if new_x { TRUE } else { FALSE },
1364 &mut obj,
1365 self.user_data,
1366 )
1367 };
1368 if ok != FALSE {
1369 Some(obj)
1370 } else {
1371 None
1372 }
1373 }
1374
1375 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
1376 let Some(cb) = self.eval_grad_f else {
1377 return false;
1378 };
1379 let ok = unsafe {
1380 cb(
1381 self.n,
1382 x.as_ptr() as *mut Number,
1383 if new_x { TRUE } else { FALSE },
1384 grad_f.as_mut_ptr(),
1385 self.user_data,
1386 )
1387 };
1388 ok != FALSE
1389 }
1390
1391 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
1392 if self.m == 0 {
1393 return true;
1394 }
1395 let Some(cb) = self.eval_g else {
1396 return false;
1397 };
1398 let ok = unsafe {
1399 cb(
1400 self.n,
1401 x.as_ptr() as *mut Number,
1402 if new_x { TRUE } else { FALSE },
1403 self.m,
1404 g.as_mut_ptr(),
1405 self.user_data,
1406 )
1407 };
1408 ok != FALSE
1409 }
1410
1411 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
1412 if self.m == 0 || self.nele_jac == 0 {
1413 return true;
1414 }
1415 let Some(cb) = self.eval_jac_g else {
1416 return false;
1417 };
1418 let x_ptr = x
1419 .map(|s| s.as_ptr() as *mut Number)
1420 .unwrap_or(std::ptr::null_mut());
1421 let ok = match mode {
1422 SparsityRequest::Structure { irow, jcol } => unsafe {
1423 cb(
1424 self.n,
1425 x_ptr,
1426 if new_x { TRUE } else { FALSE },
1427 self.m,
1428 self.nele_jac,
1429 irow.as_mut_ptr(),
1430 jcol.as_mut_ptr(),
1431 std::ptr::null_mut(),
1432 self.user_data,
1433 )
1434 },
1435 SparsityRequest::Values { values } => unsafe {
1436 cb(
1437 self.n,
1438 x_ptr,
1439 if new_x { TRUE } else { FALSE },
1440 self.m,
1441 self.nele_jac,
1442 std::ptr::null_mut(),
1443 std::ptr::null_mut(),
1444 values.as_mut_ptr(),
1445 self.user_data,
1446 )
1447 },
1448 };
1449 ok != FALSE
1450 }
1451
1452 fn eval_h(
1453 &mut self,
1454 x: Option<&[Number]>,
1455 new_x: bool,
1456 obj_factor: Number,
1457 lambda: Option<&[Number]>,
1458 new_lambda: bool,
1459 mode: SparsityRequest<'_>,
1460 ) -> bool {
1461 let Some(cb) = self.eval_h else {
1462 return false;
1463 };
1464 if self.nele_hess == 0 {
1465 return true;
1466 }
1467 let x_ptr = x
1468 .map(|s| s.as_ptr() as *mut Number)
1469 .unwrap_or(std::ptr::null_mut());
1470 let lambda_ptr = lambda
1471 .map(|s| s.as_ptr() as *mut Number)
1472 .unwrap_or(std::ptr::null_mut());
1473 let ok = match mode {
1474 SparsityRequest::Structure { irow, jcol } => unsafe {
1475 cb(
1476 self.n,
1477 x_ptr,
1478 if new_x { TRUE } else { FALSE },
1479 obj_factor,
1480 self.m,
1481 lambda_ptr,
1482 if new_lambda { TRUE } else { FALSE },
1483 self.nele_hess,
1484 irow.as_mut_ptr(),
1485 jcol.as_mut_ptr(),
1486 std::ptr::null_mut(),
1487 self.user_data,
1488 )
1489 },
1490 SparsityRequest::Values { values } => unsafe {
1491 cb(
1492 self.n,
1493 x_ptr,
1494 if new_x { TRUE } else { FALSE },
1495 obj_factor,
1496 self.m,
1497 lambda_ptr,
1498 if new_lambda { TRUE } else { FALSE },
1499 self.nele_hess,
1500 std::ptr::null_mut(),
1501 std::ptr::null_mut(),
1502 values.as_mut_ptr(),
1503 self.user_data,
1504 )
1505 },
1506 };
1507 ok != FALSE
1508 }
1509
1510 fn intermediate_callback(
1511 &mut self,
1512 stats: pounce_nlp::tnlp::IterStats,
1513 _ip_data: &IpoptData,
1514 _ip_cq: &IpoptCq,
1515 ) -> bool {
1516 let Some(cb) = self.intermediate_cb else {
1517 return true;
1518 };
1519 let ok = unsafe {
1520 cb(
1521 stats.mode as Index,
1522 stats.iter as Index,
1523 stats.obj_value,
1524 stats.inf_pr,
1525 stats.inf_du,
1526 stats.mu,
1527 stats.d_norm,
1528 stats.regularization_size,
1529 stats.alpha_du,
1530 stats.alpha_pr,
1531 stats.ls_trials as Index,
1532 self.user_data,
1533 )
1534 };
1535 ok != FALSE
1536 }
1537
1538 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1539 self.final_status = Some(sol.status);
1540 if !sol.x.is_empty() {
1541 self.final_x.copy_from_slice(sol.x);
1542 }
1543 if !sol.z_l.is_empty() {
1544 self.final_z_l.copy_from_slice(sol.z_l);
1545 }
1546 if !sol.z_u.is_empty() {
1547 self.final_z_u.copy_from_slice(sol.z_u);
1548 }
1549 if !sol.g.is_empty() {
1550 self.final_g.copy_from_slice(sol.g);
1551 }
1552 if !sol.lambda.is_empty() {
1553 self.final_lambda.copy_from_slice(sol.lambda);
1554 }
1555 self.final_obj = sol.obj_value;
1556 }
1557}
1558
1559#[no_mangle]
1572pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
1573 if ipopt_problem.is_null() {
1574 return FALSE;
1575 }
1576 let info = unsafe { &mut *ipopt_problem };
1577 info.app.enable_iter_history();
1578 TRUE
1579}
1580
1581#[no_mangle]
1602pub unsafe extern "C" fn IpoptWriteSolveReport(
1603 ipopt_problem: IpoptProblem,
1604 path: *const c_char,
1605 detail: *const c_char,
1606) -> Bool {
1607 use pounce_solve_report::{
1608 status_to_solve_result_num, write_report_file, InputDescriptor, ReportBuilder, ReportDetail,
1609 };
1610
1611 ffi_guard(FALSE, || unsafe {
1616 if ipopt_problem.is_null() || path.is_null() {
1617 return FALSE;
1618 }
1619 let info = &*ipopt_problem;
1620 let Some(last) = info.last_solve.as_ref() else {
1621 return FALSE;
1622 };
1623
1624 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
1625 return FALSE;
1626 };
1627
1628 let detail_choice = if detail.is_null() {
1629 ReportDetail::Summary
1630 } else {
1631 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
1632 return FALSE;
1633 };
1634 match ReportDetail::parse(detail_str) {
1635 Ok(d) => d,
1636 Err(_) => return FALSE,
1637 }
1638 };
1639
1640 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
1641 builder.problem.n_variables = info.n;
1642 builder.problem.n_constraints = info.m;
1643 builder.problem.n_objectives = 1;
1644 builder.problem.nnz_jac_g = Some(info.nele_jac);
1645 builder.problem.nnz_h_lag = Some(info.nele_hess);
1646
1647 builder.solution.status = last.status;
1648 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
1649 builder.solution.objective = last.final_obj;
1650 builder.solution.x = last.final_x.clone();
1651 builder.solution.lambda = last.final_lambda.clone();
1652
1653 builder.ingest_stats(&last.stats);
1654 if let Some(linsol) = last.linear_solver.clone() {
1655 builder.set_linear_solver_summary(linsol);
1656 }
1657
1658 let report = builder.finish();
1659 match write_report_file(std::path::Path::new(path_str), &report) {
1660 Ok(_) => TRUE,
1661 Err(_) => FALSE,
1662 }
1663 })
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use std::ffi::CString;
1670
1671 unsafe extern "C" fn dummy_eval_f(
1672 _n: Index,
1673 _x: *const Number,
1674 _new_x: Bool,
1675 _obj_value: *mut Number,
1676 _user_data: *mut c_void,
1677 ) -> Bool {
1678 TRUE
1679 }
1680 unsafe extern "C" fn dummy_eval_grad_f(
1681 _n: Index,
1682 _x: *const Number,
1683 _new_x: Bool,
1684 _grad_f: *mut Number,
1685 _user_data: *mut c_void,
1686 ) -> Bool {
1687 TRUE
1688 }
1689
1690 fn create_unconstrained() -> IpoptProblem {
1691 let xl = [-1.0; 4];
1692 let xu = [1.0; 4];
1693 unsafe {
1694 CreateIpoptProblem(
1695 4,
1696 xl.as_ptr(),
1697 xu.as_ptr(),
1698 0,
1699 std::ptr::null(),
1700 std::ptr::null(),
1701 0,
1702 10,
1703 0,
1704 Some(dummy_eval_f),
1705 None,
1706 Some(dummy_eval_grad_f),
1707 None,
1708 None,
1709 )
1710 }
1711 }
1712
1713 #[test]
1714 fn create_succeeds_for_unconstrained_problem() {
1715 let p = create_unconstrained();
1716 assert!(!p.is_null());
1717 unsafe { FreeIpoptProblem(p) };
1718 }
1719
1720 #[test]
1721 fn create_returns_null_on_missing_required_callbacks() {
1722 let xl = [-1.0; 4];
1723 let xu = [1.0; 4];
1724 let p = unsafe {
1725 CreateIpoptProblem(
1726 4,
1727 xl.as_ptr(),
1728 xu.as_ptr(),
1729 0,
1730 std::ptr::null(),
1731 std::ptr::null(),
1732 0,
1733 10,
1734 0,
1735 None, None,
1737 Some(dummy_eval_grad_f),
1738 None,
1739 None,
1740 )
1741 };
1742 assert!(p.is_null());
1743 }
1744
1745 #[test]
1746 fn create_returns_null_on_negative_n() {
1747 let p = unsafe {
1748 CreateIpoptProblem(
1749 -1,
1750 std::ptr::null(),
1751 std::ptr::null(),
1752 0,
1753 std::ptr::null(),
1754 std::ptr::null(),
1755 0,
1756 10,
1757 0,
1758 Some(dummy_eval_f),
1759 None,
1760 Some(dummy_eval_grad_f),
1761 None,
1762 None,
1763 )
1764 };
1765 assert!(p.is_null());
1766 }
1767
1768 #[test]
1769 fn create_returns_null_on_invalid_index_style() {
1770 let xl = [0.0; 1];
1771 let xu = [1.0; 1];
1772 let p = unsafe {
1773 CreateIpoptProblem(
1774 1,
1775 xl.as_ptr(),
1776 xu.as_ptr(),
1777 0,
1778 std::ptr::null(),
1779 std::ptr::null(),
1780 0,
1781 1,
1782 2, Some(dummy_eval_f),
1784 None,
1785 Some(dummy_eval_grad_f),
1786 None,
1787 None,
1788 )
1789 };
1790 assert!(p.is_null());
1791 }
1792
1793 #[test]
1794 fn add_int_option_forwards_to_application() {
1795 let p = create_unconstrained();
1796 let key = CString::new("print_level").unwrap();
1797 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
1798 assert_eq!(ok, TRUE);
1799 let info = unsafe { &*p };
1800 let (level, found) = info
1801 .app
1802 .options()
1803 .get_integer_value("print_level", "")
1804 .unwrap();
1805 assert!(found);
1806 assert_eq!(level, 5);
1807 unsafe { FreeIpoptProblem(p) };
1808 }
1809
1810 #[test]
1811 fn add_str_option_with_invalid_key_returns_false() {
1812 let p = create_unconstrained();
1813 let key = CString::new("totally_unknown_option").unwrap();
1814 let val = CString::new("yes").unwrap();
1815 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
1816 assert_eq!(ok, FALSE);
1817 unsafe { FreeIpoptProblem(p) };
1818 }
1819
1820 #[test]
1821 fn add_options_on_null_problem_returns_false() {
1822 let key = CString::new("print_level").unwrap();
1823 let v = CString::new("yes").unwrap();
1824 unsafe {
1825 assert_eq!(
1826 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
1827 FALSE
1828 );
1829 assert_eq!(
1830 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
1831 FALSE
1832 );
1833 assert_eq!(
1834 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
1835 FALSE
1836 );
1837 }
1838 }
1839
1840 unsafe extern "C" fn dummy_intermediate(
1841 _alg_mod: Index,
1842 _iter_count: Index,
1843 _obj_value: Number,
1844 _inf_pr: Number,
1845 _inf_du: Number,
1846 _mu: Number,
1847 _d_norm: Number,
1848 _regularization_size: Number,
1849 _alpha_du: Number,
1850 _alpha_pr: Number,
1851 _ls_trials: Index,
1852 _user_data: *mut c_void,
1853 ) -> Bool {
1854 TRUE
1855 }
1856
1857 #[test]
1858 fn set_intermediate_callback_stores_pointer() {
1859 let p = create_unconstrained();
1860 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
1861 assert_eq!(ok, TRUE);
1862 let info = unsafe { &*p };
1863 assert!(info.intermediate_cb.is_some());
1864 unsafe { FreeIpoptProblem(p) };
1865 }
1866
1867 #[test]
1868 fn solve_returns_internal_error_on_null_problem() {
1869 let rc = unsafe {
1870 IpoptSolve(
1871 std::ptr::null_mut(),
1872 std::ptr::null_mut(),
1873 std::ptr::null_mut(),
1874 std::ptr::null_mut(),
1875 std::ptr::null_mut(),
1876 std::ptr::null_mut(),
1877 std::ptr::null_mut(),
1878 std::ptr::null_mut(),
1879 )
1880 };
1881 assert_eq!(rc, -199);
1882 }
1883
1884 #[test]
1885 fn free_null_is_safe() {
1886 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
1887 }
1888
1889 unsafe extern "C" fn quad_eval_f(
1895 _n: Index,
1896 x: *const Number,
1897 _new_x: Bool,
1898 obj_value: *mut Number,
1899 _user_data: *mut c_void,
1900 ) -> Bool {
1901 let v = *x.offset(0);
1902 *obj_value = (v - 2.0) * (v - 2.0);
1903 TRUE
1904 }
1905 unsafe extern "C" fn quad_eval_grad_f(
1906 _n: Index,
1907 x: *const Number,
1908 _new_x: Bool,
1909 grad: *mut Number,
1910 _user_data: *mut c_void,
1911 ) -> Bool {
1912 let v = *x.offset(0);
1913 *grad.offset(0) = 2.0 * (v - 2.0);
1914 TRUE
1915 }
1916 unsafe extern "C" fn quad_eval_h(
1917 _n: Index,
1918 _x: *const Number,
1919 _new_x: Bool,
1920 obj_factor: Number,
1921 _m: Index,
1922 _lambda: *const Number,
1923 _new_lambda: Bool,
1924 _nele_hess: Index,
1925 irow: *mut Index,
1926 jcol: *mut Index,
1927 values: *mut Number,
1928 _user_data: *mut c_void,
1929 ) -> Bool {
1930 if !irow.is_null() && !jcol.is_null() && values.is_null() {
1931 *irow.offset(0) = 0;
1932 *jcol.offset(0) = 0;
1933 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
1934 *values.offset(0) = 2.0 * obj_factor;
1935 } else {
1936 return FALSE;
1937 }
1938 TRUE
1939 }
1940
1941 #[test]
1942 fn solve_drives_unconstrained_quadratic_through_bridge() {
1943 let xl = [-1.0e20];
1946 let xu = [1.0e20];
1947 let p = unsafe {
1948 CreateIpoptProblem(
1949 1,
1950 xl.as_ptr(),
1951 xu.as_ptr(),
1952 0,
1953 std::ptr::null(),
1954 std::ptr::null(),
1955 0,
1956 1,
1957 0,
1958 Some(quad_eval_f),
1959 None,
1960 Some(quad_eval_grad_f),
1961 None,
1962 Some(quad_eval_h),
1963 )
1964 };
1965 assert!(!p.is_null());
1966 let mut x = [0.0_f64];
1967 let mut obj = 0.0_f64;
1968 let rc = unsafe {
1969 IpoptSolve(
1970 p,
1971 x.as_mut_ptr(),
1972 std::ptr::null_mut(),
1973 &mut obj,
1974 std::ptr::null_mut(),
1975 std::ptr::null_mut(),
1976 std::ptr::null_mut(),
1977 std::ptr::null_mut(),
1978 )
1979 };
1980 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
1981 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
1982 assert!(obj.abs() < 1e-10, "obj = {}", obj);
1983 unsafe { FreeIpoptProblem(p) };
1984 }
1985
1986 #[test]
2001 fn stale_stats_cleared_when_resolve_bails() {
2002 let xl = [-1.0e20];
2003 let xu = [1.0e20];
2004 let p = unsafe {
2005 CreateIpoptProblem(
2006 1,
2007 xl.as_ptr(),
2008 xu.as_ptr(),
2009 0,
2010 std::ptr::null(),
2011 std::ptr::null(),
2012 0,
2013 1,
2014 0,
2015 Some(quad_eval_f),
2016 None,
2017 Some(quad_eval_grad_f),
2018 None,
2019 Some(quad_eval_h),
2020 )
2021 };
2022 assert!(!p.is_null());
2023
2024 let mut x = [0.0_f64];
2025 let mut obj = 0.0_f64;
2026 let rc = unsafe {
2027 IpoptSolve(
2028 p,
2029 x.as_mut_ptr(),
2030 std::ptr::null_mut(),
2031 &mut obj,
2032 std::ptr::null_mut(),
2033 std::ptr::null_mut(),
2034 std::ptr::null_mut(),
2035 std::ptr::null_mut(),
2036 )
2037 };
2038 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2039 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2041 assert!(
2042 iters_after_success >= 1,
2043 "a converged solve should record >=1 iteration, got {iters_after_success}"
2044 );
2045 assert!(unsafe { (*p).last_solve.is_some() });
2046
2047 unsafe { (*p).n = -1 };
2050 let mut x2 = [0.0_f64];
2051 let rc2 = unsafe {
2052 IpoptSolve(
2053 p,
2054 x2.as_mut_ptr(),
2055 std::ptr::null_mut(),
2056 std::ptr::null_mut(),
2057 std::ptr::null_mut(),
2058 std::ptr::null_mut(),
2059 std::ptr::null_mut(),
2060 std::ptr::null_mut(),
2061 )
2062 };
2063 assert_eq!(
2064 rc2,
2065 ApplicationReturnStatus::InvalidProblemDefinition as Index
2066 );
2067
2068 assert!(
2072 unsafe { (*p).last_solve.is_none() },
2073 "a bailed re-solve must clear stale last_solve (F5)"
2074 );
2075 assert_eq!(
2076 unsafe { GetIpoptIterCount(p) },
2077 0,
2078 "stale iteration count must not survive a bailed re-solve (F5)"
2079 );
2080
2081 unsafe { FreeIpoptProblem(p) };
2082 }
2083
2084 #[test]
2085 fn solve_invalid_problem_definition_when_x_null() {
2086 let p = create_unconstrained();
2087 let rc = unsafe {
2088 IpoptSolve(
2089 p,
2090 std::ptr::null_mut(), std::ptr::null_mut(),
2092 std::ptr::null_mut(),
2093 std::ptr::null_mut(),
2094 std::ptr::null_mut(),
2095 std::ptr::null_mut(),
2096 std::ptr::null_mut(),
2097 )
2098 };
2099 assert_eq!(
2100 rc,
2101 ApplicationReturnStatus::InvalidProblemDefinition as Index
2102 );
2103 unsafe { FreeIpoptProblem(p) };
2104 }
2105
2106 #[test]
2109 fn get_version_writes_pkg_version() {
2110 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
2111 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
2112 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
2113 assert_eq!((mj, mn, pt), expected);
2114 }
2115
2116 #[test]
2117 fn get_version_tolerates_null_buffers() {
2118 unsafe {
2120 GetIpoptVersion(
2121 std::ptr::null_mut(),
2122 std::ptr::null_mut(),
2123 std::ptr::null_mut(),
2124 )
2125 };
2126 }
2127
2128 #[test]
2129 fn set_scaling_stores_user_supplied_arrays() {
2130 let p = create_unconstrained();
2131 let xs = [2.0, 3.0, 4.0, 5.0];
2132 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
2133 assert_eq!(ok, TRUE);
2134 let info = unsafe { &*p };
2135 let s = info.user_scaling.as_ref().unwrap();
2136 assert_eq!(s.obj_scaling, 7.0);
2137 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
2138 assert!(s.g_scaling.is_none());
2139 unsafe { FreeIpoptProblem(p) };
2140 }
2141
2142 #[test]
2143 fn set_scaling_on_null_problem_returns_false() {
2144 let ok = unsafe {
2145 SetIpoptProblemScaling(
2146 std::ptr::null_mut(),
2147 1.0,
2148 std::ptr::null(),
2149 std::ptr::null(),
2150 )
2151 };
2152 assert_eq!(ok, FALSE);
2153 }
2154
2155 #[test]
2156 fn open_output_file_writes_and_attaches_journal() {
2157 let p = create_unconstrained();
2158 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2159 let _ = std::fs::create_dir_all(&dir);
2160 let path = dir.join("output.log");
2161 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2162 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2163 assert_eq!(ok, TRUE);
2164 let info = unsafe { &*p };
2166 let (level, found) = info
2167 .app
2168 .options()
2169 .get_integer_value("file_print_level", "")
2170 .unwrap();
2171 assert!(found);
2172 assert_eq!(level, 5);
2173 unsafe { FreeIpoptProblem(p) };
2174 let _ = std::fs::remove_file(&path);
2175 }
2176
2177 #[test]
2178 fn open_output_file_with_null_inputs_returns_false() {
2179 let key = CString::new("nope").unwrap();
2180 unsafe {
2181 assert_eq!(
2182 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2183 FALSE
2184 );
2185 }
2186 let p = create_unconstrained();
2187 unsafe {
2188 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2189 FreeIpoptProblem(p);
2190 }
2191 }
2192
2193 #[test]
2194 fn get_current_iterate_returns_false_outside_callback() {
2195 let p = create_unconstrained();
2196 let rc = unsafe {
2197 GetIpoptCurrentIterate(
2198 p,
2199 FALSE,
2200 0,
2201 std::ptr::null_mut(),
2202 std::ptr::null_mut(),
2203 std::ptr::null_mut(),
2204 0,
2205 std::ptr::null_mut(),
2206 std::ptr::null_mut(),
2207 )
2208 };
2209 assert_eq!(rc, FALSE);
2210 unsafe { FreeIpoptProblem(p) };
2211 }
2212
2213 #[test]
2214 fn get_current_violations_returns_false_outside_callback() {
2215 let p = create_unconstrained();
2216 let rc = unsafe {
2217 GetIpoptCurrentViolations(
2218 p,
2219 FALSE,
2220 0,
2221 std::ptr::null_mut(),
2222 std::ptr::null_mut(),
2223 std::ptr::null_mut(),
2224 std::ptr::null_mut(),
2225 std::ptr::null_mut(),
2226 0,
2227 std::ptr::null_mut(),
2228 std::ptr::null_mut(),
2229 )
2230 };
2231 assert_eq!(rc, FALSE);
2232 unsafe { FreeIpoptProblem(p) };
2233 }
2234
2235 #[test]
2236 fn post_solve_stats_zero_before_solve() {
2237 let p = create_unconstrained();
2238 unsafe {
2239 assert_eq!(GetIpoptIterCount(p), 0);
2240 assert_eq!(GetIpoptSolveTime(p), 0.0);
2241 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2242 assert_eq!(GetIpoptDualInf(p), 0.0);
2243 assert_eq!(GetIpoptComplInf(p), 0.0);
2244 FreeIpoptProblem(p);
2245 }
2246 }
2247
2248 #[test]
2249 fn post_solve_stats_populated_after_solve() {
2250 let xl = [-1.0e20];
2252 let xu = [1.0e20];
2253 let p = unsafe {
2254 CreateIpoptProblem(
2255 1,
2256 xl.as_ptr(),
2257 xu.as_ptr(),
2258 0,
2259 std::ptr::null(),
2260 std::ptr::null(),
2261 0,
2262 1,
2263 0,
2264 Some(quad_eval_f),
2265 None,
2266 Some(quad_eval_grad_f),
2267 None,
2268 Some(quad_eval_h),
2269 )
2270 };
2271 let mut x = [0.0_f64];
2272 let mut obj = 0.0_f64;
2273 let rc = unsafe {
2274 IpoptSolve(
2275 p,
2276 x.as_mut_ptr(),
2277 std::ptr::null_mut(),
2278 &mut obj,
2279 std::ptr::null_mut(),
2280 std::ptr::null_mut(),
2281 std::ptr::null_mut(),
2282 std::ptr::null_mut(),
2283 )
2284 };
2285 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2286 unsafe {
2289 assert!(GetIpoptIterCount(p) >= 0);
2290 assert!(GetIpoptSolveTime(p) >= 0.0);
2291 assert!(GetIpoptPrimalInf(p).is_finite());
2292 assert!(GetIpoptDualInf(p).is_finite());
2293 assert!(GetIpoptComplInf(p).is_finite());
2294 FreeIpoptProblem(p);
2295 }
2296 }
2297
2298 #[test]
2299 fn write_solve_report_emits_v1_json_with_iter_history() {
2300 let xl = [-1.0e20];
2303 let xu = [1.0e20];
2304 let p = unsafe {
2305 CreateIpoptProblem(
2306 1,
2307 xl.as_ptr(),
2308 xu.as_ptr(),
2309 0,
2310 std::ptr::null(),
2311 std::ptr::null(),
2312 0,
2313 1,
2314 0,
2315 Some(quad_eval_f),
2316 None,
2317 Some(quad_eval_grad_f),
2318 None,
2319 Some(quad_eval_h),
2320 )
2321 };
2322
2323 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
2325 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
2326 assert_eq!(bad, FALSE);
2327
2328 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
2330 let mut x = [0.0_f64];
2331 let mut obj = 0.0_f64;
2332 let rc = unsafe {
2333 IpoptSolve(
2334 p,
2335 x.as_mut_ptr(),
2336 std::ptr::null_mut(),
2337 &mut obj,
2338 std::ptr::null_mut(),
2339 std::ptr::null_mut(),
2340 std::ptr::null_mut(),
2341 std::ptr::null_mut(),
2342 )
2343 };
2344 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2345
2346 let dir = std::env::temp_dir();
2347 let path = dir.join("pounce_cinterface_report.json");
2348 let cpath = CString::new(path.to_str().unwrap()).unwrap();
2349 let cdetail = CString::new("full").unwrap();
2350 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
2351 assert_eq!(ok, TRUE);
2352
2353 let txt = std::fs::read_to_string(&path).unwrap();
2356 assert!(
2357 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
2358 "{txt}"
2359 );
2360 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
2361 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
2362 assert_eq!(parsed.problem.n_variables, 1);
2363 assert_eq!(parsed.problem.n_constraints, 0);
2364
2365 let bad_detail = CString::new("verbose").unwrap();
2367 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
2368 assert_eq!(bad, FALSE);
2369
2370 let _ = std::fs::remove_file(&path);
2371 unsafe { FreeIpoptProblem(p) };
2372 }
2373
2374 unsafe extern "C" fn cb_quad_eval_g(
2381 _n: Index,
2382 x: *const Number,
2383 _new_x: Bool,
2384 _m: Index,
2385 g: *mut Number,
2386 _user_data: *mut c_void,
2387 ) -> Bool {
2388 *g.offset(0) = *x.offset(0);
2389 TRUE
2390 }
2391 unsafe extern "C" fn cb_quad_eval_jac_g(
2392 _n: Index,
2393 _x: *const Number,
2394 _new_x: Bool,
2395 _m: Index,
2396 nele_jac: Index,
2397 irow: *mut Index,
2398 jcol: *mut Index,
2399 values: *mut Number,
2400 _user_data: *mut c_void,
2401 ) -> Bool {
2402 assert_eq!(nele_jac, 1);
2403 if !irow.is_null() {
2404 *irow.offset(0) = 0;
2405 *jcol.offset(0) = 0;
2406 }
2407 if !values.is_null() {
2408 *values.offset(0) = 1.0;
2409 }
2410 TRUE
2411 }
2412 unsafe extern "C" fn cb_quad_eval_h(
2413 _n: Index,
2414 _x: *const Number,
2415 _new_x: Bool,
2416 obj_factor: Number,
2417 _m: Index,
2418 _lambda: *const Number,
2419 _new_lambda: Bool,
2420 _nele_hess: Index,
2421 irow: *mut Index,
2422 jcol: *mut Index,
2423 values: *mut Number,
2424 _user_data: *mut c_void,
2425 ) -> Bool {
2426 if !irow.is_null() {
2427 *irow.offset(0) = 0;
2428 *jcol.offset(0) = 0;
2429 }
2430 if !values.is_null() {
2431 *values.offset(0) = 2.0 * obj_factor;
2432 }
2433 TRUE
2434 }
2435
2436 fn create_callback_test_problem() -> IpoptProblem {
2437 let xl = [-1.0e20];
2439 let xu = [1.0e20];
2440 let gl = [-10.0];
2441 let gu = [10.0];
2442 unsafe {
2443 CreateIpoptProblem(
2444 1,
2445 xl.as_ptr(),
2446 xu.as_ptr(),
2447 1,
2448 gl.as_ptr(),
2449 gu.as_ptr(),
2450 1,
2451 1,
2452 0,
2453 Some(quad_eval_f),
2454 Some(cb_quad_eval_g),
2455 Some(quad_eval_grad_f),
2456 Some(cb_quad_eval_jac_g),
2457 Some(cb_quad_eval_h),
2458 )
2459 }
2460 }
2461
2462 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2463 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
2464 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
2465 std::sync::atomic::AtomicBool::new(false);
2466
2467 unsafe extern "C" fn counting_cb(
2468 _alg_mod: Index,
2469 iter_count: Index,
2470 _obj_value: Number,
2471 _inf_pr: Number,
2472 _inf_du: Number,
2473 _mu: Number,
2474 _d_norm: Number,
2475 _regularization_size: Number,
2476 _alpha_du: Number,
2477 _alpha_pr: Number,
2478 _ls_trials: Index,
2479 user_data: *mut c_void,
2480 ) -> Bool {
2481 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2482 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
2483 let problem = user_data as IpoptProblem;
2486 let mut x = [0.0_f64];
2487 let rc = GetIpoptCurrentIterate(
2488 problem,
2489 FALSE,
2490 1,
2491 x.as_mut_ptr(),
2492 std::ptr::null_mut(),
2493 std::ptr::null_mut(),
2494 1,
2495 std::ptr::null_mut(),
2496 std::ptr::null_mut(),
2497 );
2498 if rc == TRUE && x[0].is_finite() {
2499 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2500 }
2501 TRUE
2502 }
2503
2504 #[test]
2505 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
2506 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
2507 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
2508 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2509
2510 let p = create_callback_test_problem();
2511 assert!(!p.is_null());
2512 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
2513 assert_eq!(ok, TRUE);
2514 let mut x = [0.0_f64];
2515 let mut obj = 0.0_f64;
2516 let rc = unsafe {
2517 IpoptSolve(
2518 p,
2519 x.as_mut_ptr(),
2520 std::ptr::null_mut(),
2521 &mut obj,
2522 std::ptr::null_mut(),
2523 std::ptr::null_mut(),
2524 std::ptr::null_mut(),
2525 p as *mut c_void,
2526 )
2527 };
2528 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2529 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
2531 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
2532 assert!(
2533 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
2534 "last iter should be >= 1 after at least one accepted step"
2535 );
2536 assert!(
2537 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
2538 "GetIpoptCurrentIterate did not return a usable x"
2539 );
2540 unsafe { FreeIpoptProblem(p) };
2541 }
2542
2543 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2544
2545 fn create_bounded_callback_test_problem() -> IpoptProblem {
2550 let xl = [0.0];
2552 let xu = [10.0];
2553 let gl = [-10.0];
2554 let gu = [10.0];
2555 unsafe {
2556 CreateIpoptProblem(
2557 1,
2558 xl.as_ptr(),
2559 xu.as_ptr(),
2560 1,
2561 gl.as_ptr(),
2562 gu.as_ptr(),
2563 1,
2564 1,
2565 0,
2566 Some(quad_eval_f),
2567 Some(cb_quad_eval_g),
2568 Some(quad_eval_grad_f),
2569 Some(cb_quad_eval_jac_g),
2570 Some(cb_quad_eval_h),
2571 )
2572 }
2573 }
2574
2575 unsafe extern "C" fn violations_inspecting_cb(
2576 _alg_mod: Index,
2577 _iter_count: Index,
2578 _obj_value: Number,
2579 _inf_pr: Number,
2580 _inf_du: Number,
2581 _mu: Number,
2582 _d_norm: Number,
2583 _regularization_size: Number,
2584 _alpha_du: Number,
2585 _alpha_pr: Number,
2586 _ls_trials: Index,
2587 user_data: *mut c_void,
2588 ) -> Bool {
2589 let problem = user_data as IpoptProblem;
2590 let mut x_l_viol = [f64::NAN];
2595 let mut x_u_viol = [f64::NAN];
2596 let rc = GetIpoptCurrentViolations(
2597 problem,
2598 FALSE,
2599 1,
2600 x_l_viol.as_mut_ptr(),
2601 x_u_viol.as_mut_ptr(),
2602 std::ptr::null_mut(),
2603 std::ptr::null_mut(),
2604 std::ptr::null_mut(),
2605 1,
2606 std::ptr::null_mut(),
2607 std::ptr::null_mut(),
2608 );
2609 if rc == TRUE
2610 && x_l_viol[0].is_finite()
2611 && x_l_viol[0] >= 0.0
2612 && x_u_viol[0].is_finite()
2613 && x_u_viol[0] >= 0.0
2614 {
2615 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2616 }
2617 TRUE
2618 }
2619
2620 #[test]
2621 fn get_current_violations_inside_callback_reports_finite_bounds() {
2622 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2623 let p = create_bounded_callback_test_problem();
2624 assert!(!p.is_null());
2625 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
2626 assert_eq!(ok, TRUE);
2627 let mut x = [5.0_f64];
2628 let mut obj = 0.0_f64;
2629 let rc = unsafe {
2630 IpoptSolve(
2631 p,
2632 x.as_mut_ptr(),
2633 std::ptr::null_mut(),
2634 &mut obj,
2635 std::ptr::null_mut(),
2636 std::ptr::null_mut(),
2637 std::ptr::null_mut(),
2638 p as *mut c_void,
2639 )
2640 };
2641 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2642 assert!(
2643 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
2644 "GetIpoptCurrentViolations did not return finite, non-negative \
2645 bound violations from inside the callback"
2646 );
2647 unsafe { FreeIpoptProblem(p) };
2648 }
2649
2650 #[test]
2651 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
2652 let n_us = 1usize;
2662 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
2666 let mut v = vec![0.0; n_us];
2667 for (i, s) in packed.iter().enumerate() {
2668 v[i] = (-s).max(0.0);
2669 }
2670 v
2671 });
2672 assert!(
2673 unguarded.is_err(),
2674 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
2675 );
2676
2677 let guarded: Result<Vec<f64>, ()> = (|| {
2679 if packed.len() != n_us {
2680 return Err(());
2681 }
2682 let mut v = vec![0.0; n_us];
2683 for (i, s) in packed.iter().enumerate() {
2684 v[i] = (-s).max(0.0);
2685 }
2686 Ok(v)
2687 })();
2688 assert!(
2689 guarded.is_err(),
2690 "guarded scatter should reject the length mismatch (return FALSE), not panic"
2691 );
2692 }
2693
2694 unsafe extern "C" fn user_stop_cb(
2695 _alg_mod: Index,
2696 _iter_count: Index,
2697 _obj_value: Number,
2698 _inf_pr: Number,
2699 _inf_du: Number,
2700 _mu: Number,
2701 _d_norm: Number,
2702 _regularization_size: Number,
2703 _alpha_du: Number,
2704 _alpha_pr: Number,
2705 _ls_trials: Index,
2706 _user_data: *mut c_void,
2707 ) -> Bool {
2708 FALSE
2709 }
2710
2711 #[test]
2712 fn intermediate_callback_false_surfaces_user_requested_stop() {
2713 let p = create_callback_test_problem();
2714 assert!(!p.is_null());
2715 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
2716 assert_eq!(ok, TRUE);
2717 let mut x = [0.0_f64];
2718 let rc = unsafe {
2719 IpoptSolve(
2720 p,
2721 x.as_mut_ptr(),
2722 std::ptr::null_mut(),
2723 std::ptr::null_mut(),
2724 std::ptr::null_mut(),
2725 std::ptr::null_mut(),
2726 std::ptr::null_mut(),
2727 std::ptr::null_mut(),
2728 )
2729 };
2730 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
2731 unsafe { FreeIpoptProblem(p) };
2732 }
2733
2734 #[test]
2735 fn ffi_guard_converts_panic_to_fallback() {
2736 let fallback = ApplicationReturnStatus::InternalError as Index;
2743 let got = ffi_guard(fallback, || -> Index {
2744 panic!("boom inside solver core");
2745 });
2746 assert_eq!(got, fallback);
2747 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
2748 }
2749
2750 #[test]
2751 fn ffi_guard_is_transparent_on_success() {
2752 let got = ffi_guard(-99, || 7);
2756 assert_eq!(got, 7);
2757 }
2758
2759 #[test]
2760 fn parse_pkg_version_handles_missing_components() {
2761 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
2762 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
2763 assert_eq!(parse_pkg_version(""), (0, 0, 0));
2764 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
2765 }
2766
2767 use crate::solver::{
2770 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
2771 IpoptSolverSolve,
2772 };
2773
2774 #[test]
2775 fn solver_create_consumes_problem_handle() {
2776 let mut p = create_unconstrained();
2777 assert!(!p.is_null());
2778 let s = unsafe { IpoptCreateSolver(&mut p) };
2779 assert!(!s.is_null());
2780 assert!(
2781 p.is_null(),
2782 "IpoptCreateSolver should NULL out the caller's handle"
2783 );
2784 unsafe { IpoptFreeSolver(s) };
2785 }
2786
2787 #[test]
2788 fn solver_create_null_inputs_return_null() {
2789 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2791 assert!(s.is_null());
2792 let mut p: IpoptProblem = std::ptr::null_mut();
2794 let s = unsafe { IpoptCreateSolver(&mut p) };
2795 assert!(s.is_null());
2796 }
2797
2798 #[test]
2799 fn solver_free_null_is_safe() {
2800 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
2801 }
2802
2803 #[test]
2804 fn solver_solve_drives_quadratic_and_retains_factor() {
2805 let xl = [-1.0e20];
2806 let xu = [1.0e20];
2807 let mut p = unsafe {
2808 CreateIpoptProblem(
2809 1,
2810 xl.as_ptr(),
2811 xu.as_ptr(),
2812 0,
2813 std::ptr::null(),
2814 std::ptr::null(),
2815 0,
2816 1,
2817 0,
2818 Some(quad_eval_f),
2819 None,
2820 Some(quad_eval_grad_f),
2821 None,
2822 Some(quad_eval_h),
2823 )
2824 };
2825 assert!(!p.is_null());
2826 let s = unsafe { IpoptCreateSolver(&mut p) };
2827 assert!(!s.is_null());
2828 let mut x = [0.0_f64];
2829 let mut obj = 0.0_f64;
2830 let rc = unsafe {
2831 IpoptSolverSolve(
2832 s,
2833 x.as_mut_ptr(),
2834 std::ptr::null_mut(),
2835 &mut obj,
2836 std::ptr::null_mut(),
2837 std::ptr::null_mut(),
2838 std::ptr::null_mut(),
2839 std::ptr::null_mut(),
2840 )
2841 };
2842 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2843 assert!((x[0] - 2.0).abs() < 1e-6);
2844 assert!(obj.abs() < 1e-10);
2845
2846 let dim = unsafe { IpoptSolverGetKktDim(s) };
2849 assert!(dim > 0, "expected positive KKT dim, got {dim}");
2850 let rhs = vec![0.0_f64; dim as usize];
2851 let mut lhs = vec![1.0_f64; dim as usize];
2852 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
2853 assert_eq!(ok, TRUE);
2854 for (i, v) in lhs.iter().enumerate() {
2855 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
2856 }
2857 unsafe { IpoptFreeSolver(s) };
2858 }
2859
2860 #[test]
2861 fn solver_kkt_dim_minus_one_before_solve() {
2862 let mut p = create_unconstrained();
2863 let s = unsafe { IpoptCreateSolver(&mut p) };
2864 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
2865 unsafe { IpoptFreeSolver(s) };
2866 }
2867
2868 #[test]
2873 fn c_get_working_set_returns_false_before_any_solve() {
2874 let p = create_unconstrained();
2875 let mut bound_buf = [0; 4];
2876 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
2877 assert_eq!(rc, FALSE);
2878 unsafe { FreeIpoptProblem(p) };
2879 }
2880
2881 #[test]
2882 fn c_set_warm_start_with_both_null_returns_false() {
2883 let p = create_unconstrained();
2884 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
2885 assert_eq!(rc, FALSE);
2886 unsafe { FreeIpoptProblem(p) };
2887 }
2888
2889 #[test]
2890 fn c_set_warm_start_with_bad_status_code_returns_false() {
2891 let p = create_unconstrained();
2892 let bogus = [
2894 POUNCE_WS_INACTIVE,
2895 7,
2896 POUNCE_WS_AT_LOWER,
2897 POUNCE_WS_INACTIVE,
2898 ];
2899 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
2900 assert_eq!(rc, FALSE);
2901 unsafe { FreeIpoptProblem(p) };
2902 }
2903
2904 #[test]
2905 fn c_set_warm_start_then_clear_succeeds() {
2906 let p = create_unconstrained();
2907 let in_buf = [POUNCE_WS_INACTIVE; 4];
2908 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
2909 assert_eq!(set_rc, TRUE);
2910 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
2911 assert_eq!(clr_rc, TRUE);
2912 unsafe { FreeIpoptProblem(p) };
2913 }
2914
2915 #[test]
2916 fn c_set_warm_start_on_null_problem_returns_false() {
2917 let in_buf = [POUNCE_WS_INACTIVE; 1];
2918 let rc = unsafe {
2919 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
2920 };
2921 assert_eq!(rc, FALSE);
2922 }
2923
2924 #[test]
2925 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
2926 let p = create_callback_test_problem();
2932 let key = CString::new("algorithm").unwrap();
2933 let val = CString::new("active-set-sqp").unwrap();
2934 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2935 assert_eq!(ok, TRUE);
2936
2937 let mut x = [0.0_f64];
2938 let mut obj = 0.0_f64;
2939 let rc1 = unsafe {
2940 IpoptSolve(
2941 p,
2942 x.as_mut_ptr(),
2943 std::ptr::null_mut(),
2944 &mut obj,
2945 std::ptr::null_mut(),
2946 std::ptr::null_mut(),
2947 std::ptr::null_mut(),
2948 std::ptr::null_mut(),
2949 )
2950 };
2951 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
2952
2953 let mut bound_buf = [-1; 1];
2954 let mut cons_buf = [-1; 1];
2955 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
2956 assert_eq!(got, TRUE);
2957 assert!((0..=3).contains(&bound_buf[0]));
2959 assert!((0..=3).contains(&cons_buf[0]));
2960
2961 x[0] = 0.0;
2966 let mut obj2 = 0.0_f64;
2967 let mut bound_out = [-1; 1];
2968 let mut cons_out = [-1; 1];
2969 let rc2 = unsafe {
2970 IpoptSolveWarmStart(
2971 p,
2972 x.as_mut_ptr(),
2973 std::ptr::null_mut(),
2974 &mut obj2,
2975 std::ptr::null_mut(),
2976 std::ptr::null_mut(),
2977 std::ptr::null_mut(),
2978 bound_buf.as_ptr(),
2979 cons_buf.as_ptr(),
2980 bound_out.as_mut_ptr(),
2981 cons_out.as_mut_ptr(),
2982 std::ptr::null_mut(),
2983 )
2984 };
2985 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
2986 assert!((0..=3).contains(&bound_out[0]));
2987 assert!((0..=3).contains(&cons_out[0]));
2988
2989 unsafe { FreeIpoptProblem(p) };
2990 }
2991}