1#![allow(non_camel_case_types, non_snake_case)]
31#![allow(unsafe_op_in_unsafe_fn, dead_code)]
32#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
33
34pub mod fortran;
35pub mod solver;
36
37use pounce_algorithm::application::{
38 IpoptApplication, default_backend_factory, feral_config_from_options, ma57_config_from_options,
39};
40use pounce_algorithm::intermediate as ip_intermediate;
41use pounce_common::reg_options::OptionType;
42use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
43use pounce_nlp::return_codes::ApplicationReturnStatus;
44use pounce_nlp::solve_statistics::SolveStatistics;
45use pounce_nlp::tnlp::{
46 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
47 StartingPoint, TNLP,
48};
49use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
50use pounce_restoration::resto_inner_solver::{
51 InnerBackendFactoryFactory, make_default_restoration_factory_provider,
52};
53use pounce_restoration::second_opinion_driver::run_second_opinion_ladder;
54use std::cell::RefCell;
55use std::ffi::{CStr, c_char, c_int, c_void};
56use std::rc::Rc;
57
58pub type Number = f64;
60pub type Index = c_int;
62pub type Bool = u8;
88
89const TRUE: Bool = 1;
90const FALSE: Bool = 0;
91
92const _: () = assert!(
96 core::mem::size_of::<Bool>() == 1,
97 "Bool must be one byte to match `typedef bool Bool` in pounce.h"
98);
99
100pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
113 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
114 Ok(r) => r,
115 Err(_) => fallback,
116 }
117}
118
119pub type IpoptBoundStatus = c_int;
123pub type IpoptConsStatus = c_int;
126
127const POUNCE_WS_INACTIVE: c_int = 0;
128const POUNCE_WS_AT_LOWER: c_int = 1;
129const POUNCE_WS_AT_UPPER: c_int = 2;
130const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
131
132pub struct IpoptProblemInfo {
135 pub(crate) app: IpoptApplication,
136 pub(crate) n: Index,
137 pub(crate) m: Index,
138 pub(crate) nele_jac: Index,
139 pub(crate) nele_hess: Index,
140 pub(crate) index_style: Index,
141 pub(crate) x_l: Vec<Number>,
142 pub(crate) x_u: Vec<Number>,
143 pub(crate) g_l: Vec<Number>,
144 pub(crate) g_u: Vec<Number>,
145 pub(crate) eval_f: Option<Eval_F_CB>,
146 pub(crate) eval_g: Option<Eval_G_CB>,
147 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
148 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
149 pub(crate) eval_h: Option<Eval_H_CB>,
150 pub(crate) intermediate_cb: Option<Intermediate_CB>,
151 pub(crate) user_scaling: Option<UserScaling>,
155 pub(crate) last_solve: Option<LastSolve>,
159 pub(crate) pending_working_set: Option<pounce_qp::WorkingSet>,
173 pub(crate) nonlinear_vars: Option<Vec<Index>>,
182}
183
184#[derive(Clone)]
187pub(crate) struct UserScaling {
188 obj_scaling: Number,
189 x_scaling: Option<Vec<Number>>,
190 g_scaling: Option<Vec<Number>>,
191}
192
193#[derive(Clone)]
199pub(crate) struct LastSolve {
200 pub(crate) stats: SolveStatistics,
201 pub(crate) status: ApplicationReturnStatus,
202 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
203 pub(crate) final_x: Vec<Number>,
204 pub(crate) final_lambda: Vec<Number>,
205 pub(crate) final_obj: Number,
206}
207
208impl Default for LastSolve {
209 fn default() -> Self {
210 Self {
211 stats: SolveStatistics::default(),
212 status: ApplicationReturnStatus::InternalError,
213 linear_solver: None,
214 final_x: Vec::new(),
215 final_lambda: Vec::new(),
216 final_obj: 0.0,
217 }
218 }
219}
220
221pub type IpoptProblem = *mut IpoptProblemInfo;
222
223pub type Eval_F_CB = unsafe extern "C" fn(
227 n: Index,
228 x: *const Number,
229 new_x: Bool,
230 obj_value: *mut Number,
231 user_data: *mut c_void,
232) -> Bool;
233
234pub type Eval_Grad_F_CB = unsafe extern "C" fn(
235 n: Index,
236 x: *const Number,
237 new_x: Bool,
238 grad_f: *mut Number,
239 user_data: *mut c_void,
240) -> Bool;
241
242pub type Eval_G_CB = unsafe extern "C" fn(
243 n: Index,
244 x: *const Number,
245 new_x: Bool,
246 m: Index,
247 g: *mut Number,
248 user_data: *mut c_void,
249) -> Bool;
250
251pub type Eval_Jac_G_CB = unsafe extern "C" fn(
252 n: Index,
253 x: *const Number,
254 new_x: Bool,
255 m: Index,
256 nele_jac: Index,
257 iRow: *mut Index,
258 jCol: *mut Index,
259 values: *mut Number,
260 user_data: *mut c_void,
261) -> Bool;
262
263pub type Eval_H_CB = unsafe extern "C" fn(
264 n: Index,
265 x: *const Number,
266 new_x: Bool,
267 obj_factor: Number,
268 m: Index,
269 lambda: *const Number,
270 new_lambda: Bool,
271 nele_hess: Index,
272 iRow: *mut Index,
273 jCol: *mut Index,
274 values: *mut Number,
275 user_data: *mut c_void,
276) -> Bool;
277
278pub type Intermediate_CB = unsafe extern "C" fn(
279 alg_mod: Index,
280 iter_count: Index,
281 obj_value: Number,
282 inf_pr: Number,
283 inf_du: Number,
284 mu: Number,
285 d_norm: Number,
286 regularization_size: Number,
287 alpha_du: Number,
288 alpha_pr: Number,
289 ls_trials: Index,
290 user_data: *mut c_void,
291) -> Bool;
292
293#[unsafe(no_mangle)]
304pub unsafe extern "C" fn CreateIpoptProblem(
305 n: Index,
306 x_L: *const Number,
307 x_U: *const Number,
308 m: Index,
309 g_L: *const Number,
310 g_U: *const Number,
311 nele_jac: Index,
312 nele_hess: Index,
313 index_style: Index,
314 eval_f: Option<Eval_F_CB>,
315 eval_g: Option<Eval_G_CB>,
316 eval_grad_f: Option<Eval_Grad_F_CB>,
317 eval_jac_g: Option<Eval_Jac_G_CB>,
318 eval_h: Option<Eval_H_CB>,
319) -> IpoptProblem {
320 unsafe {
321 pounce_observability::init_subscriber();
325
326 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
327 return std::ptr::null_mut();
328 }
329 if !(0..=1).contains(&index_style) {
330 return std::ptr::null_mut();
331 }
332 if eval_f.is_none() || eval_grad_f.is_none() {
333 return std::ptr::null_mut();
334 }
335 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
336 return std::ptr::null_mut();
337 }
338 if n > 0 && (x_L.is_null() || x_U.is_null()) {
339 return std::ptr::null_mut();
340 }
341 if m > 0 && (g_L.is_null() || g_U.is_null()) {
342 return std::ptr::null_mut();
343 }
344
345 let x_l = if n > 0 {
346 std::slice::from_raw_parts(x_L, n as usize).to_vec()
347 } else {
348 Vec::new()
349 };
350 let x_u = if n > 0 {
351 std::slice::from_raw_parts(x_U, n as usize).to_vec()
352 } else {
353 Vec::new()
354 };
355 let g_l_vec = if m > 0 {
356 std::slice::from_raw_parts(g_L, m as usize).to_vec()
357 } else {
358 Vec::new()
359 };
360 let g_u_vec = if m > 0 {
361 std::slice::from_raw_parts(g_U, m as usize).to_vec()
362 } else {
363 Vec::new()
364 };
365
366 let info = Box::new(IpoptProblemInfo {
367 app: IpoptApplication::new(),
368 n,
369 m,
370 nele_jac,
371 nele_hess,
372 index_style,
373 x_l,
374 x_u,
375 g_l: g_l_vec,
376 g_u: g_u_vec,
377 eval_f,
378 eval_g,
379 eval_grad_f,
380 eval_jac_g,
381 eval_h,
382 intermediate_cb: None,
383 user_scaling: None,
384 nonlinear_vars: None,
385 last_solve: None,
386 pending_working_set: None,
387 });
388 Box::into_raw(info)
389 }
390}
391
392#[unsafe(no_mangle)]
399pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
400 unsafe {
401 if ipopt_problem.is_null() {
402 return;
403 }
404 drop(Box::from_raw(ipopt_problem));
405 }
406}
407
408unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
409 unsafe {
410 if keyword.is_null() {
411 return None;
412 }
413 CStr::from_ptr(keyword).to_str().ok()
414 }
415}
416
417#[unsafe(no_mangle)]
424pub unsafe extern "C" fn AddIpoptStrOption(
425 ipopt_problem: IpoptProblem,
426 keyword: *const c_char,
427 val: *const c_char,
428) -> Bool {
429 unsafe {
430 if ipopt_problem.is_null() {
431 return FALSE;
432 }
433 let info = &mut *ipopt_problem;
434 let Some(k) = keyword_str(keyword) else {
435 return FALSE;
436 };
437 if val.is_null() {
438 return FALSE;
439 }
440 let Ok(v) = CStr::from_ptr(val).to_str() else {
441 return FALSE;
442 };
443 match info.app.options_mut().set_string_value(k, v, true, false) {
444 Ok(_) => TRUE,
445 Err(_) => FALSE,
446 }
447 }
448}
449
450#[unsafe(no_mangle)]
457pub unsafe extern "C" fn AddIpoptNumOption(
458 ipopt_problem: IpoptProblem,
459 keyword: *const c_char,
460 val: Number,
461) -> Bool {
462 unsafe {
463 if ipopt_problem.is_null() {
464 return FALSE;
465 }
466 let info = &mut *ipopt_problem;
467 let Some(k) = keyword_str(keyword) else {
468 return FALSE;
469 };
470 match info
471 .app
472 .options_mut()
473 .set_numeric_value(k, val, true, false)
474 {
475 Ok(_) => TRUE,
476 Err(_) => FALSE,
477 }
478 }
479}
480
481#[unsafe(no_mangle)]
488pub unsafe extern "C" fn AddIpoptIntOption(
489 ipopt_problem: IpoptProblem,
490 keyword: *const c_char,
491 val: Index,
492) -> Bool {
493 unsafe {
494 if ipopt_problem.is_null() {
495 return FALSE;
496 }
497 let info = &mut *ipopt_problem;
498 let Some(k) = keyword_str(keyword) else {
499 return FALSE;
500 };
501 match info.app.options_mut().set_integer_value(
502 k,
503 val as pounce_common::types::Index,
504 true,
505 false,
506 ) {
507 Ok(_) => TRUE,
508 Err(_) => FALSE,
509 }
510 }
511}
512
513#[unsafe(no_mangle)]
527pub unsafe extern "C" fn OpenIpoptOutputFile(
528 ipopt_problem: IpoptProblem,
529 file_name: *const c_char,
530 print_level: c_int,
531) -> Bool {
532 unsafe {
533 if ipopt_problem.is_null() || file_name.is_null() {
534 return FALSE;
535 }
536 let info = &mut *ipopt_problem;
537 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
538 return FALSE;
539 };
540 if info.app.open_output_file(fname, print_level) {
541 TRUE
542 } else {
543 FALSE
544 }
545 }
546}
547
548#[unsafe(no_mangle)]
568pub unsafe extern "C" fn SetIpoptProblemScaling(
569 ipopt_problem: IpoptProblem,
570 obj_scaling: Number,
571 x_scaling: *const Number,
572 g_scaling: *const Number,
573) -> Bool {
574 unsafe {
575 if ipopt_problem.is_null() {
576 return FALSE;
577 }
578 let info = &mut *ipopt_problem;
579 let n = info.n as usize;
580 let m = info.m as usize;
581 let x_vec = if !x_scaling.is_null() && n > 0 {
582 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
583 } else {
584 None
585 };
586 let g_vec = if !g_scaling.is_null() && m > 0 {
587 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
588 } else {
589 None
590 };
591 info.user_scaling = Some(UserScaling {
592 obj_scaling,
593 x_scaling: x_vec,
594 g_scaling: g_vec,
595 });
596 TRUE
597 }
598}
599
600#[allow(clippy::too_many_arguments)]
617#[unsafe(no_mangle)]
618pub unsafe extern "C" fn IpoptSolve(
619 ipopt_problem: IpoptProblem,
620 x: *mut Number,
621 g: *mut Number,
622 obj_val: *mut Number,
623 mult_g: *mut Number,
624 mult_x_L: *mut Number,
625 mult_x_U: *mut Number,
626 user_data: *mut c_void,
627) -> Index {
628 unsafe {
629 if ipopt_problem.is_null() {
630 return ApplicationReturnStatus::InternalError as Index;
631 }
632 (*ipopt_problem).last_solve = None;
640 ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
646 let info = &mut *ipopt_problem;
647 if info.n < 0 || info.m < 0 {
648 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
649 }
650 if info.n > 0 && x.is_null() {
651 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
652 }
653
654 let n_us = info.n as usize;
655 let m_us = info.m as usize;
656 let initial_x = if n_us > 0 {
657 std::slice::from_raw_parts(x, n_us).to_vec()
658 } else {
659 Vec::new()
660 };
661
662 if let Some(working) = info.pending_working_set.take() {
675 let seed_duals = matches!(
676 info.app
677 .options()
678 .get_bool_value("warm_start_init_point", ""),
679 Ok((true, true))
680 );
681 let read_in = |p: *const Number, len: usize| -> Vec<Number> {
682 if seed_duals && !p.is_null() && len > 0 {
683 std::slice::from_raw_parts(p, len).to_vec()
684 } else {
685 vec![0.0; len]
686 }
687 };
688 let lambda_g = read_in(mult_g as *const Number, m_us);
689 let z_l = read_in(mult_x_L as *const Number, n_us);
690 let z_u = read_in(mult_x_U as *const Number, n_us);
691 let lambda_x = z_l.iter().zip(&z_u).map(|(l, u)| l - u).collect();
696 info.app
697 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
698 x: initial_x.clone(),
699 lambda_g,
700 lambda_x,
701 working: Some(working),
702 });
703 }
704
705 let (lower_inf, upper_inf, zero_fixed_grad_lag) =
706 bridge_violation_options(info.app.options());
707 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
708 n: info.n,
709 m: info.m,
710 nele_jac: info.nele_jac,
711 nele_hess: info.nele_hess,
712 index_style: info.index_style,
713 x_l: info.x_l.clone(),
714 x_u: info.x_u.clone(),
715 g_l: info.g_l.clone(),
716 g_u: info.g_u.clone(),
717 initial_x,
718 start_lambda: mult_g as *const Number,
719 start_z_l: mult_x_L as *const Number,
720 start_z_u: mult_x_U as *const Number,
721 eval_f: info.eval_f,
722 eval_grad_f: info.eval_grad_f,
723 eval_g: info.eval_g,
724 eval_jac_g: info.eval_jac_g,
725 eval_h: info.eval_h,
726 user_data,
727 intermediate_cb: info.intermediate_cb,
728 user_scaling: info.user_scaling.clone(),
729 nonlinear_vars: info.nonlinear_vars.clone(),
730 lower_inf,
731 upper_inf,
732 zero_fixed_grad_lag,
733 final_status: None,
734 final_x: vec![0.0; n_us],
735 final_z_l: vec![0.0; n_us],
736 final_z_u: vec![0.0; n_us],
737 final_g: vec![0.0; m_us],
738 final_lambda: vec![0.0; m_us],
739 final_obj: 0.0,
740 }));
741
742 let feral_cfg = feral_config_from_options(info.app.options());
752 let ma57_cfg = ma57_config_from_options(info.app.options(), "resto.");
755 let bff_mint = move || -> InnerBackendFactoryFactory {
756 let feral_cfg = feral_cfg.clone();
757 let ma57_cfg = ma57_cfg.clone();
758 Box::new(move || default_backend_factory(feral_cfg.clone(), ma57_cfg.clone()))
759 };
760 let resto_provider = make_default_restoration_factory_provider(
761 RestoAlgorithmBuilder::new(),
762 info.app.algorithm_builder_from_options(),
763 bff_mint,
764 );
765 info.app.set_restoration_factory_provider(resto_provider);
766
767 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
768 info.app.defer_end_verdict();
775 let status = info.app.optimize_tnlp(bridge_for_solve);
776 let stats = info.app.statistics();
777 let narrate = pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
792 let ladder = run_second_opinion_ladder(
793 &mut info.app,
794 bridge.clone() as Rc<RefCell<dyn TNLP>>,
795 status,
796 stats,
797 &mut |line| {
798 if narrate {
799 eprintln!("{line}");
800 }
801 },
802 );
803 let status = ladder.status;
804 let bridge_ref = bridge.borrow();
805 info.last_solve = Some(LastSolve {
806 stats: ladder.statistics.clone(),
807 status,
808 linear_solver: info.app.linear_solver_summary(),
809 final_x: bridge_ref.final_x.clone(),
810 final_lambda: bridge_ref.final_lambda.clone(),
811 final_obj: bridge_ref.final_obj,
812 });
813 if !x.is_null() && n_us > 0 {
814 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
815 }
816 if !g.is_null() && m_us > 0 {
817 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
818 }
819 if !obj_val.is_null() {
820 *obj_val = bridge_ref.final_obj;
821 }
822 if !mult_g.is_null() && m_us > 0 {
823 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
824 }
825 if !mult_x_L.is_null() && n_us > 0 {
826 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
827 }
828 if !mult_x_U.is_null() && n_us > 0 {
829 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
830 }
831 status as Index
832 })
833 }
834}
835
836#[unsafe(no_mangle)]
842pub unsafe extern "C" fn SetIntermediateCallback(
843 ipopt_problem: IpoptProblem,
844 intermediate_cb: Option<Intermediate_CB>,
845) -> Bool {
846 unsafe {
847 if ipopt_problem.is_null() {
848 return FALSE;
849 }
850 let info = &mut *ipopt_problem;
851 info.intermediate_cb = intermediate_cb;
852 TRUE
853 }
854}
855
856#[allow(clippy::too_many_arguments)]
901#[unsafe(no_mangle)]
902pub unsafe extern "C" fn GetIpoptCurrentIterate(
903 ipopt_problem: IpoptProblem,
904 scaled: Bool,
905 n: Index,
906 x: *mut Number,
907 z_l: *mut Number,
908 z_u: *mut Number,
909 m: Index,
910 g: *mut Number,
911 lambda: *mut Number,
912) -> Bool {
913 unsafe {
914 if ipopt_problem.is_null() {
915 return FALSE;
916 }
917 let info = &*ipopt_problem;
918 if n != info.n || m != info.m {
919 return FALSE;
920 }
921 if let Some(r) = ip_intermediate::with_current_user_iterate(|it| {
924 let scaled_it;
925 let it = if scaled != FALSE {
926 scaled_it = pounce_nlp::live_iterate::scaled_iterate(it);
927 &scaled_it
928 } else {
929 it
930 };
931 let n_us = n as usize;
932 let m_us = m as usize;
933 let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
934 if dst.is_null() || len == 0 {
935 return true;
936 }
937 if src.len() != len {
938 return false;
939 }
940 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
941 true
942 };
943 copy(&it.x, x, n_us)
944 && copy(&it.z_l, z_l, n_us)
945 && copy(&it.z_u, z_u, n_us)
946 && copy(&it.g, g, m_us)
947 && copy(&it.lambda, lambda, m_us)
948 }) {
949 return if r { TRUE } else { FALSE };
950 }
951 let result = ip_intermediate::with_current(|ctx| {
952 let curr = {
961 let data = ctx.data.borrow();
962 match data.curr.as_ref() {
963 Some(curr) => curr.clone(),
964 None => return false,
965 }
966 };
967 let n_us = n as usize;
968 let m_us = m as usize;
969 let d = ctx.nlp.borrow().variable_scaling();
980 let d = d.as_deref();
981 if !x.is_null() && n_us > 0 {
982 let mut full_x = ctx.nlp.borrow().lift_x_to_full(&*curr.x);
983 if full_x.len() != n_us {
984 return false;
985 }
986 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut full_x, &mut [], &mut []);
987 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
988 }
989 if !z_l.is_null() && n_us > 0 {
990 let mut full = ctx.nlp.borrow().finalize_solution_z_l(&*curr.z_l);
991 if full.len() != n_us {
992 return false;
993 }
994 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut full, &mut []);
995 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
996 }
997 if !z_u.is_null() && n_us > 0 {
998 let mut full = ctx.nlp.borrow().finalize_solution_z_u(&*curr.z_u);
999 if full.len() != n_us {
1000 return false;
1001 }
1002 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut [], &mut full);
1003 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
1004 }
1005 if !g.is_null() && m_us > 0 {
1006 let (c, d) = {
1009 let cq = ctx.cq.borrow();
1010 (cq.curr_c(), cq.curr_d())
1011 };
1012 let full = ctx.nlp.borrow().pack_g_for_user(&*c, &*d);
1013 if full.len() != m_us {
1014 return false;
1015 }
1016 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
1017 }
1018 if !lambda.is_null() && m_us > 0 {
1019 let full = ctx
1020 .nlp
1021 .borrow()
1022 .finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
1023 if full.len() != m_us {
1024 return false;
1025 }
1026 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
1027 }
1028 true
1029 });
1030 if result.unwrap_or(false) { TRUE } else { FALSE }
1031 }
1032}
1033
1034#[allow(clippy::too_many_arguments)]
1068#[unsafe(no_mangle)]
1069pub unsafe extern "C" fn GetIpoptCurrentViolations(
1070 ipopt_problem: IpoptProblem,
1071 scaled: Bool,
1072 n: Index,
1073 x_l_violation: *mut Number,
1074 x_u_violation: *mut Number,
1075 compl_x_l: *mut Number,
1076 compl_x_u: *mut Number,
1077 grad_lag_x: *mut Number,
1078 m: Index,
1079 nlp_constraint_violation: *mut Number,
1080 compl_g: *mut Number,
1081) -> Bool {
1082 unsafe {
1083 if ipopt_problem.is_null() {
1084 return FALSE;
1085 }
1086 let info = &*ipopt_problem;
1087 if n != info.n || m != info.m {
1088 return FALSE;
1089 }
1090 let n_us = n as usize;
1091 let m_us = m as usize;
1092 let Some(it) = ip_intermediate::with_current_user_iterate(|it| it.clone()) else {
1093 return FALSE;
1094 };
1095 let bridge = CURRENT_BRIDGE.with(|c| c.get());
1096 if bridge.is_null() {
1097 return FALSE;
1098 }
1099 let bridge = &*bridge;
1100 if it.x.len() != n_us
1101 || it.z_l.len() != n_us
1102 || it.z_u.len() != n_us
1103 || it.g.len() != m_us
1104 || it.lambda.len() != m_us
1105 {
1106 return FALSE;
1107 }
1108 let grad_lag = if grad_lag_x.is_null() || n_us == 0 {
1109 None
1110 } else if it.grad_lag_x.len() == n_us {
1111 Some(it.grad_lag_x.clone())
1114 } else {
1115 match bridge.user_grad_lag(&it) {
1116 Some(g) => Some(g),
1117 None => return FALSE,
1118 }
1119 };
1120 let bounds = pounce_nlp::live_iterate::DeclaredBounds {
1121 x_l: &bridge.x_l,
1122 x_u: &bridge.x_u,
1123 g_l: &bridge.g_l,
1124 g_u: &bridge.g_u,
1125 lower_inf: bridge.lower_inf,
1126 upper_inf: bridge.upper_inf,
1127 };
1128 let violations = Some(pounce_nlp::live_iterate::current_violations(
1129 &it,
1130 &bounds,
1131 grad_lag,
1132 bridge.zero_fixed_grad_lag,
1133 scaled != FALSE,
1134 ));
1135 let Some(v) = violations else {
1136 return FALSE;
1137 };
1138 let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
1139 if dst.is_null() || len == 0 {
1140 return true;
1141 }
1142 if src.len() != len {
1143 return false;
1144 }
1145 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
1146 true
1147 };
1148 let ok = copy(&v.x_l_violation, x_l_violation, n_us)
1149 && copy(&v.x_u_violation, x_u_violation, n_us)
1150 && copy(&v.compl_x_l, compl_x_l, n_us)
1151 && copy(&v.compl_x_u, compl_x_u, n_us)
1152 && copy(&v.grad_lag_x, grad_lag_x, n_us)
1153 && copy(&v.constraint_violation, nlp_constraint_violation, m_us)
1154 && copy(&v.compl_g, compl_g, m_us);
1155 if ok { TRUE } else { FALSE }
1156 }
1157}
1158
1159#[unsafe(no_mangle)]
1167pub unsafe extern "C" fn GetIpoptVersion(
1168 major: *mut c_int,
1169 minor: *mut c_int,
1170 release: *mut c_int,
1171) {
1172 unsafe {
1173 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1178 if !major.is_null() {
1179 *major = mj;
1180 }
1181 if !minor.is_null() {
1182 *minor = mn;
1183 }
1184 if !release.is_null() {
1185 *release = pt;
1186 }
1187 }
1188}
1189
1190fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
1191 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
1192 (
1193 it.next().unwrap_or(0),
1194 it.next().unwrap_or(0),
1195 it.next().unwrap_or(0),
1196 )
1197}
1198
1199#[unsafe(no_mangle)]
1216pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
1217 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
1218}
1219
1220#[unsafe(no_mangle)]
1227pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
1228 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
1229}
1230
1231#[unsafe(no_mangle)]
1238pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
1239 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1240}
1241
1242#[unsafe(no_mangle)]
1249pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1250 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1251}
1252
1253#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1260 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1261}
1262
1263unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1264where
1265 F: FnOnce(&SolveStatistics) -> T,
1266{
1267 unsafe {
1268 if ipopt_problem.is_null() {
1269 return None;
1270 }
1271 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1272 }
1273}
1274
1275#[unsafe(no_mangle)]
1290pub unsafe extern "C" fn GetPounceRestorationStats(
1291 ipopt_problem: IpoptProblem,
1292 calls: *mut Index,
1293 inner_iters: *mut Index,
1294 outer_iters: *mut Index,
1295 wall_secs: *mut Number,
1296) {
1297 unsafe {
1298 let stats = last_stat(ipopt_problem, |s| {
1299 (
1300 s.restoration_calls,
1301 s.restoration_inner_iters,
1302 s.restoration_outer_iters,
1303 s.restoration_wall_secs,
1304 )
1305 });
1306 let (c, i, o, w) = stats.unwrap_or((0, 0, 0, 0.0));
1307 if !calls.is_null() {
1308 *calls = c;
1309 }
1310 if !inner_iters.is_null() {
1311 *inner_iters = i;
1312 }
1313 if !outer_iters.is_null() {
1314 *outer_iters = o;
1315 }
1316 if !wall_secs.is_null() {
1317 *wall_secs = w;
1318 }
1319 }
1320}
1321
1322#[unsafe(no_mangle)]
1338pub unsafe extern "C" fn GetPounceFdHessianStats(
1339 ipopt_problem: IpoptProblem,
1340 pattern_used: *mut Index,
1341 nnz: *mut Index,
1342 n: *mut Index,
1343 groups: *mut Index,
1344 rho_max: *mut Index,
1345 coloring_fell_back: *mut Index,
1346 objective_clique_widened: *mut Index,
1347) {
1348 unsafe {
1349 let stats = last_stat(ipopt_problem, |s| {
1350 (
1351 s.fd_hessian_pattern_used,
1352 s.fd_hessian_nnz,
1353 s.fd_hessian_n,
1354 s.fd_hessian_groups,
1355 s.fd_hessian_rho_max,
1356 if s.fd_hessian_coloring_fell_back {
1357 1
1358 } else {
1359 0
1360 },
1361 if s.fd_hessian_objective_clique_widened {
1362 1
1363 } else {
1364 0
1365 },
1366 )
1367 });
1368 let (p, nz, cols, g, r, f, w) = stats.unwrap_or((-1, 0, 0, 0, 0, 0, 0));
1369 if !pattern_used.is_null() {
1370 *pattern_used = p;
1371 }
1372 if !nnz.is_null() {
1373 *nnz = nz;
1374 }
1375 if !n.is_null() {
1376 *n = cols;
1377 }
1378 if !groups.is_null() {
1379 *groups = g;
1380 }
1381 if !rho_max.is_null() {
1382 *rho_max = r;
1383 }
1384 if !coloring_fell_back.is_null() {
1385 *coloring_fell_back = f;
1386 }
1387 if !objective_clique_widened.is_null() {
1388 *objective_clique_widened = w;
1389 }
1390 }
1391}
1392
1393#[repr(C)]
1399#[derive(Debug, Clone, Copy)]
1400pub struct PounceLinearSolverStats {
1401 pub solver_name: [c_char; 32],
1402 pub n_factors: Index,
1403 pub n_pattern_reuse: Index,
1404 pub n_pattern_changes: Index,
1405 pub max_fill_ratio: Number,
1406 pub min_abs_pivot: Number,
1407 pub max_abs_pivot: Number,
1408 pub last_inertia_positive: Index,
1409 pub last_inertia_negative: Index,
1410 pub last_inertia_zero: Index,
1411 pub last_nnz_a: Index,
1412 pub last_nnz_l: Index,
1413}
1414
1415#[unsafe(no_mangle)]
1428pub unsafe extern "C" fn GetPounceLinearSolverStats(
1429 ipopt_problem: IpoptProblem,
1430 stats: *mut PounceLinearSolverStats,
1431) -> Bool {
1432 unsafe {
1433 if ipopt_problem.is_null() || stats.is_null() {
1434 return FALSE;
1435 }
1436 let Some(summary) = (*ipopt_problem)
1437 .last_solve
1438 .as_ref()
1439 .and_then(|ls| ls.linear_solver.as_ref())
1440 else {
1441 return FALSE;
1442 };
1443 let count = |v: u64| Index::try_from(v).unwrap_or(Index::MAX);
1447 let size = |x: usize| Index::try_from(x).unwrap_or(Index::MAX);
1448 let opt_size = |v: Option<usize>| v.map_or(-1, size);
1449 let inertia = summary.last_inertia;
1450 let mut out = PounceLinearSolverStats {
1451 solver_name: [0; 32],
1452 n_factors: count(summary.n_factors),
1453 n_pattern_reuse: count(summary.n_pattern_reuse),
1454 n_pattern_changes: count(summary.n_pattern_changes),
1455 max_fill_ratio: summary.max_fill_ratio.unwrap_or(Number::NAN),
1456 min_abs_pivot: summary.min_abs_pivot.unwrap_or(Number::NAN),
1457 max_abs_pivot: summary.max_abs_pivot.unwrap_or(Number::NAN),
1458 last_inertia_positive: inertia.map_or(-1, |(p, _, _)| size(p)),
1459 last_inertia_negative: inertia.map_or(-1, |(_, n, _)| size(n)),
1460 last_inertia_zero: inertia.map_or(-1, |(_, _, z)| size(z)),
1461 last_nnz_a: opt_size(summary.last_nnz_a),
1462 last_nnz_l: opt_size(summary.last_nnz_l),
1463 };
1464 let name = summary.solver_name.as_bytes();
1468 let keep = name.len().min(out.solver_name.len() - 1);
1469 for (slot, b) in out.solver_name.iter_mut().zip(&name[..keep]) {
1470 *slot = *b as c_char;
1471 }
1472 *stats = out;
1473 TRUE
1474 }
1475}
1476
1477thread_local! {
1478 static DEFAULT_REGISTRY: Rc<pounce_common::reg_options::RegisteredOptions> =
1484 Rc::clone(IpoptApplication::new().registered_options());
1485}
1486
1487#[unsafe(no_mangle)]
1506pub unsafe extern "C" fn GetPounceOptionType(
1507 ipopt_problem: IpoptProblem,
1508 keyword: *const c_char,
1509) -> c_int {
1510 unsafe {
1511 if keyword.is_null() {
1512 return 0;
1513 }
1514 let Ok(name) = CStr::from_ptr(keyword).to_str() else {
1515 return 0;
1516 };
1517 let registered = if ipopt_problem.is_null() {
1518 DEFAULT_REGISTRY.with(|r| r.get_option(name))
1519 } else {
1520 (*ipopt_problem).app.registered_options().get_option(name)
1521 };
1522 let Some(opt) = registered else {
1523 return 0;
1524 };
1525 match opt.option_type {
1526 OptionType::OT_Number => 1,
1527 OptionType::OT_Integer => 2,
1528 OptionType::OT_String => 3,
1529 OptionType::OT_Unknown => 0,
1530 }
1531 }
1532}
1533
1534fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1544 use pounce_qp::BoundStatus::*;
1545 match s {
1546 Inactive => POUNCE_WS_INACTIVE,
1547 AtLower => POUNCE_WS_AT_LOWER,
1548 AtUpper => POUNCE_WS_AT_UPPER,
1549 Fixed => POUNCE_WS_FIXED_OR_EQ,
1550 }
1551}
1552
1553fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1554 use pounce_qp::BoundStatus::*;
1555 match v {
1556 POUNCE_WS_INACTIVE => Some(Inactive),
1557 POUNCE_WS_AT_LOWER => Some(AtLower),
1558 POUNCE_WS_AT_UPPER => Some(AtUpper),
1559 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1560 _ => None,
1561 }
1562}
1563
1564fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1565 use pounce_qp::ConsStatus::*;
1566 match s {
1567 Inactive => POUNCE_WS_INACTIVE,
1568 AtLower => POUNCE_WS_AT_LOWER,
1569 AtUpper => POUNCE_WS_AT_UPPER,
1570 Equality => POUNCE_WS_FIXED_OR_EQ,
1571 }
1572}
1573
1574fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1575 use pounce_qp::ConsStatus::*;
1576 match v {
1577 POUNCE_WS_INACTIVE => Some(Inactive),
1578 POUNCE_WS_AT_LOWER => Some(AtLower),
1579 POUNCE_WS_AT_UPPER => Some(AtUpper),
1580 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1581 _ => None,
1582 }
1583}
1584
1585#[unsafe(no_mangle)]
1604pub unsafe extern "C" fn IpoptGetWorkingSet(
1605 ipopt_problem: IpoptProblem,
1606 bound_status_out: *mut IpoptBoundStatus,
1607 cons_status_out: *mut IpoptConsStatus,
1608) -> Bool {
1609 unsafe {
1610 if ipopt_problem.is_null() {
1611 return FALSE;
1612 }
1613 let info = &*ipopt_problem;
1614 let ws = match info.app.last_sqp_working_set() {
1615 Some(w) => w,
1616 None => return FALSE,
1617 };
1618 let n = info.x_l.len();
1626 let m = info.g_l.len();
1627 if ws.constraints.len() != m || ws.bounds.len() != n {
1628 return FALSE;
1631 }
1632 if !bound_status_out.is_null() {
1633 for (i, &s) in ws.bounds.iter().enumerate() {
1634 *bound_status_out.add(i) = bound_status_to_int(s);
1635 }
1636 }
1637 if !cons_status_out.is_null() {
1638 for (i, &s) in ws.constraints.iter().enumerate() {
1639 *cons_status_out.add(i) = cons_status_to_int(s);
1640 }
1641 }
1642 TRUE
1643 }
1644}
1645
1646#[unsafe(no_mangle)]
1663pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1664 ipopt_problem: IpoptProblem,
1665 bound_status_in: *const IpoptBoundStatus,
1666 cons_status_in: *const IpoptConsStatus,
1667) -> Bool {
1668 unsafe {
1669 if ipopt_problem.is_null() {
1670 return FALSE;
1671 }
1672 if bound_status_in.is_null() && cons_status_in.is_null() {
1673 return FALSE;
1674 }
1675 let info = &mut *ipopt_problem;
1676 let n = info.n.max(0) as usize;
1677 let m = info.m.max(0) as usize;
1678 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1702 if !bound_status_in.is_null() {
1703 for i in 0..n {
1707 let v = *bound_status_in.add(i);
1708 let Some(s) = int_to_bound_status(v) else {
1709 return FALSE;
1710 };
1711 let lo_finite = info.x_l[i] > NLP_LOWER_BOUND_INF;
1712 let hi_finite = info.x_u[i] < NLP_UPPER_BOUND_INF;
1713 let consistent = match s {
1714 pounce_qp::BoundStatus::Fixed => info.x_l[i] == info.x_u[i],
1715 pounce_qp::BoundStatus::AtLower => lo_finite,
1716 pounce_qp::BoundStatus::AtUpper => hi_finite,
1717 pounce_qp::BoundStatus::Inactive => true,
1718 };
1719 if !consistent {
1720 return FALSE;
1721 }
1722 }
1723 for (i, b) in bounds.iter_mut().enumerate() {
1724 if let Some(s) = int_to_bound_status(*bound_status_in.add(i)) {
1726 *b = s;
1727 }
1728 }
1729 }
1730 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1731 if !cons_status_in.is_null() {
1732 for i in 0..m {
1733 let v = *cons_status_in.add(i);
1734 let Some(s) = int_to_cons_status(v) else {
1735 return FALSE;
1736 };
1737 let lo_finite = info.g_l[i] > NLP_LOWER_BOUND_INF;
1738 let hi_finite = info.g_u[i] < NLP_UPPER_BOUND_INF;
1739 let consistent = match s {
1740 pounce_qp::ConsStatus::Equality => {
1741 lo_finite && hi_finite && info.g_l[i] == info.g_u[i]
1742 }
1743 pounce_qp::ConsStatus::AtLower => lo_finite,
1744 pounce_qp::ConsStatus::AtUpper => hi_finite,
1745 pounce_qp::ConsStatus::Inactive => true,
1746 };
1747 if !consistent {
1748 return FALSE;
1749 }
1750 }
1751 for (i, c) in constraints.iter_mut().enumerate() {
1752 if let Some(s) = int_to_cons_status(*cons_status_in.add(i)) {
1753 *c = s;
1754 }
1755 }
1756 }
1757 info.pending_working_set = Some(pounce_qp::WorkingSet {
1769 bounds,
1770 constraints,
1771 });
1772 TRUE
1773 }
1774}
1775
1776#[unsafe(no_mangle)]
1816pub unsafe extern "C" fn IpoptSetNonlinearVariables(
1817 ipopt_problem: IpoptProblem,
1818 num_nonlin_vars: Index,
1819 pos_nonlin_vars: *const Index,
1820) -> Bool {
1821 unsafe {
1822 if ipopt_problem.is_null() {
1823 return FALSE;
1824 }
1825 let info = &mut *ipopt_problem;
1826 if num_nonlin_vars < 0 || num_nonlin_vars > info.n {
1827 return FALSE;
1828 }
1829 if num_nonlin_vars > 0 && pos_nonlin_vars.is_null() {
1830 return FALSE;
1831 }
1832 let offset = if info.index_style == 1 { 1 } else { 0 };
1833 let raw = if num_nonlin_vars == 0 {
1834 &[][..]
1835 } else {
1836 std::slice::from_raw_parts(pos_nonlin_vars, num_nonlin_vars as usize)
1837 };
1838 for &p in raw {
1841 let zero_based = p - offset;
1842 if zero_based < 0 || zero_based >= info.n {
1843 return FALSE;
1844 }
1845 }
1846 info.nonlinear_vars = Some(raw.to_vec());
1847 TRUE
1848 }
1849}
1850
1851#[unsafe(no_mangle)]
1858pub unsafe extern "C" fn IpoptClearNonlinearVariables(ipopt_problem: IpoptProblem) -> Bool {
1859 unsafe {
1860 if ipopt_problem.is_null() {
1861 return FALSE;
1862 }
1863 (*ipopt_problem).nonlinear_vars = None;
1864 TRUE
1865 }
1866}
1867
1868#[unsafe(no_mangle)]
1875pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1876 unsafe {
1877 if ipopt_problem.is_null() {
1878 return FALSE;
1879 }
1880 (*ipopt_problem).pending_working_set = None;
1881 (*ipopt_problem).app.clear_sqp_warm_start();
1882 TRUE
1883 }
1884}
1885
1886#[allow(clippy::too_many_arguments)]
1902#[unsafe(no_mangle)]
1903pub unsafe extern "C" fn IpoptSolveWarmStart(
1904 ipopt_problem: IpoptProblem,
1905 x: *mut Number,
1906 g: *mut Number,
1907 obj_val: *mut Number,
1908 mult_g: *mut Number,
1909 mult_x_L: *mut Number,
1910 mult_x_U: *mut Number,
1911 bound_status_in: *const IpoptBoundStatus,
1912 cons_status_in: *const IpoptConsStatus,
1913 bound_status_out: *mut IpoptBoundStatus,
1914 cons_status_out: *mut IpoptConsStatus,
1915 user_data: *mut c_void,
1916) -> Index {
1917 if ipopt_problem.is_null() {
1918 return ApplicationReturnStatus::InternalError as Index;
1919 }
1920 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
1924 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1929 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1930 }
1931 let status = IpoptSolve(
1932 ipopt_problem,
1933 x,
1934 g,
1935 obj_val,
1936 mult_g,
1937 mult_x_L,
1938 mult_x_U,
1939 user_data,
1940 );
1941 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1942 status
1943 })
1944}
1945
1946thread_local! {
1947 static CURRENT_BRIDGE: std::cell::Cell<*const CCallbackTnlp> =
1950 const { std::cell::Cell::new(std::ptr::null()) };
1951}
1952
1953pub(crate) fn bridge_violation_options(
1956 o: &pounce_common::options_list::OptionsList,
1957) -> (Number, Number, bool) {
1958 let num = |k: &str, d: Number| {
1959 o.get_numeric_value(k, "")
1960 .ok()
1961 .and_then(|(v, f)| f.then_some(v))
1962 .unwrap_or(d)
1963 };
1964 let relax = matches!(
1965 o.get_string_value("fixed_variable_treatment", "")
1966 .ok()
1967 .and_then(|(v, f)| f.then_some(v))
1968 .as_deref(),
1969 Some("relax_bounds")
1970 );
1971 (
1972 num("nlp_lower_bound_inf", NLP_LOWER_BOUND_INF),
1973 num("nlp_upper_bound_inf", NLP_UPPER_BOUND_INF),
1974 !relax,
1975 )
1976}
1977
1978struct BridgeGuard {
1979 prev: *const CCallbackTnlp,
1980}
1981
1982impl BridgeGuard {
1983 fn install(bridge: &CCallbackTnlp) -> Self {
1984 let prev = CURRENT_BRIDGE.with(|c| c.replace(bridge as *const _));
1985 Self { prev }
1986 }
1987}
1988
1989impl Drop for BridgeGuard {
1990 fn drop(&mut self) {
1991 CURRENT_BRIDGE.with(|c| c.set(self.prev));
1992 }
1993}
1994
1995impl CCallbackTnlp {
1996 fn user_grad_lag(&self, it: &ip_intermediate::UserIterate) -> Option<Vec<Number>> {
2000 let n = self.n as usize;
2001 let cb = self.eval_grad_f?;
2002 let mut grad = vec![0.0; n];
2003 let x = it.x.as_ptr() as *mut Number;
2004 if unsafe { cb(self.n, x, TRUE, grad.as_mut_ptr(), self.user_data) } == FALSE {
2005 return None;
2006 }
2007 let nnz = self.nele_jac.max(0) as usize;
2008 let (mut rows, mut cols, mut vals) = (vec![0; nnz], vec![0; nnz], vec![0.0; nnz]);
2009 if self.m > 0 && nnz > 0 {
2010 let jac = self.eval_jac_g?;
2011 let structure = unsafe {
2012 jac(
2013 self.n,
2014 x,
2015 FALSE,
2016 self.m,
2017 self.nele_jac,
2018 rows.as_mut_ptr(),
2019 cols.as_mut_ptr(),
2020 std::ptr::null_mut(),
2021 self.user_data,
2022 )
2023 };
2024 let values = unsafe {
2025 jac(
2026 self.n,
2027 x,
2028 FALSE,
2029 self.m,
2030 self.nele_jac,
2031 std::ptr::null_mut(),
2032 std::ptr::null_mut(),
2033 vals.as_mut_ptr(),
2034 self.user_data,
2035 )
2036 };
2037 if structure == FALSE || values == FALSE {
2038 return None;
2039 }
2040 }
2041 let off = if self.index_style == 1 { 1 } else { 0 };
2042 let to_usize = |v: &[Index]| -> Vec<usize> {
2043 v.iter()
2044 .map(|&k| usize::try_from(k - off).unwrap_or(usize::MAX))
2045 .collect()
2046 };
2047 Some(pounce_nlp::live_iterate::lagrangian_gradient(
2048 it,
2049 &grad,
2050 &to_usize(&rows),
2051 &to_usize(&cols),
2052 &vals,
2053 ))
2054 }
2055}
2056
2057pub(crate) struct CCallbackTnlp {
2068 pub(crate) n: Index,
2069 pub(crate) m: Index,
2070 pub(crate) nele_jac: Index,
2071 pub(crate) nele_hess: Index,
2072 pub(crate) index_style: Index,
2073 pub(crate) x_l: Vec<Number>,
2074 pub(crate) x_u: Vec<Number>,
2075 pub(crate) g_l: Vec<Number>,
2076 pub(crate) g_u: Vec<Number>,
2077 pub(crate) initial_x: Vec<Number>,
2078 pub(crate) start_lambda: *const Number,
2089 pub(crate) start_z_l: *const Number,
2092 pub(crate) start_z_u: *const Number,
2095 pub(crate) eval_f: Option<Eval_F_CB>,
2096 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
2097 pub(crate) eval_g: Option<Eval_G_CB>,
2098 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
2099 pub(crate) eval_h: Option<Eval_H_CB>,
2100 pub(crate) user_data: *mut c_void,
2101 pub(crate) intermediate_cb: Option<Intermediate_CB>,
2104 pub(crate) user_scaling: Option<UserScaling>,
2106 pub(crate) nonlinear_vars: Option<Vec<Index>>,
2109 pub(crate) lower_inf: Number,
2113 pub(crate) upper_inf: Number,
2114 pub(crate) zero_fixed_grad_lag: bool,
2118 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
2119 pub(crate) final_x: Vec<Number>,
2120 pub(crate) final_z_l: Vec<Number>,
2121 pub(crate) final_z_u: Vec<Number>,
2122 pub(crate) final_g: Vec<Number>,
2123 pub(crate) final_lambda: Vec<Number>,
2124 pub(crate) final_obj: Number,
2125}
2126
2127impl TNLP for CCallbackTnlp {
2128 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2129 Some(NlpInfo {
2130 n: self.n as pounce_common::types::Index,
2131 m: self.m as pounce_common::types::Index,
2132 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
2133 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
2134 index_style: if self.index_style == 1 {
2135 IndexStyle::Fortran
2136 } else {
2137 IndexStyle::C
2138 },
2139 })
2140 }
2141
2142 fn get_number_of_nonlinear_variables(&mut self) -> pounce_common::types::Index {
2146 match &self.nonlinear_vars {
2147 Some(v) => v.len() as pounce_common::types::Index,
2148 None => -1,
2149 }
2150 }
2151
2152 fn get_list_of_nonlinear_variables(
2153 &mut self,
2154 pos_nonlin_vars: &mut [pounce_common::types::Index],
2155 ) -> bool {
2156 let Some(v) = self.nonlinear_vars.as_ref() else {
2157 return false;
2158 };
2159 if v.len() != pos_nonlin_vars.len() {
2160 return false;
2161 }
2162 pos_nonlin_vars.copy_from_slice(v);
2163 true
2164 }
2165
2166 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2167 if !self.x_l.is_empty() {
2168 b.x_l.copy_from_slice(&self.x_l);
2169 }
2170 if !self.x_u.is_empty() {
2171 b.x_u.copy_from_slice(&self.x_u);
2172 }
2173 if !self.g_l.is_empty() {
2174 b.g_l.copy_from_slice(&self.g_l);
2175 }
2176 if !self.g_u.is_empty() {
2177 b.g_u.copy_from_slice(&self.g_u);
2178 }
2179 true
2180 }
2181
2182 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2183 if !self.initial_x.is_empty() {
2184 sp.x.copy_from_slice(&self.initial_x);
2185 }
2186 let copy_in = |dst: &mut [Number], src: *const Number| {
2199 if !src.is_null() && !dst.is_empty() {
2200 dst.copy_from_slice(unsafe { std::slice::from_raw_parts(src, dst.len()) });
2204 }
2205 };
2206 if sp.init_z {
2207 copy_in(sp.z_l, self.start_z_l);
2208 copy_in(sp.z_u, self.start_z_u);
2209 }
2210 if sp.init_lambda {
2211 copy_in(sp.lambda, self.start_lambda);
2212 }
2213 true
2214 }
2215
2216 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
2217 let Some(s) = self.user_scaling.as_ref() else {
2218 return false;
2219 };
2220 *req.obj_scaling = s.obj_scaling;
2221 if let Some(x) = s.x_scaling.as_ref() {
2222 if x.len() == req.x_scaling.len() {
2223 req.x_scaling.copy_from_slice(x);
2224 *req.use_x_scaling = true;
2225 }
2226 } else {
2227 *req.use_x_scaling = false;
2228 }
2229 if let Some(g) = s.g_scaling.as_ref() {
2230 if g.len() == req.g_scaling.len() {
2231 req.g_scaling.copy_from_slice(g);
2232 *req.use_g_scaling = true;
2233 }
2234 } else {
2235 *req.use_g_scaling = false;
2236 }
2237 true
2238 }
2239
2240 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
2241 let cb = self.eval_f?;
2242 let mut obj = 0.0;
2243 let ok = unsafe {
2244 cb(
2245 self.n,
2246 x.as_ptr() as *mut Number,
2247 if new_x { TRUE } else { FALSE },
2248 &mut obj,
2249 self.user_data,
2250 )
2251 };
2252 if ok != FALSE { Some(obj) } else { None }
2253 }
2254
2255 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
2256 let Some(cb) = self.eval_grad_f else {
2257 return false;
2258 };
2259 let ok = unsafe {
2260 cb(
2261 self.n,
2262 x.as_ptr() as *mut Number,
2263 if new_x { TRUE } else { FALSE },
2264 grad_f.as_mut_ptr(),
2265 self.user_data,
2266 )
2267 };
2268 ok != FALSE
2269 }
2270
2271 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
2272 if self.m == 0 {
2273 return true;
2274 }
2275 let Some(cb) = self.eval_g else {
2276 return false;
2277 };
2278 let ok = unsafe {
2279 cb(
2280 self.n,
2281 x.as_ptr() as *mut Number,
2282 if new_x { TRUE } else { FALSE },
2283 self.m,
2284 g.as_mut_ptr(),
2285 self.user_data,
2286 )
2287 };
2288 ok != FALSE
2289 }
2290
2291 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
2292 if self.m == 0 || self.nele_jac == 0 {
2293 return true;
2294 }
2295 let Some(cb) = self.eval_jac_g else {
2296 return false;
2297 };
2298 let x_ptr = x
2299 .map(|s| s.as_ptr() as *mut Number)
2300 .unwrap_or(std::ptr::null_mut());
2301 let ok = match mode {
2302 SparsityRequest::Structure { irow, jcol } => unsafe {
2303 cb(
2304 self.n,
2305 x_ptr,
2306 if new_x { TRUE } else { FALSE },
2307 self.m,
2308 self.nele_jac,
2309 irow.as_mut_ptr(),
2310 jcol.as_mut_ptr(),
2311 std::ptr::null_mut(),
2312 self.user_data,
2313 )
2314 },
2315 SparsityRequest::Values { values } => unsafe {
2316 cb(
2317 self.n,
2318 x_ptr,
2319 if new_x { TRUE } else { FALSE },
2320 self.m,
2321 self.nele_jac,
2322 std::ptr::null_mut(),
2323 std::ptr::null_mut(),
2324 values.as_mut_ptr(),
2325 self.user_data,
2326 )
2327 },
2328 };
2329 ok != FALSE
2330 }
2331
2332 fn eval_h(
2333 &mut self,
2334 x: Option<&[Number]>,
2335 new_x: bool,
2336 obj_factor: Number,
2337 lambda: Option<&[Number]>,
2338 new_lambda: bool,
2339 mode: SparsityRequest<'_>,
2340 ) -> bool {
2341 let Some(cb) = self.eval_h else {
2342 return false;
2343 };
2344 if self.nele_hess == 0 {
2345 return true;
2346 }
2347 let x_ptr = x
2348 .map(|s| s.as_ptr() as *mut Number)
2349 .unwrap_or(std::ptr::null_mut());
2350 let lambda_ptr = lambda
2351 .map(|s| s.as_ptr() as *mut Number)
2352 .unwrap_or(std::ptr::null_mut());
2353 let ok = match mode {
2354 SparsityRequest::Structure { irow, jcol } => unsafe {
2355 cb(
2356 self.n,
2357 x_ptr,
2358 if new_x { TRUE } else { FALSE },
2359 obj_factor,
2360 self.m,
2361 lambda_ptr,
2362 if new_lambda { TRUE } else { FALSE },
2363 self.nele_hess,
2364 irow.as_mut_ptr(),
2365 jcol.as_mut_ptr(),
2366 std::ptr::null_mut(),
2367 self.user_data,
2368 )
2369 },
2370 SparsityRequest::Values { values } => unsafe {
2371 cb(
2372 self.n,
2373 x_ptr,
2374 if new_x { TRUE } else { FALSE },
2375 obj_factor,
2376 self.m,
2377 lambda_ptr,
2378 if new_lambda { TRUE } else { FALSE },
2379 self.nele_hess,
2380 std::ptr::null_mut(),
2381 std::ptr::null_mut(),
2382 values.as_mut_ptr(),
2383 self.user_data,
2384 )
2385 },
2386 };
2387 ok != FALSE
2388 }
2389
2390 fn wants_intermediate_callback(&self) -> bool {
2393 self.intermediate_cb.is_some()
2394 }
2395
2396 fn wants_current_iterate(&self) -> bool {
2400 self.intermediate_cb.is_some()
2401 }
2402
2403 fn intermediate_callback(
2404 &mut self,
2405 stats: pounce_nlp::tnlp::IterStats,
2406 _ip_data: &IpoptData,
2407 _ip_cq: &IpoptCq,
2408 ) -> bool {
2409 let Some(cb) = self.intermediate_cb else {
2410 return true;
2411 };
2412 let _bridge = BridgeGuard::install(self);
2417 let ok = unsafe {
2418 cb(
2419 stats.mode as Index,
2420 stats.iter as Index,
2421 stats.obj_value,
2422 stats.inf_pr,
2423 stats.inf_du,
2424 stats.mu,
2425 stats.d_norm,
2426 stats.regularization_size,
2427 stats.alpha_du,
2428 stats.alpha_pr,
2429 stats.ls_trials as Index,
2430 self.user_data,
2431 )
2432 };
2433 ok != FALSE
2434 }
2435
2436 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2437 self.final_status = Some(sol.status);
2438 if !sol.x.is_empty() {
2439 self.final_x.copy_from_slice(sol.x);
2440 }
2441 if !sol.z_l.is_empty() {
2442 self.final_z_l.copy_from_slice(sol.z_l);
2443 }
2444 if !sol.z_u.is_empty() {
2445 self.final_z_u.copy_from_slice(sol.z_u);
2446 }
2447 if !sol.g.is_empty() {
2448 self.final_g.copy_from_slice(sol.g);
2449 }
2450 if !sol.lambda.is_empty() {
2451 self.final_lambda.copy_from_slice(sol.lambda);
2452 }
2453 self.final_obj = sol.obj_value;
2454 }
2455}
2456
2457#[unsafe(no_mangle)]
2470pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
2471 if ipopt_problem.is_null() {
2472 return FALSE;
2473 }
2474 let info = unsafe { &mut *ipopt_problem };
2475 info.app.enable_iter_history();
2476 TRUE
2477}
2478
2479#[unsafe(no_mangle)]
2500pub unsafe extern "C" fn IpoptWriteSolveReport(
2501 ipopt_problem: IpoptProblem,
2502 path: *const c_char,
2503 detail: *const c_char,
2504) -> Bool {
2505 use pounce_solve_report::{
2506 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
2507 };
2508
2509 ffi_guard(FALSE, || unsafe {
2514 if ipopt_problem.is_null() || path.is_null() {
2515 return FALSE;
2516 }
2517 let info = &*ipopt_problem;
2518 let Some(last) = info.last_solve.as_ref() else {
2519 return FALSE;
2520 };
2521
2522 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
2523 return FALSE;
2524 };
2525
2526 let detail_choice = if detail.is_null() {
2527 ReportDetail::Summary
2528 } else {
2529 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
2530 return FALSE;
2531 };
2532 match ReportDetail::parse(detail_str) {
2533 Ok(d) => d,
2534 Err(_) => return FALSE,
2535 }
2536 };
2537
2538 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
2539 builder.problem.n_variables = info.n;
2540 builder.problem.n_constraints = info.m;
2541 builder.problem.n_objectives = 1;
2542 builder.problem.nnz_jac_g = Some(info.nele_jac);
2543 builder.problem.nnz_h_lag = Some(info.nele_hess);
2544
2545 builder.solution.status = last.status;
2546 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
2547 builder.solution.objective = last.final_obj;
2548 builder.solution.x = last.final_x.clone();
2549 builder.solution.lambda = last.final_lambda.clone();
2550
2551 builder.ingest_stats(&last.stats);
2552 if let Some(linsol) = last.linear_solver.clone() {
2553 builder.set_linear_solver_summary(linsol);
2554 }
2555
2556 let report = builder.finish();
2557 match write_report_file(std::path::Path::new(path_str), &report) {
2558 Ok(_) => TRUE,
2559 Err(_) => FALSE,
2560 }
2561 })
2562}
2563
2564#[cfg(test)]
2565mod tests {
2566 use super::*;
2567 use std::ffi::CString;
2568
2569 unsafe extern "C" fn dummy_eval_f(
2570 _n: Index,
2571 _x: *const Number,
2572 _new_x: Bool,
2573 _obj_value: *mut Number,
2574 _user_data: *mut c_void,
2575 ) -> Bool {
2576 TRUE
2577 }
2578 unsafe extern "C" fn dummy_eval_grad_f(
2579 _n: Index,
2580 _x: *const Number,
2581 _new_x: Bool,
2582 _grad_f: *mut Number,
2583 _user_data: *mut c_void,
2584 ) -> Bool {
2585 TRUE
2586 }
2587
2588 fn create_unconstrained() -> IpoptProblem {
2589 let xl = [-1.0; 4];
2590 let xu = [1.0; 4];
2591 unsafe {
2592 CreateIpoptProblem(
2593 4,
2594 xl.as_ptr(),
2595 xu.as_ptr(),
2596 0,
2597 std::ptr::null(),
2598 std::ptr::null(),
2599 0,
2600 10,
2601 0,
2602 Some(dummy_eval_f),
2603 None,
2604 Some(dummy_eval_grad_f),
2605 None,
2606 None,
2607 )
2608 }
2609 }
2610
2611 #[test]
2612 fn create_succeeds_for_unconstrained_problem() {
2613 let p = create_unconstrained();
2614 assert!(!p.is_null());
2615 unsafe { FreeIpoptProblem(p) };
2616 }
2617
2618 #[test]
2619 fn create_returns_null_on_missing_required_callbacks() {
2620 let xl = [-1.0; 4];
2621 let xu = [1.0; 4];
2622 let p = unsafe {
2623 CreateIpoptProblem(
2624 4,
2625 xl.as_ptr(),
2626 xu.as_ptr(),
2627 0,
2628 std::ptr::null(),
2629 std::ptr::null(),
2630 0,
2631 10,
2632 0,
2633 None, None,
2635 Some(dummy_eval_grad_f),
2636 None,
2637 None,
2638 )
2639 };
2640 assert!(p.is_null());
2641 }
2642
2643 #[test]
2644 fn create_returns_null_on_negative_n() {
2645 let p = unsafe {
2646 CreateIpoptProblem(
2647 -1,
2648 std::ptr::null(),
2649 std::ptr::null(),
2650 0,
2651 std::ptr::null(),
2652 std::ptr::null(),
2653 0,
2654 10,
2655 0,
2656 Some(dummy_eval_f),
2657 None,
2658 Some(dummy_eval_grad_f),
2659 None,
2660 None,
2661 )
2662 };
2663 assert!(p.is_null());
2664 }
2665
2666 #[test]
2667 fn create_returns_null_on_invalid_index_style() {
2668 let xl = [0.0; 1];
2669 let xu = [1.0; 1];
2670 let p = unsafe {
2671 CreateIpoptProblem(
2672 1,
2673 xl.as_ptr(),
2674 xu.as_ptr(),
2675 0,
2676 std::ptr::null(),
2677 std::ptr::null(),
2678 0,
2679 1,
2680 2, Some(dummy_eval_f),
2682 None,
2683 Some(dummy_eval_grad_f),
2684 None,
2685 None,
2686 )
2687 };
2688 assert!(p.is_null());
2689 }
2690
2691 #[test]
2692 fn add_int_option_forwards_to_application() {
2693 let p = create_unconstrained();
2694 let key = CString::new("print_level").unwrap();
2695 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
2696 assert_eq!(ok, TRUE);
2697 let info = unsafe { &*p };
2698 let (level, found) = info
2699 .app
2700 .options()
2701 .get_integer_value("print_level", "")
2702 .unwrap();
2703 assert!(found);
2704 assert_eq!(level, 5);
2705 unsafe { FreeIpoptProblem(p) };
2706 }
2707
2708 #[test]
2709 fn add_str_option_with_invalid_key_returns_false() {
2710 let p = create_unconstrained();
2711 let key = CString::new("totally_unknown_option").unwrap();
2712 let val = CString::new("yes").unwrap();
2713 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2714 assert_eq!(ok, FALSE);
2715 unsafe { FreeIpoptProblem(p) };
2716 }
2717
2718 #[test]
2719 fn add_options_on_null_problem_returns_false() {
2720 let key = CString::new("print_level").unwrap();
2721 let v = CString::new("yes").unwrap();
2722 unsafe {
2723 assert_eq!(
2724 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
2725 FALSE
2726 );
2727 assert_eq!(
2728 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
2729 FALSE
2730 );
2731 assert_eq!(
2732 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
2733 FALSE
2734 );
2735 }
2736 }
2737
2738 unsafe extern "C" fn dummy_intermediate(
2739 _alg_mod: Index,
2740 _iter_count: Index,
2741 _obj_value: Number,
2742 _inf_pr: Number,
2743 _inf_du: Number,
2744 _mu: Number,
2745 _d_norm: Number,
2746 _regularization_size: Number,
2747 _alpha_du: Number,
2748 _alpha_pr: Number,
2749 _ls_trials: Index,
2750 _user_data: *mut c_void,
2751 ) -> Bool {
2752 TRUE
2753 }
2754
2755 #[test]
2756 fn set_intermediate_callback_stores_pointer() {
2757 let p = create_unconstrained();
2758 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
2759 assert_eq!(ok, TRUE);
2760 let info = unsafe { &*p };
2761 assert!(info.intermediate_cb.is_some());
2762 unsafe { FreeIpoptProblem(p) };
2763 }
2764
2765 #[test]
2766 fn solve_returns_internal_error_on_null_problem() {
2767 let rc = unsafe {
2768 IpoptSolve(
2769 std::ptr::null_mut(),
2770 std::ptr::null_mut(),
2771 std::ptr::null_mut(),
2772 std::ptr::null_mut(),
2773 std::ptr::null_mut(),
2774 std::ptr::null_mut(),
2775 std::ptr::null_mut(),
2776 std::ptr::null_mut(),
2777 )
2778 };
2779 assert_eq!(rc, -199);
2780 }
2781
2782 #[test]
2783 fn free_null_is_safe() {
2784 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
2785 }
2786
2787 unsafe extern "C" fn quad_eval_f(
2793 _n: Index,
2794 x: *const Number,
2795 _new_x: Bool,
2796 obj_value: *mut Number,
2797 _user_data: *mut c_void,
2798 ) -> Bool {
2799 unsafe {
2800 let v = *x.offset(0);
2801 *obj_value = (v - 2.0) * (v - 2.0);
2802 TRUE
2803 }
2804 }
2805 unsafe extern "C" fn quad_eval_grad_f(
2806 _n: Index,
2807 x: *const Number,
2808 _new_x: Bool,
2809 grad: *mut Number,
2810 _user_data: *mut c_void,
2811 ) -> Bool {
2812 unsafe {
2813 let v = *x.offset(0);
2814 *grad.offset(0) = 2.0 * (v - 2.0);
2815 TRUE
2816 }
2817 }
2818 unsafe extern "C" fn quad_eval_h(
2819 _n: Index,
2820 _x: *const Number,
2821 _new_x: Bool,
2822 obj_factor: Number,
2823 _m: Index,
2824 _lambda: *const Number,
2825 _new_lambda: Bool,
2826 _nele_hess: Index,
2827 irow: *mut Index,
2828 jcol: *mut Index,
2829 values: *mut Number,
2830 _user_data: *mut c_void,
2831 ) -> Bool {
2832 unsafe {
2833 if !irow.is_null() && !jcol.is_null() && values.is_null() {
2834 *irow.offset(0) = 0;
2835 *jcol.offset(0) = 0;
2836 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
2837 *values.offset(0) = 2.0 * obj_factor;
2838 } else {
2839 return FALSE;
2840 }
2841 TRUE
2842 }
2843 }
2844
2845 #[test]
2846 fn solve_drives_unconstrained_quadratic_through_bridge() {
2847 let xl = [-1.0e20];
2850 let xu = [1.0e20];
2851 let p = unsafe {
2852 CreateIpoptProblem(
2853 1,
2854 xl.as_ptr(),
2855 xu.as_ptr(),
2856 0,
2857 std::ptr::null(),
2858 std::ptr::null(),
2859 0,
2860 1,
2861 0,
2862 Some(quad_eval_f),
2863 None,
2864 Some(quad_eval_grad_f),
2865 None,
2866 Some(quad_eval_h),
2867 )
2868 };
2869 assert!(!p.is_null());
2870 let mut x = [0.0_f64];
2871 let mut obj = 0.0_f64;
2872 let rc = unsafe {
2873 IpoptSolve(
2874 p,
2875 x.as_mut_ptr(),
2876 std::ptr::null_mut(),
2877 &mut obj,
2878 std::ptr::null_mut(),
2879 std::ptr::null_mut(),
2880 std::ptr::null_mut(),
2881 std::ptr::null_mut(),
2882 )
2883 };
2884 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2885 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
2886 assert!(obj.abs() < 1e-10, "obj = {}", obj);
2887 unsafe { FreeIpoptProblem(p) };
2888 }
2889
2890 #[test]
2905 fn stale_stats_cleared_when_resolve_bails() {
2906 let xl = [-1.0e20];
2907 let xu = [1.0e20];
2908 let p = unsafe {
2909 CreateIpoptProblem(
2910 1,
2911 xl.as_ptr(),
2912 xu.as_ptr(),
2913 0,
2914 std::ptr::null(),
2915 std::ptr::null(),
2916 0,
2917 1,
2918 0,
2919 Some(quad_eval_f),
2920 None,
2921 Some(quad_eval_grad_f),
2922 None,
2923 Some(quad_eval_h),
2924 )
2925 };
2926 assert!(!p.is_null());
2927
2928 let mut x = [0.0_f64];
2929 let mut obj = 0.0_f64;
2930 let rc = unsafe {
2931 IpoptSolve(
2932 p,
2933 x.as_mut_ptr(),
2934 std::ptr::null_mut(),
2935 &mut obj,
2936 std::ptr::null_mut(),
2937 std::ptr::null_mut(),
2938 std::ptr::null_mut(),
2939 std::ptr::null_mut(),
2940 )
2941 };
2942 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2943 let iters_after_success = unsafe { GetIpoptIterCount(p) };
2945 assert!(
2946 iters_after_success >= 1,
2947 "a converged solve should record >=1 iteration, got {iters_after_success}"
2948 );
2949 assert!(unsafe { (*p).last_solve.is_some() });
2950
2951 unsafe { (*p).n = -1 };
2954 let mut x2 = [0.0_f64];
2955 let rc2 = unsafe {
2956 IpoptSolve(
2957 p,
2958 x2.as_mut_ptr(),
2959 std::ptr::null_mut(),
2960 std::ptr::null_mut(),
2961 std::ptr::null_mut(),
2962 std::ptr::null_mut(),
2963 std::ptr::null_mut(),
2964 std::ptr::null_mut(),
2965 )
2966 };
2967 assert_eq!(
2968 rc2,
2969 ApplicationReturnStatus::InvalidProblemDefinition as Index
2970 );
2971
2972 assert!(
2976 unsafe { (*p).last_solve.is_none() },
2977 "a bailed re-solve must clear stale last_solve (F5)"
2978 );
2979 assert_eq!(
2980 unsafe { GetIpoptIterCount(p) },
2981 0,
2982 "stale iteration count must not survive a bailed re-solve (F5)"
2983 );
2984
2985 unsafe { FreeIpoptProblem(p) };
2986 }
2987
2988 #[test]
2989 fn solve_invalid_problem_definition_when_x_null() {
2990 let p = create_unconstrained();
2991 let rc = unsafe {
2992 IpoptSolve(
2993 p,
2994 std::ptr::null_mut(), std::ptr::null_mut(),
2996 std::ptr::null_mut(),
2997 std::ptr::null_mut(),
2998 std::ptr::null_mut(),
2999 std::ptr::null_mut(),
3000 std::ptr::null_mut(),
3001 )
3002 };
3003 assert_eq!(
3004 rc,
3005 ApplicationReturnStatus::InvalidProblemDefinition as Index
3006 );
3007 unsafe { FreeIpoptProblem(p) };
3008 }
3009
3010 #[test]
3013 fn get_version_writes_pkg_version() {
3014 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
3015 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
3016 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
3017 assert_eq!((mj, mn, pt), expected);
3018 }
3019
3020 #[test]
3021 fn get_version_tolerates_null_buffers() {
3022 unsafe {
3024 GetIpoptVersion(
3025 std::ptr::null_mut(),
3026 std::ptr::null_mut(),
3027 std::ptr::null_mut(),
3028 )
3029 };
3030 }
3031
3032 #[test]
3033 fn set_scaling_stores_user_supplied_arrays() {
3034 let p = create_unconstrained();
3035 let xs = [2.0, 3.0, 4.0, 5.0];
3036 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
3037 assert_eq!(ok, TRUE);
3038 let info = unsafe { &*p };
3039 let s = info.user_scaling.as_ref().unwrap();
3040 assert_eq!(s.obj_scaling, 7.0);
3041 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
3042 assert!(s.g_scaling.is_none());
3043 unsafe { FreeIpoptProblem(p) };
3044 }
3045
3046 #[test]
3047 fn set_scaling_on_null_problem_returns_false() {
3048 let ok = unsafe {
3049 SetIpoptProblemScaling(
3050 std::ptr::null_mut(),
3051 1.0,
3052 std::ptr::null(),
3053 std::ptr::null(),
3054 )
3055 };
3056 assert_eq!(ok, FALSE);
3057 }
3058
3059 #[test]
3060 fn open_output_file_writes_and_attaches_journal() {
3061 let p = create_unconstrained();
3062 let dir = std::env::temp_dir().join("pounce-cinterface-test");
3063 let _ = std::fs::create_dir_all(&dir);
3064 let path = dir.join("output.log");
3065 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
3066 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
3067 assert_eq!(ok, TRUE);
3068 let info = unsafe { &*p };
3070 let (level, found) = info
3071 .app
3072 .options()
3073 .get_integer_value("file_print_level", "")
3074 .unwrap();
3075 assert!(found);
3076 assert_eq!(level, 5);
3077 unsafe { FreeIpoptProblem(p) };
3078 let _ = std::fs::remove_file(&path);
3079 }
3080
3081 #[test]
3082 fn open_output_file_with_null_inputs_returns_false() {
3083 let key = CString::new("nope").unwrap();
3084 unsafe {
3085 assert_eq!(
3086 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
3087 FALSE
3088 );
3089 }
3090 let p = create_unconstrained();
3091 unsafe {
3092 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
3093 FreeIpoptProblem(p);
3094 }
3095 }
3096
3097 #[test]
3098 fn get_current_iterate_returns_false_outside_callback() {
3099 let p = create_unconstrained();
3100 let rc = unsafe {
3101 GetIpoptCurrentIterate(
3102 p,
3103 FALSE,
3104 0,
3105 std::ptr::null_mut(),
3106 std::ptr::null_mut(),
3107 std::ptr::null_mut(),
3108 0,
3109 std::ptr::null_mut(),
3110 std::ptr::null_mut(),
3111 )
3112 };
3113 assert_eq!(rc, FALSE);
3114 unsafe { FreeIpoptProblem(p) };
3115 }
3116
3117 #[test]
3118 fn get_current_violations_returns_false_outside_callback() {
3119 let p = create_unconstrained();
3120 let rc = unsafe {
3121 GetIpoptCurrentViolations(
3122 p,
3123 FALSE,
3124 0,
3125 std::ptr::null_mut(),
3126 std::ptr::null_mut(),
3127 std::ptr::null_mut(),
3128 std::ptr::null_mut(),
3129 std::ptr::null_mut(),
3130 0,
3131 std::ptr::null_mut(),
3132 std::ptr::null_mut(),
3133 )
3134 };
3135 assert_eq!(rc, FALSE);
3136 unsafe { FreeIpoptProblem(p) };
3137 }
3138
3139 #[test]
3140 fn post_solve_stats_zero_before_solve() {
3141 let p = create_unconstrained();
3142 unsafe {
3143 assert_eq!(GetIpoptIterCount(p), 0);
3144 assert_eq!(GetIpoptSolveTime(p), 0.0);
3145 assert_eq!(GetIpoptPrimalInf(p), 0.0);
3146 assert_eq!(GetIpoptDualInf(p), 0.0);
3147 assert_eq!(GetIpoptComplInf(p), 0.0);
3148 FreeIpoptProblem(p);
3149 }
3150 }
3151
3152 #[test]
3153 fn post_solve_stats_populated_after_solve() {
3154 let xl = [-1.0e20];
3156 let xu = [1.0e20];
3157 let p = unsafe {
3158 CreateIpoptProblem(
3159 1,
3160 xl.as_ptr(),
3161 xu.as_ptr(),
3162 0,
3163 std::ptr::null(),
3164 std::ptr::null(),
3165 0,
3166 1,
3167 0,
3168 Some(quad_eval_f),
3169 None,
3170 Some(quad_eval_grad_f),
3171 None,
3172 Some(quad_eval_h),
3173 )
3174 };
3175 let mut x = [0.0_f64];
3176 let mut obj = 0.0_f64;
3177 let rc = unsafe {
3178 IpoptSolve(
3179 p,
3180 x.as_mut_ptr(),
3181 std::ptr::null_mut(),
3182 &mut obj,
3183 std::ptr::null_mut(),
3184 std::ptr::null_mut(),
3185 std::ptr::null_mut(),
3186 std::ptr::null_mut(),
3187 )
3188 };
3189 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3190 unsafe {
3193 assert!(GetIpoptIterCount(p) >= 0);
3194 assert!(GetIpoptSolveTime(p) >= 0.0);
3195 assert!(GetIpoptPrimalInf(p).is_finite());
3196 assert!(GetIpoptDualInf(p).is_finite());
3197 assert!(GetIpoptComplInf(p).is_finite());
3198 FreeIpoptProblem(p);
3199 }
3200 }
3201
3202 #[test]
3207 fn linear_solver_stats_populated_after_solve() {
3208 let xl = [-1.0e20];
3209 let xu = [1.0e20];
3210 let p = unsafe {
3211 CreateIpoptProblem(
3212 1,
3213 xl.as_ptr(),
3214 xu.as_ptr(),
3215 0,
3216 std::ptr::null(),
3217 std::ptr::null(),
3218 0,
3219 1,
3220 0,
3221 Some(quad_eval_f),
3222 None,
3223 Some(quad_eval_grad_f),
3224 None,
3225 Some(quad_eval_h),
3226 )
3227 };
3228 let mut stats = unsafe { std::mem::zeroed::<PounceLinearSolverStats>() };
3229 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, FALSE);
3231
3232 let mut x = [0.0_f64];
3233 let mut obj = 0.0_f64;
3234 let rc = unsafe {
3235 IpoptSolve(
3236 p,
3237 x.as_mut_ptr(),
3238 std::ptr::null_mut(),
3239 &mut obj,
3240 std::ptr::null_mut(),
3241 std::ptr::null_mut(),
3242 std::ptr::null_mut(),
3243 std::ptr::null_mut(),
3244 )
3245 };
3246 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3247 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, TRUE);
3248
3249 let name = unsafe { CStr::from_ptr(stats.solver_name.as_ptr()) }
3250 .to_str()
3251 .expect("solver name is ASCII");
3252 assert_eq!(name, "feral", "default backend should report itself");
3253 assert!(stats.n_factors > 0, "n_factors = {}", stats.n_factors);
3254 assert_eq!(
3255 stats.n_pattern_reuse + stats.n_pattern_changes,
3256 stats.n_factors,
3257 "every factor is either a pattern reuse or a pattern change"
3258 );
3259 assert!(stats.max_fill_ratio.is_nan() || stats.max_fill_ratio > 0.0);
3262 assert!(stats.last_nnz_l == -1 || stats.last_nnz_l > 0);
3263 unsafe { FreeIpoptProblem(p) };
3264 }
3265
3266 #[test]
3267 fn option_type_reports_the_setter_a_keyword_expects() {
3268 let p = create_unconstrained();
3269 let ty = |s: &str| {
3270 let c = std::ffi::CString::new(s).unwrap();
3271 unsafe { GetPounceOptionType(p, c.as_ptr()) }
3272 };
3273 assert_eq!(ty("tol"), 1, "tol is a number");
3274 assert_eq!(ty("max_iter"), 2, "max_iter is an integer");
3275 assert_eq!(ty("linear_solver"), 3, "linear_solver is a string");
3276 assert_eq!(ty("hessian_approximation"), 3);
3279 assert_eq!(ty("no_such_option_at_all"), 0);
3282 assert_eq!(unsafe { GetPounceOptionType(p, std::ptr::null()) }, 0);
3283 unsafe { FreeIpoptProblem(p) };
3284 }
3285
3286 #[test]
3289 fn option_type_answers_without_a_problem_handle() {
3290 let ty = |s: &str| {
3291 let c = std::ffi::CString::new(s).unwrap();
3292 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) }
3293 };
3294 assert_eq!(ty("tol"), 1);
3295 assert_eq!(ty("max_iter"), 2);
3296 assert_eq!(ty("linear_solver"), 3);
3297 assert_eq!(ty("no_such_option_at_all"), 0);
3298
3299 let p = create_unconstrained();
3302 for name in [
3303 "tol",
3304 "max_iter",
3305 "linear_solver",
3306 "mu_strategy",
3307 "print_level",
3308 ] {
3309 let c = std::ffi::CString::new(name).unwrap();
3310 assert_eq!(
3311 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) },
3312 unsafe { GetPounceOptionType(p, c.as_ptr()) },
3313 "handle-free and problem-bound disagree on {name}"
3314 );
3315 }
3316 unsafe { FreeIpoptProblem(p) };
3317 }
3318
3319 #[test]
3320 fn write_solve_report_emits_v1_json_with_iter_history() {
3321 let xl = [-1.0e20];
3324 let xu = [1.0e20];
3325 let p = unsafe {
3326 CreateIpoptProblem(
3327 1,
3328 xl.as_ptr(),
3329 xu.as_ptr(),
3330 0,
3331 std::ptr::null(),
3332 std::ptr::null(),
3333 0,
3334 1,
3335 0,
3336 Some(quad_eval_f),
3337 None,
3338 Some(quad_eval_grad_f),
3339 None,
3340 Some(quad_eval_h),
3341 )
3342 };
3343
3344 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
3346 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
3347 assert_eq!(bad, FALSE);
3348
3349 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
3351 let mut x = [0.0_f64];
3352 let mut obj = 0.0_f64;
3353 let rc = unsafe {
3354 IpoptSolve(
3355 p,
3356 x.as_mut_ptr(),
3357 std::ptr::null_mut(),
3358 &mut obj,
3359 std::ptr::null_mut(),
3360 std::ptr::null_mut(),
3361 std::ptr::null_mut(),
3362 std::ptr::null_mut(),
3363 )
3364 };
3365 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3366
3367 let dir = std::env::temp_dir();
3368 let path = dir.join("pounce_cinterface_report.json");
3369 let cpath = CString::new(path.to_str().unwrap()).unwrap();
3370 let cdetail = CString::new("full").unwrap();
3371 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
3372 assert_eq!(ok, TRUE);
3373
3374 let txt = std::fs::read_to_string(&path).unwrap();
3377 assert!(
3378 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
3379 "{txt}"
3380 );
3381 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
3382 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
3383 assert_eq!(parsed.problem.n_variables, 1);
3384 assert_eq!(parsed.problem.n_constraints, 0);
3385
3386 let bad_detail = CString::new("verbose").unwrap();
3388 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
3389 assert_eq!(bad, FALSE);
3390
3391 let _ = std::fs::remove_file(&path);
3392 unsafe { FreeIpoptProblem(p) };
3393 }
3394
3395 unsafe extern "C" fn cb_quad_eval_g(
3402 _n: Index,
3403 x: *const Number,
3404 _new_x: Bool,
3405 _m: Index,
3406 g: *mut Number,
3407 _user_data: *mut c_void,
3408 ) -> Bool {
3409 unsafe {
3410 *g.offset(0) = *x.offset(0);
3411 TRUE
3412 }
3413 }
3414 unsafe extern "C" fn cb_quad_eval_jac_g(
3415 _n: Index,
3416 _x: *const Number,
3417 _new_x: Bool,
3418 _m: Index,
3419 nele_jac: Index,
3420 irow: *mut Index,
3421 jcol: *mut Index,
3422 values: *mut Number,
3423 _user_data: *mut c_void,
3424 ) -> Bool {
3425 unsafe {
3426 assert_eq!(nele_jac, 1);
3427 if !irow.is_null() {
3428 *irow.offset(0) = 0;
3429 *jcol.offset(0) = 0;
3430 }
3431 if !values.is_null() {
3432 *values.offset(0) = 1.0;
3433 }
3434 TRUE
3435 }
3436 }
3437 unsafe extern "C" fn cb_quad_eval_h(
3438 _n: Index,
3439 _x: *const Number,
3440 _new_x: Bool,
3441 obj_factor: Number,
3442 _m: Index,
3443 _lambda: *const Number,
3444 _new_lambda: Bool,
3445 _nele_hess: Index,
3446 irow: *mut Index,
3447 jcol: *mut Index,
3448 values: *mut Number,
3449 _user_data: *mut c_void,
3450 ) -> Bool {
3451 unsafe {
3452 if !irow.is_null() {
3453 *irow.offset(0) = 0;
3454 *jcol.offset(0) = 0;
3455 }
3456 if !values.is_null() {
3457 *values.offset(0) = 2.0 * obj_factor;
3458 }
3459 TRUE
3460 }
3461 }
3462
3463 fn create_callback_test_problem() -> IpoptProblem {
3464 let xl = [-1.0e20];
3466 let xu = [1.0e20];
3467 let gl = [-10.0];
3468 let gu = [10.0];
3469 unsafe {
3470 CreateIpoptProblem(
3471 1,
3472 xl.as_ptr(),
3473 xu.as_ptr(),
3474 1,
3475 gl.as_ptr(),
3476 gu.as_ptr(),
3477 1,
3478 1,
3479 0,
3480 Some(quad_eval_f),
3481 Some(cb_quad_eval_g),
3482 Some(quad_eval_grad_f),
3483 Some(cb_quad_eval_jac_g),
3484 Some(cb_quad_eval_h),
3485 )
3486 }
3487 }
3488
3489 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
3490 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
3491 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
3492 std::sync::atomic::AtomicBool::new(false);
3493
3494 unsafe extern "C" fn counting_cb(
3495 _alg_mod: Index,
3496 iter_count: Index,
3497 _obj_value: Number,
3498 _inf_pr: Number,
3499 _inf_du: Number,
3500 _mu: Number,
3501 _d_norm: Number,
3502 _regularization_size: Number,
3503 _alpha_du: Number,
3504 _alpha_pr: Number,
3505 _ls_trials: Index,
3506 user_data: *mut c_void,
3507 ) -> Bool {
3508 unsafe {
3509 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3510 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
3511 let problem = user_data as IpoptProblem;
3514 let mut x = [0.0_f64];
3515 let rc = GetIpoptCurrentIterate(
3516 problem,
3517 FALSE,
3518 1,
3519 x.as_mut_ptr(),
3520 std::ptr::null_mut(),
3521 std::ptr::null_mut(),
3522 1,
3523 std::ptr::null_mut(),
3524 std::ptr::null_mut(),
3525 );
3526 if rc == TRUE && x[0].is_finite() {
3527 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3528 }
3529 TRUE
3530 }
3531 }
3532
3533 #[test]
3534 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
3535 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
3536 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
3537 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3538
3539 let p = create_callback_test_problem();
3540 assert!(!p.is_null());
3541 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
3542 assert_eq!(ok, TRUE);
3543 let mut x = [0.0_f64];
3544 let mut obj = 0.0_f64;
3545 let rc = unsafe {
3546 IpoptSolve(
3547 p,
3548 x.as_mut_ptr(),
3549 std::ptr::null_mut(),
3550 &mut obj,
3551 std::ptr::null_mut(),
3552 std::ptr::null_mut(),
3553 std::ptr::null_mut(),
3554 p as *mut c_void,
3555 )
3556 };
3557 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3558 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
3560 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
3561 assert!(
3562 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
3563 "last iter should be >= 1 after at least one accepted step"
3564 );
3565 assert!(
3566 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
3567 "GetIpoptCurrentIterate did not return a usable x"
3568 );
3569 unsafe { FreeIpoptProblem(p) };
3570 }
3571
3572 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
3573
3574 fn create_bounded_callback_test_problem() -> IpoptProblem {
3579 let xl = [0.0];
3581 let xu = [10.0];
3582 let gl = [-10.0];
3583 let gu = [10.0];
3584 unsafe {
3585 CreateIpoptProblem(
3586 1,
3587 xl.as_ptr(),
3588 xu.as_ptr(),
3589 1,
3590 gl.as_ptr(),
3591 gu.as_ptr(),
3592 1,
3593 1,
3594 0,
3595 Some(quad_eval_f),
3596 Some(cb_quad_eval_g),
3597 Some(quad_eval_grad_f),
3598 Some(cb_quad_eval_jac_g),
3599 Some(cb_quad_eval_h),
3600 )
3601 }
3602 }
3603
3604 unsafe extern "C" fn violations_inspecting_cb(
3605 _alg_mod: Index,
3606 _iter_count: Index,
3607 _obj_value: Number,
3608 _inf_pr: Number,
3609 _inf_du: Number,
3610 _mu: Number,
3611 _d_norm: Number,
3612 _regularization_size: Number,
3613 _alpha_du: Number,
3614 _alpha_pr: Number,
3615 _ls_trials: Index,
3616 user_data: *mut c_void,
3617 ) -> Bool {
3618 unsafe {
3619 let problem = user_data as IpoptProblem;
3620 let mut x_l_viol = [f64::NAN];
3625 let mut x_u_viol = [f64::NAN];
3626 let rc = GetIpoptCurrentViolations(
3627 problem,
3628 FALSE,
3629 1,
3630 x_l_viol.as_mut_ptr(),
3631 x_u_viol.as_mut_ptr(),
3632 std::ptr::null_mut(),
3633 std::ptr::null_mut(),
3634 std::ptr::null_mut(),
3635 1,
3636 std::ptr::null_mut(),
3637 std::ptr::null_mut(),
3638 );
3639 if rc == TRUE
3640 && x_l_viol[0].is_finite()
3641 && x_l_viol[0] >= 0.0
3642 && x_u_viol[0].is_finite()
3643 && x_u_viol[0] >= 0.0
3644 {
3645 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
3646 }
3647 TRUE
3648 }
3649 }
3650
3651 #[test]
3652 fn get_current_violations_inside_callback_reports_finite_bounds() {
3653 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
3654 let p = create_bounded_callback_test_problem();
3655 assert!(!p.is_null());
3656 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
3657 assert_eq!(ok, TRUE);
3658 let mut x = [5.0_f64];
3659 let mut obj = 0.0_f64;
3660 let rc = unsafe {
3661 IpoptSolve(
3662 p,
3663 x.as_mut_ptr(),
3664 std::ptr::null_mut(),
3665 &mut obj,
3666 std::ptr::null_mut(),
3667 std::ptr::null_mut(),
3668 std::ptr::null_mut(),
3669 p as *mut c_void,
3670 )
3671 };
3672 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3673 assert!(
3674 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
3675 "GetIpoptCurrentViolations did not return finite, non-negative \
3676 bound violations from inside the callback"
3677 );
3678 unsafe { FreeIpoptProblem(p) };
3679 }
3680
3681 #[test]
3682 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
3683 let n_us = 1usize;
3693 let packed = vec![0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
3697 let mut v = vec![0.0; n_us];
3698 for (i, s) in packed.iter().enumerate() {
3699 v[i] = (-s).max(0.0);
3700 }
3701 v
3702 });
3703 assert!(
3704 unguarded.is_err(),
3705 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
3706 );
3707
3708 let guarded: Result<Vec<f64>, ()> = (|| {
3710 if packed.len() != n_us {
3711 return Err(());
3712 }
3713 let mut v = vec![0.0; n_us];
3714 for (i, s) in packed.iter().enumerate() {
3715 v[i] = (-s).max(0.0);
3716 }
3717 Ok(v)
3718 })();
3719 assert!(
3720 guarded.is_err(),
3721 "guarded scatter should reject the length mismatch (return FALSE), not panic"
3722 );
3723 }
3724
3725 unsafe extern "C" fn user_stop_cb(
3726 _alg_mod: Index,
3727 _iter_count: Index,
3728 _obj_value: Number,
3729 _inf_pr: Number,
3730 _inf_du: Number,
3731 _mu: Number,
3732 _d_norm: Number,
3733 _regularization_size: Number,
3734 _alpha_du: Number,
3735 _alpha_pr: Number,
3736 _ls_trials: Index,
3737 _user_data: *mut c_void,
3738 ) -> Bool {
3739 FALSE
3740 }
3741
3742 #[test]
3743 fn intermediate_callback_false_surfaces_user_requested_stop() {
3744 let p = create_callback_test_problem();
3745 assert!(!p.is_null());
3746 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
3747 assert_eq!(ok, TRUE);
3748 let mut x = [0.0_f64];
3749 let rc = unsafe {
3750 IpoptSolve(
3751 p,
3752 x.as_mut_ptr(),
3753 std::ptr::null_mut(),
3754 std::ptr::null_mut(),
3755 std::ptr::null_mut(),
3756 std::ptr::null_mut(),
3757 std::ptr::null_mut(),
3758 std::ptr::null_mut(),
3759 )
3760 };
3761 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
3762 unsafe { FreeIpoptProblem(p) };
3763 }
3764
3765 #[test]
3766 fn ffi_guard_converts_panic_to_fallback() {
3767 let fallback = ApplicationReturnStatus::InternalError as Index;
3774 let got = ffi_guard(fallback, || -> Index {
3775 panic!("boom inside solver core");
3776 });
3777 assert_eq!(got, fallback);
3778 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
3779 }
3780
3781 #[test]
3782 fn ffi_guard_is_transparent_on_success() {
3783 let got = ffi_guard(-99, || 7);
3787 assert_eq!(got, 7);
3788 }
3789
3790 #[test]
3791 fn parse_pkg_version_handles_missing_components() {
3792 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
3793 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
3794 assert_eq!(parse_pkg_version(""), (0, 0, 0));
3795 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
3796 }
3797
3798 use crate::solver::{
3801 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
3802 IpoptSolverSolve,
3803 };
3804
3805 #[test]
3806 fn solver_create_consumes_problem_handle() {
3807 let mut p = create_unconstrained();
3808 assert!(!p.is_null());
3809 let s = unsafe { IpoptCreateSolver(&mut p) };
3810 assert!(!s.is_null());
3811 assert!(
3812 p.is_null(),
3813 "IpoptCreateSolver should NULL out the caller's handle"
3814 );
3815 unsafe { IpoptFreeSolver(s) };
3816 }
3817
3818 #[test]
3819 fn solver_create_null_inputs_return_null() {
3820 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
3822 assert!(s.is_null());
3823 let mut p: IpoptProblem = std::ptr::null_mut();
3825 let s = unsafe { IpoptCreateSolver(&mut p) };
3826 assert!(s.is_null());
3827 }
3828
3829 #[test]
3830 fn solver_free_null_is_safe() {
3831 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
3832 }
3833
3834 #[test]
3835 fn solver_solve_drives_quadratic_and_retains_factor() {
3836 let xl = [-1.0e20];
3837 let xu = [1.0e20];
3838 let mut p = unsafe {
3839 CreateIpoptProblem(
3840 1,
3841 xl.as_ptr(),
3842 xu.as_ptr(),
3843 0,
3844 std::ptr::null(),
3845 std::ptr::null(),
3846 0,
3847 1,
3848 0,
3849 Some(quad_eval_f),
3850 None,
3851 Some(quad_eval_grad_f),
3852 None,
3853 Some(quad_eval_h),
3854 )
3855 };
3856 assert!(!p.is_null());
3857 let s = unsafe { IpoptCreateSolver(&mut p) };
3858 assert!(!s.is_null());
3859 let mut x = [0.0_f64];
3860 let mut obj = 0.0_f64;
3861 let rc = unsafe {
3862 IpoptSolverSolve(
3863 s,
3864 x.as_mut_ptr(),
3865 std::ptr::null_mut(),
3866 &mut obj,
3867 std::ptr::null_mut(),
3868 std::ptr::null_mut(),
3869 std::ptr::null_mut(),
3870 std::ptr::null_mut(),
3871 )
3872 };
3873 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
3874 assert!((x[0] - 2.0).abs() < 1e-6);
3875 assert!(obj.abs() < 1e-10);
3876
3877 let dim = unsafe { IpoptSolverGetKktDim(s) };
3880 assert!(dim > 0, "expected positive KKT dim, got {dim}");
3881 let rhs = vec![0.0_f64; dim as usize];
3882 let mut lhs = vec![1.0_f64; dim as usize];
3883 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
3884 assert_eq!(ok, TRUE);
3885 for (i, v) in lhs.iter().enumerate() {
3886 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
3887 }
3888 unsafe { IpoptFreeSolver(s) };
3889 }
3890
3891 #[test]
3892 fn solver_kkt_dim_minus_one_before_solve() {
3893 let mut p = create_unconstrained();
3894 let s = unsafe { IpoptCreateSolver(&mut p) };
3895 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
3896 unsafe { IpoptFreeSolver(s) };
3897 }
3898
3899 #[test]
3904 fn c_get_working_set_returns_false_before_any_solve() {
3905 let p = create_unconstrained();
3906 let mut bound_buf = [0; 4];
3907 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
3908 assert_eq!(rc, FALSE);
3909 unsafe { FreeIpoptProblem(p) };
3910 }
3911
3912 #[test]
3913 fn c_set_warm_start_with_both_null_returns_false() {
3914 let p = create_unconstrained();
3915 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
3916 assert_eq!(rc, FALSE);
3917 unsafe { FreeIpoptProblem(p) };
3918 }
3919
3920 #[test]
3921 fn c_set_warm_start_with_bad_status_code_returns_false() {
3922 let p = create_unconstrained();
3923 let bogus = [
3925 POUNCE_WS_INACTIVE,
3926 7,
3927 POUNCE_WS_AT_LOWER,
3928 POUNCE_WS_INACTIVE,
3929 ];
3930 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
3931 assert_eq!(rc, FALSE);
3932 unsafe { FreeIpoptProblem(p) };
3933 }
3934
3935 #[test]
3936 fn c_set_warm_start_then_clear_succeeds() {
3937 let p = create_unconstrained();
3938 let in_buf = [POUNCE_WS_INACTIVE; 4];
3939 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
3940 assert_eq!(set_rc, TRUE);
3941 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
3942 assert_eq!(clr_rc, TRUE);
3943 unsafe { FreeIpoptProblem(p) };
3944 }
3945
3946 #[test]
3947 fn c_set_warm_start_on_null_problem_returns_false() {
3948 let in_buf = [POUNCE_WS_INACTIVE; 1];
3949 let rc = unsafe {
3950 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
3951 };
3952 assert_eq!(rc, FALSE);
3953 }
3954
3955 #[test]
3956 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
3957 let p = create_callback_test_problem();
3963 let key = CString::new("algorithm").unwrap();
3964 let val = CString::new("active-set-sqp").unwrap();
3965 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
3966 assert_eq!(ok, TRUE);
3967
3968 let mut x = [0.0_f64];
3969 let mut obj = 0.0_f64;
3970 let rc1 = unsafe {
3971 IpoptSolve(
3972 p,
3973 x.as_mut_ptr(),
3974 std::ptr::null_mut(),
3975 &mut obj,
3976 std::ptr::null_mut(),
3977 std::ptr::null_mut(),
3978 std::ptr::null_mut(),
3979 std::ptr::null_mut(),
3980 )
3981 };
3982 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
3983
3984 let mut bound_buf = [-1; 1];
3985 let mut cons_buf = [-1; 1];
3986 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
3987 assert_eq!(got, TRUE);
3988 assert!((0..=3).contains(&bound_buf[0]));
3990 assert!((0..=3).contains(&cons_buf[0]));
3991
3992 x[0] = 0.0;
3997 let mut obj2 = 0.0_f64;
3998 let mut bound_out = [-1; 1];
3999 let mut cons_out = [-1; 1];
4000 let rc2 = unsafe {
4001 IpoptSolveWarmStart(
4002 p,
4003 x.as_mut_ptr(),
4004 std::ptr::null_mut(),
4005 &mut obj2,
4006 std::ptr::null_mut(),
4007 std::ptr::null_mut(),
4008 std::ptr::null_mut(),
4009 bound_buf.as_ptr(),
4010 cons_buf.as_ptr(),
4011 bound_out.as_mut_ptr(),
4012 cons_out.as_mut_ptr(),
4013 std::ptr::null_mut(),
4014 )
4015 };
4016 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
4017 assert!((0..=3).contains(&bound_out[0]));
4018 assert!((0..=3).contains(&cons_out[0]));
4019
4020 unsafe { FreeIpoptProblem(p) };
4021 }
4022}