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 bounds;
35pub mod errors;
36pub mod fortran;
37pub mod qp;
38pub mod solver;
39
40pub use bounds::*;
41pub use errors::*;
42pub use qp::*;
43
44use bounds::{ComplementsLedger, ComplementsRow, ComplementsUse, DeclaredPairs};
45use pounce_algorithm::application::{
46 IpoptApplication, default_backend_factory, feral_config_from_options, ma57_config_from_options,
47};
48use pounce_algorithm::intermediate as ip_intermediate;
49use pounce_common::reg_options::OptionType;
50use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
51use pounce_nlp::mpcc::{ComplementarityPair, PairErrorKind, PairShape, classify_pair};
52use pounce_nlp::return_codes::ApplicationReturnStatus;
53use pounce_nlp::solve_statistics::SolveStatistics;
54use pounce_nlp::tnlp::{
55 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
56 StartingPoint, TNLP,
57};
58use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
59use pounce_restoration::resto_inner_solver::{
60 InnerBackendFactoryFactory, make_default_restoration_factory_provider,
61};
62use pounce_restoration::second_opinion_driver::run_second_opinion_ladder;
63use std::cell::RefCell;
64use std::ffi::{CStr, c_char, c_int, c_uint, c_void};
65use std::rc::Rc;
66
67pub type Number = f64;
69pub type Index = c_int;
71pub type Bool = u8;
97
98const TRUE: Bool = 1;
99const FALSE: Bool = 0;
100
101const _: () = assert!(
105 core::mem::size_of::<Bool>() == 1,
106 "Bool must be one byte to match `typedef bool Bool` in pounce.h"
107);
108
109pub(crate) fn ffi_guard<R>(fallback: R, body: impl FnOnce() -> R) -> R {
122 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
123 Ok(r) => r,
124 Err(_) => fallback,
125 }
126}
127
128pub(crate) unsafe trait ExtensibleOut: Copy {}
148
149pub(crate) const fn struct_size_of<T: ExtensibleOut>() -> Index {
151 std::mem::size_of::<T>() as Index
152}
153
154pub(crate) unsafe fn write_extensible<T: ExtensibleOut>(out: *mut T, value: &T) -> bool {
181 const HEAD: usize = std::mem::size_of::<Index>();
182 let size = std::mem::size_of::<T>();
183 let claimed = unsafe { std::ptr::read(out.cast::<Index>()) };
186 if usize::try_from(claimed).map_or(true, |c| c < size) {
187 return false;
188 }
189 unsafe {
193 std::ptr::copy_nonoverlapping(
194 (value as *const T).cast::<u8>().add(HEAD),
195 out.cast::<u8>().add(HEAD),
196 size - HEAD,
197 );
198 }
199 true
200}
201
202pub type IpoptBoundStatus = c_int;
206pub type IpoptConsStatus = c_int;
209
210const POUNCE_WS_INACTIVE: c_int = 0;
211const POUNCE_WS_AT_LOWER: c_int = 1;
212const POUNCE_WS_AT_UPPER: c_int = 2;
213const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
214
215pub struct IpoptProblemInfo {
218 pub(crate) app: IpoptApplication,
219 pub(crate) n: Index,
220 pub(crate) m: Index,
221 pub(crate) nele_jac: Index,
222 pub(crate) nele_hess: Index,
223 pub(crate) index_style: Index,
224 pub(crate) x_l: Vec<Number>,
225 pub(crate) x_u: Vec<Number>,
226 pub(crate) g_l: Vec<Number>,
227 pub(crate) g_u: Vec<Number>,
228 pub(crate) eval_f: Option<Eval_F_CB>,
229 pub(crate) eval_g: Option<Eval_G_CB>,
230 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
231 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
232 pub(crate) eval_h: Option<Eval_H_CB>,
233 pub(crate) intermediate_cb: Option<Intermediate_CB>,
234 pub(crate) user_scaling: Option<UserScaling>,
238 pub(crate) last_solve: Option<LastSolve>,
242 pub(crate) pending_working_set: Option<pounce_qp::WorkingSet>,
256 pub(crate) nonlinear_vars: Option<Vec<Index>>,
265 pub(crate) complementarity: DeclaredPairs,
271 pub(crate) complements_used: ComplementsLedger,
278}
279
280#[derive(Clone)]
283pub(crate) struct UserScaling {
284 obj_scaling: Number,
285 x_scaling: Option<Vec<Number>>,
286 g_scaling: Option<Vec<Number>>,
287}
288
289#[derive(Clone)]
295pub(crate) struct LastSolve {
296 pub(crate) stats: SolveStatistics,
297 pub(crate) status: ApplicationReturnStatus,
298 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
299 pub(crate) final_x: Vec<Number>,
300 pub(crate) final_lambda: Vec<Number>,
301 pub(crate) final_obj: Number,
302 pub(crate) mpcc: Option<pounce_restoration::mpcc_driver::MpccOutcome>,
305 pub(crate) second_opinion:
311 Option<pounce_restoration::second_opinion_driver::SecondOpinionOutcome>,
312}
313
314impl Default for LastSolve {
315 fn default() -> Self {
316 Self {
317 stats: SolveStatistics::default(),
318 status: ApplicationReturnStatus::InternalError,
319 linear_solver: None,
320 final_x: Vec::new(),
321 final_lambda: Vec::new(),
322 final_obj: 0.0,
323 mpcc: None,
324 second_opinion: None,
325 }
326 }
327}
328
329pub type IpoptProblem = *mut IpoptProblemInfo;
330
331pub type Eval_F_CB = unsafe extern "C" fn(
335 n: Index,
336 x: *const Number,
337 new_x: Bool,
338 obj_value: *mut Number,
339 user_data: *mut c_void,
340) -> Bool;
341
342pub type Eval_Grad_F_CB = unsafe extern "C" fn(
343 n: Index,
344 x: *const Number,
345 new_x: Bool,
346 grad_f: *mut Number,
347 user_data: *mut c_void,
348) -> Bool;
349
350pub type Eval_G_CB = unsafe extern "C" fn(
351 n: Index,
352 x: *const Number,
353 new_x: Bool,
354 m: Index,
355 g: *mut Number,
356 user_data: *mut c_void,
357) -> Bool;
358
359pub type Eval_Jac_G_CB = unsafe extern "C" fn(
360 n: Index,
361 x: *const Number,
362 new_x: Bool,
363 m: Index,
364 nele_jac: Index,
365 iRow: *mut Index,
366 jCol: *mut Index,
367 values: *mut Number,
368 user_data: *mut c_void,
369) -> Bool;
370
371pub type Eval_H_CB = unsafe extern "C" fn(
372 n: Index,
373 x: *const Number,
374 new_x: Bool,
375 obj_factor: Number,
376 m: Index,
377 lambda: *const Number,
378 new_lambda: Bool,
379 nele_hess: Index,
380 iRow: *mut Index,
381 jCol: *mut Index,
382 values: *mut Number,
383 user_data: *mut c_void,
384) -> Bool;
385
386pub type Intermediate_CB = unsafe extern "C" fn(
387 alg_mod: Index,
388 iter_count: Index,
389 obj_value: Number,
390 inf_pr: Number,
391 inf_du: Number,
392 mu: Number,
393 d_norm: Number,
394 regularization_size: Number,
395 alpha_du: Number,
396 alpha_pr: Number,
397 ls_trials: Index,
398 user_data: *mut c_void,
399) -> Bool;
400
401#[unsafe(no_mangle)]
412pub unsafe extern "C" fn CreateIpoptProblem(
413 n: Index,
414 x_L: *const Number,
415 x_U: *const Number,
416 m: Index,
417 g_L: *const Number,
418 g_U: *const Number,
419 nele_jac: Index,
420 nele_hess: Index,
421 index_style: Index,
422 eval_f: Option<Eval_F_CB>,
423 eval_g: Option<Eval_G_CB>,
424 eval_grad_f: Option<Eval_Grad_F_CB>,
425 eval_jac_g: Option<Eval_Jac_G_CB>,
426 eval_h: Option<Eval_H_CB>,
427) -> IpoptProblem {
428 unsafe {
429 pounce_observability::init_subscriber();
433
434 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
435 return std::ptr::null_mut();
436 }
437 if !(0..=1).contains(&index_style) {
438 return std::ptr::null_mut();
439 }
440 if eval_f.is_none() || eval_grad_f.is_none() {
441 return std::ptr::null_mut();
442 }
443 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
444 return std::ptr::null_mut();
445 }
446 if n > 0 && (x_L.is_null() || x_U.is_null()) {
447 return std::ptr::null_mut();
448 }
449 if m > 0 && (g_L.is_null() || g_U.is_null()) {
450 return std::ptr::null_mut();
451 }
452
453 let x_l = if n > 0 {
454 std::slice::from_raw_parts(x_L, n as usize).to_vec()
455 } else {
456 Vec::new()
457 };
458 let x_u = if n > 0 {
459 std::slice::from_raw_parts(x_U, n as usize).to_vec()
460 } else {
461 Vec::new()
462 };
463 let g_l_vec = if m > 0 {
464 std::slice::from_raw_parts(g_L, m as usize).to_vec()
465 } else {
466 Vec::new()
467 };
468 let g_u_vec = if m > 0 {
469 std::slice::from_raw_parts(g_U, m as usize).to_vec()
470 } else {
471 Vec::new()
472 };
473
474 let info = Box::new(IpoptProblemInfo {
475 app: IpoptApplication::new(),
476 n,
477 m,
478 nele_jac,
479 nele_hess,
480 index_style,
481 x_l,
482 x_u,
483 g_l: g_l_vec,
484 g_u: g_u_vec,
485 eval_f,
486 eval_g,
487 eval_grad_f,
488 eval_jac_g,
489 eval_h,
490 intermediate_cb: None,
491 user_scaling: None,
492 nonlinear_vars: None,
493 complementarity: DeclaredPairs::default(),
494 complements_used: ComplementsLedger::default(),
495 last_solve: None,
496 pending_working_set: None,
497 });
498 Box::into_raw(info)
499 }
500}
501
502#[unsafe(no_mangle)]
509pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
510 unsafe {
511 if ipopt_problem.is_null() {
512 return;
513 }
514 drop(Box::from_raw(ipopt_problem));
515 }
516}
517
518unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
519 unsafe {
520 if keyword.is_null() {
521 return None;
522 }
523 CStr::from_ptr(keyword).to_str().ok()
524 }
525}
526
527#[unsafe(no_mangle)]
534pub unsafe extern "C" fn AddIpoptStrOption(
535 ipopt_problem: IpoptProblem,
536 keyword: *const c_char,
537 val: *const c_char,
538) -> Bool {
539 unsafe {
540 if ipopt_problem.is_null() {
541 return FALSE;
542 }
543 let info = &mut *ipopt_problem;
544 let Some(k) = keyword_str(keyword) else {
545 return FALSE;
546 };
547 if val.is_null() {
548 return FALSE;
549 }
550 let Ok(v) = CStr::from_ptr(val).to_str() else {
551 return FALSE;
552 };
553 match info.app.options_mut().set_string_value(k, v, true, false) {
554 Ok(_) => TRUE,
555 Err(_) => FALSE,
556 }
557 }
558}
559
560#[unsafe(no_mangle)]
567pub unsafe extern "C" fn AddIpoptNumOption(
568 ipopt_problem: IpoptProblem,
569 keyword: *const c_char,
570 val: Number,
571) -> Bool {
572 unsafe {
573 if ipopt_problem.is_null() {
574 return FALSE;
575 }
576 let info = &mut *ipopt_problem;
577 let Some(k) = keyword_str(keyword) else {
578 return FALSE;
579 };
580 match info
581 .app
582 .options_mut()
583 .set_numeric_value(k, val, true, false)
584 {
585 Ok(_) => TRUE,
586 Err(_) => FALSE,
587 }
588 }
589}
590
591#[unsafe(no_mangle)]
598pub unsafe extern "C" fn AddIpoptIntOption(
599 ipopt_problem: IpoptProblem,
600 keyword: *const c_char,
601 val: Index,
602) -> Bool {
603 unsafe {
604 if ipopt_problem.is_null() {
605 return FALSE;
606 }
607 let info = &mut *ipopt_problem;
608 let Some(k) = keyword_str(keyword) else {
609 return FALSE;
610 };
611 match info.app.options_mut().set_integer_value(
612 k,
613 val as pounce_common::types::Index,
614 true,
615 false,
616 ) {
617 Ok(_) => TRUE,
618 Err(_) => FALSE,
619 }
620 }
621}
622
623#[unsafe(no_mangle)]
637pub unsafe extern "C" fn OpenIpoptOutputFile(
638 ipopt_problem: IpoptProblem,
639 file_name: *const c_char,
640 print_level: c_int,
641) -> Bool {
642 unsafe {
643 if ipopt_problem.is_null() || file_name.is_null() {
644 return FALSE;
645 }
646 let info = &mut *ipopt_problem;
647 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
648 return FALSE;
649 };
650 if info.app.open_output_file(fname, print_level) {
651 TRUE
652 } else {
653 FALSE
654 }
655 }
656}
657
658#[unsafe(no_mangle)]
678pub unsafe extern "C" fn SetIpoptProblemScaling(
679 ipopt_problem: IpoptProblem,
680 obj_scaling: Number,
681 x_scaling: *const Number,
682 g_scaling: *const Number,
683) -> Bool {
684 unsafe {
685 if ipopt_problem.is_null() {
686 return FALSE;
687 }
688 let info = &mut *ipopt_problem;
689 let n = info.n as usize;
690 let m = info.m as usize;
691 let x_vec = if !x_scaling.is_null() && n > 0 {
692 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
693 } else {
694 None
695 };
696 let g_vec = if !g_scaling.is_null() && m > 0 {
697 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
698 } else {
699 None
700 };
701 info.user_scaling = Some(UserScaling {
702 obj_scaling,
703 x_scaling: x_vec,
704 g_scaling: g_vec,
705 });
706 TRUE
707 }
708}
709
710#[allow(clippy::too_many_arguments)]
734#[unsafe(no_mangle)]
735pub unsafe extern "C" fn IpoptSolve(
736 ipopt_problem: IpoptProblem,
737 x: *mut Number,
738 g: *mut Number,
739 obj_val: *mut Number,
740 mult_g: *mut Number,
741 mult_x_L: *mut Number,
742 mult_x_U: *mut Number,
743 user_data: *mut c_void,
744) -> Index {
745 unsafe {
746 if ipopt_problem.is_null() {
747 return ApplicationReturnStatus::InternalError as Index;
748 }
749 (*ipopt_problem).last_solve = None;
757 ffi_guard(ApplicationReturnStatus::InternalError as Index, || {
763 let info = &mut *ipopt_problem;
764 let _lowered = LoweredSolveGuard::set(!info.complementarity.is_empty());
770 if info.n < 0 || info.m < 0 {
771 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
772 }
773 if info.n > 0 && x.is_null() {
774 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
775 }
776 if !info.complementarity.is_empty() && info.pending_working_set.is_some() {
782 if pounce_algorithm::second_opinion::narration_is_wanted(info.app.options()) {
783 eprintln!(
784 "pounce: a warm-start working set cannot seed a solve with \
785 complementarity pairs ({} declared); clear one of them \
786 (IpoptClearWarmStartWorkingSet or ClearPounceComplementarityPairs).",
787 info.complementarity.len()
788 );
789 }
790 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
791 }
792
793 let n_us = info.n as usize;
794 let m_us = info.m as usize;
795 let initial_x = if n_us > 0 {
796 std::slice::from_raw_parts(x, n_us).to_vec()
797 } else {
798 Vec::new()
799 };
800
801 if let Some(working) = info.pending_working_set.take() {
814 let seed_duals = matches!(
815 info.app
816 .options()
817 .get_bool_value("warm_start_init_point", ""),
818 Ok((true, true))
819 );
820 let read_in = |p: *const Number, len: usize| -> Vec<Number> {
821 if seed_duals && !p.is_null() && len > 0 {
822 std::slice::from_raw_parts(p, len).to_vec()
823 } else {
824 vec![0.0; len]
825 }
826 };
827 let lambda_g = read_in(mult_g as *const Number, m_us);
828 let z_l = read_in(mult_x_L as *const Number, n_us);
829 let z_u = read_in(mult_x_U as *const Number, n_us);
830 let lambda_x = z_l.iter().zip(&z_u).map(|(l, u)| l - u).collect();
835 info.app
836 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
837 x: initial_x.clone(),
838 lambda_g,
839 lambda_x,
840 working: Some(working),
841 });
842 }
843
844 let (lower_inf, upper_inf, zero_fixed_grad_lag) =
845 bridge_violation_options(info.app.options());
846 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
847 n: info.n,
848 m: info.m,
849 nele_jac: info.nele_jac,
850 nele_hess: info.nele_hess,
851 index_style: info.index_style,
852 x_l: info.x_l.clone(),
853 x_u: info.x_u.clone(),
854 g_l: info.g_l.clone(),
855 g_u: info.g_u.clone(),
856 initial_x,
857 start_lambda: mult_g as *const Number,
858 start_z_l: mult_x_L as *const Number,
859 start_z_u: mult_x_U as *const Number,
860 eval_f: info.eval_f,
861 eval_grad_f: info.eval_grad_f,
862 eval_g: info.eval_g,
863 eval_jac_g: info.eval_jac_g,
864 eval_h: info.eval_h,
865 user_data,
866 intermediate_cb: info.intermediate_cb,
867 user_scaling: info.user_scaling.clone(),
868 nonlinear_vars: info.nonlinear_vars.clone(),
869 lower_inf,
870 upper_inf,
871 zero_fixed_grad_lag,
872 final_status: None,
873 final_x: vec![0.0; n_us],
874 final_z_l: vec![0.0; n_us],
875 final_z_u: vec![0.0; n_us],
876 final_g: vec![0.0; m_us],
877 final_lambda: vec![0.0; m_us],
878 final_obj: 0.0,
879 }));
880
881 let feral_cfg = feral_config_from_options(info.app.options());
891 let ma57_cfg = ma57_config_from_options(info.app.options(), "resto.");
894 let bff_mint = move || -> InnerBackendFactoryFactory {
895 let feral_cfg = feral_cfg.clone();
896 let ma57_cfg = ma57_cfg.clone();
897 Box::new(move || default_backend_factory(feral_cfg.clone(), ma57_cfg.clone()))
898 };
899 let resto_provider = make_default_restoration_factory_provider(
900 RestoAlgorithmBuilder::new(),
901 info.app.algorithm_builder_from_options(),
902 bff_mint,
903 );
904 info.app.set_restoration_factory_provider(resto_provider);
905
906 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
907 info.app.defer_end_verdict();
914 let (status, statistics, mpcc, second_opinion) = if !info.complementarity.is_empty() {
922 let narrate =
923 pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
924 let result = pounce_restoration::mpcc_driver::solve_mpcc(
925 &mut info.app,
926 bridge_for_solve,
927 info.complementarity.to_vec(),
928 &mut |line| {
929 if narrate {
930 eprintln!("{line}");
931 }
932 },
933 );
934 let out = match result {
935 Ok(out) => out,
936 Err(e) => {
941 if narrate {
942 eprintln!(
943 "pounce: cannot solve this model's complementarity constraints: {e}"
944 );
945 }
946 let status = ApplicationReturnStatus::InvalidProblemDefinition;
947 if info.app.release_end_verdict() {
948 info.app.print_end_verdict(status);
949 }
950 return status as Index;
951 }
952 };
953 if narrate {
954 eprintln!(
955 "{}",
956 pounce_algorithm::mpcc::describe_outcome(
957 out.reported,
958 out.solves.len(),
959 out.total_iterations,
960 out.max_abs_product,
961 out.certificate.as_ref(),
962 )
963 );
964 }
965 if info.app.release_end_verdict() {
966 info.app.print_end_verdict(out.status);
967 }
968 (out.status, out.statistics.clone(), Some(out), None)
969 } else {
970 let status = info.app.optimize_tnlp(bridge_for_solve);
972 let stats = info.app.statistics();
973 let narrate =
988 pounce_algorithm::second_opinion::narration_is_wanted(info.app.options());
989 let ladder = run_second_opinion_ladder(
990 &mut info.app,
991 bridge.clone() as Rc<RefCell<dyn TNLP>>,
992 status,
993 stats,
994 &mut |line| {
995 if narrate {
996 eprintln!("{line}");
997 }
998 },
999 );
1000 (ladder.status, ladder.statistics.clone(), None, Some(ladder))
1001 };
1002 let bridge_ref = bridge.borrow();
1003 info.last_solve = Some(LastSolve {
1004 stats: statistics,
1005 status,
1006 linear_solver: info.app.linear_solver_summary(),
1007 final_x: bridge_ref.final_x.clone(),
1008 final_lambda: bridge_ref.final_lambda.clone(),
1009 final_obj: bridge_ref.final_obj,
1010 mpcc,
1011 second_opinion,
1012 });
1013 if !x.is_null() && n_us > 0 {
1014 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
1015 }
1016 if !g.is_null() && m_us > 0 {
1017 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
1018 }
1019 if !obj_val.is_null() {
1020 *obj_val = bridge_ref.final_obj;
1021 }
1022 if !mult_g.is_null() && m_us > 0 {
1023 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
1024 }
1025 if !mult_x_L.is_null() && n_us > 0 {
1026 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
1027 }
1028 if !mult_x_U.is_null() && n_us > 0 {
1029 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
1030 }
1031 status as Index
1032 })
1033 }
1034}
1035
1036#[unsafe(no_mangle)]
1042pub unsafe extern "C" fn SetIntermediateCallback(
1043 ipopt_problem: IpoptProblem,
1044 intermediate_cb: Option<Intermediate_CB>,
1045) -> Bool {
1046 unsafe {
1047 if ipopt_problem.is_null() {
1048 return FALSE;
1049 }
1050 let info = &mut *ipopt_problem;
1051 info.intermediate_cb = intermediate_cb;
1052 TRUE
1053 }
1054}
1055
1056#[allow(clippy::too_many_arguments)]
1101#[unsafe(no_mangle)]
1102pub unsafe extern "C" fn GetIpoptCurrentIterate(
1103 ipopt_problem: IpoptProblem,
1104 scaled: Bool,
1105 n: Index,
1106 x: *mut Number,
1107 z_l: *mut Number,
1108 z_u: *mut Number,
1109 m: Index,
1110 g: *mut Number,
1111 lambda: *mut Number,
1112) -> Bool {
1113 unsafe {
1114 if ipopt_problem.is_null() {
1115 return FALSE;
1116 }
1117 if in_lowered_solve() {
1121 return FALSE;
1122 }
1123 let info = &*ipopt_problem;
1124 if n != info.n || m != info.m {
1125 return FALSE;
1126 }
1127 if let Some(r) = ip_intermediate::with_current_user_iterate(|it| {
1130 let scaled_it;
1131 let it = if scaled != FALSE {
1132 scaled_it = pounce_nlp::live_iterate::scaled_iterate(it);
1133 &scaled_it
1134 } else {
1135 it
1136 };
1137 let n_us = n as usize;
1138 let m_us = m as usize;
1139 let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
1140 if dst.is_null() || len == 0 {
1141 return true;
1142 }
1143 if src.len() != len {
1144 return false;
1145 }
1146 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
1147 true
1148 };
1149 copy(&it.x, x, n_us)
1150 && copy(&it.z_l, z_l, n_us)
1151 && copy(&it.z_u, z_u, n_us)
1152 && copy(&it.g, g, m_us)
1153 && copy(&it.lambda, lambda, m_us)
1154 }) {
1155 return if r { TRUE } else { FALSE };
1156 }
1157 let result = ip_intermediate::with_current(|ctx| {
1158 let curr = {
1167 let data = ctx.data.borrow();
1168 match data.curr.as_ref() {
1169 Some(curr) => curr.clone(),
1170 None => return false,
1171 }
1172 };
1173 let n_us = n as usize;
1174 let m_us = m as usize;
1175 let d = ctx.nlp.borrow().variable_scaling();
1186 let d = d.as_deref();
1187 if !x.is_null() && n_us > 0 {
1188 let mut full_x = ctx.nlp.borrow().lift_x_to_full(&*curr.x);
1189 if full_x.len() != n_us {
1190 return false;
1191 }
1192 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut full_x, &mut [], &mut []);
1193 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
1194 }
1195 if !z_l.is_null() && n_us > 0 {
1196 let mut full = ctx.nlp.borrow().finalize_solution_z_l(&*curr.z_l);
1197 if full.len() != n_us {
1198 return false;
1199 }
1200 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut full, &mut []);
1201 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
1202 }
1203 if !z_u.is_null() && n_us > 0 {
1204 let mut full = ctx.nlp.borrow().finalize_solution_z_u(&*curr.z_u);
1205 if full.len() != n_us {
1206 return false;
1207 }
1208 pounce_nlp::scaling_tnlp::unscale_iterate(d, &mut [], &mut [], &mut full);
1209 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
1210 }
1211 if !g.is_null() && m_us > 0 {
1212 let (c, d) = {
1215 let cq = ctx.cq.borrow();
1216 (cq.curr_c(), cq.curr_d())
1217 };
1218 let full = ctx.nlp.borrow().pack_g_for_user(&*c, &*d);
1219 if full.len() != m_us {
1220 return false;
1221 }
1222 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
1223 }
1224 if !lambda.is_null() && m_us > 0 {
1225 let full = ctx
1226 .nlp
1227 .borrow()
1228 .finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
1229 if full.len() != m_us {
1230 return false;
1231 }
1232 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
1233 }
1234 true
1235 });
1236 if result.unwrap_or(false) { TRUE } else { FALSE }
1237 }
1238}
1239
1240#[allow(clippy::too_many_arguments)]
1274#[unsafe(no_mangle)]
1275pub unsafe extern "C" fn GetIpoptCurrentViolations(
1276 ipopt_problem: IpoptProblem,
1277 scaled: Bool,
1278 n: Index,
1279 x_l_violation: *mut Number,
1280 x_u_violation: *mut Number,
1281 compl_x_l: *mut Number,
1282 compl_x_u: *mut Number,
1283 grad_lag_x: *mut Number,
1284 m: Index,
1285 nlp_constraint_violation: *mut Number,
1286 compl_g: *mut Number,
1287) -> Bool {
1288 unsafe {
1289 if ipopt_problem.is_null() {
1290 return FALSE;
1291 }
1292 if in_lowered_solve() {
1296 return FALSE;
1297 }
1298 let info = &*ipopt_problem;
1299 if n != info.n || m != info.m {
1300 return FALSE;
1301 }
1302 let n_us = n as usize;
1303 let m_us = m as usize;
1304 let Some(it) = ip_intermediate::with_current_user_iterate(|it| it.clone()) else {
1305 return FALSE;
1306 };
1307 let bridge = CURRENT_BRIDGE.with(|c| c.get());
1308 if bridge.is_null() {
1309 return FALSE;
1310 }
1311 let bridge = &*bridge;
1312 if it.x.len() != n_us
1313 || it.z_l.len() != n_us
1314 || it.z_u.len() != n_us
1315 || it.g.len() != m_us
1316 || it.lambda.len() != m_us
1317 {
1318 return FALSE;
1319 }
1320 let grad_lag = if grad_lag_x.is_null() || n_us == 0 {
1321 None
1322 } else if it.grad_lag_x.len() == n_us {
1323 Some(it.grad_lag_x.clone())
1326 } else {
1327 match bridge.user_grad_lag(&it) {
1328 Some(g) => Some(g),
1329 None => return FALSE,
1330 }
1331 };
1332 let bounds = pounce_nlp::live_iterate::DeclaredBounds {
1333 x_l: &bridge.x_l,
1334 x_u: &bridge.x_u,
1335 g_l: &bridge.g_l,
1336 g_u: &bridge.g_u,
1337 lower_inf: bridge.lower_inf,
1338 upper_inf: bridge.upper_inf,
1339 };
1340 let violations = Some(pounce_nlp::live_iterate::current_violations(
1341 &it,
1342 &bounds,
1343 grad_lag,
1344 bridge.zero_fixed_grad_lag,
1345 scaled != FALSE,
1346 ));
1347 let Some(v) = violations else {
1348 return FALSE;
1349 };
1350 let copy = |src: &[Number], dst: *mut Number, len: usize| -> bool {
1351 if dst.is_null() || len == 0 {
1352 return true;
1353 }
1354 if src.len() != len {
1355 return false;
1356 }
1357 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
1358 true
1359 };
1360 let ok = copy(&v.x_l_violation, x_l_violation, n_us)
1361 && copy(&v.x_u_violation, x_u_violation, n_us)
1362 && copy(&v.compl_x_l, compl_x_l, n_us)
1363 && copy(&v.compl_x_u, compl_x_u, n_us)
1364 && copy(&v.grad_lag_x, grad_lag_x, n_us)
1365 && copy(&v.constraint_violation, nlp_constraint_violation, m_us)
1366 && copy(&v.compl_g, compl_g, m_us);
1367 if ok { TRUE } else { FALSE }
1368 }
1369}
1370
1371#[unsafe(no_mangle)]
1379pub unsafe extern "C" fn GetIpoptVersion(
1380 major: *mut c_int,
1381 minor: *mut c_int,
1382 release: *mut c_int,
1383) {
1384 unsafe {
1385 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1390 if !major.is_null() {
1391 *major = mj;
1392 }
1393 if !minor.is_null() {
1394 *minor = mn;
1395 }
1396 if !release.is_null() {
1397 *release = pt;
1398 }
1399 }
1400}
1401
1402fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
1403 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
1404 (
1405 it.next().unwrap_or(0),
1406 it.next().unwrap_or(0),
1407 it.next().unwrap_or(0),
1408 )
1409}
1410
1411#[unsafe(no_mangle)]
1428pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
1429 unsafe { last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0) }
1430}
1431
1432#[unsafe(no_mangle)]
1439pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
1440 unsafe { last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0) }
1441}
1442
1443#[unsafe(no_mangle)]
1450pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
1451 unsafe { last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0) }
1452}
1453
1454#[unsafe(no_mangle)]
1461pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
1462 unsafe { last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0) }
1463}
1464
1465#[unsafe(no_mangle)]
1471pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
1472 unsafe { last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0) }
1473}
1474
1475unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
1476where
1477 F: FnOnce(&SolveStatistics) -> T,
1478{
1479 unsafe {
1480 if ipopt_problem.is_null() {
1481 return None;
1482 }
1483 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
1484 }
1485}
1486
1487#[unsafe(no_mangle)]
1502pub unsafe extern "C" fn GetPounceRestorationStats(
1503 ipopt_problem: IpoptProblem,
1504 calls: *mut Index,
1505 inner_iters: *mut Index,
1506 outer_iters: *mut Index,
1507 wall_secs: *mut Number,
1508) {
1509 unsafe {
1510 let stats = last_stat(ipopt_problem, |s| {
1511 (
1512 s.restoration_calls,
1513 s.restoration_inner_iters,
1514 s.restoration_outer_iters,
1515 s.restoration_wall_secs,
1516 )
1517 });
1518 let (c, i, o, w) = stats.unwrap_or((0, 0, 0, 0.0));
1519 if !calls.is_null() {
1520 *calls = c;
1521 }
1522 if !inner_iters.is_null() {
1523 *inner_iters = i;
1524 }
1525 if !outer_iters.is_null() {
1526 *outer_iters = o;
1527 }
1528 if !wall_secs.is_null() {
1529 *wall_secs = w;
1530 }
1531 }
1532}
1533
1534#[unsafe(no_mangle)]
1550pub unsafe extern "C" fn GetPounceFdHessianStats(
1551 ipopt_problem: IpoptProblem,
1552 pattern_used: *mut Index,
1553 nnz: *mut Index,
1554 n: *mut Index,
1555 groups: *mut Index,
1556 rho_max: *mut Index,
1557 coloring_fell_back: *mut Index,
1558 objective_clique_widened: *mut Index,
1559) {
1560 unsafe {
1561 let stats = last_stat(ipopt_problem, |s| {
1562 (
1563 s.fd_hessian_pattern_used,
1564 s.fd_hessian_nnz,
1565 s.fd_hessian_n,
1566 s.fd_hessian_groups,
1567 s.fd_hessian_rho_max,
1568 if s.fd_hessian_coloring_fell_back {
1569 1
1570 } else {
1571 0
1572 },
1573 if s.fd_hessian_objective_clique_widened {
1574 1
1575 } else {
1576 0
1577 },
1578 )
1579 });
1580 let (p, nz, cols, g, r, f, w) = stats.unwrap_or((-1, 0, 0, 0, 0, 0, 0));
1581 if !pattern_used.is_null() {
1582 *pattern_used = p;
1583 }
1584 if !nnz.is_null() {
1585 *nnz = nz;
1586 }
1587 if !n.is_null() {
1588 *n = cols;
1589 }
1590 if !groups.is_null() {
1591 *groups = g;
1592 }
1593 if !rho_max.is_null() {
1594 *rho_max = r;
1595 }
1596 if !coloring_fell_back.is_null() {
1597 *coloring_fell_back = f;
1598 }
1599 if !objective_clique_widened.is_null() {
1600 *objective_clique_widened = w;
1601 }
1602 }
1603}
1604
1605#[repr(C)]
1611#[derive(Debug, Clone, Copy)]
1612pub struct PounceLinearSolverStats {
1613 pub solver_name: [c_char; 32],
1614 pub n_factors: Index,
1615 pub n_pattern_reuse: Index,
1616 pub n_pattern_changes: Index,
1617 pub max_fill_ratio: Number,
1618 pub min_abs_pivot: Number,
1619 pub max_abs_pivot: Number,
1620 pub last_inertia_positive: Index,
1621 pub last_inertia_negative: Index,
1622 pub last_inertia_zero: Index,
1623 pub last_nnz_a: Index,
1624 pub last_nnz_l: Index,
1625}
1626
1627#[unsafe(no_mangle)]
1640pub unsafe extern "C" fn GetPounceLinearSolverStats(
1641 ipopt_problem: IpoptProblem,
1642 stats: *mut PounceLinearSolverStats,
1643) -> Bool {
1644 unsafe {
1645 if ipopt_problem.is_null() || stats.is_null() {
1646 return FALSE;
1647 }
1648 let Some(summary) = (*ipopt_problem)
1649 .last_solve
1650 .as_ref()
1651 .and_then(|ls| ls.linear_solver.as_ref())
1652 else {
1653 return FALSE;
1654 };
1655 let count = |v: u64| Index::try_from(v).unwrap_or(Index::MAX);
1659 let size = |x: usize| Index::try_from(x).unwrap_or(Index::MAX);
1660 let opt_size = |v: Option<usize>| v.map_or(-1, size);
1661 let inertia = summary.last_inertia;
1662 let mut out = PounceLinearSolverStats {
1663 solver_name: [0; 32],
1664 n_factors: count(summary.n_factors),
1665 n_pattern_reuse: count(summary.n_pattern_reuse),
1666 n_pattern_changes: count(summary.n_pattern_changes),
1667 max_fill_ratio: summary.max_fill_ratio.unwrap_or(Number::NAN),
1668 min_abs_pivot: summary.min_abs_pivot.unwrap_or(Number::NAN),
1669 max_abs_pivot: summary.max_abs_pivot.unwrap_or(Number::NAN),
1670 last_inertia_positive: inertia.map_or(-1, |(p, _, _)| size(p)),
1671 last_inertia_negative: inertia.map_or(-1, |(_, n, _)| size(n)),
1672 last_inertia_zero: inertia.map_or(-1, |(_, _, z)| size(z)),
1673 last_nnz_a: opt_size(summary.last_nnz_a),
1674 last_nnz_l: opt_size(summary.last_nnz_l),
1675 };
1676 let name = summary.solver_name.as_bytes();
1680 let keep = name.len().min(out.solver_name.len() - 1);
1681 for (slot, b) in out.solver_name.iter_mut().zip(&name[..keep]) {
1682 *slot = *b as c_char;
1683 }
1684 *stats = out;
1685 TRUE
1686 }
1687}
1688
1689thread_local! {
1690 static DEFAULT_REGISTRY: Rc<pounce_common::reg_options::RegisteredOptions> =
1696 Rc::clone(IpoptApplication::new().registered_options());
1697}
1698
1699#[unsafe(no_mangle)]
1718pub unsafe extern "C" fn GetPounceOptionType(
1719 ipopt_problem: IpoptProblem,
1720 keyword: *const c_char,
1721) -> c_int {
1722 unsafe {
1723 if keyword.is_null() {
1724 return 0;
1725 }
1726 let Ok(name) = CStr::from_ptr(keyword).to_str() else {
1727 return 0;
1728 };
1729 let registered = if ipopt_problem.is_null() {
1730 DEFAULT_REGISTRY.with(|r| r.get_option(name))
1731 } else {
1732 (*ipopt_problem).app.registered_options().get_option(name)
1733 };
1734 let Some(opt) = registered else {
1735 return 0;
1736 };
1737 match opt.option_type {
1738 OptionType::OT_Number => 1,
1739 OptionType::OT_Integer => 2,
1740 OptionType::OT_String => 3,
1741 OptionType::OT_Unknown => 0,
1742 }
1743 }
1744}
1745
1746fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
1756 use pounce_qp::BoundStatus::*;
1757 match s {
1758 Inactive => POUNCE_WS_INACTIVE,
1759 AtLower => POUNCE_WS_AT_LOWER,
1760 AtUpper => POUNCE_WS_AT_UPPER,
1761 Fixed => POUNCE_WS_FIXED_OR_EQ,
1762 }
1763}
1764
1765pub(crate) fn bound_status_consistent(s: pounce_qp::BoundStatus, lo: Number, hi: Number) -> bool {
1770 match s {
1771 pounce_qp::BoundStatus::Fixed => lo == hi,
1772 pounce_qp::BoundStatus::AtLower => lo > NLP_LOWER_BOUND_INF,
1773 pounce_qp::BoundStatus::AtUpper => hi < NLP_UPPER_BOUND_INF,
1774 pounce_qp::BoundStatus::Inactive => true,
1775 }
1776}
1777
1778pub(crate) fn cons_status_consistent(s: pounce_qp::ConsStatus, lo: Number, hi: Number) -> bool {
1781 let lo_finite = lo > NLP_LOWER_BOUND_INF;
1782 let hi_finite = hi < NLP_UPPER_BOUND_INF;
1783 match s {
1784 pounce_qp::ConsStatus::Equality => lo_finite && hi_finite && lo == hi,
1785 pounce_qp::ConsStatus::AtLower => lo_finite,
1786 pounce_qp::ConsStatus::AtUpper => hi_finite,
1787 pounce_qp::ConsStatus::Inactive => true,
1788 }
1789}
1790
1791fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
1792 use pounce_qp::BoundStatus::*;
1793 match v {
1794 POUNCE_WS_INACTIVE => Some(Inactive),
1795 POUNCE_WS_AT_LOWER => Some(AtLower),
1796 POUNCE_WS_AT_UPPER => Some(AtUpper),
1797 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
1798 _ => None,
1799 }
1800}
1801
1802fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1803 use pounce_qp::ConsStatus::*;
1804 match s {
1805 Inactive => POUNCE_WS_INACTIVE,
1806 AtLower => POUNCE_WS_AT_LOWER,
1807 AtUpper => POUNCE_WS_AT_UPPER,
1808 Equality => POUNCE_WS_FIXED_OR_EQ,
1809 }
1810}
1811
1812fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1813 use pounce_qp::ConsStatus::*;
1814 match v {
1815 POUNCE_WS_INACTIVE => Some(Inactive),
1816 POUNCE_WS_AT_LOWER => Some(AtLower),
1817 POUNCE_WS_AT_UPPER => Some(AtUpper),
1818 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1819 _ => None,
1820 }
1821}
1822
1823#[unsafe(no_mangle)]
1845pub unsafe extern "C" fn IpoptGetWorkingSet(
1846 ipopt_problem: IpoptProblem,
1847 bound_status_out: *mut IpoptBoundStatus,
1848 cons_status_out: *mut IpoptConsStatus,
1849) -> Bool {
1850 unsafe {
1851 if ipopt_problem.is_null() {
1852 return FALSE;
1853 }
1854 let info = &*ipopt_problem;
1855 let Some(last) = info.last_solve.as_ref() else {
1863 return FALSE;
1864 };
1865 if last.mpcc.is_some() || !info.complementarity.is_empty() {
1866 return FALSE;
1867 }
1868 let ws = match info.app.last_sqp_working_set() {
1869 Some(w) => w,
1870 None => return FALSE,
1871 };
1872 let n = info.x_l.len();
1880 let m = info.g_l.len();
1881 if ws.constraints.len() != m || ws.bounds.len() != n {
1882 return FALSE;
1885 }
1886 if !bound_status_out.is_null() {
1887 for (i, &s) in ws.bounds.iter().enumerate() {
1888 *bound_status_out.add(i) = bound_status_to_int(s);
1889 }
1890 }
1891 if !cons_status_out.is_null() {
1892 for (i, &s) in ws.constraints.iter().enumerate() {
1893 *cons_status_out.add(i) = cons_status_to_int(s);
1894 }
1895 }
1896 TRUE
1897 }
1898}
1899
1900#[unsafe(no_mangle)]
1917pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1918 ipopt_problem: IpoptProblem,
1919 bound_status_in: *const IpoptBoundStatus,
1920 cons_status_in: *const IpoptConsStatus,
1921) -> Bool {
1922 unsafe {
1923 if ipopt_problem.is_null() {
1924 return FALSE;
1925 }
1926 if bound_status_in.is_null() && cons_status_in.is_null() {
1927 return FALSE;
1928 }
1929 let info = &mut *ipopt_problem;
1930 let n = info.n.max(0) as usize;
1931 let m = info.m.max(0) as usize;
1932 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1956 if !bound_status_in.is_null() {
1957 for i in 0..n {
1961 let v = *bound_status_in.add(i);
1962 let Some(s) = int_to_bound_status(v) else {
1963 return FALSE;
1964 };
1965 if !bound_status_consistent(s, info.x_l[i], info.x_u[i]) {
1966 return FALSE;
1967 }
1968 }
1969 for (i, b) in bounds.iter_mut().enumerate() {
1970 if let Some(s) = int_to_bound_status(*bound_status_in.add(i)) {
1972 *b = s;
1973 }
1974 }
1975 }
1976 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1977 if !cons_status_in.is_null() {
1978 for i in 0..m {
1979 let v = *cons_status_in.add(i);
1980 let Some(s) = int_to_cons_status(v) else {
1981 return FALSE;
1982 };
1983 if !cons_status_consistent(s, info.g_l[i], info.g_u[i]) {
1984 return FALSE;
1985 }
1986 }
1987 for (i, c) in constraints.iter_mut().enumerate() {
1988 if let Some(s) = int_to_cons_status(*cons_status_in.add(i)) {
1989 *c = s;
1990 }
1991 }
1992 }
1993 info.pending_working_set = Some(pounce_qp::WorkingSet {
2005 bounds,
2006 constraints,
2007 });
2008 TRUE
2009 }
2010}
2011
2012#[unsafe(no_mangle)]
2052pub unsafe extern "C" fn IpoptSetNonlinearVariables(
2053 ipopt_problem: IpoptProblem,
2054 num_nonlin_vars: Index,
2055 pos_nonlin_vars: *const Index,
2056) -> Bool {
2057 unsafe {
2058 if ipopt_problem.is_null() {
2059 return FALSE;
2060 }
2061 let info = &mut *ipopt_problem;
2062 if num_nonlin_vars < 0 || num_nonlin_vars > info.n {
2063 return FALSE;
2064 }
2065 if num_nonlin_vars > 0 && pos_nonlin_vars.is_null() {
2066 return FALSE;
2067 }
2068 let offset = if info.index_style == 1 { 1 } else { 0 };
2069 let raw = if num_nonlin_vars == 0 {
2070 &[][..]
2071 } else {
2072 std::slice::from_raw_parts(pos_nonlin_vars, num_nonlin_vars as usize)
2073 };
2074 for &p in raw {
2077 let zero_based = p - offset;
2078 if zero_based < 0 || zero_based >= info.n {
2079 return FALSE;
2080 }
2081 }
2082 info.nonlinear_vars = Some(raw.to_vec());
2083 TRUE
2084 }
2085}
2086
2087#[unsafe(no_mangle)]
2094pub unsafe extern "C" fn IpoptClearNonlinearVariables(ipopt_problem: IpoptProblem) -> Bool {
2095 unsafe {
2096 if ipopt_problem.is_null() {
2097 return FALSE;
2098 }
2099 (*ipopt_problem).nonlinear_vars = None;
2100 TRUE
2101 }
2102}
2103
2104const KNOWN_PAIR_FLAGS: c_uint = 0;
2111
2112fn pair_error_code(kind: &PairErrorKind) -> c_int {
2113 match kind {
2114 PairErrorKind::OutOfRange => POUNCE_ERR_OUT_OF_RANGE,
2115 PairErrorKind::VarAlreadyPaired => POUNCE_ERR_VAR_ALREADY_PAIRED,
2116 PairErrorKind::RowAlreadyPaired => POUNCE_ERR_ROW_ALREADY_PAIRED,
2117 PairErrorKind::UnsupportedShape => POUNCE_ERR_SHAPE,
2118 }
2119}
2120
2121pub(crate) fn zero_based(p: Index, offset: Index, len: Index) -> usize {
2126 match p.checked_sub(offset) {
2127 Some(z) if z >= 0 && z < len => z as usize,
2128 _ => usize::MAX,
2129 }
2130}
2131
2132fn replace_pairs(
2149 info: &mut IpoptProblemInfo,
2150 new: &[(Index, Index)],
2151) -> Result<(), (usize, PairErrorKind)> {
2152 let offset = if info.index_style == 1 { 1 } else { 0 };
2153 let candidate: Vec<ComplementarityPair> = new
2154 .iter()
2155 .map(|&(row, var)| ComplementarityPair {
2156 row: zero_based(row, offset, info.m),
2157 var: zero_based(var, offset, info.n),
2158 })
2159 .collect();
2160 let mut seen_var = vec![false; info.n as usize];
2161 let mut seen_row = vec![false; info.m as usize];
2162 for (k, p) in candidate.iter().enumerate() {
2163 if p.var == usize::MAX || p.row == usize::MAX {
2164 return Err((k, PairErrorKind::OutOfRange));
2165 }
2166 if std::mem::replace(&mut seen_var[p.var], true) {
2167 return Err((k, PairErrorKind::VarAlreadyPaired));
2168 }
2169 if std::mem::replace(&mut seen_row[p.row], true) {
2170 return Err((k, PairErrorKind::RowAlreadyPaired));
2171 }
2172 }
2173 for (k, p) in candidate.iter().enumerate() {
2174 if classify_pair(
2175 info.x_l[p.var],
2176 info.x_u[p.var],
2177 info.g_l[p.row],
2178 info.g_u[p.row],
2179 )
2180 .is_none()
2181 {
2182 return Err((k, PairErrorKind::UnsupportedShape));
2183 }
2184 }
2185 info.complementarity.replace(candidate);
2186 Ok(())
2187}
2188
2189fn append_pair(info: &mut IpoptProblemInfo, row: Index, var: Index) -> Result<(), PairErrorKind> {
2197 let offset = if info.index_style == 1 { 1 } else { 0 };
2198 let r = zero_based(row, offset, info.m);
2199 let v = zero_based(var, offset, info.n);
2200 if r == usize::MAX || v == usize::MAX {
2201 return Err(PairErrorKind::OutOfRange);
2202 }
2203 if info.complementarity.of_var(v).is_some() {
2204 return Err(PairErrorKind::VarAlreadyPaired);
2205 }
2206 if info.complementarity.of_row(r).is_some() {
2207 return Err(PairErrorKind::RowAlreadyPaired);
2208 }
2209 if classify_pair(info.x_l[v], info.x_u[v], info.g_l[r], info.g_u[r]).is_none() {
2210 return Err(PairErrorKind::UnsupportedShape);
2211 }
2212 info.complementarity
2213 .push(ComplementarityPair { row: r, var: v });
2214 Ok(())
2215}
2216
2217#[unsafe(no_mangle)]
2236pub unsafe extern "C" fn AddPounceComplementarityPair(
2237 ipopt_problem: IpoptProblem,
2238 row: Index,
2239 var: Index,
2240) -> c_int {
2241 ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
2242 if ipopt_problem.is_null() {
2243 return POUNCE_ERR_NULL;
2244 }
2245 match append_pair(&mut *ipopt_problem, row, var) {
2246 Ok(()) => POUNCE_OK,
2247 Err(kind) => pair_error_code(&kind),
2248 }
2249 })
2250}
2251
2252#[unsafe(no_mangle)]
2284pub unsafe extern "C" fn SetPounceComplementarityPairs(
2285 ipopt_problem: IpoptProblem,
2286 n_pairs: Index,
2287 rows: *const Index,
2288 vars: *const Index,
2289 flags: c_uint,
2290 bad_index: *mut Index,
2291) -> c_int {
2292 ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
2293 let report = |k: Index| {
2294 if !bad_index.is_null() {
2295 *bad_index = k;
2296 }
2297 };
2298 report(-1);
2299 if ipopt_problem.is_null() {
2300 return POUNCE_ERR_NULL;
2301 }
2302 if flags & !KNOWN_PAIR_FLAGS != 0 {
2303 return POUNCE_ERR_FLAGS;
2304 }
2305 let max_pairs = (*ipopt_problem).n.min((*ipopt_problem).m);
2311 if n_pairs < 0 || n_pairs > max_pairs {
2312 return POUNCE_ERR_COUNT;
2313 }
2314 if n_pairs > 0 && (rows.is_null() || vars.is_null()) {
2315 return POUNCE_ERR_NULL;
2316 }
2317 let new: Vec<(Index, Index)> = if n_pairs == 0 {
2318 Vec::new()
2319 } else {
2320 let r = std::slice::from_raw_parts(rows, n_pairs as usize);
2321 let v = std::slice::from_raw_parts(vars, n_pairs as usize);
2322 r.iter().copied().zip(v.iter().copied()).collect()
2323 };
2324 match replace_pairs(&mut *ipopt_problem, &new) {
2325 Ok(()) => POUNCE_OK,
2326 Err((k, kind)) => {
2327 report(k as Index);
2328 pair_error_code(&kind)
2329 }
2330 }
2331 })
2332}
2333
2334#[unsafe(no_mangle)]
2341pub unsafe extern "C" fn ClearPounceComplementarityPairs(ipopt_problem: IpoptProblem) -> Bool {
2342 ffi_guard(FALSE, || unsafe {
2343 if ipopt_problem.is_null() {
2344 return FALSE;
2345 }
2346 (*ipopt_problem).complementarity.clear();
2347 TRUE
2348 })
2349}
2350
2351#[unsafe(no_mangle)]
2357pub unsafe extern "C" fn GetPounceComplementarityPairCount(ipopt_problem: IpoptProblem) -> Index {
2358 ffi_guard(-1, || unsafe {
2359 if ipopt_problem.is_null() {
2360 return -1;
2361 }
2362 (&*ipopt_problem).complementarity.len() as Index
2363 })
2364}
2365
2366#[unsafe(no_mangle)]
2377pub unsafe extern "C" fn GetPounceComplementarityPairs(
2378 ipopt_problem: IpoptProblem,
2379 rows: *mut Index,
2380 vars: *mut Index,
2381) -> Bool {
2382 ffi_guard(FALSE, || unsafe {
2383 if ipopt_problem.is_null() {
2384 return FALSE;
2385 }
2386 let info = &*ipopt_problem;
2387 let k = info.complementarity.len();
2388 if k == 0 {
2389 return TRUE;
2390 }
2391 if rows.is_null() || vars.is_null() {
2392 return FALSE;
2393 }
2394 let offset = if info.index_style == 1 { 1 } else { 0 };
2395 let rows = std::slice::from_raw_parts_mut(rows, k);
2396 let vars = std::slice::from_raw_parts_mut(vars, k);
2397 for (i, p) in info.complementarity.iter().enumerate() {
2398 rows[i] = p.row as Index + offset;
2399 vars[i] = p.var as Index + offset;
2400 }
2401 TRUE
2402 })
2403}
2404
2405pub const POUNCE_COMPLEMENTS_PAIR_MIXED: c_int = 0;
2412pub const POUNCE_COMPLEMENTS_PAIR_LOWER: c_int = 1;
2415pub const POUNCE_COMPLEMENTS_PAIR_UPPER: c_int = 2;
2418pub const POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR: c_int = 3;
2420pub const POUNCE_COMPLEMENTS_VAR_FIXED_NO_PAIR: c_int = 4;
2422
2423pub(crate) fn complements_row(xl: Number, xu: Number) -> Option<ComplementsRow> {
2424 if xl.is_nan() || xu.is_nan() {
2425 return None;
2426 }
2427 match (xl > NLP_LOWER_BOUND_INF, xu < NLP_UPPER_BOUND_INF) {
2428 (true, true) if xl < xu => Some(ComplementsRow::Mixed),
2429 (true, true) if xl == xu => Some(ComplementsRow::VarFixed),
2430 (true, true) => None,
2431 (true, false) => Some(ComplementsRow::Lower),
2432 (false, true) => Some(ComplementsRow::Upper),
2433 (false, false) => Some(ComplementsRow::RowFixed),
2434 }
2435}
2436
2437impl ComplementsRow {
2438 pub(crate) fn row_bounds(self, gl: Number, gu: Number) -> (Number, Number, bool) {
2441 match self {
2442 ComplementsRow::Mixed => (gl, gu, true),
2443 ComplementsRow::Lower => (0.0, gu, true),
2444 ComplementsRow::Upper => (gl, 0.0, true),
2445 ComplementsRow::RowFixed => (0.0, 0.0, false),
2446 ComplementsRow::VarFixed => (gl, gu, false),
2447 }
2448 }
2449
2450 fn code(self) -> c_int {
2451 match self {
2452 ComplementsRow::Mixed => POUNCE_COMPLEMENTS_PAIR_MIXED,
2453 ComplementsRow::Lower => POUNCE_COMPLEMENTS_PAIR_LOWER,
2454 ComplementsRow::Upper => POUNCE_COMPLEMENTS_PAIR_UPPER,
2455 ComplementsRow::RowFixed => POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR,
2456 ComplementsRow::VarFixed => POUNCE_COMPLEMENTS_VAR_FIXED_NO_PAIR,
2457 }
2458 }
2459
2460 fn shape_matches(self, shape: Option<PairShape>) -> bool {
2462 matches!(
2463 (self, shape),
2464 (ComplementsRow::Mixed, Some(PairShape::Mixed { .. }))
2465 | (ComplementsRow::Lower, Some(PairShape::OneSided { .. }))
2466 | (ComplementsRow::Upper, Some(PairShape::OneSided { .. }))
2467 )
2468 }
2469}
2470
2471fn add_complements(info: &mut IpoptProblemInfo, row: Index, var: Index) -> c_int {
2472 let offset = if info.index_style == 1 { 1 } else { 0 };
2473 let (r, v) = (
2474 zero_based(row, offset, info.m),
2475 zero_based(var, offset, info.n),
2476 );
2477 if r == usize::MAX || v == usize::MAX {
2478 return POUNCE_ERR_OUT_OF_RANGE;
2479 }
2480 if info.complementarity.of_var(v).is_some() || info.complements_used.of_var(v).is_some() {
2484 return POUNCE_ERR_VAR_ALREADY_PAIRED;
2485 }
2486 if info.complementarity.of_row(r).is_some() || info.complements_used.of_row(r).is_some() {
2487 return POUNCE_ERR_ROW_ALREADY_PAIRED;
2488 }
2489 let (gl, gu) = (info.g_l[r], info.g_u[r]);
2490 if !(gl <= NLP_LOWER_BOUND_INF && gu >= NLP_UPPER_BOUND_INF) {
2491 return POUNCE_ERR_ROW_NOT_FREE;
2492 }
2493 let Some(table_row) = complements_row(info.x_l[v], info.x_u[v]) else {
2494 return POUNCE_ERR_VAR_BOUNDS;
2495 };
2496 let (new_gl, new_gu, declares) = table_row.row_bounds(gl, gu);
2497 debug_assert!(
2498 !declares
2499 || table_row.shape_matches(classify_pair(info.x_l[v], info.x_u[v], new_gl, new_gu)),
2500 "MOI table row {table_row:?} edits to a bound pair that is not its native shape"
2501 );
2502 info.g_l[r] = new_gl;
2508 info.g_u[r] = new_gu;
2509 if declares && append_pair(info, row, var).is_err() {
2510 info.g_l[r] = gl;
2511 info.g_u[r] = gu;
2512 return POUNCE_ERR_INTERNAL;
2513 }
2514 info.complements_used.push(ComplementsUse {
2515 row: r,
2516 var: v,
2517 table: table_row,
2518 user_gl: gl,
2519 user_gu: gu,
2520 });
2521 table_row.code()
2522}
2523
2524#[unsafe(no_mangle)]
2554pub unsafe extern "C" fn AddPounceComplementsConstraint(
2555 ipopt_problem: IpoptProblem,
2556 row: Index,
2557 var: Index,
2558) -> c_int {
2559 ffi_guard(POUNCE_ERR_INTERNAL, || unsafe {
2560 if ipopt_problem.is_null() {
2561 return POUNCE_ERR_NULL;
2562 }
2563 add_complements(&mut *ipopt_problem, row, var)
2564 })
2565}
2566
2567pub const POUNCE_CC_CLASS_S: c_int = 0;
2580pub const POUNCE_CC_CLASS_W: c_int = 1;
2582pub const POUNCE_CC_CLASS_NONE: c_int = 2;
2584pub const POUNCE_CC_CLASS_NOT_SEARCHED: c_int = 3;
2587pub const POUNCE_CC_CLASS_ABSENT: c_int = -1;
2590
2591pub const POUNCE_CC_REPORTED_ANSWER: c_int = 0;
2593pub const POUNCE_CC_REPORTED_WEAK_FALLBACK: c_int = 1;
2595pub const POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED: c_int = 2;
2598pub const POUNCE_CC_REPORTED_VERDICT: c_int = 3;
2600
2601pub const POUNCE_CC_B_VERDICT_B: c_int = 0;
2603pub const POUNCE_CC_B_VERDICT_NOT_B: c_int = 1;
2605pub const POUNCE_CC_B_VERDICT_NOT_CHECKED: c_int = 2;
2607pub const POUNCE_CC_B_VERDICT_ABSENT: c_int = -1;
2609
2610pub const POUNCE_CC_TEXT_SUMMARY: c_int = 0;
2612pub const POUNCE_CC_TEXT_POLISH_REASON: c_int = 1;
2614pub const POUNCE_CC_TEXT_B_REASON: c_int = 2;
2617
2618#[repr(C)]
2630#[derive(Debug, Clone, Copy)]
2631pub struct PounceComplementarityStats {
2632 pub struct_size: Index,
2633 pub strategy: [c_char; 16],
2634 pub reported: Index,
2635 pub accepted_by: [c_char; 32],
2636 pub fallback: [c_char; 32],
2637 pub n_solves: Index,
2638 pub total_iterations: Index,
2639 pub max_abs_product: Number,
2640 pub certificate_class: Index,
2641 pub n_pairs: Index,
2642 pub n_biactive: Index,
2643 pub n_neither: Index,
2644 pub multipliers_unique: Index,
2645 pub residual: Number,
2646 pub resid_tol: Number,
2647 pub tight: Index,
2648 pub feasible: Index,
2649 pub primal_violation: Number,
2650 pub b_verdict: Index,
2651 pub b_n_biactive: Index,
2652 pub b_branches_solved: Index,
2653 pub b_enumerated: Index,
2654 pub escapes: Index,
2655 pub lifted: Index,
2656 pub natural_residual: Number,
2657 pub polish_ran: Index,
2658 pub polish_taken: Index,
2659 pub polish_residual_before: Number,
2660 pub polish_residual_after: Number,
2661 pub polish_threshold: Number,
2662}
2663
2664#[repr(C)]
2668#[derive(Debug, Clone, Copy)]
2669pub struct PounceComplementaritySolve {
2670 pub struct_size: Index,
2671 pub stage: [c_char; 32],
2672 pub relaxed: Index,
2673 pub tau: Number,
2674 pub status: Index,
2675 pub iterations: Index,
2676 pub max_abs_product: Number,
2677 pub seconds: Number,
2678}
2679
2680unsafe impl ExtensibleOut for PounceComplementarityStats {}
2682unsafe impl ExtensibleOut for PounceComplementaritySolve {}
2683
2684fn copy_label<const N: usize>(dst: &mut [c_char; N], s: &str) {
2687 dst.fill(0);
2688 let keep = s.len().min(N.saturating_sub(1));
2689 for (slot, b) in dst.iter_mut().zip(&s.as_bytes()[..keep]) {
2690 *slot = *b as c_char;
2691 }
2692}
2693
2694fn cc_count(v: usize) -> Index {
2696 Index::try_from(v).unwrap_or(Index::MAX)
2697}
2698
2699fn cc_real(v: Number) -> Number {
2702 if v.is_finite() { v } else { Number::NAN }
2703}
2704
2705fn cc_flag(b: bool) -> Index {
2706 Index::from(b)
2707}
2708
2709pub(crate) fn complementarity_stats(
2713 out: &pounce_restoration::mpcc_driver::MpccOutcome,
2714) -> PounceComplementarityStats {
2715 use pounce_algorithm::mpcc::{BVerdict, Reported, StationarityClass};
2716 let mut s = PounceComplementarityStats {
2717 struct_size: struct_size_of::<PounceComplementarityStats>(),
2718 strategy: [0; 16],
2719 reported: match out.reported {
2720 Reported::Answer(_) => POUNCE_CC_REPORTED_ANSWER,
2721 Reported::WeakFallback(_) => POUNCE_CC_REPORTED_WEAK_FALLBACK,
2722 Reported::FeasibleUncertified(_) => POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED,
2723 Reported::Verdict => POUNCE_CC_REPORTED_VERDICT,
2724 },
2725 accepted_by: [0; 32],
2726 fallback: [0; 32],
2727 n_solves: cc_count(out.solves.len()),
2728 total_iterations: cc_count(out.total_iterations),
2729 max_abs_product: cc_real(out.max_abs_product),
2730 certificate_class: POUNCE_CC_CLASS_ABSENT,
2731 n_pairs: -1,
2732 n_biactive: -1,
2733 n_neither: -1,
2734 multipliers_unique: -1,
2735 residual: Number::NAN,
2736 resid_tol: Number::NAN,
2737 tight: -1,
2738 feasible: -1,
2739 primal_violation: Number::NAN,
2740 b_verdict: POUNCE_CC_B_VERDICT_ABSENT,
2741 b_n_biactive: -1,
2742 b_branches_solved: -1,
2743 b_enumerated: -1,
2744 escapes: cc_count(out.escapes),
2745 lifted: cc_count(out.lifted),
2746 natural_residual: out.natural_residual.map_or(Number::NAN, cc_real),
2747 polish_ran: 0,
2748 polish_taken: 0,
2749 polish_residual_before: Number::NAN,
2750 polish_residual_after: Number::NAN,
2751 polish_threshold: cc_real(out.polish_threshold),
2752 };
2753 copy_label(&mut s.strategy, out.strategy.name());
2754 if let Some(stage) = out.accepted_by {
2755 copy_label(&mut s.accepted_by, stage.label());
2756 }
2757 if let Some(stage) = out.fallback {
2758 copy_label(&mut s.fallback, stage.label());
2759 }
2760 if let Some(c) = &out.certificate {
2761 s.certificate_class = match c.class {
2762 StationarityClass::S => POUNCE_CC_CLASS_S,
2763 StationarityClass::W => POUNCE_CC_CLASS_W,
2764 StationarityClass::None => POUNCE_CC_CLASS_NONE,
2765 StationarityClass::NotSearched => POUNCE_CC_CLASS_NOT_SEARCHED,
2766 };
2767 s.n_pairs = cc_count(c.n_pairs);
2768 s.n_biactive = cc_count(c.n_biactive);
2769 s.n_neither = cc_count(c.n_neither);
2770 s.multipliers_unique = cc_flag(c.multipliers_unique);
2771 s.residual = cc_real(c.residual);
2772 s.resid_tol = cc_real(c.resid_tol);
2773 s.tight = cc_flag(c.tight);
2774 s.feasible = cc_flag(c.feasible);
2775 s.primal_violation = cc_real(c.primal_violation);
2776 }
2777 if let Some(b) = &out.b_check {
2778 s.b_verdict = match b.verdict {
2779 BVerdict::B => POUNCE_CC_B_VERDICT_B,
2780 BVerdict::NotB { .. } => POUNCE_CC_B_VERDICT_NOT_B,
2781 BVerdict::NotChecked(_) => POUNCE_CC_B_VERDICT_NOT_CHECKED,
2782 };
2783 s.b_n_biactive = cc_count(b.n_biactive);
2784 s.b_branches_solved = cc_count(b.branches_solved);
2785 s.b_enumerated = cc_flag(b.enumerated);
2786 }
2787 if let Some(p) = &out.polish {
2788 s.polish_ran = 1;
2789 s.polish_taken = cc_flag(p.taken);
2790 s.polish_residual_before = cc_real(p.residual_before);
2791 s.polish_residual_after = cc_real(p.residual_after);
2792 }
2793 s
2794}
2795
2796pub(crate) fn complementarity_solve(
2798 r: &pounce_restoration::mpcc_driver::MpccSolveRecord,
2799) -> PounceComplementaritySolve {
2800 let mut s = PounceComplementaritySolve {
2801 struct_size: struct_size_of::<PounceComplementaritySolve>(),
2802 stage: [0; 32],
2803 relaxed: cc_flag(r.tau.is_some()),
2804 tau: r.tau.unwrap_or(Number::NAN),
2805 status: r.status as Index,
2806 iterations: cc_count(r.iterations),
2807 max_abs_product: cc_real(r.max_abs_product),
2808 seconds: cc_real(r.seconds),
2809 };
2810 copy_label(&mut s.stage, r.stage.label());
2811 s
2812}
2813
2814pub(crate) fn complementarity_text(
2817 out: &pounce_restoration::mpcc_driver::MpccOutcome,
2818 which: c_int,
2819) -> Option<String> {
2820 use pounce_algorithm::mpcc::BVerdict;
2821 match which {
2822 POUNCE_CC_TEXT_SUMMARY => Some(pounce_algorithm::mpcc::describe_outcome(
2823 out.reported,
2824 out.solves.len(),
2825 out.total_iterations,
2826 out.max_abs_product,
2827 out.certificate.as_ref(),
2828 )),
2829 POUNCE_CC_TEXT_POLISH_REASON => out.polish.as_ref().and_then(|p| p.reason.clone()),
2830 POUNCE_CC_TEXT_B_REASON => match out.b_check.as_ref().map(|b| &b.verdict) {
2831 Some(BVerdict::NotChecked(why)) => Some((*why).to_string()),
2832 _ => None,
2833 },
2834 _ => None,
2835 }
2836}
2837
2838unsafe fn last_mpcc<'a>(
2840 ipopt_problem: IpoptProblem,
2841) -> Option<&'a pounce_restoration::mpcc_driver::MpccOutcome> {
2842 unsafe {
2843 if ipopt_problem.is_null() {
2844 return None;
2845 }
2846 (*ipopt_problem)
2847 .last_solve
2848 .as_ref()
2849 .and_then(|ls| ls.mpcc.as_ref())
2850 }
2851}
2852
2853#[unsafe(no_mangle)]
2871pub unsafe extern "C" fn GetPounceComplementarityStats(
2872 ipopt_problem: IpoptProblem,
2873 stats: *mut PounceComplementarityStats,
2874) -> Bool {
2875 ffi_guard(FALSE, || unsafe {
2876 if stats.is_null() {
2877 return FALSE;
2878 }
2879 let Some(out) = last_mpcc(ipopt_problem) else {
2880 return FALSE;
2881 };
2882 Bool::from(write_extensible(stats, &complementarity_stats(out)))
2885 })
2886}
2887
2888#[unsafe(no_mangle)]
2901pub unsafe extern "C" fn GetPounceComplementaritySolve(
2902 ipopt_problem: IpoptProblem,
2903 k: Index,
2904 solve: *mut PounceComplementaritySolve,
2905) -> Bool {
2906 ffi_guard(FALSE, || unsafe {
2907 if solve.is_null() {
2908 return FALSE;
2909 }
2910 let Some(out) = last_mpcc(ipopt_problem) else {
2911 return FALSE;
2912 };
2913 let Some(r) = usize::try_from(k).ok().and_then(|k| out.solves.get(k)) else {
2914 return FALSE;
2915 };
2916 Bool::from(write_extensible(solve, &complementarity_solve(r)))
2917 })
2918}
2919
2920#[unsafe(no_mangle)]
2937pub unsafe extern "C" fn GetPounceComplementarityText(
2938 ipopt_problem: IpoptProblem,
2939 which: c_int,
2940 buf: *mut c_char,
2941 buflen: Index,
2942) -> Index {
2943 ffi_guard(-1, || unsafe {
2944 let Some(out) = last_mpcc(ipopt_problem) else {
2945 return -1;
2946 };
2947 let Some(text) = complementarity_text(out, which) else {
2948 return -1;
2949 };
2950 let bytes = text.as_bytes();
2951 if !buf.is_null() && buflen > 0 {
2952 let keep = bytes.len().min(buflen as usize - 1);
2953 let dst = std::slice::from_raw_parts_mut(buf, keep + 1);
2954 for (slot, b) in dst.iter_mut().zip(&bytes[..keep]) {
2955 *slot = *b as c_char;
2956 }
2957 dst[keep] = 0;
2958 }
2959 cc_count(bytes.len())
2960 })
2961}
2962
2963#[unsafe(no_mangle)]
2970pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
2971 unsafe {
2972 if ipopt_problem.is_null() {
2973 return FALSE;
2974 }
2975 (*ipopt_problem).pending_working_set = None;
2976 (*ipopt_problem).app.clear_sqp_warm_start();
2977 TRUE
2978 }
2979}
2980
2981#[allow(clippy::too_many_arguments)]
2997#[unsafe(no_mangle)]
2998pub unsafe extern "C" fn IpoptSolveWarmStart(
2999 ipopt_problem: IpoptProblem,
3000 x: *mut Number,
3001 g: *mut Number,
3002 obj_val: *mut Number,
3003 mult_g: *mut Number,
3004 mult_x_L: *mut Number,
3005 mult_x_U: *mut Number,
3006 bound_status_in: *const IpoptBoundStatus,
3007 cons_status_in: *const IpoptConsStatus,
3008 bound_status_out: *mut IpoptBoundStatus,
3009 cons_status_out: *mut IpoptConsStatus,
3010 user_data: *mut c_void,
3011) -> Index {
3012 if ipopt_problem.is_null() {
3013 return ApplicationReturnStatus::InternalError as Index;
3014 }
3015 ffi_guard(ApplicationReturnStatus::InternalError as Index, || unsafe {
3019 if !bound_status_in.is_null() || !cons_status_in.is_null() {
3024 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
3025 }
3026 let status = IpoptSolve(
3027 ipopt_problem,
3028 x,
3029 g,
3030 obj_val,
3031 mult_g,
3032 mult_x_L,
3033 mult_x_U,
3034 user_data,
3035 );
3036 if (*ipopt_problem).last_solve.is_some() {
3040 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
3041 }
3042 status
3043 })
3044}
3045
3046thread_local! {
3047 static CURRENT_BRIDGE: std::cell::Cell<*const CCallbackTnlp> =
3050 const { std::cell::Cell::new(std::ptr::null()) };
3051}
3052
3053pub(crate) fn bridge_violation_options(
3056 o: &pounce_common::options_list::OptionsList,
3057) -> (Number, Number, bool) {
3058 let num = |k: &str, d: Number| {
3059 o.get_numeric_value(k, "")
3060 .ok()
3061 .and_then(|(v, f)| f.then_some(v))
3062 .unwrap_or(d)
3063 };
3064 let relax = matches!(
3065 o.get_string_value("fixed_variable_treatment", "")
3066 .ok()
3067 .and_then(|(v, f)| f.then_some(v))
3068 .as_deref(),
3069 Some("relax_bounds")
3070 );
3071 (
3072 num("nlp_lower_bound_inf", NLP_LOWER_BOUND_INF),
3073 num("nlp_upper_bound_inf", NLP_UPPER_BOUND_INF),
3074 !relax,
3075 )
3076}
3077
3078thread_local! {
3079 static IN_LOWERED_SOLVE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3091}
3092
3093struct LoweredSolveGuard {
3097 prev: bool,
3098}
3099
3100impl LoweredSolveGuard {
3101 fn set(lowered: bool) -> Self {
3102 Self {
3103 prev: IN_LOWERED_SOLVE.with(|c| c.replace(lowered)),
3104 }
3105 }
3106}
3107
3108impl Drop for LoweredSolveGuard {
3109 fn drop(&mut self) {
3110 IN_LOWERED_SOLVE.with(|c| c.set(self.prev));
3111 }
3112}
3113
3114fn in_lowered_solve() -> bool {
3115 IN_LOWERED_SOLVE.with(|c| c.get())
3116}
3117
3118struct BridgeGuard {
3119 prev: *const CCallbackTnlp,
3120}
3121
3122impl BridgeGuard {
3123 fn install(bridge: &CCallbackTnlp) -> Self {
3124 let prev = CURRENT_BRIDGE.with(|c| c.replace(bridge as *const _));
3125 Self { prev }
3126 }
3127}
3128
3129impl Drop for BridgeGuard {
3130 fn drop(&mut self) {
3131 CURRENT_BRIDGE.with(|c| c.set(self.prev));
3132 }
3133}
3134
3135impl CCallbackTnlp {
3136 fn user_grad_lag(&self, it: &ip_intermediate::UserIterate) -> Option<Vec<Number>> {
3140 let n = self.n as usize;
3141 let cb = self.eval_grad_f?;
3142 let mut grad = vec![0.0; n];
3143 let x = it.x.as_ptr() as *mut Number;
3144 if unsafe { cb(self.n, x, TRUE, grad.as_mut_ptr(), self.user_data) } == FALSE {
3145 return None;
3146 }
3147 let nnz = self.nele_jac.max(0) as usize;
3148 let (mut rows, mut cols, mut vals) = (vec![0; nnz], vec![0; nnz], vec![0.0; nnz]);
3149 if self.m > 0 && nnz > 0 {
3150 let jac = self.eval_jac_g?;
3151 let structure = unsafe {
3152 jac(
3153 self.n,
3154 x,
3155 FALSE,
3156 self.m,
3157 self.nele_jac,
3158 rows.as_mut_ptr(),
3159 cols.as_mut_ptr(),
3160 std::ptr::null_mut(),
3161 self.user_data,
3162 )
3163 };
3164 let values = unsafe {
3165 jac(
3166 self.n,
3167 x,
3168 FALSE,
3169 self.m,
3170 self.nele_jac,
3171 std::ptr::null_mut(),
3172 std::ptr::null_mut(),
3173 vals.as_mut_ptr(),
3174 self.user_data,
3175 )
3176 };
3177 if structure == FALSE || values == FALSE {
3178 return None;
3179 }
3180 }
3181 let off = if self.index_style == 1 { 1 } else { 0 };
3182 let to_usize = |v: &[Index]| -> Vec<usize> {
3183 v.iter()
3184 .map(|&k| usize::try_from(k - off).unwrap_or(usize::MAX))
3185 .collect()
3186 };
3187 Some(pounce_nlp::live_iterate::lagrangian_gradient(
3188 it,
3189 &grad,
3190 &to_usize(&rows),
3191 &to_usize(&cols),
3192 &vals,
3193 ))
3194 }
3195}
3196
3197pub(crate) struct CCallbackTnlp {
3208 pub(crate) n: Index,
3209 pub(crate) m: Index,
3210 pub(crate) nele_jac: Index,
3211 pub(crate) nele_hess: Index,
3212 pub(crate) index_style: Index,
3213 pub(crate) x_l: Vec<Number>,
3214 pub(crate) x_u: Vec<Number>,
3215 pub(crate) g_l: Vec<Number>,
3216 pub(crate) g_u: Vec<Number>,
3217 pub(crate) initial_x: Vec<Number>,
3218 pub(crate) start_lambda: *const Number,
3229 pub(crate) start_z_l: *const Number,
3232 pub(crate) start_z_u: *const Number,
3235 pub(crate) eval_f: Option<Eval_F_CB>,
3236 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
3237 pub(crate) eval_g: Option<Eval_G_CB>,
3238 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
3239 pub(crate) eval_h: Option<Eval_H_CB>,
3240 pub(crate) user_data: *mut c_void,
3241 pub(crate) intermediate_cb: Option<Intermediate_CB>,
3244 pub(crate) user_scaling: Option<UserScaling>,
3246 pub(crate) nonlinear_vars: Option<Vec<Index>>,
3249 pub(crate) lower_inf: Number,
3253 pub(crate) upper_inf: Number,
3254 pub(crate) zero_fixed_grad_lag: bool,
3258 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
3259 pub(crate) final_x: Vec<Number>,
3260 pub(crate) final_z_l: Vec<Number>,
3261 pub(crate) final_z_u: Vec<Number>,
3262 pub(crate) final_g: Vec<Number>,
3263 pub(crate) final_lambda: Vec<Number>,
3264 pub(crate) final_obj: Number,
3265}
3266
3267impl TNLP for CCallbackTnlp {
3268 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
3269 Some(NlpInfo {
3270 n: self.n as pounce_common::types::Index,
3271 m: self.m as pounce_common::types::Index,
3272 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
3273 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
3274 index_style: if self.index_style == 1 {
3275 IndexStyle::Fortran
3276 } else {
3277 IndexStyle::C
3278 },
3279 })
3280 }
3281
3282 fn get_number_of_nonlinear_variables(&mut self) -> pounce_common::types::Index {
3286 match &self.nonlinear_vars {
3287 Some(v) => v.len() as pounce_common::types::Index,
3288 None => -1,
3289 }
3290 }
3291
3292 fn get_list_of_nonlinear_variables(
3293 &mut self,
3294 pos_nonlin_vars: &mut [pounce_common::types::Index],
3295 ) -> bool {
3296 let Some(v) = self.nonlinear_vars.as_ref() else {
3297 return false;
3298 };
3299 if v.len() != pos_nonlin_vars.len() {
3300 return false;
3301 }
3302 pos_nonlin_vars.copy_from_slice(v);
3303 true
3304 }
3305
3306 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
3307 if !self.x_l.is_empty() {
3308 b.x_l.copy_from_slice(&self.x_l);
3309 }
3310 if !self.x_u.is_empty() {
3311 b.x_u.copy_from_slice(&self.x_u);
3312 }
3313 if !self.g_l.is_empty() {
3314 b.g_l.copy_from_slice(&self.g_l);
3315 }
3316 if !self.g_u.is_empty() {
3317 b.g_u.copy_from_slice(&self.g_u);
3318 }
3319 true
3320 }
3321
3322 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
3323 if !self.initial_x.is_empty() {
3324 sp.x.copy_from_slice(&self.initial_x);
3325 }
3326 let copy_in = |dst: &mut [Number], src: *const Number| {
3339 if !src.is_null() && !dst.is_empty() {
3340 dst.copy_from_slice(unsafe { std::slice::from_raw_parts(src, dst.len()) });
3344 }
3345 };
3346 if sp.init_z {
3347 copy_in(sp.z_l, self.start_z_l);
3348 copy_in(sp.z_u, self.start_z_u);
3349 }
3350 if sp.init_lambda {
3351 copy_in(sp.lambda, self.start_lambda);
3352 }
3353 true
3354 }
3355
3356 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
3357 let Some(s) = self.user_scaling.as_ref() else {
3358 return false;
3359 };
3360 *req.obj_scaling = s.obj_scaling;
3361 if let Some(x) = s.x_scaling.as_ref() {
3362 if x.len() == req.x_scaling.len() {
3363 req.x_scaling.copy_from_slice(x);
3364 *req.use_x_scaling = true;
3365 }
3366 } else {
3367 *req.use_x_scaling = false;
3368 }
3369 if let Some(g) = s.g_scaling.as_ref() {
3370 if g.len() == req.g_scaling.len() {
3371 req.g_scaling.copy_from_slice(g);
3372 *req.use_g_scaling = true;
3373 }
3374 } else {
3375 *req.use_g_scaling = false;
3376 }
3377 true
3378 }
3379
3380 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
3381 let cb = self.eval_f?;
3382 let mut obj = 0.0;
3383 let ok = unsafe {
3384 cb(
3385 self.n,
3386 x.as_ptr() as *mut Number,
3387 if new_x { TRUE } else { FALSE },
3388 &mut obj,
3389 self.user_data,
3390 )
3391 };
3392 if ok != FALSE { Some(obj) } else { None }
3393 }
3394
3395 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
3396 let Some(cb) = self.eval_grad_f else {
3397 return false;
3398 };
3399 let ok = unsafe {
3400 cb(
3401 self.n,
3402 x.as_ptr() as *mut Number,
3403 if new_x { TRUE } else { FALSE },
3404 grad_f.as_mut_ptr(),
3405 self.user_data,
3406 )
3407 };
3408 ok != FALSE
3409 }
3410
3411 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
3412 if self.m == 0 {
3413 return true;
3414 }
3415 let Some(cb) = self.eval_g else {
3416 return false;
3417 };
3418 let ok = unsafe {
3419 cb(
3420 self.n,
3421 x.as_ptr() as *mut Number,
3422 if new_x { TRUE } else { FALSE },
3423 self.m,
3424 g.as_mut_ptr(),
3425 self.user_data,
3426 )
3427 };
3428 ok != FALSE
3429 }
3430
3431 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
3432 if self.m == 0 || self.nele_jac == 0 {
3433 return true;
3434 }
3435 let Some(cb) = self.eval_jac_g else {
3436 return false;
3437 };
3438 let x_ptr = x
3439 .map(|s| s.as_ptr() as *mut Number)
3440 .unwrap_or(std::ptr::null_mut());
3441 let ok = match mode {
3442 SparsityRequest::Structure { irow, jcol } => unsafe {
3443 cb(
3444 self.n,
3445 x_ptr,
3446 if new_x { TRUE } else { FALSE },
3447 self.m,
3448 self.nele_jac,
3449 irow.as_mut_ptr(),
3450 jcol.as_mut_ptr(),
3451 std::ptr::null_mut(),
3452 self.user_data,
3453 )
3454 },
3455 SparsityRequest::Values { values } => unsafe {
3456 cb(
3457 self.n,
3458 x_ptr,
3459 if new_x { TRUE } else { FALSE },
3460 self.m,
3461 self.nele_jac,
3462 std::ptr::null_mut(),
3463 std::ptr::null_mut(),
3464 values.as_mut_ptr(),
3465 self.user_data,
3466 )
3467 },
3468 };
3469 ok != FALSE
3470 }
3471
3472 fn eval_h(
3473 &mut self,
3474 x: Option<&[Number]>,
3475 new_x: bool,
3476 obj_factor: Number,
3477 lambda: Option<&[Number]>,
3478 new_lambda: bool,
3479 mode: SparsityRequest<'_>,
3480 ) -> bool {
3481 let Some(cb) = self.eval_h else {
3482 return false;
3483 };
3484 if self.nele_hess == 0 {
3485 return true;
3486 }
3487 let x_ptr = x
3488 .map(|s| s.as_ptr() as *mut Number)
3489 .unwrap_or(std::ptr::null_mut());
3490 let lambda_ptr = lambda
3491 .map(|s| s.as_ptr() as *mut Number)
3492 .unwrap_or(std::ptr::null_mut());
3493 let ok = match mode {
3494 SparsityRequest::Structure { irow, jcol } => unsafe {
3495 cb(
3496 self.n,
3497 x_ptr,
3498 if new_x { TRUE } else { FALSE },
3499 obj_factor,
3500 self.m,
3501 lambda_ptr,
3502 if new_lambda { TRUE } else { FALSE },
3503 self.nele_hess,
3504 irow.as_mut_ptr(),
3505 jcol.as_mut_ptr(),
3506 std::ptr::null_mut(),
3507 self.user_data,
3508 )
3509 },
3510 SparsityRequest::Values { values } => unsafe {
3511 cb(
3512 self.n,
3513 x_ptr,
3514 if new_x { TRUE } else { FALSE },
3515 obj_factor,
3516 self.m,
3517 lambda_ptr,
3518 if new_lambda { TRUE } else { FALSE },
3519 self.nele_hess,
3520 std::ptr::null_mut(),
3521 std::ptr::null_mut(),
3522 values.as_mut_ptr(),
3523 self.user_data,
3524 )
3525 },
3526 };
3527 ok != FALSE
3528 }
3529
3530 fn wants_intermediate_callback(&self) -> bool {
3533 self.intermediate_cb.is_some()
3534 }
3535
3536 fn wants_current_iterate(&self) -> bool {
3540 self.intermediate_cb.is_some()
3541 }
3542
3543 fn intermediate_callback(
3544 &mut self,
3545 stats: pounce_nlp::tnlp::IterStats,
3546 _ip_data: &IpoptData,
3547 _ip_cq: &IpoptCq,
3548 ) -> bool {
3549 let Some(cb) = self.intermediate_cb else {
3550 return true;
3551 };
3552 let _bridge = BridgeGuard::install(self);
3557 let ok = unsafe {
3558 cb(
3559 stats.mode as Index,
3560 stats.iter as Index,
3561 stats.obj_value,
3562 stats.inf_pr,
3563 stats.inf_du,
3564 stats.mu,
3565 stats.d_norm,
3566 stats.regularization_size,
3567 stats.alpha_du,
3568 stats.alpha_pr,
3569 stats.ls_trials as Index,
3570 self.user_data,
3571 )
3572 };
3573 ok != FALSE
3574 }
3575
3576 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
3577 self.final_status = Some(sol.status);
3578 if !sol.x.is_empty() {
3579 self.final_x.copy_from_slice(sol.x);
3580 }
3581 if !sol.z_l.is_empty() {
3582 self.final_z_l.copy_from_slice(sol.z_l);
3583 }
3584 if !sol.z_u.is_empty() {
3585 self.final_z_u.copy_from_slice(sol.z_u);
3586 }
3587 if !sol.g.is_empty() {
3588 self.final_g.copy_from_slice(sol.g);
3589 }
3590 if !sol.lambda.is_empty() {
3591 self.final_lambda.copy_from_slice(sol.lambda);
3592 }
3593 self.final_obj = sol.obj_value;
3594 }
3595}
3596
3597#[unsafe(no_mangle)]
3610pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
3611 if ipopt_problem.is_null() {
3612 return FALSE;
3613 }
3614 let info = unsafe { &mut *ipopt_problem };
3615 info.app.enable_iter_history();
3616 TRUE
3617}
3618
3619#[unsafe(no_mangle)]
3651pub unsafe extern "C" fn IpoptWriteSolveReport(
3652 ipopt_problem: IpoptProblem,
3653 path: *const c_char,
3654 detail: *const c_char,
3655) -> Bool {
3656 use pounce_solve_report::{
3657 InputDescriptor, ReportBuilder, ReportDetail, status_to_solve_result_num, write_report_file,
3658 };
3659
3660 ffi_guard(FALSE, || unsafe {
3665 if ipopt_problem.is_null() || path.is_null() {
3666 return FALSE;
3667 }
3668 let info = &*ipopt_problem;
3669 let Some(last) = info.last_solve.as_ref() else {
3670 return FALSE;
3671 };
3672
3673 let Ok(path_str) = CStr::from_ptr(path).to_str() else {
3674 return FALSE;
3675 };
3676
3677 let detail_choice = if detail.is_null() {
3678 ReportDetail::Summary
3679 } else {
3680 let Ok(detail_str) = CStr::from_ptr(detail).to_str() else {
3681 return FALSE;
3682 };
3683 match ReportDetail::parse(detail_str) {
3684 Ok(d) => d,
3685 Err(_) => return FALSE,
3686 }
3687 };
3688
3689 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
3690 builder.problem.n_variables = info.n;
3691 builder.problem.n_constraints = info.m;
3692 builder.problem.n_objectives = 1;
3693 builder.problem.nnz_jac_g = Some(info.nele_jac);
3694 builder.problem.nnz_h_lag = Some(info.nele_hess);
3695
3696 builder.solution.status = last.status;
3697 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
3698 builder.solution.objective = last.final_obj;
3699 builder.solution.x = last.final_x.clone();
3700 builder.solution.lambda = last.final_lambda.clone();
3701
3702 builder.ingest_stats(&last.stats);
3703 if let Some(linsol) = last.linear_solver.clone() {
3704 builder.set_linear_solver_summary(linsol);
3705 }
3706 if let Some(out) = &last.mpcc {
3713 builder.set_complementarity(out.report_info());
3714 }
3715 if let Some(so) = &last.second_opinion
3723 && so.ran()
3724 {
3725 builder.set_second_opinion(so.report_info());
3726 }
3727
3728 let report = builder.finish();
3729 match write_report_file(std::path::Path::new(path_str), &report) {
3730 Ok(_) => TRUE,
3731 Err(_) => FALSE,
3732 }
3733 })
3734}
3735
3736#[cfg(test)]
3737mod tests {
3738 use super::*;
3739 use std::ffi::CString;
3740
3741 unsafe extern "C" fn dummy_eval_f(
3742 _n: Index,
3743 _x: *const Number,
3744 _new_x: Bool,
3745 _obj_value: *mut Number,
3746 _user_data: *mut c_void,
3747 ) -> Bool {
3748 TRUE
3749 }
3750 unsafe extern "C" fn dummy_eval_grad_f(
3751 _n: Index,
3752 _x: *const Number,
3753 _new_x: Bool,
3754 _grad_f: *mut Number,
3755 _user_data: *mut c_void,
3756 ) -> Bool {
3757 TRUE
3758 }
3759
3760 fn create_unconstrained() -> IpoptProblem {
3761 let xl = [-1.0; 4];
3762 let xu = [1.0; 4];
3763 unsafe {
3764 CreateIpoptProblem(
3765 4,
3766 xl.as_ptr(),
3767 xu.as_ptr(),
3768 0,
3769 std::ptr::null(),
3770 std::ptr::null(),
3771 0,
3772 10,
3773 0,
3774 Some(dummy_eval_f),
3775 None,
3776 Some(dummy_eval_grad_f),
3777 None,
3778 None,
3779 )
3780 }
3781 }
3782
3783 #[test]
3784 fn create_succeeds_for_unconstrained_problem() {
3785 let p = create_unconstrained();
3786 assert!(!p.is_null());
3787 unsafe { FreeIpoptProblem(p) };
3788 }
3789
3790 #[test]
3791 fn create_returns_null_on_missing_required_callbacks() {
3792 let xl = [-1.0; 4];
3794 let xu = [1.0; 4];
3795 let p = unsafe {
3796 CreateIpoptProblem(
3797 4,
3798 xl.as_ptr(),
3799 xu.as_ptr(),
3800 0,
3801 std::ptr::null(),
3802 std::ptr::null(),
3803 0,
3804 10,
3805 0,
3806 None, None,
3808 Some(dummy_eval_grad_f),
3809 None,
3810 None,
3811 )
3812 };
3813 assert!(p.is_null());
3814 }
3815
3816 #[test]
3817 fn create_returns_null_on_negative_n() {
3818 let p = unsafe {
3819 CreateIpoptProblem(
3820 -1,
3821 std::ptr::null(),
3822 std::ptr::null(),
3823 0,
3824 std::ptr::null(),
3825 std::ptr::null(),
3826 0,
3827 10,
3828 0,
3829 Some(dummy_eval_f),
3830 None,
3831 Some(dummy_eval_grad_f),
3832 None,
3833 None,
3834 )
3835 };
3836 assert!(p.is_null());
3837 }
3838
3839 #[test]
3840 fn create_returns_null_on_invalid_index_style() {
3841 let xl = [0.0; 1];
3842 let xu = [1.0; 1];
3843 let p = unsafe {
3844 CreateIpoptProblem(
3845 1,
3846 xl.as_ptr(),
3847 xu.as_ptr(),
3848 0,
3849 std::ptr::null(),
3850 std::ptr::null(),
3851 0,
3852 1,
3853 2, Some(dummy_eval_f),
3855 None,
3856 Some(dummy_eval_grad_f),
3857 None,
3858 None,
3859 )
3860 };
3861 assert!(p.is_null());
3862 }
3863
3864 #[test]
3865 fn add_int_option_forwards_to_application() {
3866 let p = create_unconstrained();
3867 let key = CString::new("print_level").unwrap();
3868 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
3869 assert_eq!(ok, TRUE);
3870 let info = unsafe { &*p };
3871 let (level, found) = info
3872 .app
3873 .options()
3874 .get_integer_value("print_level", "")
3875 .unwrap();
3876 assert!(found);
3877 assert_eq!(level, 5);
3878 unsafe { FreeIpoptProblem(p) };
3879 }
3880
3881 #[test]
3882 fn add_str_option_with_invalid_key_returns_false() {
3883 let p = create_unconstrained();
3884 let key = CString::new("totally_unknown_option").unwrap();
3885 let val = CString::new("yes").unwrap();
3886 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
3887 assert_eq!(ok, FALSE);
3888 unsafe { FreeIpoptProblem(p) };
3889 }
3890
3891 #[test]
3892 fn add_options_on_null_problem_returns_false() {
3893 let key = CString::new("print_level").unwrap();
3897 let v = CString::new("yes").unwrap();
3898 unsafe {
3899 assert_eq!(
3900 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
3901 FALSE
3902 );
3903 assert_eq!(
3904 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
3905 FALSE
3906 );
3907 assert_eq!(
3908 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
3909 FALSE
3910 );
3911 }
3912 }
3913
3914 unsafe extern "C" fn dummy_intermediate(
3915 _alg_mod: Index,
3916 _iter_count: Index,
3917 _obj_value: Number,
3918 _inf_pr: Number,
3919 _inf_du: Number,
3920 _mu: Number,
3921 _d_norm: Number,
3922 _regularization_size: Number,
3923 _alpha_du: Number,
3924 _alpha_pr: Number,
3925 _ls_trials: Index,
3926 _user_data: *mut c_void,
3927 ) -> Bool {
3928 TRUE
3929 }
3930
3931 #[test]
3932 fn set_intermediate_callback_stores_pointer() {
3933 let p = create_unconstrained();
3934 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
3935 assert_eq!(ok, TRUE);
3936 let info = unsafe { &*p };
3937 assert!(info.intermediate_cb.is_some());
3938 unsafe { FreeIpoptProblem(p) };
3939 }
3940
3941 #[test]
3942 fn solve_returns_internal_error_on_null_problem() {
3943 let rc = unsafe {
3945 IpoptSolve(
3946 std::ptr::null_mut(),
3947 std::ptr::null_mut(),
3948 std::ptr::null_mut(),
3949 std::ptr::null_mut(),
3950 std::ptr::null_mut(),
3951 std::ptr::null_mut(),
3952 std::ptr::null_mut(),
3953 std::ptr::null_mut(),
3954 )
3955 };
3956 assert_eq!(rc, -199);
3957 }
3958
3959 #[test]
3960 fn free_null_is_safe() {
3961 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
3963 }
3964
3965 unsafe extern "C" fn quad_eval_f(
3971 _n: Index,
3972 x: *const Number,
3973 _new_x: Bool,
3974 obj_value: *mut Number,
3975 _user_data: *mut c_void,
3976 ) -> Bool {
3977 unsafe {
3978 let v = *x.offset(0);
3979 *obj_value = (v - 2.0) * (v - 2.0);
3980 TRUE
3981 }
3982 }
3983 unsafe extern "C" fn quad_eval_grad_f(
3984 _n: Index,
3985 x: *const Number,
3986 _new_x: Bool,
3987 grad: *mut Number,
3988 _user_data: *mut c_void,
3989 ) -> Bool {
3990 unsafe {
3991 let v = *x.offset(0);
3992 *grad.offset(0) = 2.0 * (v - 2.0);
3993 TRUE
3994 }
3995 }
3996 unsafe extern "C" fn quad_eval_h(
3997 _n: Index,
3998 _x: *const Number,
3999 _new_x: Bool,
4000 obj_factor: Number,
4001 _m: Index,
4002 _lambda: *const Number,
4003 _new_lambda: Bool,
4004 _nele_hess: Index,
4005 irow: *mut Index,
4006 jcol: *mut Index,
4007 values: *mut Number,
4008 _user_data: *mut c_void,
4009 ) -> Bool {
4010 unsafe {
4011 if !irow.is_null() && !jcol.is_null() && values.is_null() {
4012 *irow.offset(0) = 0;
4013 *jcol.offset(0) = 0;
4014 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
4015 *values.offset(0) = 2.0 * obj_factor;
4016 } else {
4017 return FALSE;
4018 }
4019 TRUE
4020 }
4021 }
4022
4023 #[test]
4024 fn solve_drives_unconstrained_quadratic_through_bridge() {
4025 let xl = [-1.0e20];
4028 let xu = [1.0e20];
4029 let p = unsafe {
4030 CreateIpoptProblem(
4031 1,
4032 xl.as_ptr(),
4033 xu.as_ptr(),
4034 0,
4035 std::ptr::null(),
4036 std::ptr::null(),
4037 0,
4038 1,
4039 0,
4040 Some(quad_eval_f),
4041 None,
4042 Some(quad_eval_grad_f),
4043 None,
4044 Some(quad_eval_h),
4045 )
4046 };
4047 assert!(!p.is_null());
4048 let mut x = [0.0_f64];
4049 let mut obj = 0.0_f64;
4050 let rc = unsafe {
4051 IpoptSolve(
4052 p,
4053 x.as_mut_ptr(),
4054 std::ptr::null_mut(),
4055 &mut obj,
4056 std::ptr::null_mut(),
4057 std::ptr::null_mut(),
4058 std::ptr::null_mut(),
4059 std::ptr::null_mut(),
4060 )
4061 };
4062 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4063 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
4064 assert!(obj.abs() < 1e-10, "obj = {}", obj);
4065 unsafe { FreeIpoptProblem(p) };
4066 }
4067
4068 #[test]
4083 fn stale_stats_cleared_when_resolve_bails() {
4084 let xl = [-1.0e20];
4085 let xu = [1.0e20];
4086 let p = unsafe {
4087 CreateIpoptProblem(
4088 1,
4089 xl.as_ptr(),
4090 xu.as_ptr(),
4091 0,
4092 std::ptr::null(),
4093 std::ptr::null(),
4094 0,
4095 1,
4096 0,
4097 Some(quad_eval_f),
4098 None,
4099 Some(quad_eval_grad_f),
4100 None,
4101 Some(quad_eval_h),
4102 )
4103 };
4104 assert!(!p.is_null());
4105
4106 let mut x = [0.0_f64];
4107 let mut obj = 0.0_f64;
4108 let rc = unsafe {
4109 IpoptSolve(
4110 p,
4111 x.as_mut_ptr(),
4112 std::ptr::null_mut(),
4113 &mut obj,
4114 std::ptr::null_mut(),
4115 std::ptr::null_mut(),
4116 std::ptr::null_mut(),
4117 std::ptr::null_mut(),
4118 )
4119 };
4120 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4121 let iters_after_success = unsafe { GetIpoptIterCount(p) };
4123 assert!(
4124 iters_after_success >= 1,
4125 "a converged solve should record >=1 iteration, got {iters_after_success}"
4126 );
4127 assert!(unsafe { (*p).last_solve.is_some() });
4128
4129 unsafe { (*p).n = -1 };
4132 let mut x2 = [0.0_f64];
4133 let rc2 = unsafe {
4134 IpoptSolve(
4135 p,
4136 x2.as_mut_ptr(),
4137 std::ptr::null_mut(),
4138 std::ptr::null_mut(),
4139 std::ptr::null_mut(),
4140 std::ptr::null_mut(),
4141 std::ptr::null_mut(),
4142 std::ptr::null_mut(),
4143 )
4144 };
4145 assert_eq!(
4146 rc2,
4147 ApplicationReturnStatus::InvalidProblemDefinition as Index
4148 );
4149
4150 assert!(
4154 unsafe { (*p).last_solve.is_none() },
4155 "a bailed re-solve must clear stale last_solve (F5)"
4156 );
4157 assert_eq!(
4158 unsafe { GetIpoptIterCount(p) },
4159 0,
4160 "stale iteration count must not survive a bailed re-solve (F5)"
4161 );
4162
4163 unsafe { FreeIpoptProblem(p) };
4164 }
4165
4166 #[test]
4167 fn solve_invalid_problem_definition_when_x_null() {
4168 let p = create_unconstrained();
4169 let rc = unsafe {
4170 IpoptSolve(
4171 p,
4172 std::ptr::null_mut(), std::ptr::null_mut(),
4174 std::ptr::null_mut(),
4175 std::ptr::null_mut(),
4176 std::ptr::null_mut(),
4177 std::ptr::null_mut(),
4178 std::ptr::null_mut(),
4179 )
4180 };
4181 assert_eq!(
4182 rc,
4183 ApplicationReturnStatus::InvalidProblemDefinition as Index
4184 );
4185 unsafe { FreeIpoptProblem(p) };
4186 }
4187
4188 #[test]
4191 fn get_version_writes_pkg_version() {
4192 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
4193 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
4194 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
4195 assert_eq!((mj, mn, pt), expected);
4196 }
4197
4198 #[test]
4199 fn get_version_tolerates_null_buffers() {
4200 unsafe {
4203 GetIpoptVersion(
4204 std::ptr::null_mut(),
4205 std::ptr::null_mut(),
4206 std::ptr::null_mut(),
4207 )
4208 };
4209 }
4210
4211 #[test]
4212 fn set_scaling_stores_user_supplied_arrays() {
4213 let p = create_unconstrained();
4214 let xs = [2.0, 3.0, 4.0, 5.0];
4215 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
4216 assert_eq!(ok, TRUE);
4217 let info = unsafe { &*p };
4218 let s = info.user_scaling.as_ref().unwrap();
4219 assert_eq!(s.obj_scaling, 7.0);
4220 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
4221 assert!(s.g_scaling.is_none());
4222 unsafe { FreeIpoptProblem(p) };
4223 }
4224
4225 #[test]
4226 fn set_scaling_on_null_problem_returns_false() {
4227 let ok = unsafe {
4229 SetIpoptProblemScaling(
4230 std::ptr::null_mut(),
4231 1.0,
4232 std::ptr::null(),
4233 std::ptr::null(),
4234 )
4235 };
4236 assert_eq!(ok, FALSE);
4237 }
4238
4239 #[test]
4240 fn open_output_file_writes_and_attaches_journal() {
4241 let p = create_unconstrained();
4242 let dir = std::env::temp_dir().join("pounce-cinterface-test");
4243 let _ = std::fs::create_dir_all(&dir);
4244 let path = dir.join("output.log");
4245 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
4246 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
4247 assert_eq!(ok, TRUE);
4248 let info = unsafe { &*p };
4250 let (level, found) = info
4251 .app
4252 .options()
4253 .get_integer_value("file_print_level", "")
4254 .unwrap();
4255 assert!(found);
4256 assert_eq!(level, 5);
4257 unsafe { FreeIpoptProblem(p) };
4258 let _ = std::fs::remove_file(&path);
4259 }
4260
4261 #[test]
4262 fn open_output_file_with_null_inputs_returns_false() {
4263 let key = CString::new("nope").unwrap();
4265 unsafe {
4266 assert_eq!(
4267 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
4268 FALSE
4269 );
4270 }
4271 let p = create_unconstrained();
4272 unsafe {
4273 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
4274 FreeIpoptProblem(p);
4275 }
4276 }
4277
4278 #[test]
4279 fn get_current_iterate_returns_false_outside_callback() {
4280 let p = create_unconstrained();
4282 let rc = unsafe {
4283 GetIpoptCurrentIterate(
4284 p,
4285 FALSE,
4286 0,
4287 std::ptr::null_mut(),
4288 std::ptr::null_mut(),
4289 std::ptr::null_mut(),
4290 0,
4291 std::ptr::null_mut(),
4292 std::ptr::null_mut(),
4293 )
4294 };
4295 assert_eq!(rc, FALSE);
4296 unsafe { FreeIpoptProblem(p) };
4297 }
4298
4299 #[test]
4300 fn get_current_violations_returns_false_outside_callback() {
4301 let p = create_unconstrained();
4303 let rc = unsafe {
4304 GetIpoptCurrentViolations(
4305 p,
4306 FALSE,
4307 0,
4308 std::ptr::null_mut(),
4309 std::ptr::null_mut(),
4310 std::ptr::null_mut(),
4311 std::ptr::null_mut(),
4312 std::ptr::null_mut(),
4313 0,
4314 std::ptr::null_mut(),
4315 std::ptr::null_mut(),
4316 )
4317 };
4318 assert_eq!(rc, FALSE);
4319 unsafe { FreeIpoptProblem(p) };
4320 }
4321
4322 #[test]
4323 fn post_solve_stats_zero_before_solve() {
4324 let p = create_unconstrained();
4330 unsafe {
4331 assert_eq!(GetIpoptIterCount(p), 0);
4332 assert_eq!(GetIpoptSolveTime(p), 0.0);
4333 assert_eq!(GetIpoptPrimalInf(p), 0.0);
4334 assert_eq!(GetIpoptDualInf(p), 0.0);
4335 assert_eq!(GetIpoptComplInf(p), 0.0);
4336 FreeIpoptProblem(p);
4337 }
4338 }
4339
4340 #[test]
4341 fn post_solve_stats_populated_after_solve() {
4342 let xl = [-1.0e20];
4344 let xu = [1.0e20];
4345 let p = unsafe {
4346 CreateIpoptProblem(
4347 1,
4348 xl.as_ptr(),
4349 xu.as_ptr(),
4350 0,
4351 std::ptr::null(),
4352 std::ptr::null(),
4353 0,
4354 1,
4355 0,
4356 Some(quad_eval_f),
4357 None,
4358 Some(quad_eval_grad_f),
4359 None,
4360 Some(quad_eval_h),
4361 )
4362 };
4363 let mut x = [0.0_f64];
4364 let mut obj = 0.0_f64;
4365 let rc = unsafe {
4366 IpoptSolve(
4367 p,
4368 x.as_mut_ptr(),
4369 std::ptr::null_mut(),
4370 &mut obj,
4371 std::ptr::null_mut(),
4372 std::ptr::null_mut(),
4373 std::ptr::null_mut(),
4374 std::ptr::null_mut(),
4375 )
4376 };
4377 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4378 unsafe {
4381 assert!(GetIpoptIterCount(p) >= 0);
4382 assert!(GetIpoptSolveTime(p) >= 0.0);
4383 assert!(GetIpoptPrimalInf(p).is_finite());
4384 assert!(GetIpoptDualInf(p).is_finite());
4385 assert!(GetIpoptComplInf(p).is_finite());
4386 FreeIpoptProblem(p);
4387 }
4388 }
4389
4390 #[test]
4395 fn linear_solver_stats_populated_after_solve() {
4396 let xl = [-1.0e20];
4398 let xu = [1.0e20];
4399 let p = unsafe {
4400 CreateIpoptProblem(
4401 1,
4402 xl.as_ptr(),
4403 xu.as_ptr(),
4404 0,
4405 std::ptr::null(),
4406 std::ptr::null(),
4407 0,
4408 1,
4409 0,
4410 Some(quad_eval_f),
4411 None,
4412 Some(quad_eval_grad_f),
4413 None,
4414 Some(quad_eval_h),
4415 )
4416 };
4417 let mut stats = unsafe { std::mem::zeroed::<PounceLinearSolverStats>() };
4418 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, FALSE);
4420
4421 let mut x = [0.0_f64];
4422 let mut obj = 0.0_f64;
4423 let rc = unsafe {
4424 IpoptSolve(
4425 p,
4426 x.as_mut_ptr(),
4427 std::ptr::null_mut(),
4428 &mut obj,
4429 std::ptr::null_mut(),
4430 std::ptr::null_mut(),
4431 std::ptr::null_mut(),
4432 std::ptr::null_mut(),
4433 )
4434 };
4435 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4436 assert_eq!(unsafe { GetPounceLinearSolverStats(p, &mut stats) }, TRUE);
4437
4438 let name = unsafe { CStr::from_ptr(stats.solver_name.as_ptr()) }
4439 .to_str()
4440 .expect("solver name is ASCII");
4441 assert_eq!(name, "feral", "default backend should report itself");
4442 assert!(stats.n_factors > 0, "n_factors = {}", stats.n_factors);
4443 assert_eq!(
4444 stats.n_pattern_reuse + stats.n_pattern_changes,
4445 stats.n_factors,
4446 "every factor is either a pattern reuse or a pattern change"
4447 );
4448 assert!(stats.max_fill_ratio.is_nan() || stats.max_fill_ratio > 0.0);
4451 assert!(stats.last_nnz_l == -1 || stats.last_nnz_l > 0);
4452 unsafe { FreeIpoptProblem(p) };
4453 }
4454
4455 #[test]
4456 fn option_type_reports_the_setter_a_keyword_expects() {
4457 let p = create_unconstrained();
4459 let ty = |s: &str| {
4460 let c = std::ffi::CString::new(s).unwrap();
4461 unsafe { GetPounceOptionType(p, c.as_ptr()) }
4462 };
4463 assert_eq!(ty("tol"), 1, "tol is a number");
4464 assert_eq!(ty("max_iter"), 2, "max_iter is an integer");
4465 assert_eq!(ty("linear_solver"), 3, "linear_solver is a string");
4466 assert_eq!(ty("hessian_approximation"), 3);
4469 assert_eq!(ty("no_such_option_at_all"), 0);
4472 assert_eq!(unsafe { GetPounceOptionType(p, std::ptr::null()) }, 0);
4473 unsafe { FreeIpoptProblem(p) };
4474 }
4475
4476 #[test]
4479 fn option_type_answers_without_a_problem_handle() {
4480 let ty = |s: &str| {
4481 let c = std::ffi::CString::new(s).unwrap();
4482 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) }
4483 };
4484 assert_eq!(ty("tol"), 1);
4485 assert_eq!(ty("max_iter"), 2);
4486 assert_eq!(ty("linear_solver"), 3);
4487 assert_eq!(ty("no_such_option_at_all"), 0);
4488
4489 let p = create_unconstrained();
4492 for name in [
4493 "tol",
4494 "max_iter",
4495 "linear_solver",
4496 "mu_strategy",
4497 "print_level",
4498 ] {
4499 let c = std::ffi::CString::new(name).unwrap();
4500 assert_eq!(
4501 unsafe { GetPounceOptionType(std::ptr::null_mut(), c.as_ptr()) },
4502 unsafe { GetPounceOptionType(p, c.as_ptr()) },
4503 "handle-free and problem-bound disagree on {name}"
4504 );
4505 }
4506 unsafe { FreeIpoptProblem(p) };
4507 }
4508
4509 #[test]
4510 fn write_solve_report_emits_v1_json_with_iter_history() {
4511 let xl = [-1.0e20];
4514 let xu = [1.0e20];
4515 let p = unsafe {
4516 CreateIpoptProblem(
4517 1,
4518 xl.as_ptr(),
4519 xu.as_ptr(),
4520 0,
4521 std::ptr::null(),
4522 std::ptr::null(),
4523 0,
4524 1,
4525 0,
4526 Some(quad_eval_f),
4527 None,
4528 Some(quad_eval_grad_f),
4529 None,
4530 Some(quad_eval_h),
4531 )
4532 };
4533
4534 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
4536 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
4537 assert_eq!(bad, FALSE);
4538
4539 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
4541 let mut x = [0.0_f64];
4542 let mut obj = 0.0_f64;
4543 let rc = unsafe {
4544 IpoptSolve(
4545 p,
4546 x.as_mut_ptr(),
4547 std::ptr::null_mut(),
4548 &mut obj,
4549 std::ptr::null_mut(),
4550 std::ptr::null_mut(),
4551 std::ptr::null_mut(),
4552 std::ptr::null_mut(),
4553 )
4554 };
4555 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4556
4557 let dir = std::env::temp_dir();
4558 let path = dir.join("pounce_cinterface_report.json");
4559 let cpath = CString::new(path.to_str().unwrap()).unwrap();
4560 let cdetail = CString::new("full").unwrap();
4561 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
4562 assert_eq!(ok, TRUE);
4563
4564 let txt = std::fs::read_to_string(&path).unwrap();
4567 assert!(
4568 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
4569 "{txt}"
4570 );
4571 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
4572 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
4573 assert_eq!(parsed.problem.n_variables, 1);
4574 assert_eq!(parsed.problem.n_constraints, 0);
4575
4576 let bad_detail = CString::new("verbose").unwrap();
4578 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
4579 assert_eq!(bad, FALSE);
4580
4581 let _ = std::fs::remove_file(&path);
4582 unsafe { FreeIpoptProblem(p) };
4583 }
4584
4585 unsafe extern "C" fn cb_quad_eval_g(
4592 _n: Index,
4593 x: *const Number,
4594 _new_x: Bool,
4595 _m: Index,
4596 g: *mut Number,
4597 _user_data: *mut c_void,
4598 ) -> Bool {
4599 unsafe {
4600 *g.offset(0) = *x.offset(0);
4601 TRUE
4602 }
4603 }
4604 unsafe extern "C" fn cb_quad_eval_jac_g(
4605 _n: Index,
4606 _x: *const Number,
4607 _new_x: Bool,
4608 _m: Index,
4609 nele_jac: Index,
4610 irow: *mut Index,
4611 jcol: *mut Index,
4612 values: *mut Number,
4613 _user_data: *mut c_void,
4614 ) -> Bool {
4615 unsafe {
4616 assert_eq!(nele_jac, 1);
4617 if !irow.is_null() {
4618 *irow.offset(0) = 0;
4619 *jcol.offset(0) = 0;
4620 }
4621 if !values.is_null() {
4622 *values.offset(0) = 1.0;
4623 }
4624 TRUE
4625 }
4626 }
4627 unsafe extern "C" fn cb_quad_eval_h(
4628 _n: Index,
4629 _x: *const Number,
4630 _new_x: Bool,
4631 obj_factor: Number,
4632 _m: Index,
4633 _lambda: *const Number,
4634 _new_lambda: Bool,
4635 _nele_hess: Index,
4636 irow: *mut Index,
4637 jcol: *mut Index,
4638 values: *mut Number,
4639 _user_data: *mut c_void,
4640 ) -> Bool {
4641 unsafe {
4642 if !irow.is_null() {
4643 *irow.offset(0) = 0;
4644 *jcol.offset(0) = 0;
4645 }
4646 if !values.is_null() {
4647 *values.offset(0) = 2.0 * obj_factor;
4648 }
4649 TRUE
4650 }
4651 }
4652
4653 fn create_callback_test_problem() -> IpoptProblem {
4654 let xl = [-1.0e20];
4656 let xu = [1.0e20];
4657 let gl = [-10.0];
4658 let gu = [10.0];
4659 unsafe {
4660 CreateIpoptProblem(
4661 1,
4662 xl.as_ptr(),
4663 xu.as_ptr(),
4664 1,
4665 gl.as_ptr(),
4666 gu.as_ptr(),
4667 1,
4668 1,
4669 0,
4670 Some(quad_eval_f),
4671 Some(cb_quad_eval_g),
4672 Some(quad_eval_grad_f),
4673 Some(cb_quad_eval_jac_g),
4674 Some(cb_quad_eval_h),
4675 )
4676 }
4677 }
4678
4679 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
4680 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
4681 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
4682 std::sync::atomic::AtomicBool::new(false);
4683
4684 unsafe extern "C" fn counting_cb(
4685 _alg_mod: Index,
4686 iter_count: Index,
4687 _obj_value: Number,
4688 _inf_pr: Number,
4689 _inf_du: Number,
4690 _mu: Number,
4691 _d_norm: Number,
4692 _regularization_size: Number,
4693 _alpha_du: Number,
4694 _alpha_pr: Number,
4695 _ls_trials: Index,
4696 user_data: *mut c_void,
4697 ) -> Bool {
4698 unsafe {
4699 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4700 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
4701 let problem = user_data as IpoptProblem;
4704 let mut x = [0.0_f64];
4705 let rc = GetIpoptCurrentIterate(
4706 problem,
4707 FALSE,
4708 1,
4709 x.as_mut_ptr(),
4710 std::ptr::null_mut(),
4711 std::ptr::null_mut(),
4712 1,
4713 std::ptr::null_mut(),
4714 std::ptr::null_mut(),
4715 );
4716 if rc == TRUE && x[0].is_finite() {
4717 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
4718 }
4719 TRUE
4720 }
4721 }
4722
4723 #[test]
4724 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
4725 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
4726 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
4727 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
4728
4729 let p = create_callback_test_problem();
4730 assert!(!p.is_null());
4731 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
4732 assert_eq!(ok, TRUE);
4733 let mut x = [0.0_f64];
4734 let mut obj = 0.0_f64;
4735 let rc = unsafe {
4736 IpoptSolve(
4737 p,
4738 x.as_mut_ptr(),
4739 std::ptr::null_mut(),
4740 &mut obj,
4741 std::ptr::null_mut(),
4742 std::ptr::null_mut(),
4743 std::ptr::null_mut(),
4744 p as *mut c_void,
4745 )
4746 };
4747 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4748 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
4750 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
4751 assert!(
4752 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
4753 "last iter should be >= 1 after at least one accepted step"
4754 );
4755 assert!(
4756 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
4757 "GetIpoptCurrentIterate did not return a usable x"
4758 );
4759 unsafe { FreeIpoptProblem(p) };
4760 }
4761
4762 static CB_VIOL_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
4763
4764 fn create_bounded_callback_test_problem() -> IpoptProblem {
4769 let xl = [0.0];
4771 let xu = [10.0];
4772 let gl = [-10.0];
4773 let gu = [10.0];
4774 unsafe {
4775 CreateIpoptProblem(
4776 1,
4777 xl.as_ptr(),
4778 xu.as_ptr(),
4779 1,
4780 gl.as_ptr(),
4781 gu.as_ptr(),
4782 1,
4783 1,
4784 0,
4785 Some(quad_eval_f),
4786 Some(cb_quad_eval_g),
4787 Some(quad_eval_grad_f),
4788 Some(cb_quad_eval_jac_g),
4789 Some(cb_quad_eval_h),
4790 )
4791 }
4792 }
4793
4794 unsafe extern "C" fn violations_inspecting_cb(
4795 _alg_mod: Index,
4796 _iter_count: Index,
4797 _obj_value: Number,
4798 _inf_pr: Number,
4799 _inf_du: Number,
4800 _mu: Number,
4801 _d_norm: Number,
4802 _regularization_size: Number,
4803 _alpha_du: Number,
4804 _alpha_pr: Number,
4805 _ls_trials: Index,
4806 user_data: *mut c_void,
4807 ) -> Bool {
4808 unsafe {
4809 let problem = user_data as IpoptProblem;
4810 let mut x_l_viol = [f64::NAN];
4815 let mut x_u_viol = [f64::NAN];
4816 let rc = GetIpoptCurrentViolations(
4817 problem,
4818 FALSE,
4819 1,
4820 x_l_viol.as_mut_ptr(),
4821 x_u_viol.as_mut_ptr(),
4822 std::ptr::null_mut(),
4823 std::ptr::null_mut(),
4824 std::ptr::null_mut(),
4825 1,
4826 std::ptr::null_mut(),
4827 std::ptr::null_mut(),
4828 );
4829 if rc == TRUE
4830 && x_l_viol[0].is_finite()
4831 && x_l_viol[0] >= 0.0
4832 && x_u_viol[0].is_finite()
4833 && x_u_viol[0] >= 0.0
4834 {
4835 CB_VIOL_OK.store(true, std::sync::atomic::Ordering::SeqCst);
4836 }
4837 TRUE
4838 }
4839 }
4840
4841 #[test]
4842 fn get_current_violations_inside_callback_reports_finite_bounds() {
4843 CB_VIOL_OK.store(false, std::sync::atomic::Ordering::SeqCst);
4844 let p = create_bounded_callback_test_problem();
4845 assert!(!p.is_null());
4846 let ok = unsafe { SetIntermediateCallback(p, Some(violations_inspecting_cb)) };
4847 assert_eq!(ok, TRUE);
4848 let mut x = [5.0_f64];
4849 let mut obj = 0.0_f64;
4850 let rc = unsafe {
4851 IpoptSolve(
4852 p,
4853 x.as_mut_ptr(),
4854 std::ptr::null_mut(),
4855 &mut obj,
4856 std::ptr::null_mut(),
4857 std::ptr::null_mut(),
4858 std::ptr::null_mut(),
4859 p as *mut c_void,
4860 )
4861 };
4862 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
4863 assert!(
4864 CB_VIOL_OK.load(std::sync::atomic::Ordering::SeqCst),
4865 "GetIpoptCurrentViolations did not return finite, non-negative \
4866 bound violations from inside the callback"
4867 );
4868 unsafe { FreeIpoptProblem(p) };
4869 }
4870
4871 #[test]
4872 fn bound_violation_scatter_rejects_oversized_pack_instead_of_panicking() {
4873 let n_us = 1usize;
4883 let packed = [0.5_f64, -0.3]; let unguarded = std::panic::catch_unwind(|| {
4887 let mut v = vec![0.0; n_us];
4888 for (i, s) in packed.iter().enumerate() {
4889 v[i] = (-s).max(0.0);
4890 }
4891 v
4892 });
4893 assert!(
4894 unguarded.is_err(),
4895 "unguarded scatter should panic (→ abort across extern \"C\") on an oversized pack"
4896 );
4897
4898 let guarded: Result<Vec<f64>, ()> = (|| {
4900 if packed.len() != n_us {
4901 return Err(());
4902 }
4903 let mut v = vec![0.0; n_us];
4904 for (i, s) in packed.iter().enumerate() {
4905 v[i] = (-s).max(0.0);
4906 }
4907 Ok(v)
4908 })();
4909 assert!(
4910 guarded.is_err(),
4911 "guarded scatter should reject the length mismatch (return FALSE), not panic"
4912 );
4913 }
4914
4915 unsafe extern "C" fn user_stop_cb(
4916 _alg_mod: Index,
4917 _iter_count: Index,
4918 _obj_value: Number,
4919 _inf_pr: Number,
4920 _inf_du: Number,
4921 _mu: Number,
4922 _d_norm: Number,
4923 _regularization_size: Number,
4924 _alpha_du: Number,
4925 _alpha_pr: Number,
4926 _ls_trials: Index,
4927 _user_data: *mut c_void,
4928 ) -> Bool {
4929 FALSE
4930 }
4931
4932 #[test]
4933 fn intermediate_callback_false_surfaces_user_requested_stop() {
4934 let p = create_callback_test_problem();
4935 assert!(!p.is_null());
4936 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
4937 assert_eq!(ok, TRUE);
4938 let mut x = [0.0_f64];
4939 let rc = unsafe {
4940 IpoptSolve(
4941 p,
4942 x.as_mut_ptr(),
4943 std::ptr::null_mut(),
4944 std::ptr::null_mut(),
4945 std::ptr::null_mut(),
4946 std::ptr::null_mut(),
4947 std::ptr::null_mut(),
4948 std::ptr::null_mut(),
4949 )
4950 };
4951 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
4952 unsafe { FreeIpoptProblem(p) };
4953 }
4954
4955 #[test]
4956 fn ffi_guard_converts_panic_to_fallback() {
4957 let fallback = ApplicationReturnStatus::InternalError as Index;
4964 let got = ffi_guard(fallback, || -> Index {
4965 panic!("boom inside solver core");
4966 });
4967 assert_eq!(got, fallback);
4968 assert_eq!(got, ApplicationReturnStatus::InternalError as Index);
4969 }
4970
4971 #[test]
4972 fn ffi_guard_is_transparent_on_success() {
4973 let got = ffi_guard(-99, || 7);
4977 assert_eq!(got, 7);
4978 }
4979
4980 #[test]
4981 fn parse_pkg_version_handles_missing_components() {
4982 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
4983 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
4984 assert_eq!(parse_pkg_version(""), (0, 0, 0));
4985 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
4986 }
4987
4988 use crate::solver::{
4991 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
4992 IpoptSolverSolve,
4993 };
4994
4995 #[test]
4996 fn solver_create_consumes_problem_handle() {
4997 let mut p = create_unconstrained();
4998 assert!(!p.is_null());
4999 let s = unsafe { IpoptCreateSolver(&mut p) };
5000 assert!(!s.is_null());
5001 assert!(
5002 p.is_null(),
5003 "IpoptCreateSolver should NULL out the caller's handle"
5004 );
5005 unsafe { IpoptFreeSolver(s) };
5006 }
5007
5008 #[test]
5009 fn solver_create_null_inputs_return_null() {
5010 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
5013 assert!(s.is_null());
5014 let mut p: IpoptProblem = std::ptr::null_mut();
5016 let s = unsafe { IpoptCreateSolver(&mut p) };
5017 assert!(s.is_null());
5018 }
5019
5020 #[test]
5021 fn solver_free_null_is_safe() {
5022 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
5024 }
5025
5026 #[test]
5027 fn solver_solve_drives_quadratic_and_retains_factor() {
5028 let xl = [-1.0e20];
5029 let xu = [1.0e20];
5030 let mut p = unsafe {
5031 CreateIpoptProblem(
5032 1,
5033 xl.as_ptr(),
5034 xu.as_ptr(),
5035 0,
5036 std::ptr::null(),
5037 std::ptr::null(),
5038 0,
5039 1,
5040 0,
5041 Some(quad_eval_f),
5042 None,
5043 Some(quad_eval_grad_f),
5044 None,
5045 Some(quad_eval_h),
5046 )
5047 };
5048 assert!(!p.is_null());
5049 let s = unsafe { IpoptCreateSolver(&mut p) };
5050 assert!(!s.is_null());
5051 let mut x = [0.0_f64];
5052 let mut obj = 0.0_f64;
5053 let rc = unsafe {
5054 IpoptSolverSolve(
5055 s,
5056 x.as_mut_ptr(),
5057 std::ptr::null_mut(),
5058 &mut obj,
5059 std::ptr::null_mut(),
5060 std::ptr::null_mut(),
5061 std::ptr::null_mut(),
5062 std::ptr::null_mut(),
5063 )
5064 };
5065 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
5066 assert!((x[0] - 2.0).abs() < 1e-6);
5067 assert!(obj.abs() < 1e-10);
5068
5069 let dim = unsafe { IpoptSolverGetKktDim(s) };
5072 assert!(dim > 0, "expected positive KKT dim, got {dim}");
5073 let rhs = vec![0.0_f64; dim as usize];
5074 let mut lhs = vec![1.0_f64; dim as usize];
5075 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
5076 assert_eq!(ok, TRUE);
5077 for (i, v) in lhs.iter().enumerate() {
5078 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
5079 }
5080 unsafe { IpoptFreeSolver(s) };
5081 }
5082
5083 #[test]
5084 fn solver_kkt_dim_minus_one_before_solve() {
5085 let mut p = create_unconstrained();
5087 let s = unsafe { IpoptCreateSolver(&mut p) };
5088 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
5089 unsafe { IpoptFreeSolver(s) };
5090 }
5091
5092 #[test]
5097 fn c_get_working_set_returns_false_before_any_solve() {
5098 let p = create_unconstrained();
5100 let mut bound_buf = [0; 4];
5101 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
5102 assert_eq!(rc, FALSE);
5103 unsafe { FreeIpoptProblem(p) };
5104 }
5105
5106 #[test]
5107 fn c_set_warm_start_with_both_null_returns_false() {
5108 let p = create_unconstrained();
5109 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
5110 assert_eq!(rc, FALSE);
5111 unsafe { FreeIpoptProblem(p) };
5112 }
5113
5114 #[test]
5115 fn c_set_warm_start_with_bad_status_code_returns_false() {
5116 let p = create_unconstrained();
5118 let bogus = [
5120 POUNCE_WS_INACTIVE,
5121 7,
5122 POUNCE_WS_AT_LOWER,
5123 POUNCE_WS_INACTIVE,
5124 ];
5125 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
5126 assert_eq!(rc, FALSE);
5127 unsafe { FreeIpoptProblem(p) };
5128 }
5129
5130 #[test]
5131 fn c_set_warm_start_then_clear_succeeds() {
5132 let p = create_unconstrained();
5133 let in_buf = [POUNCE_WS_INACTIVE; 4];
5134 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
5135 assert_eq!(set_rc, TRUE);
5136 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
5137 assert_eq!(clr_rc, TRUE);
5138 unsafe { FreeIpoptProblem(p) };
5139 }
5140
5141 #[test]
5142 fn c_set_warm_start_on_null_problem_returns_false() {
5143 let in_buf = [POUNCE_WS_INACTIVE; 1];
5144 let rc = unsafe {
5145 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
5146 };
5147 assert_eq!(rc, FALSE);
5148 }
5149
5150 #[test]
5151 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
5152 let p = create_callback_test_problem();
5158 let key = CString::new("algorithm").unwrap();
5159 let val = CString::new("active-set-sqp").unwrap();
5160 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
5161 assert_eq!(ok, TRUE);
5162
5163 let mut x = [0.0_f64];
5164 let mut obj = 0.0_f64;
5165 let rc1 = unsafe {
5166 IpoptSolve(
5167 p,
5168 x.as_mut_ptr(),
5169 std::ptr::null_mut(),
5170 &mut obj,
5171 std::ptr::null_mut(),
5172 std::ptr::null_mut(),
5173 std::ptr::null_mut(),
5174 std::ptr::null_mut(),
5175 )
5176 };
5177 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
5178
5179 let mut bound_buf = [-1; 1];
5180 let mut cons_buf = [-1; 1];
5181 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
5182 assert_eq!(got, TRUE);
5183 assert!((0..=3).contains(&bound_buf[0]));
5185 assert!((0..=3).contains(&cons_buf[0]));
5186
5187 x[0] = 0.0;
5192 let mut obj2 = 0.0_f64;
5193 let mut bound_out = [-1; 1];
5194 let mut cons_out = [-1; 1];
5195 let rc2 = unsafe {
5196 IpoptSolveWarmStart(
5197 p,
5198 x.as_mut_ptr(),
5199 std::ptr::null_mut(),
5200 &mut obj2,
5201 std::ptr::null_mut(),
5202 std::ptr::null_mut(),
5203 std::ptr::null_mut(),
5204 bound_buf.as_ptr(),
5205 cons_buf.as_ptr(),
5206 bound_out.as_mut_ptr(),
5207 cons_out.as_mut_ptr(),
5208 std::ptr::null_mut(),
5209 )
5210 };
5211 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
5212 assert!((0..=3).contains(&bound_out[0]));
5213 assert!((0..=3).contains(&cons_out[0]));
5214
5215 unsafe { FreeIpoptProblem(p) };
5216 }
5217
5218 fn synthetic_outcome() -> pounce_restoration::mpcc_driver::MpccOutcome {
5229 use pounce_algorithm::mpcc::{
5230 Certificate, MpccStage, Reported, StationarityClass, Strategy,
5231 };
5232 use pounce_restoration::mpcc_driver::{MpccOutcome, MpccSolveRecord};
5233 MpccOutcome {
5234 status: ApplicationReturnStatus::SolveSucceeded,
5235 statistics: SolveStatistics::default(),
5236 accepted_by: Some(MpccStage::Exact),
5237 fallback: None,
5238 reported: Reported::Answer(MpccStage::Exact),
5239 strategy: Strategy::Continuation,
5240 solves: vec![MpccSolveRecord {
5241 stage: MpccStage::Branch,
5242 tau: None,
5243 status: ApplicationReturnStatus::MaximumIterationsExceeded,
5244 iterations: 7,
5245 budget_iterations: 7,
5246 max_abs_product: Number::INFINITY,
5247 seconds: Number::NAN,
5248 }],
5249 total_iterations: 7,
5250 x: vec![0.0],
5251 max_abs_product: Number::INFINITY,
5252 certificate: Some(Certificate {
5253 class: StationarityClass::S,
5254 n_pairs: 1,
5255 n_biactive: 0,
5256 n_neither: 0,
5257 multipliers_unique: true,
5258 residual: 0.0,
5259 resid_tol: Number::INFINITY,
5260 tight: true,
5261 primal_violation: 0.0,
5262 feasible: true,
5263 worst_biactive_sign: 0.0,
5264 multipliers: None,
5265 }),
5266 b_check: None,
5267 escapes: 3,
5268 lifted: 2,
5269 natural_residual: None,
5270 polish: None,
5271 polish_threshold: 1e-7,
5272 }
5273 }
5274
5275 fn c_label(chars: &[c_char]) -> String {
5276 unsafe { CStr::from_ptr(chars.as_ptr()) }
5277 .to_str()
5278 .unwrap()
5279 .to_string()
5280 }
5281
5282 #[test]
5283 fn complementarity_encodings_absent_certificate_and_non_finite_reals() {
5284 let mut out = synthetic_outcome();
5285 out.certificate = None;
5286 let s = complementarity_stats(&out);
5287 assert_eq!(s.certificate_class, POUNCE_CC_CLASS_ABSENT);
5288 assert_eq!(
5289 (
5290 s.n_pairs,
5291 s.n_biactive,
5292 s.n_neither,
5293 s.multipliers_unique,
5294 s.tight,
5295 s.feasible
5296 ),
5297 (-1, -1, -1, -1, -1, -1)
5298 );
5299 assert!(s.residual.is_nan() && s.resid_tol.is_nan() && s.primal_violation.is_nan());
5300 assert!(s.max_abs_product.is_nan() && s.natural_residual.is_nan());
5303 assert_eq!(c_label(&s.strategy), "continuation");
5304 assert_eq!(c_label(&s.accepted_by), "exact product");
5305 assert_eq!(c_label(&s.fallback), "");
5306 assert_eq!((s.escapes, s.lifted), (3, 2));
5307 assert!(complementarity_text(&out, POUNCE_CC_TEXT_SUMMARY).is_some());
5309
5310 let s = complementarity_stats(&synthetic_outcome());
5312 assert_eq!(s.certificate_class, POUNCE_CC_CLASS_S);
5313 assert!(s.resid_tol.is_nan());
5314 assert_eq!(
5315 (s.n_pairs, s.multipliers_unique, s.tight, s.feasible),
5316 (1, 1, 1, 1)
5317 );
5318
5319 let r = complementarity_solve(&out.solves[0]);
5320 assert_eq!(c_label(&r.stage), "branch NLP");
5321 assert_eq!((r.relaxed, r.status, r.iterations), (0, -1, 7));
5322 assert!(r.tau.is_nan() && r.max_abs_product.is_nan() && r.seconds.is_nan());
5323 }
5324
5325 #[test]
5326 fn complementarity_encodings_every_reported_and_class() {
5327 use pounce_algorithm::mpcc::{MpccStage, Reported, StationarityClass};
5328 let mut out = synthetic_outcome();
5329 for (reported, code) in [
5330 (
5331 Reported::Answer(MpccStage::Exact),
5332 POUNCE_CC_REPORTED_ANSWER,
5333 ),
5334 (
5335 Reported::WeakFallback(MpccStage::Penalty),
5336 POUNCE_CC_REPORTED_WEAK_FALLBACK,
5337 ),
5338 (
5339 Reported::FeasibleUncertified(MpccStage::Continuation),
5340 POUNCE_CC_REPORTED_FEASIBLE_UNCERTIFIED,
5341 ),
5342 (Reported::Verdict, POUNCE_CC_REPORTED_VERDICT),
5343 ] {
5344 out.reported = reported;
5345 assert_eq!(complementarity_stats(&out).reported, code, "{reported:?}");
5346 }
5347 for (class, code) in [
5348 (StationarityClass::S, POUNCE_CC_CLASS_S),
5349 (StationarityClass::W, POUNCE_CC_CLASS_W),
5350 (StationarityClass::None, POUNCE_CC_CLASS_NONE),
5351 (StationarityClass::NotSearched, POUNCE_CC_CLASS_NOT_SEARCHED),
5352 ] {
5353 out.certificate.as_mut().unwrap().class = class;
5354 assert_eq!(
5355 complementarity_stats(&out).certificate_class,
5356 code,
5357 "{class:?}"
5358 );
5359 }
5360 }
5361
5362 #[test]
5363 fn complementarity_encodings_b_verdicts_and_b_reason() {
5364 use pounce_algorithm::mpcc::{BCheck, BVerdict};
5365 let mut out = synthetic_outcome();
5366 assert_eq!(
5367 complementarity_stats(&out).b_verdict,
5368 POUNCE_CC_B_VERDICT_ABSENT
5369 );
5370 assert_eq!(complementarity_text(&out, POUNCE_CC_TEXT_B_REASON), None);
5371 for (verdict, code, reason) in [
5372 (BVerdict::B, POUNCE_CC_B_VERDICT_B, None),
5373 (
5374 BVerdict::NotB {
5375 held: vec![],
5376 decrease: -1.0,
5377 },
5378 POUNCE_CC_B_VERDICT_NOT_B,
5379 None,
5380 ),
5381 (
5382 BVerdict::NotChecked("a branch LP did not solve"),
5383 POUNCE_CC_B_VERDICT_NOT_CHECKED,
5384 Some("a branch LP did not solve"),
5385 ),
5386 ] {
5387 out.b_check = Some(BCheck {
5388 verdict,
5389 n_biactive: 12,
5390 branches_solved: 1,
5391 enumerated: false,
5392 });
5393 let s = complementarity_stats(&out);
5394 assert_eq!(s.b_verdict, code);
5395 assert_eq!(
5396 (s.b_n_biactive, s.b_branches_solved, s.b_enumerated),
5397 (12, 1, 0)
5398 );
5399 assert_eq!(
5400 complementarity_text(&out, POUNCE_CC_TEXT_B_REASON).as_deref(),
5401 reason
5402 );
5403 }
5404 }
5405
5406 #[test]
5407 fn complementarity_encodings_polish_not_taken_has_a_reason() {
5408 use pounce_restoration::mpcc_driver::PolishRecord;
5409 let mut out = synthetic_outcome();
5410 out.polish = Some(PolishRecord {
5411 taken: false,
5412 residual_before: 1e-5,
5413 residual_after: Number::INFINITY,
5414 reason: Some("the re-solve did not converge".to_string()),
5415 });
5416 let s = complementarity_stats(&out);
5417 assert_eq!((s.polish_ran, s.polish_taken), (1, 0));
5418 assert_eq!(s.polish_residual_before, 1e-5);
5419 assert!(
5420 s.polish_residual_after.is_nan(),
5421 "unmeasured after reads NaN"
5422 );
5423 assert_eq!(
5424 complementarity_text(&out, POUNCE_CC_TEXT_POLISH_REASON).as_deref(),
5425 Some("the re-solve did not converge")
5426 );
5427 out.polish = Some(PolishRecord {
5429 taken: true,
5430 residual_before: 1e-5,
5431 residual_after: 0.0,
5432 reason: None,
5433 });
5434 assert_eq!(complementarity_stats(&out).polish_taken, 1);
5435 assert_eq!(
5436 complementarity_text(&out, POUNCE_CC_TEXT_POLISH_REASON),
5437 None
5438 );
5439 assert_eq!(complementarity_text(&out, 3), None);
5440 }
5441
5442 #[test]
5443 fn complementarity_labels_truncate_and_terminate() {
5444 let mut a = [0x7f as c_char; 4];
5445 copy_label(&mut a, "abcdef");
5446 assert_eq!(a, [b'a' as c_char, b'b' as c_char, b'c' as c_char, 0]);
5447 copy_label(&mut a, "x");
5448 assert_eq!(a, [b'x' as c_char, 0, 0, 0], "the tail is cleared");
5449 use pounce_algorithm::mpcc::MpccStage::*;
5451 for s in [Exact, Continuation, Penalty, Branch, Polish] {
5452 assert!(s.label().len() < 32, "{}", s.label());
5453 }
5454 use pounce_algorithm::mpcc::Strategy;
5455 for s in [Strategy::Both, Strategy::Exact, Strategy::Continuation] {
5456 assert!(s.name().len() < 16, "{}", s.name());
5457 }
5458 }
5459
5460 #[test]
5464 fn complements_row_reads_every_table_row_and_refuses_nan_and_crossed() {
5465 use ComplementsRow::*;
5466 let inf = 2e19;
5467 assert_eq!(complements_row(0.0, 2.0), Some(Mixed));
5468 assert_eq!(complements_row(-1.0, inf), Some(Lower));
5469 assert_eq!(complements_row(-inf, 3.0), Some(Upper));
5470 assert_eq!(complements_row(-inf, inf), Some(RowFixed));
5471 assert_eq!(complements_row(1.0, 1.0), Some(VarFixed));
5472 assert_eq!(complements_row(-1e19, 1e19), Some(RowFixed));
5475 assert_eq!(complements_row(-9.9e18, 1e19), Some(Lower));
5476 assert_eq!(complements_row(2.0, 1.0), None);
5477 assert_eq!(complements_row(f64::NAN, 1.0), None);
5478 assert_eq!(complements_row(0.0, f64::NAN), None);
5479 }
5480
5481 #[test]
5482 fn complements_edits_classify_as_the_native_shape_they_declare() {
5483 let inf = 2e19;
5487 for (xl, xu, want) in [
5488 (0.0, 2.0, ComplementsRow::Mixed),
5489 (-1.0, inf, ComplementsRow::Lower),
5490 (-inf, 3.0, ComplementsRow::Upper),
5491 ] {
5492 let row = complements_row(xl, xu).unwrap();
5493 assert_eq!(row, want);
5494 let (gl, gu, declares) = row.row_bounds(-inf, inf);
5495 assert!(declares);
5496 let shape = classify_pair(xl, xu, gl, gu);
5497 assert!(row.shape_matches(shape), "{row:?}: {shape:?}");
5498 }
5499 assert_eq!(
5500 ComplementsRow::RowFixed.row_bounds(-inf, inf),
5501 (0.0, 0.0, false)
5502 );
5503 assert_eq!(
5504 ComplementsRow::VarFixed.row_bounds(-inf, inf),
5505 (-inf, inf, false)
5506 );
5507 assert!(!ComplementsRow::Mixed.shape_matches(None));
5509 assert!(!ComplementsRow::Lower.shape_matches(classify_pair(0.0, 2.0, -inf, inf)));
5510 }
5511
5512 fn create_two_by_two(x_l: [Number; 2], x_u: [Number; 2]) -> IpoptProblem {
5514 let g_l = [-2e19, -2e19];
5515 let g_u = [2e19, 2e19];
5516 let p = unsafe {
5517 CreateIpoptProblem(
5518 2,
5519 x_l.as_ptr(),
5520 x_u.as_ptr(),
5521 2,
5522 g_l.as_ptr(),
5523 g_u.as_ptr(),
5524 0,
5525 0,
5526 0,
5527 Some(quad_eval_f),
5528 Some(cb_quad_eval_g),
5529 Some(quad_eval_grad_f),
5530 Some(cb_quad_eval_jac_g),
5531 Some(cb_quad_eval_h),
5532 )
5533 };
5534 assert!(!p.is_null());
5535 p
5536 }
5537
5538 fn assert_maps_match(p: IpoptProblem) {
5541 let i = unsafe { &*p };
5542 for (k, q) in i.complementarity.iter().enumerate() {
5543 assert_eq!(i.complementarity.of_var(q.var), Some(k));
5544 assert_eq!(i.complementarity.of_row(q.row), Some(k));
5545 }
5546 for (k, u) in i.complements_used.iter().enumerate() {
5547 assert_eq!(i.complements_used.of_var(u.var), Some(k));
5548 assert_eq!(i.complements_used.of_row(u.row), Some(k));
5549 }
5550 for j in 0..i.n.max(i.m) as usize {
5551 assert_eq!(
5552 i.complementarity.of_var(j).is_some(),
5553 i.complementarity.iter().any(|q| q.var == j)
5554 );
5555 assert_eq!(
5556 i.complementarity.of_row(j).is_some(),
5557 i.complementarity.iter().any(|q| q.row == j)
5558 );
5559 assert_eq!(
5560 i.complements_used.of_var(j).is_some(),
5561 i.complements_used.iter().any(|u| u.var == j)
5562 );
5563 assert_eq!(
5564 i.complements_used.of_row(j).is_some(),
5565 i.complements_used.iter().any(|u| u.row == j)
5566 );
5567 }
5568 }
5569
5570 #[test]
5571 fn pair_maps_follow_every_declaration_path() {
5572 let p = create_two_by_two([0.0, 0.0], [2.0, 2.0]);
5579 unsafe {
5580 assert_eq!(AddPounceComplementarityPair(p, 0, 0), POUNCE_OK);
5581 assert_maps_match(p);
5582 assert_eq!(
5583 AddPounceComplementarityPair(p, 1, 0),
5584 POUNCE_ERR_VAR_ALREADY_PAIRED
5585 );
5586 assert_eq!(
5587 AddPounceComplementarityPair(p, 0, 1),
5588 POUNCE_ERR_ROW_ALREADY_PAIRED
5589 );
5590 assert_maps_match(p);
5591 let (rows, vars) = ([1, 0], [0, 1]);
5592 assert_eq!(
5593 SetPounceComplementarityPairs(
5594 p,
5595 2,
5596 rows.as_ptr(),
5597 vars.as_ptr(),
5598 0,
5599 std::ptr::null_mut()
5600 ),
5601 POUNCE_OK
5602 );
5603 assert_maps_match(p);
5604 assert_eq!((*p).complementarity.of_var(0), Some(0));
5605 assert_eq!((*p).complementarity.of_row(1), Some(0));
5606 let (rows, vars) = ([0, 0], [0, 1]);
5607 assert_eq!(
5608 SetPounceComplementarityPairs(
5609 p,
5610 2,
5611 rows.as_ptr(),
5612 vars.as_ptr(),
5613 0,
5614 std::ptr::null_mut()
5615 ),
5616 POUNCE_ERR_ROW_ALREADY_PAIRED
5617 );
5618 assert_eq!((*p).complementarity.of_row(1), Some(0));
5619 assert_maps_match(p);
5620 assert_eq!(ClearPounceComplementarityPairs(p), TRUE);
5621 assert_maps_match(p);
5622 assert_eq!((&*p).complementarity.of_var(0), None);
5623 assert_eq!(
5624 AddPounceComplementsConstraint(p, 1, 1),
5625 POUNCE_COMPLEMENTS_PAIR_MIXED
5626 );
5627 assert_maps_match(p);
5628 assert_eq!(
5629 AddPounceComplementsConstraint(p, 0, 1),
5630 POUNCE_ERR_VAR_ALREADY_PAIRED
5631 );
5632 assert_maps_match(p);
5633 FreeIpoptProblem(p);
5634 }
5635 }
5636
5637 #[test]
5638 fn complements_records_the_table_row_and_the_callers_row_bounds() {
5639 let p = create_two_by_two([0.0, -2e19], [2e19, 2e19]);
5642 unsafe {
5643 assert_eq!(
5644 AddPounceComplementsConstraint(p, 0, 0),
5645 POUNCE_COMPLEMENTS_PAIR_LOWER
5646 );
5647 assert_eq!(
5648 AddPounceComplementsConstraint(p, 1, 1),
5649 POUNCE_COMPLEMENTS_ROW_FIXED_NO_PAIR
5650 );
5651 let i = &*p;
5652 assert_eq!(
5653 i.complements_used[..],
5654 [
5655 ComplementsUse {
5656 row: 0,
5657 var: 0,
5658 table: ComplementsRow::Lower,
5659 user_gl: -2e19,
5660 user_gu: 2e19
5661 },
5662 ComplementsUse {
5663 row: 1,
5664 var: 1,
5665 table: ComplementsRow::RowFixed,
5666 user_gl: -2e19,
5667 user_gu: 2e19
5668 },
5669 ]
5670 );
5671 assert_eq!((i.g_l[0], i.g_u[0]), (0.0, 2e19));
5672 assert_eq!((i.g_l[1], i.g_u[1]), (0.0, 0.0));
5673 FreeIpoptProblem(p);
5674 }
5675 }
5676}