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_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
42use pounce_nlp::return_codes::ApplicationReturnStatus;
43use pounce_nlp::solve_statistics::SolveStatistics;
44use pounce_nlp::tnlp::{
45 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
46 StartingPoint, TNLP,
47};
48use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
49use pounce_restoration::resto_inner_solver::{
50 InnerBackendFactoryFactory, make_default_restoration_factory_provider,
51};
52use std::cell::RefCell;
53use std::ffi::{CStr, c_char, c_int, c_void};
54use std::rc::Rc;
55
56pub type Number = f64;
58pub type Index = c_int;
60pub type Bool = c_int;
62
63const TRUE: Bool = 1;
64const FALSE: Bool = 0;
65
66pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
79 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
80 Ok(r) => r,
81 Err(_) => fallback,
82 }
83}
84
85pub type IpoptBoundStatus = c_int;
89pub type IpoptConsStatus = c_int;
92
93const POUNCE_WS_INACTIVE: c_int = 0;
94const POUNCE_WS_AT_LOWER: c_int = 1;
95const POUNCE_WS_AT_UPPER: c_int = 2;
96const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
97
98pub struct IpoptProblemInfo {
101 pub(crate) app: IpoptApplication,
102 pub(crate) n: Index,
103 pub(crate) m: Index,
104 pub(crate) nele_jac: Index,
105 pub(crate) nele_hess: Index,
106 pub(crate) index_style: Index,
107 pub(crate) x_l: Vec<Number>,
108 pub(crate) x_u: Vec<Number>,
109 pub(crate) g_l: Vec<Number>,
110 pub(crate) g_u: Vec<Number>,
111 pub(crate) eval_f: Option<Eval_F_CB>,
112 pub(crate) eval_g: Option<Eval_G_CB>,
113 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
114 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
115 pub(crate) eval_h: Option<Eval_H_CB>,
116 pub(crate) intermediate_cb: Option<Intermediate_CB>,
117 pub(crate) user_scaling: Option<UserScaling>,
121 pub(crate) last_solve: Option<LastSolve>,
125 pub(crate) pending_working_set: Option<pounce_qp::WorkingSet>,
139}
140
141#[derive(Clone)]
144pub(crate) struct UserScaling {
145 obj_scaling: Number,
146 x_scaling: Option<Vec<Number>>,
147 g_scaling: Option<Vec<Number>>,
148}
149
150#[derive(Clone)]
156pub(crate) struct LastSolve {
157 pub(crate) stats: SolveStatistics,
158 pub(crate) status: ApplicationReturnStatus,
159 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
160 pub(crate) final_x: Vec<Number>,
161 pub(crate) final_lambda: Vec<Number>,
162 pub(crate) final_obj: Number,
163}
164
165impl Default for LastSolve {
166 fn default() -> Self {
167 Self {
168 stats: SolveStatistics::default(),
169 status: ApplicationReturnStatus::InternalError,
170 linear_solver: None,
171 final_x: Vec::new(),
172 final_lambda: Vec::new(),
173 final_obj: 0.0,
174 }
175 }
176}
177
178pub type IpoptProblem = *mut IpoptProblemInfo;
179
180pub type Eval_F_CB = unsafe extern "C" fn(
184 n: Index,
185 x: *const Number,
186 new_x: Bool,
187 obj_value: *mut Number,
188 user_data: *mut c_void,
189) -> Bool;
190
191pub type Eval_Grad_F_CB = unsafe extern "C" fn(
192 n: Index,
193 x: *const Number,
194 new_x: Bool,
195 grad_f: *mut Number,
196 user_data: *mut c_void,
197) -> Bool;
198
199pub type Eval_G_CB = unsafe extern "C" fn(
200 n: Index,
201 x: *const Number,
202 new_x: Bool,
203 m: Index,
204 g: *mut Number,
205 user_data: *mut c_void,
206) -> Bool;
207
208pub type Eval_Jac_G_CB = unsafe extern "C" fn(
209 n: Index,
210 x: *const Number,
211 new_x: Bool,
212 m: Index,
213 nele_jac: Index,
214 iRow: *mut Index,
215 jCol: *mut Index,
216 values: *mut Number,
217 user_data: *mut c_void,
218) -> Bool;
219
220pub type Eval_H_CB = unsafe extern "C" fn(
221 n: Index,
222 x: *const Number,
223 new_x: Bool,
224 obj_factor: Number,
225 m: Index,
226 lambda: *const Number,
227 new_lambda: Bool,
228 nele_hess: Index,
229 iRow: *mut Index,
230 jCol: *mut Index,
231 values: *mut Number,
232 user_data: *mut c_void,
233) -> Bool;
234
235pub type Intermediate_CB = unsafe extern "C" fn(
236 alg_mod: Index,
237 iter_count: Index,
238 obj_value: Number,
239 inf_pr: Number,
240 inf_du: Number,
241 mu: Number,
242 d_norm: Number,
243 regularization_size: Number,
244 alpha_du: Number,
245 alpha_pr: Number,
246 ls_trials: Index,
247 user_data: *mut c_void,
248) -> Bool;
249
250#[unsafe(no_mangle)]
261pub unsafe extern "C" fn CreateIpoptProblem(
262 n: Index,
263 x_L: *const Number,
264 x_U: *const Number,
265 m: Index,
266 g_L: *const Number,
267 g_U: *const Number,
268 nele_jac: Index,
269 nele_hess: Index,
270 index_style: Index,
271 eval_f: Option<Eval_F_CB>,
272 eval_g: Option<Eval_G_CB>,
273 eval_grad_f: Option<Eval_Grad_F_CB>,
274 eval_jac_g: Option<Eval_Jac_G_CB>,
275 eval_h: Option<Eval_H_CB>,
276) -> IpoptProblem {
277 unsafe {
278 pounce_observability::init_subscriber();
282
283 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
284 return std::ptr::null_mut();
285 }
286 if !(0..=1).contains(&index_style) {
287 return std::ptr::null_mut();
288 }
289 if eval_f.is_none() || eval_grad_f.is_none() {
290 return std::ptr::null_mut();
291 }
292 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
293 return std::ptr::null_mut();
294 }
295 if n > 0 && (x_L.is_null() || x_U.is_null()) {
296 return std::ptr::null_mut();
297 }
298 if m > 0 && (g_L.is_null() || g_U.is_null()) {
299 return std::ptr::null_mut();
300 }
301
302 let x_l = if n > 0 {
303 std::slice::from_raw_parts(x_L, n as usize).to_vec()
304 } else {
305 Vec::new()
306 };
307 let x_u = if n > 0 {
308 std::slice::from_raw_parts(x_U, n as usize).to_vec()
309 } else {
310 Vec::new()
311 };
312 let g_l_vec = if m > 0 {
313 std::slice::from_raw_parts(g_L, m as usize).to_vec()
314 } else {
315 Vec::new()
316 };
317 let g_u_vec = if m > 0 {
318 std::slice::from_raw_parts(g_U, m as usize).to_vec()
319 } else {
320 Vec::new()
321 };
322
323 let info = Box::new(IpoptProblemInfo {
324 app: IpoptApplication::new(),
325 n,
326 m,
327 nele_jac,
328 nele_hess,
329 index_style,
330 x_l,
331 x_u,
332 g_l: g_l_vec,
333 g_u: g_u_vec,
334 eval_f,
335 eval_g,
336 eval_grad_f,
337 eval_jac_g,
338 eval_h,
339 intermediate_cb: None,
340 user_scaling: None,
341 last_solve: None,
342 pending_working_set: None,
343 });
344 Box::into_raw(info)
345 }
346}
347
348#[unsafe(no_mangle)]
355pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
356 unsafe {
357 if ipopt_problem.is_null() {
358 return;
359 }
360 drop(Box::from_raw(ipopt_problem));
361 }
362}
363
364unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
365 unsafe {
366 if keyword.is_null() {
367 return None;
368 }
369 CStr::from_ptr(keyword).to_str().ok()
370 }
371}
372
373#[unsafe(no_mangle)]
380pub unsafe extern "C" fn AddIpoptStrOption(
381 ipopt_problem: IpoptProblem,
382 keyword: *const c_char,
383 val: *const c_char,
384) -> Bool {
385 unsafe {
386 if ipopt_problem.is_null() {
387 return FALSE;
388 }
389 let info = &mut *ipopt_problem;
390 let Some(k) = keyword_str(keyword) else {
391 return FALSE;
392 };
393 if val.is_null() {
394 return FALSE;
395 }
396 let Ok(v) = CStr::from_ptr(val).to_str() else {
397 return FALSE;
398 };
399 match info.app.options_mut().set_string_value(k, v, true, false) {
400 Ok(_) => TRUE,
401 Err(_) => FALSE,
402 }
403 }
404}
405
406#[unsafe(no_mangle)]
413pub unsafe extern "C" fn AddIpoptNumOption(
414 ipopt_problem: IpoptProblem,
415 keyword: *const c_char,
416 val: Number,
417) -> Bool {
418 unsafe {
419 if ipopt_problem.is_null() {
420 return FALSE;
421 }
422 let info = &mut *ipopt_problem;
423 let Some(k) = keyword_str(keyword) else {
424 return FALSE;
425 };
426 match info
427 .app
428 .options_mut()
429 .set_numeric_value(k, val, true, false)
430 {
431 Ok(_) => TRUE,
432 Err(_) => FALSE,
433 }
434 }
435}
436
437#[unsafe(no_mangle)]
444pub unsafe extern "C" fn AddIpoptIntOption(
445 ipopt_problem: IpoptProblem,
446 keyword: *const c_char,
447 val: Index,
448) -> Bool {
449 unsafe {
450 if ipopt_problem.is_null() {
451 return FALSE;
452 }
453 let info = &mut *ipopt_problem;
454 let Some(k) = keyword_str(keyword) else {
455 return FALSE;
456 };
457 match info.app.options_mut().set_integer_value(
458 k,
459 val as pounce_common::types::Index,
460 true,
461 false,
462 ) {
463 Ok(_) => TRUE,
464 Err(_) => FALSE,
465 }
466 }
467}
468
469#[unsafe(no_mangle)]
483pub unsafe extern "C" fn OpenIpoptOutputFile(
484 ipopt_problem: IpoptProblem,
485 file_name: *const c_char,
486 print_level: c_int,
487) -> Bool {
488 unsafe {
489 if ipopt_problem.is_null() || file_name.is_null() {
490 return FALSE;
491 }
492 let info = &mut *ipopt_problem;
493 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
494 return FALSE;
495 };
496 if info.app.open_output_file(fname, print_level) {
497 TRUE
498 } else {
499 FALSE
500 }
501 }
502}
503
504#[unsafe(no_mangle)]
524pub unsafe extern "C" fn SetIpoptProblemScaling(
525 ipopt_problem: IpoptProblem,
526 obj_scaling: Number,
527 x_scaling: *const Number,
528 g_scaling: *const Number,
529) -> Bool {
530 unsafe {
531 if ipopt_problem.is_null() {
532 return FALSE;
533 }
534 let info = &mut *ipopt_problem;
535 let n = info.n as usize;
536 let m = info.m as usize;
537 let x_vec = if !x_scaling.is_null() && n > 0 {
538 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
539 } else {
540 None
541 };
542 let g_vec = if !g_scaling.is_null() && m > 0 {
543 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
544 } else {
545 None
546 };
547 info.user_scaling = Some(UserScaling {
548 obj_scaling,
549 x_scaling: x_vec,
550 g_scaling: g_vec,
551 });
552 TRUE
553 }
554}
555
556#[allow(clippy::too_many_arguments)]
570#[unsafe(no_mangle)]
571pub unsafe extern "C" fn IpoptSolve(
572 ipopt_problem: IpoptProblem,
573 x: *mut Number,
574 g: *mut Number,
575 obj_val: *mut Number,
576 mult_g: *mut Number,
577 mult_x_L: *mut Number,
578 mult_x_U: *mut Number,
579 user_data: *mut c_void,
580) -> Index {
581 unsafe {
582 if ipopt_problem.is_null() {
583 return ApplicationReturnStatus::InternalError as Index;
584 }
585 (*ipopt_problem).last_solve = None;
593 ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
599 let info = &mut *ipopt_problem;
600 if info.n < 0 || info.m < 0 {
601 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
602 }
603 if info.n > 0 && x.is_null() {
604 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
605 }
606
607 let n_us = info.n as usize;
608 let m_us = info.m as usize;
609 let initial_x = if n_us > 0 {
610 std::slice::from_raw_parts(x, n_us).to_vec()
611 } else {
612 Vec::new()
613 };
614
615 if let Some(working) = info.pending_working_set.take() {
628 let seed_duals = matches!(
629 info.app
630 .options()
631 .get_bool_value("warm_start_init_point", ""),
632 Ok((true, true))
633 );
634 let read_in = |p: *const Number, len: usize| -> Vec<Number> {
635 if seed_duals && !p.is_null() && len > 0 {
636 std::slice::from_raw_parts(p, len).to_vec()
637 } else {
638 vec![0.0; len]
639 }
640 };
641 let lambda_g = read_in(mult_g as *const Number, m_us);
642 let z_l = read_in(mult_x_L as *const Number, n_us);
643 let z_u = read_in(mult_x_U as *const Number, n_us);
644 let lambda_x = z_l.iter().zip(&z_u).map(|(l, u)| l - u).collect();
647 info.app
648 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
649 x: initial_x.clone(),
650 lambda_g,
651 lambda_x,
652 working: Some(working),
653 });
654 }
655
656 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
657 n: info.n,
658 m: info.m,
659 nele_jac: info.nele_jac,
660 nele_hess: info.nele_hess,
661 index_style: info.index_style,
662 x_l: info.x_l.clone(),
663 x_u: info.x_u.clone(),
664 g_l: info.g_l.clone(),
665 g_u: info.g_u.clone(),
666 initial_x,
667 eval_f: info.eval_f,
668 eval_grad_f: info.eval_grad_f,
669 eval_g: info.eval_g,
670 eval_jac_g: info.eval_jac_g,
671 eval_h: info.eval_h,
672 user_data,
673 intermediate_cb: info.intermediate_cb,
674 user_scaling: info.user_scaling.clone(),
675 final_status: None,
676 final_x: vec![0.0; n_us],
677 final_z_l: vec![0.0; n_us],
678 final_z_u: vec![0.0; n_us],
679 final_g: vec![0.0; m_us],
680 final_lambda: vec![0.0; m_us],
681 final_obj: 0.0,
682 }));
683
684 let feral_cfg = feral_config_from_options(info.app.options());
694 let bff_mint = move || -> InnerBackendFactoryFactory {
695 let feral_cfg = feral_cfg.clone();
696 Box::new(move || default_backend_factory(feral_cfg.clone()))
697 };
698 let resto_provider = make_default_restoration_factory_provider(
699 RestoAlgorithmBuilder::new(),
700 info.app.algorithm_builder_from_options(),
701 bff_mint,
702 );
703 info.app.set_restoration_factory_provider(resto_provider);
704
705 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
706 let status = info.app.optimize_tnlp(bridge_for_solve);
707 let bridge_ref = bridge.borrow();
708 info.last_solve = Some(LastSolve {
709 stats: info.app.statistics(),
710 status,
711 linear_solver: info.app.linear_solver_summary(),
712 final_x: bridge_ref.final_x.clone(),
713 final_lambda: bridge_ref.final_lambda.clone(),
714 final_obj: bridge_ref.final_obj,
715 });
716 if !x.is_null() && n_us > 0 {
717 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
718 }
719 if !g.is_null() && m_us > 0 {
720 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
721 }
722 if !obj_val.is_null() {
723 *obj_val = bridge_ref.final_obj;
724 }
725 if !mult_g.is_null() && m_us > 0 {
726 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
727 }
728 if !mult_x_L.is_null() && n_us > 0 {
729 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
730 }
731 if !mult_x_U.is_null() && n_us > 0 {
732 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
733 }
734 status as Index
735 })
736 }
737}
738
739#[unsafe(no_mangle)]
745pub unsafe extern "C" fn SetIntermediateCallback(
746 ipopt_problem: IpoptProblem,
747 intermediate_cb: Option<Intermediate_CB>,
748) -> Bool {
749 unsafe {
750 if ipopt_problem.is_null() {
751 return FALSE;
752 }
753 let info = &mut *ipopt_problem;
754 info.intermediate_cb = intermediate_cb;
755 TRUE
756 }
757}
758
759#[allow(clippy::too_many_arguments)]
782#[unsafe(no_mangle)]
783pub unsafe extern "C" fn GetIpoptCurrentIterate(
784 ipopt_problem: IpoptProblem,
785 _scaled: Bool,
786 n: Index,
787 x: *mut Number,
788 z_l: *mut Number,
789 z_u: *mut Number,
790 m: Index,
791 g: *mut Number,
792 lambda: *mut Number,
793) -> Bool {
794 unsafe {
795 if ipopt_problem.is_null() {
796 return FALSE;
797 }
798 let info = &*ipopt_problem;
799 if n != info.n || m != info.m {
800 return FALSE;
801 }
802 let result = ip_intermediate::with_current(|ctx| {
803 let data = ctx.data.borrow();
804 let Some(curr) = data.curr.as_ref() else {
805 return false;
806 };
807 let nlp = ctx.nlp.borrow();
808 let n_us = n as usize;
809 let m_us = m as usize;
810 if !x.is_null() && n_us > 0 {
811 let full_x = nlp.lift_x_to_full(&*curr.x);
812 if full_x.len() != n_us {
813 return false;
814 }
815 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
816 }
817 if !z_l.is_null() && n_us > 0 {
818 let full = nlp.pack_z_l_for_user(&*curr.z_l);
819 if full.len() != n_us {
820 return false;
821 }
822 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
823 }
824 if !z_u.is_null() && n_us > 0 {
825 let full = nlp.pack_z_u_for_user(&*curr.z_u);
826 if full.len() != n_us {
827 return false;
828 }
829 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
830 }
831 if !g.is_null() && m_us > 0 {
832 let cq = ctx.cq.borrow();
833 let full = nlp.pack_g_for_user(&*cq.curr_c(), &*cq.curr_d());
834 if full.len() != m_us {
835 return false;
836 }
837 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
838 }
839 if !lambda.is_null() && m_us > 0 {
840 let full = nlp.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
841 if full.len() != m_us {
842 return false;
843 }
844 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
845 }
846 true
847 });
848 if result.unwrap_or(false) { TRUE } else { FALSE }
849 }
850}
851
852#[allow(clippy::too_many_arguments)]
867#[unsafe(no_mangle)]
868pub unsafe extern "C" fn GetIpoptCurrentViolations(
869 ipopt_problem: IpoptProblem,
870 _scaled: Bool,
871 n: Index,
872 x_l_violation: *mut Number,
873 x_u_violation: *mut Number,
874 compl_x_l: *mut Number,
875 compl_x_u: *mut Number,
876 grad_lag_x: *mut Number,
877 m: Index,
878 nlp_constraint_violation: *mut Number,
879 compl_g: *mut Number,
880) -> Bool {
881 unsafe {
882 if ipopt_problem.is_null() {
883 return FALSE;
884 }
885 let info = &*ipopt_problem;
886 if n != info.n || m != info.m {
887 return FALSE;
888 }
889 let result = ip_intermediate::with_current(|ctx| {
890 let data = ctx.data.borrow();
891 let Some(_curr) = data.curr.as_ref() else {
892 return false;
893 };
894 drop(data);
895 let nlp = ctx.nlp.borrow();
896 let cq = ctx.cq.borrow();
897 let n_us = n as usize;
898 let m_us = m as usize;
899 if !x_l_violation.is_null() && n_us > 0 {
905 let slack = cq.curr_slack_x_l();
906 let z_l_full = nlp.pack_z_l_for_user(&*slack);
907 if z_l_full.len() != n_us {
912 return false;
913 }
914 let mut v = vec![0.0; n_us];
919 for (i, s) in z_l_full.iter().enumerate() {
920 v[i] = (-s).max(0.0);
921 }
922 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
923 }
924 if !x_u_violation.is_null() && n_us > 0 {
925 let slack = cq.curr_slack_x_u();
926 let s_full = nlp.pack_z_u_for_user(&*slack);
927 if s_full.len() != n_us {
928 return false;
929 }
930 let mut v = vec![0.0; n_us];
931 for (i, s) in s_full.iter().enumerate() {
932 v[i] = (-s).max(0.0);
933 }
934 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
935 }
936 if !compl_x_l.is_null() && n_us > 0 {
937 let v = nlp.pack_z_l_for_user(&*cq.curr_compl_x_l());
938 if v.len() != n_us {
939 return false;
940 }
941 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
942 }
943 if !compl_x_u.is_null() && n_us > 0 {
944 let v = nlp.pack_z_u_for_user(&*cq.curr_compl_x_u());
945 if v.len() != n_us {
946 return false;
947 }
948 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
949 }
950 if !grad_lag_x.is_null() && n_us > 0 {
951 let glx = cq.curr_grad_lag_x();
952 let full = nlp.lift_x_to_full(&*glx);
956 if full.len() != n_us {
957 return false;
958 }
959 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
960 }
961 if !nlp_constraint_violation.is_null() && m_us > 0 {
962 let zero = vec![0.0; m_us];
968 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
969 }
970 if !compl_g.is_null() && m_us > 0 {
971 let zero = vec![0.0; m_us];
974 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
975 }
976 true
977 });
978 if result.unwrap_or(false) { TRUE } else { FALSE }
979 }
980}
981
982#[unsafe(no_mangle)]
990pub unsafe extern "C" fn GetIpoptVersion(
991 major: *mut c_int,
992 minor: *mut c_int,
993 release: *mut c_int,
994) {
995 unsafe {
996 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1001 if !major.is_null() {
1002 *major = mj;
1003 }
1004 if !minor.is_null() {
1005 *minor = mn;
1006 }
1007 if !release.is_null() {
1008 *release = pt;
1009 }
1010 }
1011}
1012
1013fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
1014 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
1015 (
1016 it.next().unwrap_or(0),
1017 it.next().unwrap_or(0),
1018 it.next().unwrap_or(0),
1019 )
1020}
1021
1022#[unsafe(no_mangle)]
1039pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
1040 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
1041}
1042
1043#[unsafe(no_mangle)]
1050pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
1051 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
1052}
1053
1054#[unsafe(no_mangle)]
1061pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
1062 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1063}
1064
1065#[unsafe(no_mangle)]
1072pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1073 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1074}
1075
1076#[unsafe(no_mangle)]
1082pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1083 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1084}
1085
1086unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1087where
1088 F: FnOnce(&SolveStatistics) -> T,
1089{
1090 unsafe {
1091 if ipopt_problem.is_null() {
1092 return None;
1093 }
1094 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1095 }
1096}
1097
1098fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1108 use pounce_qp::BoundStatus::*;
1109 match s {
1110 Inactive => POUNCE_WS_INACTIVE,
1111 AtLower => POUNCE_WS_AT_LOWER,
1112 AtUpper => POUNCE_WS_AT_UPPER,
1113 Fixed => POUNCE_WS_FIXED_OR_EQ,
1114 }
1115}
1116
1117fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1118 use pounce_qp::BoundStatus::*;
1119 match v {
1120 POUNCE_WS_INACTIVE => Some(Inactive),
1121 POUNCE_WS_AT_LOWER => Some(AtLower),
1122 POUNCE_WS_AT_UPPER => Some(AtUpper),
1123 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1124 _ => None,
1125 }
1126}
1127
1128fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1129 use pounce_qp::ConsStatus::*;
1130 match s {
1131 Inactive => POUNCE_WS_INACTIVE,
1132 AtLower => POUNCE_WS_AT_LOWER,
1133 AtUpper => POUNCE_WS_AT_UPPER,
1134 Equality => POUNCE_WS_FIXED_OR_EQ,
1135 }
1136}
1137
1138fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1139 use pounce_qp::ConsStatus::*;
1140 match v {
1141 POUNCE_WS_INACTIVE => Some(Inactive),
1142 POUNCE_WS_AT_LOWER => Some(AtLower),
1143 POUNCE_WS_AT_UPPER => Some(AtUpper),
1144 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1145 _ => None,
1146 }
1147}
1148
1149fn internal_to_user_rows(g_l: &[Number], g_u: &[Number]) -> Vec<usize> {
1165 let m = g_l.len();
1166 let is_eq =
1167 |i: usize| g_l[i] > NLP_LOWER_BOUND_INF && g_u[i] < NLP_UPPER_BOUND_INF && g_l[i] == g_u[i];
1168 let mut map: Vec<usize> = (0..m).filter(|&i| is_eq(i)).collect();
1169 map.extend((0..m).filter(|&i| !is_eq(i)));
1170 map
1171}
1172
1173fn internal_to_user_vars(x_l: &[Number], x_u: &[Number]) -> Vec<usize> {
1179 (0..x_l.len()).filter(|&i| x_l[i] != x_u[i]).collect()
1180}
1181
1182#[unsafe(no_mangle)]
1198pub unsafe extern "C" fn IpoptGetWorkingSet(
1199 ipopt_problem: IpoptProblem,
1200 bound_status_out: *mut IpoptBoundStatus,
1201 cons_status_out: *mut IpoptConsStatus,
1202) -> Bool {
1203 unsafe {
1204 if ipopt_problem.is_null() {
1205 return FALSE;
1206 }
1207 let info = &*ipopt_problem;
1208 let ws = match info.app.last_sqp_working_set() {
1209 Some(w) => w,
1210 None => return FALSE,
1211 };
1212 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1215 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1216 if ws.constraints.len() != row_map.len() || ws.bounds.len() != var_map.len() {
1217 return FALSE;
1220 }
1221 if !bound_status_out.is_null() {
1222 for i in 0..info.x_l.len() {
1225 *bound_status_out.add(i) = POUNCE_WS_FIXED_OR_EQ;
1226 }
1227 for (internal, &user) in var_map.iter().enumerate() {
1228 *bound_status_out.add(user) = bound_status_to_int(ws.bounds[internal]);
1229 }
1230 }
1231 if !cons_status_out.is_null() {
1232 for (internal, &user) in row_map.iter().enumerate() {
1233 *cons_status_out.add(user) = cons_status_to_int(ws.constraints[internal]);
1234 }
1235 }
1236 TRUE
1237 }
1238}
1239
1240#[unsafe(no_mangle)]
1256pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1257 ipopt_problem: IpoptProblem,
1258 bound_status_in: *const IpoptBoundStatus,
1259 cons_status_in: *const IpoptConsStatus,
1260) -> Bool {
1261 unsafe {
1262 if ipopt_problem.is_null() {
1263 return FALSE;
1264 }
1265 if bound_status_in.is_null() && cons_status_in.is_null() {
1266 return FALSE;
1267 }
1268 let info = &mut *ipopt_problem;
1269 let n = info.n.max(0) as usize;
1270 let m = info.m.max(0) as usize;
1271 let row_map = internal_to_user_rows(&info.g_l, &info.g_u);
1276 let var_map = internal_to_user_vars(&info.x_l, &info.x_u);
1277 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; var_map.len()];
1296 if !bound_status_in.is_null() {
1297 for i in 0..n {
1301 let v = *bound_status_in.add(i);
1302 let Some(s) = int_to_bound_status(v) else {
1303 return FALSE;
1304 };
1305 let lo_finite = info.x_l[i] > NLP_LOWER_BOUND_INF;
1306 let hi_finite = info.x_u[i] < NLP_UPPER_BOUND_INF;
1307 let consistent = match s {
1308 pounce_qp::BoundStatus::Fixed => info.x_l[i] == info.x_u[i],
1309 pounce_qp::BoundStatus::AtLower => lo_finite,
1310 pounce_qp::BoundStatus::AtUpper => hi_finite,
1311 pounce_qp::BoundStatus::Inactive => true,
1312 };
1313 if !consistent {
1314 return FALSE;
1315 }
1316 }
1317 for (internal, &user) in var_map.iter().enumerate() {
1318 if let Some(s) = int_to_bound_status(*bound_status_in.add(user)) {
1320 bounds[internal] = s;
1321 }
1322 }
1323 }
1324 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1325 if !cons_status_in.is_null() {
1326 for i in 0..m {
1327 let v = *cons_status_in.add(i);
1328 let Some(s) = int_to_cons_status(v) else {
1329 return FALSE;
1330 };
1331 let lo_finite = info.g_l[i] > NLP_LOWER_BOUND_INF;
1332 let hi_finite = info.g_u[i] < NLP_UPPER_BOUND_INF;
1333 let consistent = match s {
1334 pounce_qp::ConsStatus::Equality => {
1335 lo_finite && hi_finite && info.g_l[i] == info.g_u[i]
1336 }
1337 pounce_qp::ConsStatus::AtLower => lo_finite,
1338 pounce_qp::ConsStatus::AtUpper => hi_finite,
1339 pounce_qp::ConsStatus::Inactive => true,
1340 };
1341 if !consistent {
1342 return FALSE;
1343 }
1344 }
1345 for (internal, &user) in row_map.iter().enumerate() {
1346 if let Some(s) = int_to_cons_status(*cons_status_in.add(user)) {
1347 constraints[internal] = s;
1348 }
1349 }
1350 }
1351 info.pending_working_set = Some(pounce_qp::WorkingSet {
1363 bounds,
1364 constraints,
1365 });
1366 TRUE
1367 }
1368}
1369
1370#[unsafe(no_mangle)]
1377pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1378 unsafe {
1379 if ipopt_problem.is_null() {
1380 return FALSE;
1381 }
1382 (*ipopt_problem).pending_working_set = None;
1383 (*ipopt_problem).app.clear_sqp_warm_start();
1384 TRUE
1385 }
1386}
1387
1388#[allow(clippy::too_many_arguments)]
1404#[unsafe(no_mangle)]
1405pub unsafe extern "C" fn IpoptSolveWarmStart(
1406 ipopt_problem: IpoptProblem,
1407 x: *mut Number,
1408 g: *mut Number,
1409 obj_val: *mut Number,
1410 mult_g: *mut Number,
1411 mult_x_L: *mut Number,
1412 mult_x_U: *mut Number,
1413 bound_status_in: *const IpoptBoundStatus,
1414 cons_status_in: *const IpoptConsStatus,
1415 bound_status_out: *mut IpoptBoundStatus,
1416 cons_status_out: *mut IpoptConsStatus,
1417 user_data: *mut c_void,
1418) -> Index {
1419 if ipopt_problem.is_null() {
1420 return ApplicationReturnStatus::InternalError as Index;
1421 }
1422 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1426 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1431 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1432 }
1433 let status = IpoptSolve(
1434 ipopt_problem,
1435 x,
1436 g,
1437 obj_val,
1438 mult_g,
1439 mult_x_L,
1440 mult_x_U,
1441 user_data,
1442 );
1443 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1444 status
1445 })
1446}
1447
1448pub(crate) struct CCallbackTnlp {
1459 pub(crate) n: Index,
1460 pub(crate) m: Index,
1461 pub(crate) nele_jac: Index,
1462 pub(crate) nele_hess: Index,
1463 pub(crate) index_style: Index,
1464 pub(crate) x_l: Vec<Number>,
1465 pub(crate) x_u: Vec<Number>,
1466 pub(crate) g_l: Vec<Number>,
1467 pub(crate) g_u: Vec<Number>,
1468 pub(crate) initial_x: Vec<Number>,
1469 pub(crate) eval_f: Option<Eval_F_CB>,
1470 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1471 pub(crate) eval_g: Option<Eval_G_CB>,
1472 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1473 pub(crate) eval_h: Option<Eval_H_CB>,
1474 pub(crate) user_data: *mut c_void,
1475 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1478 pub(crate) user_scaling: Option<UserScaling>,
1480 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1481 pub(crate) final_x: Vec<Number>,
1482 pub(crate) final_z_l: Vec<Number>,
1483 pub(crate) final_z_u: Vec<Number>,
1484 pub(crate) final_g: Vec<Number>,
1485 pub(crate) final_lambda: Vec<Number>,
1486 pub(crate) final_obj: Number,
1487}
1488
1489impl TNLP for CCallbackTnlp {
1490 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1491 Some(NlpInfo {
1492 n: self.n as pounce_common::types::Index,
1493 m: self.m as pounce_common::types::Index,
1494 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1495 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1496 index_style: if self.index_style == 1 {
1497 IndexStyle::Fortran
1498 } else {
1499 IndexStyle::C
1500 },
1501 })
1502 }
1503
1504 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1505 if !self.x_l.is_empty() {
1506 b.x_l.copy_from_slice(&self.x_l);
1507 }
1508 if !self.x_u.is_empty() {
1509 b.x_u.copy_from_slice(&self.x_u);
1510 }
1511 if !self.g_l.is_empty() {
1512 b.g_l.copy_from_slice(&self.g_l);
1513 }
1514 if !self.g_u.is_empty() {
1515 b.g_u.copy_from_slice(&self.g_u);
1516 }
1517 true
1518 }
1519
1520 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1521 if !self.initial_x.is_empty() {
1522 sp.x.copy_from_slice(&self.initial_x);
1523 }
1524 true
1525 }
1526
1527 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1528 let Some(s) = self.user_scaling.as_ref() else {
1529 return false;
1530 };
1531 *req.obj_scaling = s.obj_scaling;
1532 if let Some(x) = s.x_scaling.as_ref() {
1533 if x.len() == req.x_scaling.len() {
1534 req.x_scaling.copy_from_slice(x);
1535 *req.use_x_scaling = true;
1536 }
1537 } else {
1538 *req.use_x_scaling = false;
1539 }
1540 if let Some(g) = s.g_scaling.as_ref() {
1541 if g.len() == req.g_scaling.len() {
1542 req.g_scaling.copy_from_slice(g);
1543 *req.use_g_scaling = true;
1544 }
1545 } else {
1546 *req.use_g_scaling = false;
1547 }
1548 true
1549 }
1550
1551 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
1552 let cb = self.eval_f?;
1553 let mut obj = 0.0;
1554 let ok = unsafe {
1555 cb(
1556 self.n,
1557 x.as_ptr() as *mut Number,
1558 if new_x { TRUE } else { FALSE },
1559 &mut obj,
1560 self.user_data,
1561 )
1562 };
1563 if ok != FALSE { Some(obj) } else { None }
1564 }
1565
1566 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
1567 let Some(cb) = self.eval_grad_f else {
1568 return false;
1569 };
1570 let ok = unsafe {
1571 cb(
1572 self.n,
1573 x.as_ptr() as *mut Number,
1574 if new_x { TRUE } else { FALSE },
1575 grad_f.as_mut_ptr(),
1576 self.user_data,
1577 )
1578 };
1579 ok != FALSE
1580 }
1581
1582 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
1583 if self.m == 0 {
1584 return true;
1585 }
1586 let Some(cb) = self.eval_g else {
1587 return false;
1588 };
1589 let ok = unsafe {
1590 cb(
1591 self.n,
1592 x.as_ptr() as *mut Number,
1593 if new_x { TRUE } else { FALSE },
1594 self.m,
1595 g.as_mut_ptr(),
1596 self.user_data,
1597 )
1598 };
1599 ok != FALSE
1600 }
1601
1602 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
1603 if self.m == 0 || self.nele_jac == 0 {
1604 return true;
1605 }
1606 let Some(cb) = self.eval_jac_g else {
1607 return false;
1608 };
1609 let x_ptr = x
1610 .map(|s| s.as_ptr() as *mut Number)
1611 .unwrap_or(std::ptr::null_mut());
1612 let ok = match mode {
1613 SparsityRequest::Structure { irow, jcol } => unsafe {
1614 cb(
1615 self.n,
1616 x_ptr,
1617 if new_x { TRUE } else { FALSE },
1618 self.m,
1619 self.nele_jac,
1620 irow.as_mut_ptr(),
1621 jcol.as_mut_ptr(),
1622 std::ptr::null_mut(),
1623 self.user_data,
1624 )
1625 },
1626 SparsityRequest::Values { values } => unsafe {
1627 cb(
1628 self.n,
1629 x_ptr,
1630 if new_x { TRUE } else { FALSE },
1631 self.m,
1632 self.nele_jac,
1633 std::ptr::null_mut(),
1634 std::ptr::null_mut(),
1635 values.as_mut_ptr(),
1636 self.user_data,
1637 )
1638 },
1639 };
1640 ok != FALSE
1641 }
1642
1643 fn eval_h(
1644 &mut self,
1645 x: Option<&[Number]>,
1646 new_x: bool,
1647 obj_factor: Number,
1648 lambda: Option<&[Number]>,
1649 new_lambda: bool,
1650 mode: SparsityRequest<'_>,
1651 ) -> bool {
1652 let Some(cb) = self.eval_h else {
1653 return false;
1654 };
1655 if self.nele_hess == 0 {
1656 return true;
1657 }
1658 let x_ptr = x
1659 .map(|s| s.as_ptr() as *mut Number)
1660 .unwrap_or(std::ptr::null_mut());
1661 let lambda_ptr = lambda
1662 .map(|s| s.as_ptr() as *mut Number)
1663 .unwrap_or(std::ptr::null_mut());
1664 let ok = match mode {
1665 SparsityRequest::Structure { irow, jcol } => unsafe {
1666 cb(
1667 self.n,
1668 x_ptr,
1669 if new_x { TRUE } else { FALSE },
1670 obj_factor,
1671 self.m,
1672 lambda_ptr,
1673 if new_lambda { TRUE } else { FALSE },
1674 self.nele_hess,
1675 irow.as_mut_ptr(),
1676 jcol.as_mut_ptr(),
1677 std::ptr::null_mut(),
1678 self.user_data,
1679 )
1680 },
1681 SparsityRequest::Values { values } => unsafe {
1682 cb(
1683 self.n,
1684 x_ptr,
1685 if new_x { TRUE } else { FALSE },
1686 obj_factor,
1687 self.m,
1688 lambda_ptr,
1689 if new_lambda { TRUE } else { FALSE },
1690 self.nele_hess,
1691 std::ptr::null_mut(),
1692 std::ptr::null_mut(),
1693 values.as_mut_ptr(),
1694 self.user_data,
1695 )
1696 },
1697 };
1698 ok != FALSE
1699 }
1700
1701 fn intermediate_callback(
1702 &mut self,
1703 stats: pounce_nlp::tnlp::IterStats,
1704 _ip_data: &IpoptData,
1705 _ip_cq: &IpoptCq,
1706 ) -> bool {
1707 let Some(cb) = self.intermediate_cb else {
1708 return true;
1709 };
1710 let ok = unsafe {
1711 cb(
1712 stats.mode as Index,
1713 stats.iter as Index,
1714 stats.obj_value,
1715 stats.inf_pr,
1716 stats.inf_du,
1717 stats.mu,
1718 stats.d_norm,
1719 stats.regularization_size,
1720 stats.alpha_du,
1721 stats.alpha_pr,
1722 stats.ls_trials as Index,
1723 self.user_data,
1724 )
1725 };
1726 ok != FALSE
1727 }
1728
1729 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1730 self.final_status = Some(sol.status);
1731 if !sol.x.is_empty() {
1732 self.final_x.copy_from_slice(sol.x);
1733 }
1734 if !sol.z_l.is_empty() {
1735 self.final_z_l.copy_from_slice(sol.z_l);
1736 }
1737 if !sol.z_u.is_empty() {
1738 self.final_z_u.copy_from_slice(sol.z_u);
1739 }
1740 if !sol.g.is_empty() {
1741 self.final_g.copy_from_slice(sol.g);
1742 }
1743 if !sol.lambda.is_empty() {
1744 self.final_lambda.copy_from_slice(sol.lambda);
1745 }
1746 self.final_obj = sol.obj_value;
1747 }
1748}
1749
1750#[unsafe(no_mangle)]
1763pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
1764 if ipopt_problem.is_null() {
1765 return FALSE;
1766 }
1767 let info = unsafe { &mut *ipopt_problem };
1768 info.app.enable_iter_history();
1769 TRUE
1770}
1771
1772#[unsafe(no_mangle)]
1793pub unsafe extern "C" fn IpoptWriteSolveReport(
1794 ipopt_problem: IpoptProblem,
1795 path: *const c_char,
1796 detail: *const c_char,
1797) -> Bool {
1798 use pounce_solve_report::{
1799 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
1800 };
1801
1802 ffi_guard(FALSE, || unsafe {
1807 if ipopt_problem.is_null() || path.is_null() {
1808 return FALSE;
1809 }
1810 let info = &*ipopt_problem;
1811 let Some(last) = info.last_solve.as_ref() else {
1812 return FALSE;
1813 };
1814
1815 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
1816 return FALSE;
1817 };
1818
1819 let detail_choice = if detail.is_null() {
1820 ReportDetail::Summary
1821 } else {
1822 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
1823 return FALSE;
1824 };
1825 match ReportDetail::parse(detail_str) {
1826 Ok(d) => d,
1827 Err(_) => return FALSE,
1828 }
1829 };
1830
1831 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
1832 builder.problem.n_variables = info.n;
1833 builder.problem.n_constraints = info.m;
1834 builder.problem.n_objectives = 1;
1835 builder.problem.nnz_jac_g = Some(info.nele_jac);
1836 builder.problem.nnz_h_lag = Some(info.nele_hess);
1837
1838 builder.solution.status = last.status;
1839 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
1840 builder.solution.objective = last.final_obj;
1841 builder.solution.x = last.final_x.clone();
1842 builder.solution.lambda = last.final_lambda.clone();
1843
1844 builder.ingest_stats(&last.stats);
1845 if let Some(linsol) = last.linear_solver.clone() {
1846 builder.set_linear_solver_summary(linsol);
1847 }
1848
1849 let report = builder.finish();
1850 match write_report_file(std::path::Path::new(path_str), &report) {
1851 Ok(_) => TRUE,
1852 Err(_) => FALSE,
1853 }
1854 })
1855}
1856
1857#[cfg(test)]
1858mod tests {
1859 use super::*;
1860 use std::ffi::CString;
1861
1862 unsafe extern "C" fn dummy_eval_f(
1863 _n: Index,
1864 _x: *const Number,
1865 _new_x: Bool,
1866 _obj_value: *mut Number,
1867 _user_data: *mut c_void,
1868 ) -> Bool {
1869 TRUE
1870 }
1871 unsafe extern "C" fn dummy_eval_grad_f(
1872 _n: Index,
1873 _x: *const Number,
1874 _new_x: Bool,
1875 _grad_f: *mut Number,
1876 _user_data: *mut c_void,
1877 ) -> Bool {
1878 TRUE
1879 }
1880
1881 fn create_unconstrained() -> IpoptProblem {
1882 let xl = [-1.0; 4];
1883 let xu = [1.0; 4];
1884 unsafe {
1885 CreateIpoptProblem(
1886 4,
1887 xl.as_ptr(),
1888 xu.as_ptr(),
1889 0,
1890 std::ptr::null(),
1891 std::ptr::null(),
1892 0,
1893 10,
1894 0,
1895 Some(dummy_eval_f),
1896 None,
1897 Some(dummy_eval_grad_f),
1898 None,
1899 None,
1900 )
1901 }
1902 }
1903
1904 #[test]
1905 fn create_succeeds_for_unconstrained_problem() {
1906 let p = create_unconstrained();
1907 assert!(!p.is_null());
1908 unsafe { FreeIpoptProblem(p) };
1909 }
1910
1911 #[test]
1912 fn create_returns_null_on_missing_required_callbacks() {
1913 let xl = [-1.0; 4];
1914 let xu = [1.0; 4];
1915 let p = unsafe {
1916 CreateIpoptProblem(
1917 4,
1918 xl.as_ptr(),
1919 xu.as_ptr(),
1920 0,
1921 std::ptr::null(),
1922 std::ptr::null(),
1923 0,
1924 10,
1925 0,
1926 None, None,
1928 Some(dummy_eval_grad_f),
1929 None,
1930 None,
1931 )
1932 };
1933 assert!(p.is_null());
1934 }
1935
1936 #[test]
1937 fn create_returns_null_on_negative_n() {
1938 let p = unsafe {
1939 CreateIpoptProblem(
1940 -1,
1941 std::ptr::null(),
1942 std::ptr::null(),
1943 0,
1944 std::ptr::null(),
1945 std::ptr::null(),
1946 0,
1947 10,
1948 0,
1949 Some(dummy_eval_f),
1950 None,
1951 Some(dummy_eval_grad_f),
1952 None,
1953 None,
1954 )
1955 };
1956 assert!(p.is_null());
1957 }
1958
1959 #[test]
1960 fn create_returns_null_on_invalid_index_style() {
1961 let xl = [0.0; 1];
1962 let xu = [1.0; 1];
1963 let p = unsafe {
1964 CreateIpoptProblem(
1965 1,
1966 xl.as_ptr(),
1967 xu.as_ptr(),
1968 0,
1969 std::ptr::null(),
1970 std::ptr::null(),
1971 0,
1972 1,
1973 2, Some(dummy_eval_f),
1975 None,
1976 Some(dummy_eval_grad_f),
1977 None,
1978 None,
1979 )
1980 };
1981 assert!(p.is_null());
1982 }
1983
1984 #[test]
1985 fn add_int_option_forwards_to_application() {
1986 let p = create_unconstrained();
1987 let key = CString::new("print_level").unwrap();
1988 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
1989 assert_eq!(ok, TRUE);
1990 let info = unsafe { &*p };
1991 let (level, found) = info
1992 .app
1993 .options()
1994 .get_integer_value("print_level", "")
1995 .unwrap();
1996 assert!(found);
1997 assert_eq!(level, 5);
1998 unsafe { FreeIpoptProblem(p) };
1999 }
2000
2001 #[test]
2002 fn add_str_option_with_invalid_key_returns_false() {
2003 let p = create_unconstrained();
2004 let key = CString::new("totally_unknown_option").unwrap();
2005 let val = CString::new("yes").unwrap();
2006 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2007 assert_eq!(ok, FALSE);
2008 unsafe { FreeIpoptProblem(p) };
2009 }
2010
2011 #[test]
2012 fn add_options_on_null_problem_returns_false() {
2013 let key = CString::new("print_level").unwrap();
2014 let v = CString::new("yes").unwrap();
2015 unsafe {
2016 assert_eq!(
2017 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
2018 FALSE
2019 );
2020 assert_eq!(
2021 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
2022 FALSE
2023 );
2024 assert_eq!(
2025 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
2026 FALSE
2027 );
2028 }
2029 }
2030
2031 unsafe extern "C" fn dummy_intermediate(
2032 _alg_mod: Index,
2033 _iter_count: Index,
2034 _obj_value: Number,
2035 _inf_pr: Number,
2036 _inf_du: Number,
2037 _mu: Number,
2038 _d_norm: Number,
2039 _regularization_size: Number,
2040 _alpha_du: Number,
2041 _alpha_pr: Number,
2042 _ls_trials: Index,
2043 _user_data: *mut c_void,
2044 ) -> Bool {
2045 TRUE
2046 }
2047
2048 #[test]
2049 fn set_intermediate_callback_stores_pointer() {
2050 let p = create_unconstrained();
2051 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
2052 assert_eq!(ok, TRUE);
2053 let info = unsafe { &*p };
2054 assert!(info.intermediate_cb.is_some());
2055 unsafe { FreeIpoptProblem(p) };
2056 }
2057
2058 #[test]
2059 fn solve_returns_internal_error_on_null_problem() {
2060 let rc = unsafe {
2061 IpoptSolve(
2062 std::ptr::null_mut(),
2063 std::ptr::null_mut(),
2064 std::ptr::null_mut(),
2065 std::ptr::null_mut(),
2066 std::ptr::null_mut(),
2067 std::ptr::null_mut(),
2068 std::ptr::null_mut(),
2069 std::ptr::null_mut(),
2070 )
2071 };
2072 assert_eq!(rc, -199);
2073 }
2074
2075 #[test]
2076 fn free_null_is_safe() {
2077 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
2078 }
2079
2080 unsafe extern "C" fn quad_eval_f(
2086 _n: Index,
2087 x: *const Number,
2088 _new_x: Bool,
2089 obj_value: *mut Number,
2090 _user_data: *mut c_void,
2091 ) -> Bool {
2092 unsafe {
2093 let v = *x.offset(0);
2094 *obj_value = (v - 2.0) * (v - 2.0);
2095 TRUE
2096 }
2097 }
2098 unsafe extern "C" fn quad_eval_grad_f(
2099 _n: Index,
2100 x: *const Number,
2101 _new_x: Bool,
2102 grad: *mut Number,
2103 _user_data: *mut c_void,
2104 ) -> Bool {
2105 unsafe {
2106 let v = *x.offset(0);
2107 *grad.offset(0) = 2.0 * (v - 2.0);
2108 TRUE
2109 }
2110 }
2111 unsafe extern "C" fn quad_eval_h(
2112 _n: Index,
2113 _x: *const Number,
2114 _new_x: Bool,
2115 obj_factor: Number,
2116 _m: Index,
2117 _lambda: *const Number,
2118 _new_lambda: Bool,
2119 _nele_hess: Index,
2120 irow: *mut Index,
2121 jcol: *mut Index,
2122 values: *mut Number,
2123 _user_data: *mut c_void,
2124 ) -> Bool {
2125 unsafe {
2126 if !irow.is_null() && !jcol.is_null() && values.is_null() {
2127 *irow.offset(0) = 0;
2128 *jcol.offset(0) = 0;
2129 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
2130 *values.offset(0) = 2.0 * obj_factor;
2131 } else {
2132 return FALSE;
2133 }
2134 TRUE
2135 }
2136 }
2137
2138 #[test]
2139 fn solve_drives_unconstrained_quadratic_through_bridge() {
2140 let xl = [-1.0e20];
2143 let xu = [1.0e20];
2144 let p = unsafe {
2145 CreateIpoptProblem(
2146 1,
2147 xl.as_ptr(),
2148 xu.as_ptr(),
2149 0,
2150 std::ptr::null(),
2151 std::ptr::null(),
2152 0,
2153 1,
2154 0,
2155 Some(quad_eval_f),
2156 None,
2157 Some(quad_eval_grad_f),
2158 None,
2159 Some(quad_eval_h),
2160 )
2161 };
2162 assert!(!p.is_null());
2163 let mut x = [0.0_f64];
2164 let mut obj = 0.0_f64;
2165 let rc = unsafe {
2166 IpoptSolve(
2167 p,
2168 x.as_mut_ptr(),
2169 std::ptr::null_mut(),
2170 &mut obj,
2171 std::ptr::null_mut(),
2172 std::ptr::null_mut(),
2173 std::ptr::null_mut(),
2174 std::ptr::null_mut(),
2175 )
2176 };
2177 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2178 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
2179 assert!(obj.abs() < 1e-10, "obj = {}", obj);
2180 unsafe { FreeIpoptProblem(p) };
2181 }
2182
2183 #[test]
2198 fn stale_stats_cleared_when_resolve_bails() {
2199 let xl = [-1.0e20];
2200 let xu = [1.0e20];
2201 let p = unsafe {
2202 CreateIpoptProblem(
2203 1,
2204 xl.as_ptr(),
2205 xu.as_ptr(),
2206 0,
2207 std::ptr::null(),
2208 std::ptr::null(),
2209 0,
2210 1,
2211 0,
2212 Some(quad_eval_f),
2213 None,
2214 Some(quad_eval_grad_f),
2215 None,
2216 Some(quad_eval_h),
2217 )
2218 };
2219 assert!(!p.is_null());
2220
2221 let mut x = [0.0_f64];
2222 let mut obj = 0.0_f64;
2223 let rc = unsafe {
2224 IpoptSolve(
2225 p,
2226 x.as_mut_ptr(),
2227 std::ptr::null_mut(),
2228 &mut obj,
2229 std::ptr::null_mut(),
2230 std::ptr::null_mut(),
2231 std::ptr::null_mut(),
2232 std::ptr::null_mut(),
2233 )
2234 };
2235 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2236 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2238 assert!(
2239 iters_after_success >= 1,
2240 "a converged solve should record >=1 iteration, got {iters_after_success}"
2241 );
2242 assert!(unsafe { (*p).last_solve.is_some() });
2243
2244 unsafe { (*p).n = -1 };
2247 let mut x2 = [0.0_f64];
2248 let rc2 = unsafe {
2249 IpoptSolve(
2250 p,
2251 x2.as_mut_ptr(),
2252 std::ptr::null_mut(),
2253 std::ptr::null_mut(),
2254 std::ptr::null_mut(),
2255 std::ptr::null_mut(),
2256 std::ptr::null_mut(),
2257 std::ptr::null_mut(),
2258 )
2259 };
2260 assert_eq!(
2261 rc2,
2262 ApplicationReturnStatus::InvalidProblemDefinition as Index
2263 );
2264
2265 assert!(
2269 unsafe { (*p).last_solve.is_none() },
2270 "a bailed re-solve must clear stale last_solve (F5)"
2271 );
2272 assert_eq!(
2273 unsafe { GetIpoptIterCount(p) },
2274 0,
2275 "stale iteration count must not survive a bailed re-solve (F5)"
2276 );
2277
2278 unsafe { FreeIpoptProblem(p) };
2279 }
2280
2281 #[test]
2282 fn solve_invalid_problem_definition_when_x_null() {
2283 let p = create_unconstrained();
2284 let rc = unsafe {
2285 IpoptSolve(
2286 p,
2287 std::ptr::null_mut(), std::ptr::null_mut(),
2289 std::ptr::null_mut(),
2290 std::ptr::null_mut(),
2291 std::ptr::null_mut(),
2292 std::ptr::null_mut(),
2293 std::ptr::null_mut(),
2294 )
2295 };
2296 assert_eq!(
2297 rc,
2298 ApplicationReturnStatus::InvalidProblemDefinition as Index
2299 );
2300 unsafe { FreeIpoptProblem(p) };
2301 }
2302
2303 #[test]
2306 fn get_version_writes_pkg_version() {
2307 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
2308 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
2309 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
2310 assert_eq!((mj, mn, pt), expected);
2311 }
2312
2313 #[test]
2314 fn get_version_tolerates_null_buffers() {
2315 unsafe {
2317 GetIpoptVersion(
2318 std::ptr::null_mut(),
2319 std::ptr::null_mut(),
2320 std::ptr::null_mut(),
2321 )
2322 };
2323 }
2324
2325 #[test]
2326 fn set_scaling_stores_user_supplied_arrays() {
2327 let p = create_unconstrained();
2328 let xs = [2.0, 3.0, 4.0, 5.0];
2329 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
2330 assert_eq!(ok, TRUE);
2331 let info = unsafe { &*p };
2332 let s = info.user_scaling.as_ref().unwrap();
2333 assert_eq!(s.obj_scaling, 7.0);
2334 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
2335 assert!(s.g_scaling.is_none());
2336 unsafe { FreeIpoptProblem(p) };
2337 }
2338
2339 #[test]
2340 fn set_scaling_on_null_problem_returns_false() {
2341 let ok = unsafe {
2342 SetIpoptProblemScaling(
2343 std::ptr::null_mut(),
2344 1.0,
2345 std::ptr::null(),
2346 std::ptr::null(),
2347 )
2348 };
2349 assert_eq!(ok, FALSE);
2350 }
2351
2352 #[test]
2353 fn open_output_file_writes_and_attaches_journal() {
2354 let p = create_unconstrained();
2355 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2356 let _ = std::fs::create_dir_all(&dir);
2357 let path = dir.join("output.log");
2358 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2359 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2360 assert_eq!(ok, TRUE);
2361 let info = unsafe { &*p };
2363 let (level, found) = info
2364 .app
2365 .options()
2366 .get_integer_value("file_print_level", "")
2367 .unwrap();
2368 assert!(found);
2369 assert_eq!(level, 5);
2370 unsafe { FreeIpoptProblem(p) };
2371 let _ = std::fs::remove_file(&path);
2372 }
2373
2374 #[test]
2375 fn open_output_file_with_null_inputs_returns_false() {
2376 let key = CString::new("nope").unwrap();
2377 unsafe {
2378 assert_eq!(
2379 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2380 FALSE
2381 );
2382 }
2383 let p = create_unconstrained();
2384 unsafe {
2385 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2386 FreeIpoptProblem(p);
2387 }
2388 }
2389
2390 #[test]
2391 fn get_current_iterate_returns_false_outside_callback() {
2392 let p = create_unconstrained();
2393 let rc = unsafe {
2394 GetIpoptCurrentIterate(
2395 p,
2396 FALSE,
2397 0,
2398 std::ptr::null_mut(),
2399 std::ptr::null_mut(),
2400 std::ptr::null_mut(),
2401 0,
2402 std::ptr::null_mut(),
2403 std::ptr::null_mut(),
2404 )
2405 };
2406 assert_eq!(rc, FALSE);
2407 unsafe { FreeIpoptProblem(p) };
2408 }
2409
2410 #[test]
2411 fn get_current_violations_returns_false_outside_callback() {
2412 let p = create_unconstrained();
2413 let rc = unsafe {
2414 GetIpoptCurrentViolations(
2415 p,
2416 FALSE,
2417 0,
2418 std::ptr::null_mut(),
2419 std::ptr::null_mut(),
2420 std::ptr::null_mut(),
2421 std::ptr::null_mut(),
2422 std::ptr::null_mut(),
2423 0,
2424 std::ptr::null_mut(),
2425 std::ptr::null_mut(),
2426 )
2427 };
2428 assert_eq!(rc, FALSE);
2429 unsafe { FreeIpoptProblem(p) };
2430 }
2431
2432 #[test]
2433 fn post_solve_stats_zero_before_solve() {
2434 let p = create_unconstrained();
2435 unsafe {
2436 assert_eq!(GetIpoptIterCount(p), 0);
2437 assert_eq!(GetIpoptSolveTime(p), 0.0);
2438 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2439 assert_eq!(GetIpoptDualInf(p), 0.0);
2440 assert_eq!(GetIpoptComplInf(p), 0.0);
2441 FreeIpoptProblem(p);
2442 }
2443 }
2444
2445 #[test]
2446 fn post_solve_stats_populated_after_solve() {
2447 let xl = [-1.0e20];
2449 let xu = [1.0e20];
2450 let p = unsafe {
2451 CreateIpoptProblem(
2452 1,
2453 xl.as_ptr(),
2454 xu.as_ptr(),
2455 0,
2456 std::ptr::null(),
2457 std::ptr::null(),
2458 0,
2459 1,
2460 0,
2461 Some(quad_eval_f),
2462 None,
2463 Some(quad_eval_grad_f),
2464 None,
2465 Some(quad_eval_h),
2466 )
2467 };
2468 let mut x = [0.0_f64];
2469 let mut obj = 0.0_f64;
2470 let rc = unsafe {
2471 IpoptSolve(
2472 p,
2473 x.as_mut_ptr(),
2474 std::ptr::null_mut(),
2475 &mut obj,
2476 std::ptr::null_mut(),
2477 std::ptr::null_mut(),
2478 std::ptr::null_mut(),
2479 std::ptr::null_mut(),
2480 )
2481 };
2482 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2483 unsafe {
2486 assert!(GetIpoptIterCount(p) >= 0);
2487 assert!(GetIpoptSolveTime(p) >= 0.0);
2488 assert!(GetIpoptPrimalInf(p).is_finite());
2489 assert!(GetIpoptDualInf(p).is_finite());
2490 assert!(GetIpoptComplInf(p).is_finite());
2491 FreeIpoptProblem(p);
2492 }
2493 }
2494
2495 #[test]
2496 fn write_solve_report_emits_v1_json_with_iter_history() {
2497 let xl = [-1.0e20];
2500 let xu = [1.0e20];
2501 let p = unsafe {
2502 CreateIpoptProblem(
2503 1,
2504 xl.as_ptr(),
2505 xu.as_ptr(),
2506 0,
2507 std::ptr::null(),
2508 std::ptr::null(),
2509 0,
2510 1,
2511 0,
2512 Some(quad_eval_f),
2513 None,
2514 Some(quad_eval_grad_f),
2515 None,
2516 Some(quad_eval_h),
2517 )
2518 };
2519
2520 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
2522 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
2523 assert_eq!(bad, FALSE);
2524
2525 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
2527 let mut x = [0.0_f64];
2528 let mut obj = 0.0_f64;
2529 let rc = unsafe {
2530 IpoptSolve(
2531 p,
2532 x.as_mut_ptr(),
2533 std::ptr::null_mut(),
2534 &mut obj,
2535 std::ptr::null_mut(),
2536 std::ptr::null_mut(),
2537 std::ptr::null_mut(),
2538 std::ptr::null_mut(),
2539 )
2540 };
2541 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2542
2543 let dir = std::env::temp_dir();
2544 let path = dir.join("pounce_cinterface_report.json");
2545 let cpath = CString::new(path.to_str().unwrap()).unwrap();
2546 let cdetail = CString::new("full").unwrap();
2547 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
2548 assert_eq!(ok, TRUE);
2549
2550 let txt = std::fs::read_to_string(&path).unwrap();
2553 assert!(
2554 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
2555 "{txt}"
2556 );
2557 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
2558 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
2559 assert_eq!(parsed.problem.n_variables, 1);
2560 assert_eq!(parsed.problem.n_constraints, 0);
2561
2562 let bad_detail = CString::new("verbose").unwrap();
2564 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
2565 assert_eq!(bad, FALSE);
2566
2567 let _ = std::fs::remove_file(&path);
2568 unsafe { FreeIpoptProblem(p) };
2569 }
2570
2571 unsafe extern "C" fn cb_quad_eval_g(
2578 _n: Index,
2579 x: *const Number,
2580 _new_x: Bool,
2581 _m: Index,
2582 g: *mut Number,
2583 _user_data: *mut c_void,
2584 ) -> Bool {
2585 unsafe {
2586 *g.offset(0) = *x.offset(0);
2587 TRUE
2588 }
2589 }
2590 unsafe extern "C" fn cb_quad_eval_jac_g(
2591 _n: Index,
2592 _x: *const Number,
2593 _new_x: Bool,
2594 _m: Index,
2595 nele_jac: Index,
2596 irow: *mut Index,
2597 jcol: *mut Index,
2598 values: *mut Number,
2599 _user_data: *mut c_void,
2600 ) -> Bool {
2601 unsafe {
2602 assert_eq!(nele_jac, 1);
2603 if !irow.is_null() {
2604 *irow.offset(0) = 0;
2605 *jcol.offset(0) = 0;
2606 }
2607 if !values.is_null() {
2608 *values.offset(0) = 1.0;
2609 }
2610 TRUE
2611 }
2612 }
2613 unsafe extern "C" fn cb_quad_eval_h(
2614 _n: Index,
2615 _x: *const Number,
2616 _new_x: Bool,
2617 obj_factor: Number,
2618 _m: Index,
2619 _lambda: *const Number,
2620 _new_lambda: Bool,
2621 _nele_hess: Index,
2622 irow: *mut Index,
2623 jcol: *mut Index,
2624 values: *mut Number,
2625 _user_data: *mut c_void,
2626 ) -> Bool {
2627 unsafe {
2628 if !irow.is_null() {
2629 *irow.offset(0) = 0;
2630 *jcol.offset(0) = 0;
2631 }
2632 if !values.is_null() {
2633 *values.offset(0) = 2.0 * obj_factor;
2634 }
2635 TRUE
2636 }
2637 }
2638
2639 fn create_callback_test_problem() -> IpoptProblem {
2640 let xl = [-1.0e20];
2642 let xu = [1.0e20];
2643 let gl = [-10.0];
2644 let gu = [10.0];
2645 unsafe {
2646 CreateIpoptProblem(
2647 1,
2648 xl.as_ptr(),
2649 xu.as_ptr(),
2650 1,
2651 gl.as_ptr(),
2652 gu.as_ptr(),
2653 1,
2654 1,
2655 0,
2656 Some(quad_eval_f),
2657 Some(cb_quad_eval_g),
2658 Some(quad_eval_grad_f),
2659 Some(cb_quad_eval_jac_g),
2660 Some(cb_quad_eval_h),
2661 )
2662 }
2663 }
2664
2665 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2666 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
2667 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
2668 std::sync::atomic::AtomicBool::new(false);
2669
2670 unsafe extern "C" fn counting_cb(
2671 _alg_mod: Index,
2672 iter_count: Index,
2673 _obj_value: Number,
2674 _inf_pr: Number,
2675 _inf_du: Number,
2676 _mu: Number,
2677 _d_norm: Number,
2678 _regularization_size: Number,
2679 _alpha_du: Number,
2680 _alpha_pr: Number,
2681 _ls_trials: Index,
2682 user_data: *mut c_void,
2683 ) -> Bool {
2684 unsafe {
2685 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2686 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
2687 let problem = user_data as IpoptProblem;
2690 let mut x = [0.0_f64];
2691 let rc = GetIpoptCurrentIterate(
2692 problem,
2693 FALSE,
2694 1,
2695 x.as_mut_ptr(),
2696 std::ptr::null_mut(),
2697 std::ptr::null_mut(),
2698 1,
2699 std::ptr::null_mut(),
2700 std::ptr::null_mut(),
2701 );
2702 if rc == TRUE && x[0].is_finite() {
2703 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2704 }
2705 TRUE
2706 }
2707 }
2708
2709 #[test]
2710 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
2711 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
2712 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
2713 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2714
2715 let p = create_callback_test_problem();
2716 assert!(!p.is_null());
2717 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
2718 assert_eq!(ok, TRUE);
2719 let mut x = [0.0_f64];
2720 let mut obj = 0.0_f64;
2721 let rc = unsafe {
2722 IpoptSolve(
2723 p,
2724 x.as_mut_ptr(),
2725 std::ptr::null_mut(),
2726 &mut obj,
2727 std::ptr::null_mut(),
2728 std::ptr::null_mut(),
2729 std::ptr::null_mut(),
2730 p as *mut c_void,
2731 )
2732 };
2733 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2734 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
2736 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
2737 assert!(
2738 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
2739 "last iter should be >= 1 after at least one accepted step"
2740 );
2741 assert!(
2742 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
2743 "GetIpoptCurrentIterate did not return a usable x"
2744 );
2745 unsafe { FreeIpoptProblem(p) };
2746 }
2747
2748 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2749
2750 fn create_bounded_callback_test_problem() -> IpoptProblem {
2755 let xl = [0.0];
2757 let xu = [10.0];
2758 let gl = [-10.0];
2759 let gu = [10.0];
2760 unsafe {
2761 CreateIpoptProblem(
2762 1,
2763 xl.as_ptr(),
2764 xu.as_ptr(),
2765 1,
2766 gl.as_ptr(),
2767 gu.as_ptr(),
2768 1,
2769 1,
2770 0,
2771 Some(quad_eval_f),
2772 Some(cb_quad_eval_g),
2773 Some(quad_eval_grad_f),
2774 Some(cb_quad_eval_jac_g),
2775 Some(cb_quad_eval_h),
2776 )
2777 }
2778 }
2779
2780 unsafe extern "C" fn violations_inspecting_cb(
2781 _alg_mod: Index,
2782 _iter_count: Index,
2783 _obj_value: Number,
2784 _inf_pr: Number,
2785 _inf_du: Number,
2786 _mu: Number,
2787 _d_norm: Number,
2788 _regularization_size: Number,
2789 _alpha_du: Number,
2790 _alpha_pr: Number,
2791 _ls_trials: Index,
2792 user_data: *mut c_void,
2793 ) -> Bool {
2794 unsafe {
2795 let problem = user_data as IpoptProblem;
2796 let mut x_l_viol = [f64::NAN];
2801 let mut x_u_viol = [f64::NAN];
2802 let rc = GetIpoptCurrentViolations(
2803 problem,
2804 FALSE,
2805 1,
2806 x_l_viol.as_mut_ptr(),
2807 x_u_viol.as_mut_ptr(),
2808 std::ptr::null_mut(),
2809 std::ptr::null_mut(),
2810 std::ptr::null_mut(),
2811 1,
2812 std::ptr::null_mut(),
2813 std::ptr::null_mut(),
2814 );
2815 if rc == TRUE
2816 && x_l_viol[0].is_finite()
2817 && x_l_viol[0] >= 0.0
2818 && x_u_viol[0].is_finite()
2819 && x_u_viol[0] >= 0.0
2820 {
2821 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2822 }
2823 TRUE
2824 }
2825 }
2826
2827 #[test]
2828 fn get_current_violations_inside_callback_reports_finite_bounds() {
2829 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2830 let p = create_bounded_callback_test_problem();
2831 assert!(!p.is_null());
2832 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
2833 assert_eq!(ok, TRUE);
2834 let mut x = [5.0_f64];
2835 let mut obj = 0.0_f64;
2836 let rc = unsafe {
2837 IpoptSolve(
2838 p,
2839 x.as_mut_ptr(),
2840 std::ptr::null_mut(),
2841 &mut obj,
2842 std::ptr::null_mut(),
2843 std::ptr::null_mut(),
2844 std::ptr::null_mut(),
2845 p as *mut c_void,
2846 )
2847 };
2848 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2849 assert!(
2850 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
2851 "GetIpoptCurrentViolations did not return finite, non-negative \
2852 bound violations from inside the callback"
2853 );
2854 unsafe { FreeIpoptProblem(p) };
2855 }
2856
2857 #[test]
2858 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
2859 let n_us = 1usize;
2869 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
2873 let mut v = vec![0.0; n_us];
2874 for (i, s) in packed.iter().enumerate() {
2875 v[i] = (-s).max(0.0);
2876 }
2877 v
2878 });
2879 assert!(
2880 unguarded.is_err(),
2881 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
2882 );
2883
2884 let guarded: Result<Vec<f64>, ()> = (|| {
2886 if packed.len() != n_us {
2887 return Err(());
2888 }
2889 let mut v = vec![0.0; n_us];
2890 for (i, s) in packed.iter().enumerate() {
2891 v[i] = (-s).max(0.0);
2892 }
2893 Ok(v)
2894 })();
2895 assert!(
2896 guarded.is_err(),
2897 "guarded scatter should reject the length mismatch (return FALSE), not panic"
2898 );
2899 }
2900
2901 unsafe extern "C" fn user_stop_cb(
2902 _alg_mod: Index,
2903 _iter_count: Index,
2904 _obj_value: Number,
2905 _inf_pr: Number,
2906 _inf_du: Number,
2907 _mu: Number,
2908 _d_norm: Number,
2909 _regularization_size: Number,
2910 _alpha_du: Number,
2911 _alpha_pr: Number,
2912 _ls_trials: Index,
2913 _user_data: *mut c_void,
2914 ) -> Bool {
2915 FALSE
2916 }
2917
2918 #[test]
2919 fn intermediate_callback_false_surfaces_user_requested_stop() {
2920 let p = create_callback_test_problem();
2921 assert!(!p.is_null());
2922 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
2923 assert_eq!(ok, TRUE);
2924 let mut x = [0.0_f64];
2925 let rc = unsafe {
2926 IpoptSolve(
2927 p,
2928 x.as_mut_ptr(),
2929 std::ptr::null_mut(),
2930 std::ptr::null_mut(),
2931 std::ptr::null_mut(),
2932 std::ptr::null_mut(),
2933 std::ptr::null_mut(),
2934 std::ptr::null_mut(),
2935 )
2936 };
2937 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
2938 unsafe { FreeIpoptProblem(p) };
2939 }
2940
2941 #[test]
2942 fn ffi_guard_converts_panic_to_fallback() {
2943 let fallback = ApplicationReturnStatus::InternalError as Index;
2950 let got = ffi_guard(fallback, || -> Index {
2951 panic!("boom inside solver core");
2952 });
2953 assert_eq!(got, fallback);
2954 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
2955 }
2956
2957 #[test]
2958 fn ffi_guard_is_transparent_on_success() {
2959 let got = ffi_guard(-99, || 7);
2963 assert_eq!(got, 7);
2964 }
2965
2966 #[test]
2967 fn parse_pkg_version_handles_missing_components() {
2968 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
2969 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
2970 assert_eq!(parse_pkg_version(""), (0, 0, 0));
2971 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
2972 }
2973
2974 use crate::solver::{
2977 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
2978 IpoptSolverSolve,
2979 };
2980
2981 #[test]
2982 fn solver_create_consumes_problem_handle() {
2983 let mut p = create_unconstrained();
2984 assert!(!p.is_null());
2985 let s = unsafe { IpoptCreateSolver(&mut p) };
2986 assert!(!s.is_null());
2987 assert!(
2988 p.is_null(),
2989 "IpoptCreateSolver should NULL out the caller's handle"
2990 );
2991 unsafe { IpoptFreeSolver(s) };
2992 }
2993
2994 #[test]
2995 fn solver_create_null_inputs_return_null() {
2996 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2998 assert!(s.is_null());
2999 let mut p: IpoptProblem = std::ptr::null_mut();
3001 let s = unsafe { IpoptCreateSolver(&mut p) };
3002 assert!(s.is_null());
3003 }
3004
3005 #[test]
3006 fn solver_free_null_is_safe() {
3007 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
3008 }
3009
3010 #[test]
3011 fn solver_solve_drives_quadratic_and_retains_factor() {
3012 let xl = [-1.0e20];
3013 let xu = [1.0e20];
3014 let mut p = unsafe {
3015 CreateIpoptProblem(
3016 1,
3017 xl.as_ptr(),
3018 xu.as_ptr(),
3019 0,
3020 std::ptr::null(),
3021 std::ptr::null(),
3022 0,
3023 1,
3024 0,
3025 Some(quad_eval_f),
3026 None,
3027 Some(quad_eval_grad_f),
3028 None,
3029 Some(quad_eval_h),
3030 )
3031 };
3032 assert!(!p.is_null());
3033 let s = unsafe { IpoptCreateSolver(&mut p) };
3034 assert!(!s.is_null());
3035 let mut x = [0.0_f64];
3036 let mut obj = 0.0_f64;
3037 let rc = unsafe {
3038 IpoptSolverSolve(
3039 s,
3040 x.as_mut_ptr(),
3041 std::ptr::null_mut(),
3042 &mut obj,
3043 std::ptr::null_mut(),
3044 std::ptr::null_mut(),
3045 std::ptr::null_mut(),
3046 std::ptr::null_mut(),
3047 )
3048 };
3049 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3050 assert!((x[0] - 2.0).abs() < 1e-6);
3051 assert!(obj.abs() < 1e-10);
3052
3053 let dim = unsafe { IpoptSolverGetKktDim(s) };
3056 assert!(dim > 0, "expected positive KKT dim, got {dim}");
3057 let rhs = vec![0.0_f64; dim as usize];
3058 let mut lhs = vec![1.0_f64; dim as usize];
3059 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
3060 assert_eq!(ok, TRUE);
3061 for (i, v) in lhs.iter().enumerate() {
3062 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
3063 }
3064 unsafe { IpoptFreeSolver(s) };
3065 }
3066
3067 #[test]
3068 fn solver_kkt_dim_minus_one_before_solve() {
3069 let mut p = create_unconstrained();
3070 let s = unsafe { IpoptCreateSolver(&mut p) };
3071 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
3072 unsafe { IpoptFreeSolver(s) };
3073 }
3074
3075 #[test]
3080 fn c_get_working_set_returns_false_before_any_solve() {
3081 let p = create_unconstrained();
3082 let mut bound_buf = [0; 4];
3083 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
3084 assert_eq!(rc, FALSE);
3085 unsafe { FreeIpoptProblem(p) };
3086 }
3087
3088 #[test]
3089 fn c_set_warm_start_with_both_null_returns_false() {
3090 let p = create_unconstrained();
3091 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
3092 assert_eq!(rc, FALSE);
3093 unsafe { FreeIpoptProblem(p) };
3094 }
3095
3096 #[test]
3097 fn c_set_warm_start_with_bad_status_code_returns_false() {
3098 let p = create_unconstrained();
3099 let bogus = [
3101 POUNCE_WS_INACTIVE,
3102 7,
3103 POUNCE_WS_AT_LOWER,
3104 POUNCE_WS_INACTIVE,
3105 ];
3106 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
3107 assert_eq!(rc, FALSE);
3108 unsafe { FreeIpoptProblem(p) };
3109 }
3110
3111 #[test]
3112 fn c_set_warm_start_then_clear_succeeds() {
3113 let p = create_unconstrained();
3114 let in_buf = [POUNCE_WS_INACTIVE; 4];
3115 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
3116 assert_eq!(set_rc, TRUE);
3117 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
3118 assert_eq!(clr_rc, TRUE);
3119 unsafe { FreeIpoptProblem(p) };
3120 }
3121
3122 #[test]
3123 fn c_set_warm_start_on_null_problem_returns_false() {
3124 let in_buf = [POUNCE_WS_INACTIVE; 1];
3125 let rc = unsafe {
3126 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
3127 };
3128 assert_eq!(rc, FALSE);
3129 }
3130
3131 #[test]
3132 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
3133 let p = create_callback_test_problem();
3139 let key = CString::new("algorithm").unwrap();
3140 let val = CString::new("active-set-sqp").unwrap();
3141 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
3142 assert_eq!(ok, TRUE);
3143
3144 let mut x = [0.0_f64];
3145 let mut obj = 0.0_f64;
3146 let rc1 = unsafe {
3147 IpoptSolve(
3148 p,
3149 x.as_mut_ptr(),
3150 std::ptr::null_mut(),
3151 &mut obj,
3152 std::ptr::null_mut(),
3153 std::ptr::null_mut(),
3154 std::ptr::null_mut(),
3155 std::ptr::null_mut(),
3156 )
3157 };
3158 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
3159
3160 let mut bound_buf = [-1; 1];
3161 let mut cons_buf = [-1; 1];
3162 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
3163 assert_eq!(got, TRUE);
3164 assert!((0..=3).contains(&bound_buf[0]));
3166 assert!((0..=3).contains(&cons_buf[0]));
3167
3168 x[0] = 0.0;
3173 let mut obj2 = 0.0_f64;
3174 let mut bound_out = [-1; 1];
3175 let mut cons_out = [-1; 1];
3176 let rc2 = unsafe {
3177 IpoptSolveWarmStart(
3178 p,
3179 x.as_mut_ptr(),
3180 std::ptr::null_mut(),
3181 &mut obj2,
3182 std::ptr::null_mut(),
3183 std::ptr::null_mut(),
3184 std::ptr::null_mut(),
3185 bound_buf.as_ptr(),
3186 cons_buf.as_ptr(),
3187 bound_out.as_mut_ptr(),
3188 cons_out.as_mut_ptr(),
3189 std::ptr::null_mut(),
3190 )
3191 };
3192 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
3193 assert!((0..=3).contains(&bound_out[0]));
3194 assert!((0..=3).contains(&cons_out[0]));
3195
3196 unsafe { FreeIpoptProblem(p) };
3197 }
3198}