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,
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 InnerBackendFactoryFactory, make_default_restoration_factory_provider,
50};
51use std::cell::RefCell;
52use std::ffi::{CStr, c_char, c_int, c_void};
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#[unsafe(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 unsafe {
263 pounce_observability::init_subscriber();
267
268 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
269 return std::ptr::null_mut();
270 }
271 if !(0..=1).contains(&index_style) {
272 return std::ptr::null_mut();
273 }
274 if eval_f.is_none() || eval_grad_f.is_none() {
275 return std::ptr::null_mut();
276 }
277 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
278 return std::ptr::null_mut();
279 }
280 if n > 0 && (x_L.is_null() || x_U.is_null()) {
281 return std::ptr::null_mut();
282 }
283 if m > 0 && (g_L.is_null() || g_U.is_null()) {
284 return std::ptr::null_mut();
285 }
286
287 let x_l = if n > 0 {
288 std::slice::from_raw_parts(x_L, n as usize).to_vec()
289 } else {
290 Vec::new()
291 };
292 let x_u = if n > 0 {
293 std::slice::from_raw_parts(x_U, n as usize).to_vec()
294 } else {
295 Vec::new()
296 };
297 let g_l_vec = if m > 0 {
298 std::slice::from_raw_parts(g_L, m as usize).to_vec()
299 } else {
300 Vec::new()
301 };
302 let g_u_vec = if m > 0 {
303 std::slice::from_raw_parts(g_U, m as usize).to_vec()
304 } else {
305 Vec::new()
306 };
307
308 let info = Box::new(IpoptProblemInfo {
309 app: IpoptApplication::new(),
310 n,
311 m,
312 nele_jac,
313 nele_hess,
314 index_style,
315 x_l,
316 x_u,
317 g_l: g_l_vec,
318 g_u: g_u_vec,
319 eval_f,
320 eval_g,
321 eval_grad_f,
322 eval_jac_g,
323 eval_h,
324 intermediate_cb: None,
325 user_scaling: None,
326 last_solve: None,
327 });
328 Box::into_raw(info)
329 }
330}
331
332#[unsafe(no_mangle)]
339pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
340 unsafe {
341 if ipopt_problem.is_null() {
342 return;
343 }
344 drop(Box::from_raw(ipopt_problem));
345 }
346}
347
348unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
349 unsafe {
350 if keyword.is_null() {
351 return None;
352 }
353 CStr::from_ptr(keyword).to_str().ok()
354 }
355}
356
357#[unsafe(no_mangle)]
364pub unsafe extern "C" fn AddIpoptStrOption(
365 ipopt_problem: IpoptProblem,
366 keyword: *const c_char,
367 val: *const c_char,
368) -> Bool {
369 unsafe {
370 if ipopt_problem.is_null() {
371 return FALSE;
372 }
373 let info = &mut *ipopt_problem;
374 let Some(k) = keyword_str(keyword) else {
375 return FALSE;
376 };
377 if val.is_null() {
378 return FALSE;
379 }
380 let Ok(v) = CStr::from_ptr(val).to_str() else {
381 return FALSE;
382 };
383 match info.app.options_mut().set_string_value(k, v, true, false) {
384 Ok(_) => TRUE,
385 Err(_) => FALSE,
386 }
387 }
388}
389
390#[unsafe(no_mangle)]
397pub unsafe extern "C" fn AddIpoptNumOption(
398 ipopt_problem: IpoptProblem,
399 keyword: *const c_char,
400 val: Number,
401) -> Bool {
402 unsafe {
403 if ipopt_problem.is_null() {
404 return FALSE;
405 }
406 let info = &mut *ipopt_problem;
407 let Some(k) = keyword_str(keyword) else {
408 return FALSE;
409 };
410 match info
411 .app
412 .options_mut()
413 .set_numeric_value(k, val, true, false)
414 {
415 Ok(_) => TRUE,
416 Err(_) => FALSE,
417 }
418 }
419}
420
421#[unsafe(no_mangle)]
428pub unsafe extern "C" fn AddIpoptIntOption(
429 ipopt_problem: IpoptProblem,
430 keyword: *const c_char,
431 val: Index,
432) -> Bool {
433 unsafe {
434 if ipopt_problem.is_null() {
435 return FALSE;
436 }
437 let info = &mut *ipopt_problem;
438 let Some(k) = keyword_str(keyword) else {
439 return FALSE;
440 };
441 match info.app.options_mut().set_integer_value(
442 k,
443 val as pounce_common::types::Index,
444 true,
445 false,
446 ) {
447 Ok(_) => TRUE,
448 Err(_) => FALSE,
449 }
450 }
451}
452
453#[unsafe(no_mangle)]
467pub unsafe extern "C" fn OpenIpoptOutputFile(
468 ipopt_problem: IpoptProblem,
469 file_name: *const c_char,
470 print_level: c_int,
471) -> Bool {
472 unsafe {
473 if ipopt_problem.is_null() || file_name.is_null() {
474 return FALSE;
475 }
476 let info = &mut *ipopt_problem;
477 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
478 return FALSE;
479 };
480 if info.app.open_output_file(fname, print_level) {
481 TRUE
482 } else {
483 FALSE
484 }
485 }
486}
487
488#[unsafe(no_mangle)]
502pub unsafe extern "C" fn SetIpoptProblemScaling(
503 ipopt_problem: IpoptProblem,
504 obj_scaling: Number,
505 x_scaling: *const Number,
506 g_scaling: *const Number,
507) -> Bool {
508 unsafe {
509 if ipopt_problem.is_null() {
510 return FALSE;
511 }
512 let info = &mut *ipopt_problem;
513 let n = info.n as usize;
514 let m = info.m as usize;
515 let x_vec = if !x_scaling.is_null() && n > 0 {
516 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
517 } else {
518 None
519 };
520 let g_vec = if !g_scaling.is_null() && m > 0 {
521 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
522 } else {
523 None
524 };
525 info.user_scaling = Some(UserScaling {
526 obj_scaling,
527 x_scaling: x_vec,
528 g_scaling: g_vec,
529 });
530 TRUE
531 }
532}
533
534#[allow(clippy::too_many_arguments)]
548#[unsafe(no_mangle)]
549pub unsafe extern "C" fn IpoptSolve(
550 ipopt_problem: IpoptProblem,
551 x: *mut Number,
552 g: *mut Number,
553 obj_val: *mut Number,
554 mult_g: *mut Number,
555 mult_x_L: *mut Number,
556 mult_x_U: *mut Number,
557 user_data: *mut c_void,
558) -> Index {
559 unsafe {
560 if ipopt_problem.is_null() {
561 return ApplicationReturnStatus::InternalError as Index;
562 }
563 (*ipopt_problem).last_solve = None;
571 ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
577 let info = &mut *ipopt_problem;
578 if info.n < 0 || info.m < 0 {
579 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
580 }
581 if info.n > 0 && x.is_null() {
582 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
583 }
584
585 let n_us = info.n as usize;
586 let m_us = info.m as usize;
587 let initial_x = if n_us > 0 {
588 std::slice::from_raw_parts(x, n_us).to_vec()
589 } else {
590 Vec::new()
591 };
592
593 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
594 n: info.n,
595 m: info.m,
596 nele_jac: info.nele_jac,
597 nele_hess: info.nele_hess,
598 index_style: info.index_style,
599 x_l: info.x_l.clone(),
600 x_u: info.x_u.clone(),
601 g_l: info.g_l.clone(),
602 g_u: info.g_u.clone(),
603 initial_x,
604 eval_f: info.eval_f,
605 eval_grad_f: info.eval_grad_f,
606 eval_g: info.eval_g,
607 eval_jac_g: info.eval_jac_g,
608 eval_h: info.eval_h,
609 user_data,
610 intermediate_cb: info.intermediate_cb,
611 user_scaling: info.user_scaling.clone(),
612 final_status: None,
613 final_x: vec![0.0; n_us],
614 final_z_l: vec![0.0; n_us],
615 final_z_u: vec![0.0; n_us],
616 final_g: vec![0.0; m_us],
617 final_lambda: vec![0.0; m_us],
618 final_obj: 0.0,
619 }));
620
621 let feral_cfg = feral_config_from_options(info.app.options());
631 let bff_mint = move || -> InnerBackendFactoryFactory {
632 let feral_cfg = feral_cfg.clone();
633 Box::new(move || default_backend_factory(feral_cfg.clone()))
634 };
635 let resto_provider = make_default_restoration_factory_provider(
636 RestoAlgorithmBuilder::new(),
637 info.app.algorithm_builder_from_options(),
638 bff_mint,
639 );
640 info.app.set_restoration_factory_provider(resto_provider);
641
642 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
643 let status = info.app.optimize_tnlp(bridge_for_solve);
644 let bridge_ref = bridge.borrow();
645 info.last_solve = Some(LastSolve {
646 stats: info.app.statistics(),
647 status,
648 linear_solver: info.app.linear_solver_summary(),
649 final_x: bridge_ref.final_x.clone(),
650 final_lambda: bridge_ref.final_lambda.clone(),
651 final_obj: bridge_ref.final_obj,
652 });
653 if !x.is_null() && n_us > 0 {
654 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
655 }
656 if !g.is_null() && m_us > 0 {
657 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
658 }
659 if !obj_val.is_null() {
660 *obj_val = bridge_ref.final_obj;
661 }
662 if !mult_g.is_null() && m_us > 0 {
663 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
664 }
665 if !mult_x_L.is_null() && n_us > 0 {
666 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
667 }
668 if !mult_x_U.is_null() && n_us > 0 {
669 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
670 }
671 status as Index
672 })
673 }
674}
675
676#[unsafe(no_mangle)]
682pub unsafe extern "C" fn SetIntermediateCallback(
683 ipopt_problem: IpoptProblem,
684 intermediate_cb: Option<Intermediate_CB>,
685) -> Bool {
686 unsafe {
687 if ipopt_problem.is_null() {
688 return FALSE;
689 }
690 let info = &mut *ipopt_problem;
691 info.intermediate_cb = intermediate_cb;
692 TRUE
693 }
694}
695
696#[allow(clippy::too_many_arguments)]
719#[unsafe(no_mangle)]
720pub unsafe extern "C" fn GetIpoptCurrentIterate(
721 ipopt_problem: IpoptProblem,
722 _scaled: Bool,
723 n: Index,
724 x: *mut Number,
725 z_l: *mut Number,
726 z_u: *mut Number,
727 m: Index,
728 g: *mut Number,
729 lambda: *mut Number,
730) -> Bool {
731 unsafe {
732 if ipopt_problem.is_null() {
733 return FALSE;
734 }
735 let info = &*ipopt_problem;
736 if n != info.n || m != info.m {
737 return FALSE;
738 }
739 let result = ip_intermediate::with_current(|ctx| {
740 let data = ctx.data.borrow();
741 let Some(curr) = data.curr.as_ref() else {
742 return false;
743 };
744 let nlp = ctx.nlp.borrow();
745 let n_us = n as usize;
746 let m_us = m as usize;
747 if !x.is_null() && n_us > 0 {
748 let full_x = nlp.lift_x_to_full(&*curr.x);
749 if full_x.len() != n_us {
750 return false;
751 }
752 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
753 }
754 if !z_l.is_null() && n_us > 0 {
755 let full = nlp.pack_z_l_for_user(&*curr.z_l);
756 if full.len() != n_us {
757 return false;
758 }
759 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
760 }
761 if !z_u.is_null() && n_us > 0 {
762 let full = nlp.pack_z_u_for_user(&*curr.z_u);
763 if full.len() != n_us {
764 return false;
765 }
766 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
767 }
768 if !g.is_null() && m_us > 0 {
769 let cq = ctx.cq.borrow();
770 let full = nlp.pack_g_for_user(&*cq.curr_c(), &*cq.curr_d());
771 if full.len() != m_us {
772 return false;
773 }
774 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
775 }
776 if !lambda.is_null() && m_us > 0 {
777 let full = nlp.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
778 if full.len() != m_us {
779 return false;
780 }
781 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
782 }
783 true
784 });
785 if result.unwrap_or(false) { TRUE } else { FALSE }
786 }
787}
788
789#[allow(clippy::too_many_arguments)]
804#[unsafe(no_mangle)]
805pub unsafe extern "C" fn GetIpoptCurrentViolations(
806 ipopt_problem: IpoptProblem,
807 _scaled: Bool,
808 n: Index,
809 x_l_violation: *mut Number,
810 x_u_violation: *mut Number,
811 compl_x_l: *mut Number,
812 compl_x_u: *mut Number,
813 grad_lag_x: *mut Number,
814 m: Index,
815 nlp_constraint_violation: *mut Number,
816 compl_g: *mut Number,
817) -> Bool {
818 unsafe {
819 if ipopt_problem.is_null() {
820 return FALSE;
821 }
822 let info = &*ipopt_problem;
823 if n != info.n || m != info.m {
824 return FALSE;
825 }
826 let result = ip_intermediate::with_current(|ctx| {
827 let data = ctx.data.borrow();
828 let Some(_curr) = data.curr.as_ref() else {
829 return false;
830 };
831 drop(data);
832 let nlp = ctx.nlp.borrow();
833 let cq = ctx.cq.borrow();
834 let n_us = n as usize;
835 let m_us = m as usize;
836 if !x_l_violation.is_null() && n_us > 0 {
842 let slack = cq.curr_slack_x_l();
843 let z_l_full = nlp.pack_z_l_for_user(&*slack);
844 if z_l_full.len() != n_us {
849 return false;
850 }
851 let mut v = vec![0.0; n_us];
856 for (i, s) in z_l_full.iter().enumerate() {
857 v[i] = (-s).max(0.0);
858 }
859 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
860 }
861 if !x_u_violation.is_null() && n_us > 0 {
862 let slack = cq.curr_slack_x_u();
863 let s_full = nlp.pack_z_u_for_user(&*slack);
864 if s_full.len() != n_us {
865 return false;
866 }
867 let mut v = vec![0.0; n_us];
868 for (i, s) in s_full.iter().enumerate() {
869 v[i] = (-s).max(0.0);
870 }
871 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
872 }
873 if !compl_x_l.is_null() && n_us > 0 {
874 let v = nlp.pack_z_l_for_user(&*cq.curr_compl_x_l());
875 if v.len() != n_us {
876 return false;
877 }
878 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
879 }
880 if !compl_x_u.is_null() && n_us > 0 {
881 let v = nlp.pack_z_u_for_user(&*cq.curr_compl_x_u());
882 if v.len() != n_us {
883 return false;
884 }
885 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
886 }
887 if !grad_lag_x.is_null() && n_us > 0 {
888 let glx = cq.curr_grad_lag_x();
889 let full = nlp.lift_x_to_full(&*glx);
893 if full.len() != n_us {
894 return false;
895 }
896 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
897 }
898 if !nlp_constraint_violation.is_null() && m_us > 0 {
899 let zero = vec![0.0; m_us];
905 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
906 }
907 if !compl_g.is_null() && m_us > 0 {
908 let zero = vec![0.0; m_us];
911 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
912 }
913 true
914 });
915 if result.unwrap_or(false) { TRUE } else { FALSE }
916 }
917}
918
919#[unsafe(no_mangle)]
927pub unsafe extern "C" fn GetIpoptVersion(
928 major: *mut c_int,
929 minor: *mut c_int,
930 release: *mut c_int,
931) {
932 unsafe {
933 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
938 if !major.is_null() {
939 *major = mj;
940 }
941 if !minor.is_null() {
942 *minor = mn;
943 }
944 if !release.is_null() {
945 *release = pt;
946 }
947 }
948}
949
950fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
951 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
952 (
953 it.next().unwrap_or(0),
954 it.next().unwrap_or(0),
955 it.next().unwrap_or(0),
956 )
957}
958
959#[unsafe(no_mangle)]
976pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
977 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
978}
979
980#[unsafe(no_mangle)]
987pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
988 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
989}
990
991#[unsafe(no_mangle)]
998pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
999 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1000}
1001
1002#[unsafe(no_mangle)]
1009pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1010 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1011}
1012
1013#[unsafe(no_mangle)]
1019pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1020 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1021}
1022
1023unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1024where
1025 F: FnOnce(&SolveStatistics) -> T,
1026{
1027 unsafe {
1028 if ipopt_problem.is_null() {
1029 return None;
1030 }
1031 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1032 }
1033}
1034
1035fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1045 use pounce_qp::BoundStatus::*;
1046 match s {
1047 Inactive => POUNCE_WS_INACTIVE,
1048 AtLower => POUNCE_WS_AT_LOWER,
1049 AtUpper => POUNCE_WS_AT_UPPER,
1050 Fixed => POUNCE_WS_FIXED_OR_EQ,
1051 }
1052}
1053
1054fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1055 use pounce_qp::BoundStatus::*;
1056 match v {
1057 POUNCE_WS_INACTIVE => Some(Inactive),
1058 POUNCE_WS_AT_LOWER => Some(AtLower),
1059 POUNCE_WS_AT_UPPER => Some(AtUpper),
1060 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1061 _ => None,
1062 }
1063}
1064
1065fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1066 use pounce_qp::ConsStatus::*;
1067 match s {
1068 Inactive => POUNCE_WS_INACTIVE,
1069 AtLower => POUNCE_WS_AT_LOWER,
1070 AtUpper => POUNCE_WS_AT_UPPER,
1071 Equality => POUNCE_WS_FIXED_OR_EQ,
1072 }
1073}
1074
1075fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1076 use pounce_qp::ConsStatus::*;
1077 match v {
1078 POUNCE_WS_INACTIVE => Some(Inactive),
1079 POUNCE_WS_AT_LOWER => Some(AtLower),
1080 POUNCE_WS_AT_UPPER => Some(AtUpper),
1081 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1082 _ => None,
1083 }
1084}
1085
1086#[unsafe(no_mangle)]
1102pub unsafe extern "C" fn IpoptGetWorkingSet(
1103 ipopt_problem: IpoptProblem,
1104 bound_status_out: *mut IpoptBoundStatus,
1105 cons_status_out: *mut IpoptConsStatus,
1106) -> Bool {
1107 unsafe {
1108 if ipopt_problem.is_null() {
1109 return FALSE;
1110 }
1111 let info = &*ipopt_problem;
1112 let ws = match info.app.last_sqp_working_set() {
1113 Some(w) => w,
1114 None => return FALSE,
1115 };
1116 if !bound_status_out.is_null() {
1117 for (i, &s) in ws.bounds.iter().enumerate() {
1118 *bound_status_out.add(i) = bound_status_to_int(s);
1119 }
1120 }
1121 if !cons_status_out.is_null() {
1122 for (i, &s) in ws.constraints.iter().enumerate() {
1123 *cons_status_out.add(i) = cons_status_to_int(s);
1124 }
1125 }
1126 TRUE
1127 }
1128}
1129
1130#[unsafe(no_mangle)]
1146pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1147 ipopt_problem: IpoptProblem,
1148 bound_status_in: *const IpoptBoundStatus,
1149 cons_status_in: *const IpoptConsStatus,
1150) -> Bool {
1151 unsafe {
1152 if ipopt_problem.is_null() {
1153 return FALSE;
1154 }
1155 if bound_status_in.is_null() && cons_status_in.is_null() {
1156 return FALSE;
1157 }
1158 let info = &mut *ipopt_problem;
1159 let n = info.n.max(0) as usize;
1160 let m = info.m.max(0) as usize;
1161 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1162 if !bound_status_in.is_null() {
1163 for i in 0..n {
1164 let v = *bound_status_in.add(i);
1165 match int_to_bound_status(v) {
1166 Some(s) => bounds[i] = s,
1167 None => return FALSE,
1168 }
1169 }
1170 }
1171 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1172 if !cons_status_in.is_null() {
1173 for i in 0..m {
1174 let v = *cons_status_in.add(i);
1175 match int_to_cons_status(v) {
1176 Some(s) => constraints[i] = s,
1177 None => return FALSE,
1178 }
1179 }
1180 }
1181 info.app
1189 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
1190 x: vec![0.0; n],
1191 lambda_g: vec![0.0; m],
1192 lambda_x: vec![0.0; n],
1193 working: Some(pounce_qp::WorkingSet {
1194 bounds,
1195 constraints,
1196 }),
1197 });
1198 TRUE
1199 }
1200}
1201
1202#[unsafe(no_mangle)]
1209pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1210 unsafe {
1211 if ipopt_problem.is_null() {
1212 return FALSE;
1213 }
1214 (*ipopt_problem).app.clear_sqp_warm_start();
1215 TRUE
1216 }
1217}
1218
1219#[allow(clippy::too_many_arguments)]
1235#[unsafe(no_mangle)]
1236pub unsafe extern "C" fn IpoptSolveWarmStart(
1237 ipopt_problem: IpoptProblem,
1238 x: *mut Number,
1239 g: *mut Number,
1240 obj_val: *mut Number,
1241 mult_g: *mut Number,
1242 mult_x_L: *mut Number,
1243 mult_x_U: *mut Number,
1244 bound_status_in: *const IpoptBoundStatus,
1245 cons_status_in: *const IpoptConsStatus,
1246 bound_status_out: *mut IpoptBoundStatus,
1247 cons_status_out: *mut IpoptConsStatus,
1248 user_data: *mut c_void,
1249) -> Index {
1250 if ipopt_problem.is_null() {
1251 return ApplicationReturnStatus::InternalError as Index;
1252 }
1253 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1257 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1262 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1263 }
1264 let status = IpoptSolve(
1265 ipopt_problem,
1266 x,
1267 g,
1268 obj_val,
1269 mult_g,
1270 mult_x_L,
1271 mult_x_U,
1272 user_data,
1273 );
1274 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1275 status
1276 })
1277}
1278
1279pub(crate) struct CCallbackTnlp {
1290 pub(crate) n: Index,
1291 pub(crate) m: Index,
1292 pub(crate) nele_jac: Index,
1293 pub(crate) nele_hess: Index,
1294 pub(crate) index_style: Index,
1295 pub(crate) x_l: Vec<Number>,
1296 pub(crate) x_u: Vec<Number>,
1297 pub(crate) g_l: Vec<Number>,
1298 pub(crate) g_u: Vec<Number>,
1299 pub(crate) initial_x: Vec<Number>,
1300 pub(crate) eval_f: Option<Eval_F_CB>,
1301 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1302 pub(crate) eval_g: Option<Eval_G_CB>,
1303 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1304 pub(crate) eval_h: Option<Eval_H_CB>,
1305 pub(crate) user_data: *mut c_void,
1306 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1309 pub(crate) user_scaling: Option<UserScaling>,
1311 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1312 pub(crate) final_x: Vec<Number>,
1313 pub(crate) final_z_l: Vec<Number>,
1314 pub(crate) final_z_u: Vec<Number>,
1315 pub(crate) final_g: Vec<Number>,
1316 pub(crate) final_lambda: Vec<Number>,
1317 pub(crate) final_obj: Number,
1318}
1319
1320impl TNLP for CCallbackTnlp {
1321 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1322 Some(NlpInfo {
1323 n: self.n as pounce_common::types::Index,
1324 m: self.m as pounce_common::types::Index,
1325 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1326 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1327 index_style: if self.index_style == 1 {
1328 IndexStyle::Fortran
1329 } else {
1330 IndexStyle::C
1331 },
1332 })
1333 }
1334
1335 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1336 if !self.x_l.is_empty() {
1337 b.x_l.copy_from_slice(&self.x_l);
1338 }
1339 if !self.x_u.is_empty() {
1340 b.x_u.copy_from_slice(&self.x_u);
1341 }
1342 if !self.g_l.is_empty() {
1343 b.g_l.copy_from_slice(&self.g_l);
1344 }
1345 if !self.g_u.is_empty() {
1346 b.g_u.copy_from_slice(&self.g_u);
1347 }
1348 true
1349 }
1350
1351 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1352 if !self.initial_x.is_empty() {
1353 sp.x.copy_from_slice(&self.initial_x);
1354 }
1355 true
1356 }
1357
1358 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1359 let Some(s) = self.user_scaling.as_ref() else {
1360 return false;
1361 };
1362 *req.obj_scaling = s.obj_scaling;
1363 if let Some(x) = s.x_scaling.as_ref() {
1364 if x.len() == req.x_scaling.len() {
1365 req.x_scaling.copy_from_slice(x);
1366 *req.use_x_scaling = true;
1367 }
1368 } else {
1369 *req.use_x_scaling = false;
1370 }
1371 if let Some(g) = s.g_scaling.as_ref() {
1372 if g.len() == req.g_scaling.len() {
1373 req.g_scaling.copy_from_slice(g);
1374 *req.use_g_scaling = true;
1375 }
1376 } else {
1377 *req.use_g_scaling = false;
1378 }
1379 true
1380 }
1381
1382 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
1383 let cb = self.eval_f?;
1384 let mut obj = 0.0;
1385 let ok = unsafe {
1386 cb(
1387 self.n,
1388 x.as_ptr() as *mut Number,
1389 if new_x { TRUE } else { FALSE },
1390 &mut obj,
1391 self.user_data,
1392 )
1393 };
1394 if ok != FALSE { Some(obj) } else { None }
1395 }
1396
1397 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
1398 let Some(cb) = self.eval_grad_f else {
1399 return false;
1400 };
1401 let ok = unsafe {
1402 cb(
1403 self.n,
1404 x.as_ptr() as *mut Number,
1405 if new_x { TRUE } else { FALSE },
1406 grad_f.as_mut_ptr(),
1407 self.user_data,
1408 )
1409 };
1410 ok != FALSE
1411 }
1412
1413 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
1414 if self.m == 0 {
1415 return true;
1416 }
1417 let Some(cb) = self.eval_g else {
1418 return false;
1419 };
1420 let ok = unsafe {
1421 cb(
1422 self.n,
1423 x.as_ptr() as *mut Number,
1424 if new_x { TRUE } else { FALSE },
1425 self.m,
1426 g.as_mut_ptr(),
1427 self.user_data,
1428 )
1429 };
1430 ok != FALSE
1431 }
1432
1433 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
1434 if self.m == 0 || self.nele_jac == 0 {
1435 return true;
1436 }
1437 let Some(cb) = self.eval_jac_g else {
1438 return false;
1439 };
1440 let x_ptr = x
1441 .map(|s| s.as_ptr() as *mut Number)
1442 .unwrap_or(std::ptr::null_mut());
1443 let ok = match mode {
1444 SparsityRequest::Structure { irow, jcol } => unsafe {
1445 cb(
1446 self.n,
1447 x_ptr,
1448 if new_x { TRUE } else { FALSE },
1449 self.m,
1450 self.nele_jac,
1451 irow.as_mut_ptr(),
1452 jcol.as_mut_ptr(),
1453 std::ptr::null_mut(),
1454 self.user_data,
1455 )
1456 },
1457 SparsityRequest::Values { values } => unsafe {
1458 cb(
1459 self.n,
1460 x_ptr,
1461 if new_x { TRUE } else { FALSE },
1462 self.m,
1463 self.nele_jac,
1464 std::ptr::null_mut(),
1465 std::ptr::null_mut(),
1466 values.as_mut_ptr(),
1467 self.user_data,
1468 )
1469 },
1470 };
1471 ok != FALSE
1472 }
1473
1474 fn eval_h(
1475 &mut self,
1476 x: Option<&[Number]>,
1477 new_x: bool,
1478 obj_factor: Number,
1479 lambda: Option<&[Number]>,
1480 new_lambda: bool,
1481 mode: SparsityRequest<'_>,
1482 ) -> bool {
1483 let Some(cb) = self.eval_h else {
1484 return false;
1485 };
1486 if self.nele_hess == 0 {
1487 return true;
1488 }
1489 let x_ptr = x
1490 .map(|s| s.as_ptr() as *mut Number)
1491 .unwrap_or(std::ptr::null_mut());
1492 let lambda_ptr = lambda
1493 .map(|s| s.as_ptr() as *mut Number)
1494 .unwrap_or(std::ptr::null_mut());
1495 let ok = match mode {
1496 SparsityRequest::Structure { irow, jcol } => unsafe {
1497 cb(
1498 self.n,
1499 x_ptr,
1500 if new_x { TRUE } else { FALSE },
1501 obj_factor,
1502 self.m,
1503 lambda_ptr,
1504 if new_lambda { TRUE } else { FALSE },
1505 self.nele_hess,
1506 irow.as_mut_ptr(),
1507 jcol.as_mut_ptr(),
1508 std::ptr::null_mut(),
1509 self.user_data,
1510 )
1511 },
1512 SparsityRequest::Values { values } => unsafe {
1513 cb(
1514 self.n,
1515 x_ptr,
1516 if new_x { TRUE } else { FALSE },
1517 obj_factor,
1518 self.m,
1519 lambda_ptr,
1520 if new_lambda { TRUE } else { FALSE },
1521 self.nele_hess,
1522 std::ptr::null_mut(),
1523 std::ptr::null_mut(),
1524 values.as_mut_ptr(),
1525 self.user_data,
1526 )
1527 },
1528 };
1529 ok != FALSE
1530 }
1531
1532 fn intermediate_callback(
1533 &mut self,
1534 stats: pounce_nlp::tnlp::IterStats,
1535 _ip_data: &IpoptData,
1536 _ip_cq: &IpoptCq,
1537 ) -> bool {
1538 let Some(cb) = self.intermediate_cb else {
1539 return true;
1540 };
1541 let ok = unsafe {
1542 cb(
1543 stats.mode as Index,
1544 stats.iter as Index,
1545 stats.obj_value,
1546 stats.inf_pr,
1547 stats.inf_du,
1548 stats.mu,
1549 stats.d_norm,
1550 stats.regularization_size,
1551 stats.alpha_du,
1552 stats.alpha_pr,
1553 stats.ls_trials as Index,
1554 self.user_data,
1555 )
1556 };
1557 ok != FALSE
1558 }
1559
1560 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1561 self.final_status = Some(sol.status);
1562 if !sol.x.is_empty() {
1563 self.final_x.copy_from_slice(sol.x);
1564 }
1565 if !sol.z_l.is_empty() {
1566 self.final_z_l.copy_from_slice(sol.z_l);
1567 }
1568 if !sol.z_u.is_empty() {
1569 self.final_z_u.copy_from_slice(sol.z_u);
1570 }
1571 if !sol.g.is_empty() {
1572 self.final_g.copy_from_slice(sol.g);
1573 }
1574 if !sol.lambda.is_empty() {
1575 self.final_lambda.copy_from_slice(sol.lambda);
1576 }
1577 self.final_obj = sol.obj_value;
1578 }
1579}
1580
1581#[unsafe(no_mangle)]
1594pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
1595 if ipopt_problem.is_null() {
1596 return FALSE;
1597 }
1598 let info = unsafe { &mut *ipopt_problem };
1599 info.app.enable_iter_history();
1600 TRUE
1601}
1602
1603#[unsafe(no_mangle)]
1624pub unsafe extern "C" fn IpoptWriteSolveReport(
1625 ipopt_problem: IpoptProblem,
1626 path: *const c_char,
1627 detail: *const c_char,
1628) -> Bool {
1629 use pounce_solve_report::{
1630 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
1631 };
1632
1633 ffi_guard(FALSE, || unsafe {
1638 if ipopt_problem.is_null() || path.is_null() {
1639 return FALSE;
1640 }
1641 let info = &*ipopt_problem;
1642 let Some(last) = info.last_solve.as_ref() else {
1643 return FALSE;
1644 };
1645
1646 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
1647 return FALSE;
1648 };
1649
1650 let detail_choice = if detail.is_null() {
1651 ReportDetail::Summary
1652 } else {
1653 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
1654 return FALSE;
1655 };
1656 match ReportDetail::parse(detail_str) {
1657 Ok(d) => d,
1658 Err(_) => return FALSE,
1659 }
1660 };
1661
1662 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
1663 builder.problem.n_variables = info.n;
1664 builder.problem.n_constraints = info.m;
1665 builder.problem.n_objectives = 1;
1666 builder.problem.nnz_jac_g = Some(info.nele_jac);
1667 builder.problem.nnz_h_lag = Some(info.nele_hess);
1668
1669 builder.solution.status = last.status;
1670 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
1671 builder.solution.objective = last.final_obj;
1672 builder.solution.x = last.final_x.clone();
1673 builder.solution.lambda = last.final_lambda.clone();
1674
1675 builder.ingest_stats(&last.stats);
1676 if let Some(linsol) = last.linear_solver.clone() {
1677 builder.set_linear_solver_summary(linsol);
1678 }
1679
1680 let report = builder.finish();
1681 match write_report_file(std::path::Path::new(path_str), &report) {
1682 Ok(_) => TRUE,
1683 Err(_) => FALSE,
1684 }
1685 })
1686}
1687
1688#[cfg(test)]
1689mod tests {
1690 use super::*;
1691 use std::ffi::CString;
1692
1693 unsafe extern "C" fn dummy_eval_f(
1694 _n: Index,
1695 _x: *const Number,
1696 _new_x: Bool,
1697 _obj_value: *mut Number,
1698 _user_data: *mut c_void,
1699 ) -> Bool {
1700 TRUE
1701 }
1702 unsafe extern "C" fn dummy_eval_grad_f(
1703 _n: Index,
1704 _x: *const Number,
1705 _new_x: Bool,
1706 _grad_f: *mut Number,
1707 _user_data: *mut c_void,
1708 ) -> Bool {
1709 TRUE
1710 }
1711
1712 fn create_unconstrained() -> IpoptProblem {
1713 let xl = [-1.0; 4];
1714 let xu = [1.0; 4];
1715 unsafe {
1716 CreateIpoptProblem(
1717 4,
1718 xl.as_ptr(),
1719 xu.as_ptr(),
1720 0,
1721 std::ptr::null(),
1722 std::ptr::null(),
1723 0,
1724 10,
1725 0,
1726 Some(dummy_eval_f),
1727 None,
1728 Some(dummy_eval_grad_f),
1729 None,
1730 None,
1731 )
1732 }
1733 }
1734
1735 #[test]
1736 fn create_succeeds_for_unconstrained_problem() {
1737 let p = create_unconstrained();
1738 assert!(!p.is_null());
1739 unsafe { FreeIpoptProblem(p) };
1740 }
1741
1742 #[test]
1743 fn create_returns_null_on_missing_required_callbacks() {
1744 let xl = [-1.0; 4];
1745 let xu = [1.0; 4];
1746 let p = unsafe {
1747 CreateIpoptProblem(
1748 4,
1749 xl.as_ptr(),
1750 xu.as_ptr(),
1751 0,
1752 std::ptr::null(),
1753 std::ptr::null(),
1754 0,
1755 10,
1756 0,
1757 None, None,
1759 Some(dummy_eval_grad_f),
1760 None,
1761 None,
1762 )
1763 };
1764 assert!(p.is_null());
1765 }
1766
1767 #[test]
1768 fn create_returns_null_on_negative_n() {
1769 let p = unsafe {
1770 CreateIpoptProblem(
1771 -1,
1772 std::ptr::null(),
1773 std::ptr::null(),
1774 0,
1775 std::ptr::null(),
1776 std::ptr::null(),
1777 0,
1778 10,
1779 0,
1780 Some(dummy_eval_f),
1781 None,
1782 Some(dummy_eval_grad_f),
1783 None,
1784 None,
1785 )
1786 };
1787 assert!(p.is_null());
1788 }
1789
1790 #[test]
1791 fn create_returns_null_on_invalid_index_style() {
1792 let xl = [0.0; 1];
1793 let xu = [1.0; 1];
1794 let p = unsafe {
1795 CreateIpoptProblem(
1796 1,
1797 xl.as_ptr(),
1798 xu.as_ptr(),
1799 0,
1800 std::ptr::null(),
1801 std::ptr::null(),
1802 0,
1803 1,
1804 2, Some(dummy_eval_f),
1806 None,
1807 Some(dummy_eval_grad_f),
1808 None,
1809 None,
1810 )
1811 };
1812 assert!(p.is_null());
1813 }
1814
1815 #[test]
1816 fn add_int_option_forwards_to_application() {
1817 let p = create_unconstrained();
1818 let key = CString::new("print_level").unwrap();
1819 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
1820 assert_eq!(ok, TRUE);
1821 let info = unsafe { &*p };
1822 let (level, found) = info
1823 .app
1824 .options()
1825 .get_integer_value("print_level", "")
1826 .unwrap();
1827 assert!(found);
1828 assert_eq!(level, 5);
1829 unsafe { FreeIpoptProblem(p) };
1830 }
1831
1832 #[test]
1833 fn add_str_option_with_invalid_key_returns_false() {
1834 let p = create_unconstrained();
1835 let key = CString::new("totally_unknown_option").unwrap();
1836 let val = CString::new("yes").unwrap();
1837 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
1838 assert_eq!(ok, FALSE);
1839 unsafe { FreeIpoptProblem(p) };
1840 }
1841
1842 #[test]
1843 fn add_options_on_null_problem_returns_false() {
1844 let key = CString::new("print_level").unwrap();
1845 let v = CString::new("yes").unwrap();
1846 unsafe {
1847 assert_eq!(
1848 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
1849 FALSE
1850 );
1851 assert_eq!(
1852 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
1853 FALSE
1854 );
1855 assert_eq!(
1856 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
1857 FALSE
1858 );
1859 }
1860 }
1861
1862 unsafe extern "C" fn dummy_intermediate(
1863 _alg_mod: Index,
1864 _iter_count: Index,
1865 _obj_value: Number,
1866 _inf_pr: Number,
1867 _inf_du: Number,
1868 _mu: Number,
1869 _d_norm: Number,
1870 _regularization_size: Number,
1871 _alpha_du: Number,
1872 _alpha_pr: Number,
1873 _ls_trials: Index,
1874 _user_data: *mut c_void,
1875 ) -> Bool {
1876 TRUE
1877 }
1878
1879 #[test]
1880 fn set_intermediate_callback_stores_pointer() {
1881 let p = create_unconstrained();
1882 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
1883 assert_eq!(ok, TRUE);
1884 let info = unsafe { &*p };
1885 assert!(info.intermediate_cb.is_some());
1886 unsafe { FreeIpoptProblem(p) };
1887 }
1888
1889 #[test]
1890 fn solve_returns_internal_error_on_null_problem() {
1891 let rc = unsafe {
1892 IpoptSolve(
1893 std::ptr::null_mut(),
1894 std::ptr::null_mut(),
1895 std::ptr::null_mut(),
1896 std::ptr::null_mut(),
1897 std::ptr::null_mut(),
1898 std::ptr::null_mut(),
1899 std::ptr::null_mut(),
1900 std::ptr::null_mut(),
1901 )
1902 };
1903 assert_eq!(rc, -199);
1904 }
1905
1906 #[test]
1907 fn free_null_is_safe() {
1908 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
1909 }
1910
1911 unsafe extern "C" fn quad_eval_f(
1917 _n: Index,
1918 x: *const Number,
1919 _new_x: Bool,
1920 obj_value: *mut Number,
1921 _user_data: *mut c_void,
1922 ) -> Bool {
1923 unsafe {
1924 let v = *x.offset(0);
1925 *obj_value = (v - 2.0) * (v - 2.0);
1926 TRUE
1927 }
1928 }
1929 unsafe extern "C" fn quad_eval_grad_f(
1930 _n: Index,
1931 x: *const Number,
1932 _new_x: Bool,
1933 grad: *mut Number,
1934 _user_data: *mut c_void,
1935 ) -> Bool {
1936 unsafe {
1937 let v = *x.offset(0);
1938 *grad.offset(0) = 2.0 * (v - 2.0);
1939 TRUE
1940 }
1941 }
1942 unsafe extern "C" fn quad_eval_h(
1943 _n: Index,
1944 _x: *const Number,
1945 _new_x: Bool,
1946 obj_factor: Number,
1947 _m: Index,
1948 _lambda: *const Number,
1949 _new_lambda: Bool,
1950 _nele_hess: Index,
1951 irow: *mut Index,
1952 jcol: *mut Index,
1953 values: *mut Number,
1954 _user_data: *mut c_void,
1955 ) -> Bool {
1956 unsafe {
1957 if !irow.is_null() && !jcol.is_null() && values.is_null() {
1958 *irow.offset(0) = 0;
1959 *jcol.offset(0) = 0;
1960 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
1961 *values.offset(0) = 2.0 * obj_factor;
1962 } else {
1963 return FALSE;
1964 }
1965 TRUE
1966 }
1967 }
1968
1969 #[test]
1970 fn solve_drives_unconstrained_quadratic_through_bridge() {
1971 let xl = [-1.0e20];
1974 let xu = [1.0e20];
1975 let p = unsafe {
1976 CreateIpoptProblem(
1977 1,
1978 xl.as_ptr(),
1979 xu.as_ptr(),
1980 0,
1981 std::ptr::null(),
1982 std::ptr::null(),
1983 0,
1984 1,
1985 0,
1986 Some(quad_eval_f),
1987 None,
1988 Some(quad_eval_grad_f),
1989 None,
1990 Some(quad_eval_h),
1991 )
1992 };
1993 assert!(!p.is_null());
1994 let mut x = [0.0_f64];
1995 let mut obj = 0.0_f64;
1996 let rc = unsafe {
1997 IpoptSolve(
1998 p,
1999 x.as_mut_ptr(),
2000 std::ptr::null_mut(),
2001 &mut obj,
2002 std::ptr::null_mut(),
2003 std::ptr::null_mut(),
2004 std::ptr::null_mut(),
2005 std::ptr::null_mut(),
2006 )
2007 };
2008 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2009 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
2010 assert!(obj.abs() < 1e-10, "obj = {}", obj);
2011 unsafe { FreeIpoptProblem(p) };
2012 }
2013
2014 #[test]
2029 fn stale_stats_cleared_when_resolve_bails() {
2030 let xl = [-1.0e20];
2031 let xu = [1.0e20];
2032 let p = unsafe {
2033 CreateIpoptProblem(
2034 1,
2035 xl.as_ptr(),
2036 xu.as_ptr(),
2037 0,
2038 std::ptr::null(),
2039 std::ptr::null(),
2040 0,
2041 1,
2042 0,
2043 Some(quad_eval_f),
2044 None,
2045 Some(quad_eval_grad_f),
2046 None,
2047 Some(quad_eval_h),
2048 )
2049 };
2050 assert!(!p.is_null());
2051
2052 let mut x = [0.0_f64];
2053 let mut obj = 0.0_f64;
2054 let rc = unsafe {
2055 IpoptSolve(
2056 p,
2057 x.as_mut_ptr(),
2058 std::ptr::null_mut(),
2059 &mut obj,
2060 std::ptr::null_mut(),
2061 std::ptr::null_mut(),
2062 std::ptr::null_mut(),
2063 std::ptr::null_mut(),
2064 )
2065 };
2066 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2067 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2069 assert!(
2070 iters_after_success >= 1,
2071 "a converged solve should record >=1 iteration, got {iters_after_success}"
2072 );
2073 assert!(unsafe { (*p).last_solve.is_some() });
2074
2075 unsafe { (*p).n = -1 };
2078 let mut x2 = [0.0_f64];
2079 let rc2 = unsafe {
2080 IpoptSolve(
2081 p,
2082 x2.as_mut_ptr(),
2083 std::ptr::null_mut(),
2084 std::ptr::null_mut(),
2085 std::ptr::null_mut(),
2086 std::ptr::null_mut(),
2087 std::ptr::null_mut(),
2088 std::ptr::null_mut(),
2089 )
2090 };
2091 assert_eq!(
2092 rc2,
2093 ApplicationReturnStatus::InvalidProblemDefinition as Index
2094 );
2095
2096 assert!(
2100 unsafe { (*p).last_solve.is_none() },
2101 "a bailed re-solve must clear stale last_solve (F5)"
2102 );
2103 assert_eq!(
2104 unsafe { GetIpoptIterCount(p) },
2105 0,
2106 "stale iteration count must not survive a bailed re-solve (F5)"
2107 );
2108
2109 unsafe { FreeIpoptProblem(p) };
2110 }
2111
2112 #[test]
2113 fn solve_invalid_problem_definition_when_x_null() {
2114 let p = create_unconstrained();
2115 let rc = unsafe {
2116 IpoptSolve(
2117 p,
2118 std::ptr::null_mut(), std::ptr::null_mut(),
2120 std::ptr::null_mut(),
2121 std::ptr::null_mut(),
2122 std::ptr::null_mut(),
2123 std::ptr::null_mut(),
2124 std::ptr::null_mut(),
2125 )
2126 };
2127 assert_eq!(
2128 rc,
2129 ApplicationReturnStatus::InvalidProblemDefinition as Index
2130 );
2131 unsafe { FreeIpoptProblem(p) };
2132 }
2133
2134 #[test]
2137 fn get_version_writes_pkg_version() {
2138 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
2139 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
2140 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
2141 assert_eq!((mj, mn, pt), expected);
2142 }
2143
2144 #[test]
2145 fn get_version_tolerates_null_buffers() {
2146 unsafe {
2148 GetIpoptVersion(
2149 std::ptr::null_mut(),
2150 std::ptr::null_mut(),
2151 std::ptr::null_mut(),
2152 )
2153 };
2154 }
2155
2156 #[test]
2157 fn set_scaling_stores_user_supplied_arrays() {
2158 let p = create_unconstrained();
2159 let xs = [2.0, 3.0, 4.0, 5.0];
2160 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
2161 assert_eq!(ok, TRUE);
2162 let info = unsafe { &*p };
2163 let s = info.user_scaling.as_ref().unwrap();
2164 assert_eq!(s.obj_scaling, 7.0);
2165 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
2166 assert!(s.g_scaling.is_none());
2167 unsafe { FreeIpoptProblem(p) };
2168 }
2169
2170 #[test]
2171 fn set_scaling_on_null_problem_returns_false() {
2172 let ok = unsafe {
2173 SetIpoptProblemScaling(
2174 std::ptr::null_mut(),
2175 1.0,
2176 std::ptr::null(),
2177 std::ptr::null(),
2178 )
2179 };
2180 assert_eq!(ok, FALSE);
2181 }
2182
2183 #[test]
2184 fn open_output_file_writes_and_attaches_journal() {
2185 let p = create_unconstrained();
2186 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2187 let _ = std::fs::create_dir_all(&dir);
2188 let path = dir.join("output.log");
2189 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2190 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2191 assert_eq!(ok, TRUE);
2192 let info = unsafe { &*p };
2194 let (level, found) = info
2195 .app
2196 .options()
2197 .get_integer_value("file_print_level", "")
2198 .unwrap();
2199 assert!(found);
2200 assert_eq!(level, 5);
2201 unsafe { FreeIpoptProblem(p) };
2202 let _ = std::fs::remove_file(&path);
2203 }
2204
2205 #[test]
2206 fn open_output_file_with_null_inputs_returns_false() {
2207 let key = CString::new("nope").unwrap();
2208 unsafe {
2209 assert_eq!(
2210 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2211 FALSE
2212 );
2213 }
2214 let p = create_unconstrained();
2215 unsafe {
2216 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2217 FreeIpoptProblem(p);
2218 }
2219 }
2220
2221 #[test]
2222 fn get_current_iterate_returns_false_outside_callback() {
2223 let p = create_unconstrained();
2224 let rc = unsafe {
2225 GetIpoptCurrentIterate(
2226 p,
2227 FALSE,
2228 0,
2229 std::ptr::null_mut(),
2230 std::ptr::null_mut(),
2231 std::ptr::null_mut(),
2232 0,
2233 std::ptr::null_mut(),
2234 std::ptr::null_mut(),
2235 )
2236 };
2237 assert_eq!(rc, FALSE);
2238 unsafe { FreeIpoptProblem(p) };
2239 }
2240
2241 #[test]
2242 fn get_current_violations_returns_false_outside_callback() {
2243 let p = create_unconstrained();
2244 let rc = unsafe {
2245 GetIpoptCurrentViolations(
2246 p,
2247 FALSE,
2248 0,
2249 std::ptr::null_mut(),
2250 std::ptr::null_mut(),
2251 std::ptr::null_mut(),
2252 std::ptr::null_mut(),
2253 std::ptr::null_mut(),
2254 0,
2255 std::ptr::null_mut(),
2256 std::ptr::null_mut(),
2257 )
2258 };
2259 assert_eq!(rc, FALSE);
2260 unsafe { FreeIpoptProblem(p) };
2261 }
2262
2263 #[test]
2264 fn post_solve_stats_zero_before_solve() {
2265 let p = create_unconstrained();
2266 unsafe {
2267 assert_eq!(GetIpoptIterCount(p), 0);
2268 assert_eq!(GetIpoptSolveTime(p), 0.0);
2269 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2270 assert_eq!(GetIpoptDualInf(p), 0.0);
2271 assert_eq!(GetIpoptComplInf(p), 0.0);
2272 FreeIpoptProblem(p);
2273 }
2274 }
2275
2276 #[test]
2277 fn post_solve_stats_populated_after_solve() {
2278 let xl = [-1.0e20];
2280 let xu = [1.0e20];
2281 let p = unsafe {
2282 CreateIpoptProblem(
2283 1,
2284 xl.as_ptr(),
2285 xu.as_ptr(),
2286 0,
2287 std::ptr::null(),
2288 std::ptr::null(),
2289 0,
2290 1,
2291 0,
2292 Some(quad_eval_f),
2293 None,
2294 Some(quad_eval_grad_f),
2295 None,
2296 Some(quad_eval_h),
2297 )
2298 };
2299 let mut x = [0.0_f64];
2300 let mut obj = 0.0_f64;
2301 let rc = unsafe {
2302 IpoptSolve(
2303 p,
2304 x.as_mut_ptr(),
2305 std::ptr::null_mut(),
2306 &mut obj,
2307 std::ptr::null_mut(),
2308 std::ptr::null_mut(),
2309 std::ptr::null_mut(),
2310 std::ptr::null_mut(),
2311 )
2312 };
2313 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2314 unsafe {
2317 assert!(GetIpoptIterCount(p) >= 0);
2318 assert!(GetIpoptSolveTime(p) >= 0.0);
2319 assert!(GetIpoptPrimalInf(p).is_finite());
2320 assert!(GetIpoptDualInf(p).is_finite());
2321 assert!(GetIpoptComplInf(p).is_finite());
2322 FreeIpoptProblem(p);
2323 }
2324 }
2325
2326 #[test]
2327 fn write_solve_report_emits_v1_json_with_iter_history() {
2328 let xl = [-1.0e20];
2331 let xu = [1.0e20];
2332 let p = unsafe {
2333 CreateIpoptProblem(
2334 1,
2335 xl.as_ptr(),
2336 xu.as_ptr(),
2337 0,
2338 std::ptr::null(),
2339 std::ptr::null(),
2340 0,
2341 1,
2342 0,
2343 Some(quad_eval_f),
2344 None,
2345 Some(quad_eval_grad_f),
2346 None,
2347 Some(quad_eval_h),
2348 )
2349 };
2350
2351 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
2353 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
2354 assert_eq!(bad, FALSE);
2355
2356 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
2358 let mut x = [0.0_f64];
2359 let mut obj = 0.0_f64;
2360 let rc = unsafe {
2361 IpoptSolve(
2362 p,
2363 x.as_mut_ptr(),
2364 std::ptr::null_mut(),
2365 &mut obj,
2366 std::ptr::null_mut(),
2367 std::ptr::null_mut(),
2368 std::ptr::null_mut(),
2369 std::ptr::null_mut(),
2370 )
2371 };
2372 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2373
2374 let dir = std::env::temp_dir();
2375 let path = dir.join("pounce_cinterface_report.json");
2376 let cpath = CString::new(path.to_str().unwrap()).unwrap();
2377 let cdetail = CString::new("full").unwrap();
2378 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
2379 assert_eq!(ok, TRUE);
2380
2381 let txt = std::fs::read_to_string(&path).unwrap();
2384 assert!(
2385 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
2386 "{txt}"
2387 );
2388 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
2389 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
2390 assert_eq!(parsed.problem.n_variables, 1);
2391 assert_eq!(parsed.problem.n_constraints, 0);
2392
2393 let bad_detail = CString::new("verbose").unwrap();
2395 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
2396 assert_eq!(bad, FALSE);
2397
2398 let _ = std::fs::remove_file(&path);
2399 unsafe { FreeIpoptProblem(p) };
2400 }
2401
2402 unsafe extern "C" fn cb_quad_eval_g(
2409 _n: Index,
2410 x: *const Number,
2411 _new_x: Bool,
2412 _m: Index,
2413 g: *mut Number,
2414 _user_data: *mut c_void,
2415 ) -> Bool {
2416 unsafe {
2417 *g.offset(0) = *x.offset(0);
2418 TRUE
2419 }
2420 }
2421 unsafe extern "C" fn cb_quad_eval_jac_g(
2422 _n: Index,
2423 _x: *const Number,
2424 _new_x: Bool,
2425 _m: Index,
2426 nele_jac: Index,
2427 irow: *mut Index,
2428 jcol: *mut Index,
2429 values: *mut Number,
2430 _user_data: *mut c_void,
2431 ) -> Bool {
2432 unsafe {
2433 assert_eq!(nele_jac, 1);
2434 if !irow.is_null() {
2435 *irow.offset(0) = 0;
2436 *jcol.offset(0) = 0;
2437 }
2438 if !values.is_null() {
2439 *values.offset(0) = 1.0;
2440 }
2441 TRUE
2442 }
2443 }
2444 unsafe extern "C" fn cb_quad_eval_h(
2445 _n: Index,
2446 _x: *const Number,
2447 _new_x: Bool,
2448 obj_factor: Number,
2449 _m: Index,
2450 _lambda: *const Number,
2451 _new_lambda: Bool,
2452 _nele_hess: Index,
2453 irow: *mut Index,
2454 jcol: *mut Index,
2455 values: *mut Number,
2456 _user_data: *mut c_void,
2457 ) -> Bool {
2458 unsafe {
2459 if !irow.is_null() {
2460 *irow.offset(0) = 0;
2461 *jcol.offset(0) = 0;
2462 }
2463 if !values.is_null() {
2464 *values.offset(0) = 2.0 * obj_factor;
2465 }
2466 TRUE
2467 }
2468 }
2469
2470 fn create_callback_test_problem() -> IpoptProblem {
2471 let xl = [-1.0e20];
2473 let xu = [1.0e20];
2474 let gl = [-10.0];
2475 let gu = [10.0];
2476 unsafe {
2477 CreateIpoptProblem(
2478 1,
2479 xl.as_ptr(),
2480 xu.as_ptr(),
2481 1,
2482 gl.as_ptr(),
2483 gu.as_ptr(),
2484 1,
2485 1,
2486 0,
2487 Some(quad_eval_f),
2488 Some(cb_quad_eval_g),
2489 Some(quad_eval_grad_f),
2490 Some(cb_quad_eval_jac_g),
2491 Some(cb_quad_eval_h),
2492 )
2493 }
2494 }
2495
2496 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2497 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
2498 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
2499 std::sync::atomic::AtomicBool::new(false);
2500
2501 unsafe extern "C" fn counting_cb(
2502 _alg_mod: Index,
2503 iter_count: Index,
2504 _obj_value: Number,
2505 _inf_pr: Number,
2506 _inf_du: Number,
2507 _mu: Number,
2508 _d_norm: Number,
2509 _regularization_size: Number,
2510 _alpha_du: Number,
2511 _alpha_pr: Number,
2512 _ls_trials: Index,
2513 user_data: *mut c_void,
2514 ) -> Bool {
2515 unsafe {
2516 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2517 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
2518 let problem = user_data as IpoptProblem;
2521 let mut x = [0.0_f64];
2522 let rc = GetIpoptCurrentIterate(
2523 problem,
2524 FALSE,
2525 1,
2526 x.as_mut_ptr(),
2527 std::ptr::null_mut(),
2528 std::ptr::null_mut(),
2529 1,
2530 std::ptr::null_mut(),
2531 std::ptr::null_mut(),
2532 );
2533 if rc == TRUE && x[0].is_finite() {
2534 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2535 }
2536 TRUE
2537 }
2538 }
2539
2540 #[test]
2541 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
2542 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
2543 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
2544 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2545
2546 let p = create_callback_test_problem();
2547 assert!(!p.is_null());
2548 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
2549 assert_eq!(ok, TRUE);
2550 let mut x = [0.0_f64];
2551 let mut obj = 0.0_f64;
2552 let rc = unsafe {
2553 IpoptSolve(
2554 p,
2555 x.as_mut_ptr(),
2556 std::ptr::null_mut(),
2557 &mut obj,
2558 std::ptr::null_mut(),
2559 std::ptr::null_mut(),
2560 std::ptr::null_mut(),
2561 p as *mut c_void,
2562 )
2563 };
2564 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2565 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
2567 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
2568 assert!(
2569 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
2570 "last iter should be >= 1 after at least one accepted step"
2571 );
2572 assert!(
2573 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
2574 "GetIpoptCurrentIterate did not return a usable x"
2575 );
2576 unsafe { FreeIpoptProblem(p) };
2577 }
2578
2579 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2580
2581 fn create_bounded_callback_test_problem() -> IpoptProblem {
2586 let xl = [0.0];
2588 let xu = [10.0];
2589 let gl = [-10.0];
2590 let gu = [10.0];
2591 unsafe {
2592 CreateIpoptProblem(
2593 1,
2594 xl.as_ptr(),
2595 xu.as_ptr(),
2596 1,
2597 gl.as_ptr(),
2598 gu.as_ptr(),
2599 1,
2600 1,
2601 0,
2602 Some(quad_eval_f),
2603 Some(cb_quad_eval_g),
2604 Some(quad_eval_grad_f),
2605 Some(cb_quad_eval_jac_g),
2606 Some(cb_quad_eval_h),
2607 )
2608 }
2609 }
2610
2611 unsafe extern "C" fn violations_inspecting_cb(
2612 _alg_mod: Index,
2613 _iter_count: Index,
2614 _obj_value: Number,
2615 _inf_pr: Number,
2616 _inf_du: Number,
2617 _mu: Number,
2618 _d_norm: Number,
2619 _regularization_size: Number,
2620 _alpha_du: Number,
2621 _alpha_pr: Number,
2622 _ls_trials: Index,
2623 user_data: *mut c_void,
2624 ) -> Bool {
2625 unsafe {
2626 let problem = user_data as IpoptProblem;
2627 let mut x_l_viol = [f64::NAN];
2632 let mut x_u_viol = [f64::NAN];
2633 let rc = GetIpoptCurrentViolations(
2634 problem,
2635 FALSE,
2636 1,
2637 x_l_viol.as_mut_ptr(),
2638 x_u_viol.as_mut_ptr(),
2639 std::ptr::null_mut(),
2640 std::ptr::null_mut(),
2641 std::ptr::null_mut(),
2642 1,
2643 std::ptr::null_mut(),
2644 std::ptr::null_mut(),
2645 );
2646 if rc == TRUE
2647 && x_l_viol[0].is_finite()
2648 && x_l_viol[0] >= 0.0
2649 && x_u_viol[0].is_finite()
2650 && x_u_viol[0] >= 0.0
2651 {
2652 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2653 }
2654 TRUE
2655 }
2656 }
2657
2658 #[test]
2659 fn get_current_violations_inside_callback_reports_finite_bounds() {
2660 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2661 let p = create_bounded_callback_test_problem();
2662 assert!(!p.is_null());
2663 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
2664 assert_eq!(ok, TRUE);
2665 let mut x = [5.0_f64];
2666 let mut obj = 0.0_f64;
2667 let rc = unsafe {
2668 IpoptSolve(
2669 p,
2670 x.as_mut_ptr(),
2671 std::ptr::null_mut(),
2672 &mut obj,
2673 std::ptr::null_mut(),
2674 std::ptr::null_mut(),
2675 std::ptr::null_mut(),
2676 p as *mut c_void,
2677 )
2678 };
2679 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2680 assert!(
2681 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
2682 "GetIpoptCurrentViolations did not return finite, non-negative \
2683 bound violations from inside the callback"
2684 );
2685 unsafe { FreeIpoptProblem(p) };
2686 }
2687
2688 #[test]
2689 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
2690 let n_us = 1usize;
2700 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
2704 let mut v = vec![0.0; n_us];
2705 for (i, s) in packed.iter().enumerate() {
2706 v[i] = (-s).max(0.0);
2707 }
2708 v
2709 });
2710 assert!(
2711 unguarded.is_err(),
2712 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
2713 );
2714
2715 let guarded: Result<Vec<f64>, ()> = (|| {
2717 if packed.len() != n_us {
2718 return Err(());
2719 }
2720 let mut v = vec![0.0; n_us];
2721 for (i, s) in packed.iter().enumerate() {
2722 v[i] = (-s).max(0.0);
2723 }
2724 Ok(v)
2725 })();
2726 assert!(
2727 guarded.is_err(),
2728 "guarded scatter should reject the length mismatch (return FALSE), not panic"
2729 );
2730 }
2731
2732 unsafe extern "C" fn user_stop_cb(
2733 _alg_mod: Index,
2734 _iter_count: Index,
2735 _obj_value: Number,
2736 _inf_pr: Number,
2737 _inf_du: Number,
2738 _mu: Number,
2739 _d_norm: Number,
2740 _regularization_size: Number,
2741 _alpha_du: Number,
2742 _alpha_pr: Number,
2743 _ls_trials: Index,
2744 _user_data: *mut c_void,
2745 ) -> Bool {
2746 FALSE
2747 }
2748
2749 #[test]
2750 fn intermediate_callback_false_surfaces_user_requested_stop() {
2751 let p = create_callback_test_problem();
2752 assert!(!p.is_null());
2753 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
2754 assert_eq!(ok, TRUE);
2755 let mut x = [0.0_f64];
2756 let rc = unsafe {
2757 IpoptSolve(
2758 p,
2759 x.as_mut_ptr(),
2760 std::ptr::null_mut(),
2761 std::ptr::null_mut(),
2762 std::ptr::null_mut(),
2763 std::ptr::null_mut(),
2764 std::ptr::null_mut(),
2765 std::ptr::null_mut(),
2766 )
2767 };
2768 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
2769 unsafe { FreeIpoptProblem(p) };
2770 }
2771
2772 #[test]
2773 fn ffi_guard_converts_panic_to_fallback() {
2774 let fallback = ApplicationReturnStatus::InternalError as Index;
2781 let got = ffi_guard(fallback, || -> Index {
2782 panic!("boom inside solver core");
2783 });
2784 assert_eq!(got, fallback);
2785 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
2786 }
2787
2788 #[test]
2789 fn ffi_guard_is_transparent_on_success() {
2790 let got = ffi_guard(-99, || 7);
2794 assert_eq!(got, 7);
2795 }
2796
2797 #[test]
2798 fn parse_pkg_version_handles_missing_components() {
2799 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
2800 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
2801 assert_eq!(parse_pkg_version(""), (0, 0, 0));
2802 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
2803 }
2804
2805 use crate::solver::{
2808 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
2809 IpoptSolverSolve,
2810 };
2811
2812 #[test]
2813 fn solver_create_consumes_problem_handle() {
2814 let mut p = create_unconstrained();
2815 assert!(!p.is_null());
2816 let s = unsafe { IpoptCreateSolver(&mut p) };
2817 assert!(!s.is_null());
2818 assert!(
2819 p.is_null(),
2820 "IpoptCreateSolver should NULL out the caller's handle"
2821 );
2822 unsafe { IpoptFreeSolver(s) };
2823 }
2824
2825 #[test]
2826 fn solver_create_null_inputs_return_null() {
2827 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2829 assert!(s.is_null());
2830 let mut p: IpoptProblem = std::ptr::null_mut();
2832 let s = unsafe { IpoptCreateSolver(&mut p) };
2833 assert!(s.is_null());
2834 }
2835
2836 #[test]
2837 fn solver_free_null_is_safe() {
2838 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
2839 }
2840
2841 #[test]
2842 fn solver_solve_drives_quadratic_and_retains_factor() {
2843 let xl = [-1.0e20];
2844 let xu = [1.0e20];
2845 let mut p = unsafe {
2846 CreateIpoptProblem(
2847 1,
2848 xl.as_ptr(),
2849 xu.as_ptr(),
2850 0,
2851 std::ptr::null(),
2852 std::ptr::null(),
2853 0,
2854 1,
2855 0,
2856 Some(quad_eval_f),
2857 None,
2858 Some(quad_eval_grad_f),
2859 None,
2860 Some(quad_eval_h),
2861 )
2862 };
2863 assert!(!p.is_null());
2864 let s = unsafe { IpoptCreateSolver(&mut p) };
2865 assert!(!s.is_null());
2866 let mut x = [0.0_f64];
2867 let mut obj = 0.0_f64;
2868 let rc = unsafe {
2869 IpoptSolverSolve(
2870 s,
2871 x.as_mut_ptr(),
2872 std::ptr::null_mut(),
2873 &mut obj,
2874 std::ptr::null_mut(),
2875 std::ptr::null_mut(),
2876 std::ptr::null_mut(),
2877 std::ptr::null_mut(),
2878 )
2879 };
2880 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2881 assert!((x[0] - 2.0).abs() < 1e-6);
2882 assert!(obj.abs() < 1e-10);
2883
2884 let dim = unsafe { IpoptSolverGetKktDim(s) };
2887 assert!(dim > 0, "expected positive KKT dim, got {dim}");
2888 let rhs = vec![0.0_f64; dim as usize];
2889 let mut lhs = vec![1.0_f64; dim as usize];
2890 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
2891 assert_eq!(ok, TRUE);
2892 for (i, v) in lhs.iter().enumerate() {
2893 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
2894 }
2895 unsafe { IpoptFreeSolver(s) };
2896 }
2897
2898 #[test]
2899 fn solver_kkt_dim_minus_one_before_solve() {
2900 let mut p = create_unconstrained();
2901 let s = unsafe { IpoptCreateSolver(&mut p) };
2902 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
2903 unsafe { IpoptFreeSolver(s) };
2904 }
2905
2906 #[test]
2911 fn c_get_working_set_returns_false_before_any_solve() {
2912 let p = create_unconstrained();
2913 let mut bound_buf = [0; 4];
2914 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
2915 assert_eq!(rc, FALSE);
2916 unsafe { FreeIpoptProblem(p) };
2917 }
2918
2919 #[test]
2920 fn c_set_warm_start_with_both_null_returns_false() {
2921 let p = create_unconstrained();
2922 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
2923 assert_eq!(rc, FALSE);
2924 unsafe { FreeIpoptProblem(p) };
2925 }
2926
2927 #[test]
2928 fn c_set_warm_start_with_bad_status_code_returns_false() {
2929 let p = create_unconstrained();
2930 let bogus = [
2932 POUNCE_WS_INACTIVE,
2933 7,
2934 POUNCE_WS_AT_LOWER,
2935 POUNCE_WS_INACTIVE,
2936 ];
2937 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
2938 assert_eq!(rc, FALSE);
2939 unsafe { FreeIpoptProblem(p) };
2940 }
2941
2942 #[test]
2943 fn c_set_warm_start_then_clear_succeeds() {
2944 let p = create_unconstrained();
2945 let in_buf = [POUNCE_WS_INACTIVE; 4];
2946 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
2947 assert_eq!(set_rc, TRUE);
2948 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
2949 assert_eq!(clr_rc, TRUE);
2950 unsafe { FreeIpoptProblem(p) };
2951 }
2952
2953 #[test]
2954 fn c_set_warm_start_on_null_problem_returns_false() {
2955 let in_buf = [POUNCE_WS_INACTIVE; 1];
2956 let rc = unsafe {
2957 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
2958 };
2959 assert_eq!(rc, FALSE);
2960 }
2961
2962 #[test]
2963 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
2964 let p = create_callback_test_problem();
2970 let key = CString::new("algorithm").unwrap();
2971 let val = CString::new("active-set-sqp").unwrap();
2972 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2973 assert_eq!(ok, TRUE);
2974
2975 let mut x = [0.0_f64];
2976 let mut obj = 0.0_f64;
2977 let rc1 = unsafe {
2978 IpoptSolve(
2979 p,
2980 x.as_mut_ptr(),
2981 std::ptr::null_mut(),
2982 &mut obj,
2983 std::ptr::null_mut(),
2984 std::ptr::null_mut(),
2985 std::ptr::null_mut(),
2986 std::ptr::null_mut(),
2987 )
2988 };
2989 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
2990
2991 let mut bound_buf = [-1; 1];
2992 let mut cons_buf = [-1; 1];
2993 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
2994 assert_eq!(got, TRUE);
2995 assert!((0..=3).contains(&bound_buf[0]));
2997 assert!((0..=3).contains(&cons_buf[0]));
2998
2999 x[0] = 0.0;
3004 let mut obj2 = 0.0_f64;
3005 let mut bound_out = [-1; 1];
3006 let mut cons_out = [-1; 1];
3007 let rc2 = unsafe {
3008 IpoptSolveWarmStart(
3009 p,
3010 x.as_mut_ptr(),
3011 std::ptr::null_mut(),
3012 &mut obj2,
3013 std::ptr::null_mut(),
3014 std::ptr::null_mut(),
3015 std::ptr::null_mut(),
3016 bound_buf.as_ptr(),
3017 cons_buf.as_ptr(),
3018 bound_out.as_mut_ptr(),
3019 cons_out.as_mut_ptr(),
3020 std::ptr::null_mut(),
3021 )
3022 };
3023 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
3024 assert!((0..=3).contains(&bound_out[0]));
3025 assert!((0..=3).contains(&cons_out[0]));
3026
3027 unsafe { FreeIpoptProblem(p) };
3028 }
3029}