1#![allow(non_camel_case_types, non_snake_case)]
31#![allow(unsafe_op_in_unsafe_fn, dead_code)]
32#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
33
34pub mod fortran;
35pub mod solver;
36
37use pounce_algorithm::application::{
38 default_backend_factory, feral_config_from_options, IpoptApplication,
39};
40use pounce_algorithm::intermediate as ip_intermediate;
41use pounce_nlp::return_codes::ApplicationReturnStatus;
42use pounce_nlp::solve_statistics::SolveStatistics;
43use pounce_nlp::tnlp::{
44 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, Solution, SparsityRequest,
45 StartingPoint, TNLP,
46};
47use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
48use pounce_restoration::resto_inner_solver::{
49 make_default_restoration_factory_provider, InnerBackendFactoryFactory,
50};
51use std::cell::RefCell;
52use std::ffi::{c_char, c_int, c_void, CStr};
53use std::rc::Rc;
54
55pub type Number = f64;
57pub type Index = c_int;
59pub type Bool = c_int;
61
62const TRUE: Bool = 1;
63const FALSE: Bool = 0;
64
65pub type IpoptBoundStatus = c_int;
69pub type IpoptConsStatus = c_int;
72
73const POUNCE_WS_INACTIVE: c_int = 0;
74const POUNCE_WS_AT_LOWER: c_int = 1;
75const POUNCE_WS_AT_UPPER: c_int = 2;
76const POUNCE_WS_FIXED_OR_EQ: c_int = 3;
77
78pub struct IpoptProblemInfo {
81 pub(crate) app: IpoptApplication,
82 pub(crate) n: Index,
83 pub(crate) m: Index,
84 pub(crate) nele_jac: Index,
85 pub(crate) nele_hess: Index,
86 pub(crate) index_style: Index,
87 pub(crate) x_l: Vec<Number>,
88 pub(crate) x_u: Vec<Number>,
89 pub(crate) g_l: Vec<Number>,
90 pub(crate) g_u: Vec<Number>,
91 pub(crate) eval_f: Option<Eval_F_CB>,
92 pub(crate) eval_g: Option<Eval_G_CB>,
93 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
94 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
95 pub(crate) eval_h: Option<Eval_H_CB>,
96 pub(crate) intermediate_cb: Option<Intermediate_CB>,
97 pub(crate) user_scaling: Option<UserScaling>,
101 pub(crate) last_solve: Option<LastSolve>,
105}
106
107#[derive(Clone)]
110pub(crate) struct UserScaling {
111 obj_scaling: Number,
112 x_scaling: Option<Vec<Number>>,
113 g_scaling: Option<Vec<Number>>,
114}
115
116#[derive(Clone)]
122pub(crate) struct LastSolve {
123 pub(crate) stats: SolveStatistics,
124 pub(crate) status: ApplicationReturnStatus,
125 pub(crate) linear_solver: Option<pounce_linsol::summary::LinearSolverSummary>,
126 pub(crate) final_x: Vec<Number>,
127 pub(crate) final_lambda: Vec<Number>,
128 pub(crate) final_obj: Number,
129}
130
131impl Default for LastSolve {
132 fn default() -> Self {
133 Self {
134 stats: SolveStatistics::default(),
135 status: ApplicationReturnStatus::InternalError,
136 linear_solver: None,
137 final_x: Vec::new(),
138 final_lambda: Vec::new(),
139 final_obj: 0.0,
140 }
141 }
142}
143
144pub type IpoptProblem = *mut IpoptProblemInfo;
145
146pub type Eval_F_CB = unsafe extern "C" fn(
150 n: Index,
151 x: *const Number,
152 new_x: Bool,
153 obj_value: *mut Number,
154 user_data: *mut c_void,
155) -> Bool;
156
157pub type Eval_Grad_F_CB = unsafe extern "C" fn(
158 n: Index,
159 x: *const Number,
160 new_x: Bool,
161 grad_f: *mut Number,
162 user_data: *mut c_void,
163) -> Bool;
164
165pub type Eval_G_CB = unsafe extern "C" fn(
166 n: Index,
167 x: *const Number,
168 new_x: Bool,
169 m: Index,
170 g: *mut Number,
171 user_data: *mut c_void,
172) -> Bool;
173
174pub type Eval_Jac_G_CB = unsafe extern "C" fn(
175 n: Index,
176 x: *const Number,
177 new_x: Bool,
178 m: Index,
179 nele_jac: Index,
180 iRow: *mut Index,
181 jCol: *mut Index,
182 values: *mut Number,
183 user_data: *mut c_void,
184) -> Bool;
185
186pub type Eval_H_CB = unsafe extern "C" fn(
187 n: Index,
188 x: *const Number,
189 new_x: Bool,
190 obj_factor: Number,
191 m: Index,
192 lambda: *const Number,
193 new_lambda: Bool,
194 nele_hess: Index,
195 iRow: *mut Index,
196 jCol: *mut Index,
197 values: *mut Number,
198 user_data: *mut c_void,
199) -> Bool;
200
201pub type Intermediate_CB = unsafe extern "C" fn(
202 alg_mod: Index,
203 iter_count: Index,
204 obj_value: Number,
205 inf_pr: Number,
206 inf_du: Number,
207 mu: Number,
208 d_norm: Number,
209 regularization_size: Number,
210 alpha_du: Number,
211 alpha_pr: Number,
212 ls_trials: Index,
213 user_data: *mut c_void,
214) -> Bool;
215
216#[no_mangle]
227pub unsafe extern "C" fn CreateIpoptProblem(
228 n: Index,
229 x_L: *const Number,
230 x_U: *const Number,
231 m: Index,
232 g_L: *const Number,
233 g_U: *const Number,
234 nele_jac: Index,
235 nele_hess: Index,
236 index_style: Index,
237 eval_f: Option<Eval_F_CB>,
238 eval_g: Option<Eval_G_CB>,
239 eval_grad_f: Option<Eval_Grad_F_CB>,
240 eval_jac_g: Option<Eval_Jac_G_CB>,
241 eval_h: Option<Eval_H_CB>,
242) -> IpoptProblem {
243 pounce_observability::init_subscriber();
247
248 if n < 0 || m < 0 || nele_jac < 0 || nele_hess < 0 {
249 return std::ptr::null_mut();
250 }
251 if !(0..=1).contains(&index_style) {
252 return std::ptr::null_mut();
253 }
254 if eval_f.is_none() || eval_grad_f.is_none() {
255 return std::ptr::null_mut();
256 }
257 if m > 0 && (eval_g.is_none() || eval_jac_g.is_none()) {
258 return std::ptr::null_mut();
259 }
260 if n > 0 && (x_L.is_null() || x_U.is_null()) {
261 return std::ptr::null_mut();
262 }
263 if m > 0 && (g_L.is_null() || g_U.is_null()) {
264 return std::ptr::null_mut();
265 }
266
267 let x_l = if n > 0 {
268 std::slice::from_raw_parts(x_L, n as usize).to_vec()
269 } else {
270 Vec::new()
271 };
272 let x_u = if n > 0 {
273 std::slice::from_raw_parts(x_U, n as usize).to_vec()
274 } else {
275 Vec::new()
276 };
277 let g_l_vec = if m > 0 {
278 std::slice::from_raw_parts(g_L, m as usize).to_vec()
279 } else {
280 Vec::new()
281 };
282 let g_u_vec = if m > 0 {
283 std::slice::from_raw_parts(g_U, m as usize).to_vec()
284 } else {
285 Vec::new()
286 };
287
288 let info = Box::new(IpoptProblemInfo {
289 app: IpoptApplication::new(),
290 n,
291 m,
292 nele_jac,
293 nele_hess,
294 index_style,
295 x_l,
296 x_u,
297 g_l: g_l_vec,
298 g_u: g_u_vec,
299 eval_f,
300 eval_g,
301 eval_grad_f,
302 eval_jac_g,
303 eval_h,
304 intermediate_cb: None,
305 user_scaling: None,
306 last_solve: None,
307 });
308 Box::into_raw(info)
309}
310
311#[no_mangle]
318pub unsafe extern "C" fn FreeIpoptProblem(ipopt_problem: IpoptProblem) {
319 if ipopt_problem.is_null() {
320 return;
321 }
322 drop(Box::from_raw(ipopt_problem));
323}
324
325unsafe fn keyword_str<'a>(keyword: *const c_char) -> Option<&'a str> {
326 if keyword.is_null() {
327 return None;
328 }
329 CStr::from_ptr(keyword).to_str().ok()
330}
331
332#[no_mangle]
339pub unsafe extern "C" fn AddIpoptStrOption(
340 ipopt_problem: IpoptProblem,
341 keyword: *const c_char,
342 val: *const c_char,
343) -> Bool {
344 if ipopt_problem.is_null() {
345 return FALSE;
346 }
347 let info = &mut *ipopt_problem;
348 let Some(k) = keyword_str(keyword) else {
349 return FALSE;
350 };
351 if val.is_null() {
352 return FALSE;
353 }
354 let Ok(v) = CStr::from_ptr(val).to_str() else {
355 return FALSE;
356 };
357 match info.app.options_mut().set_string_value(k, v, true, false) {
358 Ok(_) => TRUE,
359 Err(_) => FALSE,
360 }
361}
362
363#[no_mangle]
370pub unsafe extern "C" fn AddIpoptNumOption(
371 ipopt_problem: IpoptProblem,
372 keyword: *const c_char,
373 val: Number,
374) -> Bool {
375 if ipopt_problem.is_null() {
376 return FALSE;
377 }
378 let info = &mut *ipopt_problem;
379 let Some(k) = keyword_str(keyword) else {
380 return FALSE;
381 };
382 match info
383 .app
384 .options_mut()
385 .set_numeric_value(k, val, true, false)
386 {
387 Ok(_) => TRUE,
388 Err(_) => FALSE,
389 }
390}
391
392#[no_mangle]
399pub unsafe extern "C" fn AddIpoptIntOption(
400 ipopt_problem: IpoptProblem,
401 keyword: *const c_char,
402 val: Index,
403) -> Bool {
404 if ipopt_problem.is_null() {
405 return FALSE;
406 }
407 let info = &mut *ipopt_problem;
408 let Some(k) = keyword_str(keyword) else {
409 return FALSE;
410 };
411 match info.app.options_mut().set_integer_value(
412 k,
413 val as pounce_common::types::Index,
414 true,
415 false,
416 ) {
417 Ok(_) => TRUE,
418 Err(_) => FALSE,
419 }
420}
421
422#[no_mangle]
436pub unsafe extern "C" fn OpenIpoptOutputFile(
437 ipopt_problem: IpoptProblem,
438 file_name: *const c_char,
439 print_level: c_int,
440) -> Bool {
441 if ipopt_problem.is_null() || file_name.is_null() {
442 return FALSE;
443 }
444 let info = &mut *ipopt_problem;
445 let Ok(fname) = CStr::from_ptr(file_name).to_str() else {
446 return FALSE;
447 };
448 if info.app.open_output_file(fname, print_level) {
449 TRUE
450 } else {
451 FALSE
452 }
453}
454
455#[no_mangle]
469pub unsafe extern "C" fn SetIpoptProblemScaling(
470 ipopt_problem: IpoptProblem,
471 obj_scaling: Number,
472 x_scaling: *const Number,
473 g_scaling: *const Number,
474) -> Bool {
475 if ipopt_problem.is_null() {
476 return FALSE;
477 }
478 let info = &mut *ipopt_problem;
479 let n = info.n as usize;
480 let m = info.m as usize;
481 let x_vec = if !x_scaling.is_null() && n > 0 {
482 Some(std::slice::from_raw_parts(x_scaling, n).to_vec())
483 } else {
484 None
485 };
486 let g_vec = if !g_scaling.is_null() && m > 0 {
487 Some(std::slice::from_raw_parts(g_scaling, m).to_vec())
488 } else {
489 None
490 };
491 info.user_scaling = Some(UserScaling {
492 obj_scaling,
493 x_scaling: x_vec,
494 g_scaling: g_vec,
495 });
496 TRUE
497}
498
499#[allow(clippy::too_many_arguments)]
513#[no_mangle]
514pub unsafe extern "C" fn IpoptSolve(
515 ipopt_problem: IpoptProblem,
516 x: *mut Number,
517 g: *mut Number,
518 obj_val: *mut Number,
519 mult_g: *mut Number,
520 mult_x_L: *mut Number,
521 mult_x_U: *mut Number,
522 user_data: *mut c_void,
523) -> Index {
524 if ipopt_problem.is_null() {
525 return ApplicationReturnStatus::InternalError as Index;
526 }
527 let info = &mut *ipopt_problem;
528 if info.n < 0 || info.m < 0 {
529 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
530 }
531 if info.n > 0 && x.is_null() {
532 return ApplicationReturnStatus::InvalidProblemDefinition as Index;
533 }
534
535 let n_us = info.n as usize;
536 let m_us = info.m as usize;
537 let initial_x = if n_us > 0 {
538 std::slice::from_raw_parts(x, n_us).to_vec()
539 } else {
540 Vec::new()
541 };
542
543 let bridge = Rc::new(RefCell::new(CCallbackTnlp {
544 n: info.n,
545 m: info.m,
546 nele_jac: info.nele_jac,
547 nele_hess: info.nele_hess,
548 index_style: info.index_style,
549 x_l: info.x_l.clone(),
550 x_u: info.x_u.clone(),
551 g_l: info.g_l.clone(),
552 g_u: info.g_u.clone(),
553 initial_x,
554 eval_f: info.eval_f,
555 eval_grad_f: info.eval_grad_f,
556 eval_g: info.eval_g,
557 eval_jac_g: info.eval_jac_g,
558 eval_h: info.eval_h,
559 user_data,
560 intermediate_cb: info.intermediate_cb,
561 user_scaling: info.user_scaling.clone(),
562 final_status: None,
563 final_x: vec![0.0; n_us],
564 final_z_l: vec![0.0; n_us],
565 final_z_u: vec![0.0; n_us],
566 final_g: vec![0.0; m_us],
567 final_lambda: vec![0.0; m_us],
568 final_obj: 0.0,
569 }));
570
571 let feral_cfg = feral_config_from_options(info.app.options());
581 let bff_mint = move || -> InnerBackendFactoryFactory {
582 Box::new(move || default_backend_factory(feral_cfg))
583 };
584 let resto_provider = make_default_restoration_factory_provider(
585 RestoAlgorithmBuilder::new(),
586 info.app.algorithm_builder_from_options(),
587 bff_mint,
588 );
589 info.app.set_restoration_factory_provider(resto_provider);
590
591 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
592 let status = info.app.optimize_tnlp(bridge_for_solve);
593 let bridge_ref = bridge.borrow();
594 info.last_solve = Some(LastSolve {
595 stats: info.app.statistics(),
596 status,
597 linear_solver: info.app.linear_solver_summary(),
598 final_x: bridge_ref.final_x.clone(),
599 final_lambda: bridge_ref.final_lambda.clone(),
600 final_obj: bridge_ref.final_obj,
601 });
602 if !x.is_null() && n_us > 0 {
603 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
604 }
605 if !g.is_null() && m_us > 0 {
606 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
607 }
608 if !obj_val.is_null() {
609 *obj_val = bridge_ref.final_obj;
610 }
611 if !mult_g.is_null() && m_us > 0 {
612 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
613 }
614 if !mult_x_L.is_null() && n_us > 0 {
615 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
616 }
617 if !mult_x_U.is_null() && n_us > 0 {
618 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
619 }
620 status as Index
621}
622
623#[no_mangle]
629pub unsafe extern "C" fn SetIntermediateCallback(
630 ipopt_problem: IpoptProblem,
631 intermediate_cb: Option<Intermediate_CB>,
632) -> Bool {
633 if ipopt_problem.is_null() {
634 return FALSE;
635 }
636 let info = &mut *ipopt_problem;
637 info.intermediate_cb = intermediate_cb;
638 TRUE
639}
640
641#[allow(clippy::too_many_arguments)]
664#[no_mangle]
665pub unsafe extern "C" fn GetIpoptCurrentIterate(
666 ipopt_problem: IpoptProblem,
667 _scaled: Bool,
668 n: Index,
669 x: *mut Number,
670 z_l: *mut Number,
671 z_u: *mut Number,
672 m: Index,
673 g: *mut Number,
674 lambda: *mut Number,
675) -> Bool {
676 if ipopt_problem.is_null() {
677 return FALSE;
678 }
679 let info = &*ipopt_problem;
680 if n != info.n || m != info.m {
681 return FALSE;
682 }
683 let result = ip_intermediate::with_current(|ctx| {
684 let data = ctx.data.borrow();
685 let Some(curr) = data.curr.as_ref() else {
686 return false;
687 };
688 let nlp = ctx.nlp.borrow();
689 let n_us = n as usize;
690 let m_us = m as usize;
691 if !x.is_null() && n_us > 0 {
692 let full_x = nlp.lift_x_to_full(&*curr.x);
693 if full_x.len() != n_us {
694 return false;
695 }
696 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
697 }
698 if !z_l.is_null() && n_us > 0 {
699 let full = nlp.pack_z_l_for_user(&*curr.z_l);
700 if full.len() != n_us {
701 return false;
702 }
703 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
704 }
705 if !z_u.is_null() && n_us > 0 {
706 let full = nlp.pack_z_u_for_user(&*curr.z_u);
707 if full.len() != n_us {
708 return false;
709 }
710 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
711 }
712 if !g.is_null() && m_us > 0 {
713 let cq = ctx.cq.borrow();
714 let full = nlp.pack_g_for_user(&*cq.curr_c(), &*cq.curr_d());
715 if full.len() != m_us {
716 return false;
717 }
718 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
719 }
720 if !lambda.is_null() && m_us > 0 {
721 let full = nlp.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
722 if full.len() != m_us {
723 return false;
724 }
725 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
726 }
727 true
728 });
729 if result.unwrap_or(false) {
730 TRUE
731 } else {
732 FALSE
733 }
734}
735
736#[allow(clippy::too_many_arguments)]
751#[no_mangle]
752pub unsafe extern "C" fn GetIpoptCurrentViolations(
753 ipopt_problem: IpoptProblem,
754 _scaled: Bool,
755 n: Index,
756 x_l_violation: *mut Number,
757 x_u_violation: *mut Number,
758 compl_x_l: *mut Number,
759 compl_x_u: *mut Number,
760 grad_lag_x: *mut Number,
761 m: Index,
762 nlp_constraint_violation: *mut Number,
763 compl_g: *mut Number,
764) -> Bool {
765 if ipopt_problem.is_null() {
766 return FALSE;
767 }
768 let info = &*ipopt_problem;
769 if n != info.n || m != info.m {
770 return FALSE;
771 }
772 let result = ip_intermediate::with_current(|ctx| {
773 let data = ctx.data.borrow();
774 let Some(_curr) = data.curr.as_ref() else {
775 return false;
776 };
777 drop(data);
778 let nlp = ctx.nlp.borrow();
779 let cq = ctx.cq.borrow();
780 let n_us = n as usize;
781 let m_us = m as usize;
782 if !x_l_violation.is_null() && n_us > 0 {
788 let mut v = vec![0.0; n_us];
789 let slack = cq.curr_slack_x_l();
790 let z_l_full = nlp.pack_z_l_for_user(&*slack);
791 for (i, s) in z_l_full.iter().enumerate() {
796 v[i] = (-s).max(0.0);
797 }
798 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
799 }
800 if !x_u_violation.is_null() && n_us > 0 {
801 let mut v = vec![0.0; n_us];
802 let slack = cq.curr_slack_x_u();
803 let s_full = nlp.pack_z_u_for_user(&*slack);
804 for (i, s) in s_full.iter().enumerate() {
805 v[i] = (-s).max(0.0);
806 }
807 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
808 }
809 if !compl_x_l.is_null() && n_us > 0 {
810 let v = nlp.pack_z_l_for_user(&*cq.curr_compl_x_l());
811 if v.len() != n_us {
812 return false;
813 }
814 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
815 }
816 if !compl_x_u.is_null() && n_us > 0 {
817 let v = nlp.pack_z_u_for_user(&*cq.curr_compl_x_u());
818 if v.len() != n_us {
819 return false;
820 }
821 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
822 }
823 if !grad_lag_x.is_null() && n_us > 0 {
824 let glx = cq.curr_grad_lag_x();
825 let full = nlp.lift_x_to_full(&*glx);
829 if full.len() != n_us {
830 return false;
831 }
832 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
833 }
834 if !nlp_constraint_violation.is_null() && m_us > 0 {
835 let zero = vec![0.0; m_us];
841 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
842 }
843 if !compl_g.is_null() && m_us > 0 {
844 let zero = vec![0.0; m_us];
847 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
848 }
849 true
850 });
851 if result.unwrap_or(false) {
852 TRUE
853 } else {
854 FALSE
855 }
856}
857
858#[no_mangle]
866pub unsafe extern "C" fn GetIpoptVersion(
867 major: *mut c_int,
868 minor: *mut c_int,
869 release: *mut c_int,
870) {
871 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
876 if !major.is_null() {
877 *major = mj;
878 }
879 if !minor.is_null() {
880 *minor = mn;
881 }
882 if !release.is_null() {
883 *release = pt;
884 }
885}
886
887fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
888 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
889 (
890 it.next().unwrap_or(0),
891 it.next().unwrap_or(0),
892 it.next().unwrap_or(0),
893 )
894}
895
896#[no_mangle]
913pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
914 last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0)
915}
916
917#[no_mangle]
924pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
925 last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0)
926}
927
928#[no_mangle]
935pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
936 last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0)
937}
938
939#[no_mangle]
946pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
947 last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0)
948}
949
950#[no_mangle]
956pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
957 last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0)
958}
959
960unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
961where
962 F: FnOnce(&SolveStatistics) -> T,
963{
964 if ipopt_problem.is_null() {
965 return None;
966 }
967 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
968}
969
970fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
980 use pounce_qp::BoundStatus::*;
981 match s {
982 Inactive => POUNCE_WS_INACTIVE,
983 AtLower => POUNCE_WS_AT_LOWER,
984 AtUpper => POUNCE_WS_AT_UPPER,
985 Fixed => POUNCE_WS_FIXED_OR_EQ,
986 }
987}
988
989fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
990 use pounce_qp::BoundStatus::*;
991 match v {
992 POUNCE_WS_INACTIVE => Some(Inactive),
993 POUNCE_WS_AT_LOWER => Some(AtLower),
994 POUNCE_WS_AT_UPPER => Some(AtUpper),
995 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
996 _ => None,
997 }
998}
999
1000fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1001 use pounce_qp::ConsStatus::*;
1002 match s {
1003 Inactive => POUNCE_WS_INACTIVE,
1004 AtLower => POUNCE_WS_AT_LOWER,
1005 AtUpper => POUNCE_WS_AT_UPPER,
1006 Equality => POUNCE_WS_FIXED_OR_EQ,
1007 }
1008}
1009
1010fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1011 use pounce_qp::ConsStatus::*;
1012 match v {
1013 POUNCE_WS_INACTIVE => Some(Inactive),
1014 POUNCE_WS_AT_LOWER => Some(AtLower),
1015 POUNCE_WS_AT_UPPER => Some(AtUpper),
1016 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1017 _ => None,
1018 }
1019}
1020
1021#[no_mangle]
1037pub unsafe extern "C" fn IpoptGetWorkingSet(
1038 ipopt_problem: IpoptProblem,
1039 bound_status_out: *mut IpoptBoundStatus,
1040 cons_status_out: *mut IpoptConsStatus,
1041) -> Bool {
1042 if ipopt_problem.is_null() {
1043 return FALSE;
1044 }
1045 let info = &*ipopt_problem;
1046 let ws = match info.app.last_sqp_working_set() {
1047 Some(w) => w,
1048 None => return FALSE,
1049 };
1050 if !bound_status_out.is_null() {
1051 for (i, &s) in ws.bounds.iter().enumerate() {
1052 *bound_status_out.add(i) = bound_status_to_int(s);
1053 }
1054 }
1055 if !cons_status_out.is_null() {
1056 for (i, &s) in ws.constraints.iter().enumerate() {
1057 *cons_status_out.add(i) = cons_status_to_int(s);
1058 }
1059 }
1060 TRUE
1061}
1062
1063#[no_mangle]
1079pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1080 ipopt_problem: IpoptProblem,
1081 bound_status_in: *const IpoptBoundStatus,
1082 cons_status_in: *const IpoptConsStatus,
1083) -> Bool {
1084 if ipopt_problem.is_null() {
1085 return FALSE;
1086 }
1087 if bound_status_in.is_null() && cons_status_in.is_null() {
1088 return FALSE;
1089 }
1090 let info = &mut *ipopt_problem;
1091 let n = info.n.max(0) as usize;
1092 let m = info.m.max(0) as usize;
1093 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1094 if !bound_status_in.is_null() {
1095 for i in 0..n {
1096 let v = *bound_status_in.add(i);
1097 match int_to_bound_status(v) {
1098 Some(s) => bounds[i] = s,
1099 None => return FALSE,
1100 }
1101 }
1102 }
1103 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1104 if !cons_status_in.is_null() {
1105 for i in 0..m {
1106 let v = *cons_status_in.add(i);
1107 match int_to_cons_status(v) {
1108 Some(s) => constraints[i] = s,
1109 None => return FALSE,
1110 }
1111 }
1112 }
1113 info.app
1121 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
1122 x: vec![0.0; n],
1123 lambda_g: vec![0.0; m],
1124 lambda_x: vec![0.0; n],
1125 working: Some(pounce_qp::WorkingSet {
1126 bounds,
1127 constraints,
1128 }),
1129 });
1130 TRUE
1131}
1132
1133#[no_mangle]
1140pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1141 if ipopt_problem.is_null() {
1142 return FALSE;
1143 }
1144 (*ipopt_problem).app.clear_sqp_warm_start();
1145 TRUE
1146}
1147
1148#[allow(clippy::too_many_arguments)]
1164#[no_mangle]
1165pub unsafe extern "C" fn IpoptSolveWarmStart(
1166 ipopt_problem: IpoptProblem,
1167 x: *mut Number,
1168 g: *mut Number,
1169 obj_val: *mut Number,
1170 mult_g: *mut Number,
1171 mult_x_L: *mut Number,
1172 mult_x_U: *mut Number,
1173 bound_status_in: *const IpoptBoundStatus,
1174 cons_status_in: *const IpoptConsStatus,
1175 bound_status_out: *mut IpoptBoundStatus,
1176 cons_status_out: *mut IpoptConsStatus,
1177 user_data: *mut c_void,
1178) -> Index {
1179 if ipopt_problem.is_null() {
1180 return ApplicationReturnStatus::InternalError as Index;
1181 }
1182 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1187 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1188 }
1189 let status = IpoptSolve(
1190 ipopt_problem,
1191 x,
1192 g,
1193 obj_val,
1194 mult_g,
1195 mult_x_L,
1196 mult_x_U,
1197 user_data,
1198 );
1199 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1200 status
1201}
1202
1203pub(crate) struct CCallbackTnlp {
1214 pub(crate) n: Index,
1215 pub(crate) m: Index,
1216 pub(crate) nele_jac: Index,
1217 pub(crate) nele_hess: Index,
1218 pub(crate) index_style: Index,
1219 pub(crate) x_l: Vec<Number>,
1220 pub(crate) x_u: Vec<Number>,
1221 pub(crate) g_l: Vec<Number>,
1222 pub(crate) g_u: Vec<Number>,
1223 pub(crate) initial_x: Vec<Number>,
1224 pub(crate) eval_f: Option<Eval_F_CB>,
1225 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1226 pub(crate) eval_g: Option<Eval_G_CB>,
1227 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1228 pub(crate) eval_h: Option<Eval_H_CB>,
1229 pub(crate) user_data: *mut c_void,
1230 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1233 pub(crate) user_scaling: Option<UserScaling>,
1235 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1236 pub(crate) final_x: Vec<Number>,
1237 pub(crate) final_z_l: Vec<Number>,
1238 pub(crate) final_z_u: Vec<Number>,
1239 pub(crate) final_g: Vec<Number>,
1240 pub(crate) final_lambda: Vec<Number>,
1241 pub(crate) final_obj: Number,
1242}
1243
1244impl TNLP for CCallbackTnlp {
1245 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1246 Some(NlpInfo {
1247 n: self.n as pounce_common::types::Index,
1248 m: self.m as pounce_common::types::Index,
1249 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1250 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1251 index_style: if self.index_style == 1 {
1252 IndexStyle::Fortran
1253 } else {
1254 IndexStyle::C
1255 },
1256 })
1257 }
1258
1259 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1260 if !self.x_l.is_empty() {
1261 b.x_l.copy_from_slice(&self.x_l);
1262 }
1263 if !self.x_u.is_empty() {
1264 b.x_u.copy_from_slice(&self.x_u);
1265 }
1266 if !self.g_l.is_empty() {
1267 b.g_l.copy_from_slice(&self.g_l);
1268 }
1269 if !self.g_u.is_empty() {
1270 b.g_u.copy_from_slice(&self.g_u);
1271 }
1272 true
1273 }
1274
1275 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1276 if !self.initial_x.is_empty() {
1277 sp.x.copy_from_slice(&self.initial_x);
1278 }
1279 true
1280 }
1281
1282 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1283 let Some(s) = self.user_scaling.as_ref() else {
1284 return false;
1285 };
1286 *req.obj_scaling = s.obj_scaling;
1287 if let Some(x) = s.x_scaling.as_ref() {
1288 if x.len() == req.x_scaling.len() {
1289 req.x_scaling.copy_from_slice(x);
1290 *req.use_x_scaling = true;
1291 }
1292 } else {
1293 *req.use_x_scaling = false;
1294 }
1295 if let Some(g) = s.g_scaling.as_ref() {
1296 if g.len() == req.g_scaling.len() {
1297 req.g_scaling.copy_from_slice(g);
1298 *req.use_g_scaling = true;
1299 }
1300 } else {
1301 *req.use_g_scaling = false;
1302 }
1303 true
1304 }
1305
1306 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
1307 let cb = self.eval_f?;
1308 let mut obj = 0.0;
1309 let ok = unsafe {
1310 cb(
1311 self.n,
1312 x.as_ptr() as *mut Number,
1313 if new_x { TRUE } else { FALSE },
1314 &mut obj,
1315 self.user_data,
1316 )
1317 };
1318 if ok != FALSE {
1319 Some(obj)
1320 } else {
1321 None
1322 }
1323 }
1324
1325 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
1326 let Some(cb) = self.eval_grad_f else {
1327 return false;
1328 };
1329 let ok = unsafe {
1330 cb(
1331 self.n,
1332 x.as_ptr() as *mut Number,
1333 if new_x { TRUE } else { FALSE },
1334 grad_f.as_mut_ptr(),
1335 self.user_data,
1336 )
1337 };
1338 ok != FALSE
1339 }
1340
1341 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
1342 if self.m == 0 {
1343 return true;
1344 }
1345 let Some(cb) = self.eval_g else {
1346 return false;
1347 };
1348 let ok = unsafe {
1349 cb(
1350 self.n,
1351 x.as_ptr() as *mut Number,
1352 if new_x { TRUE } else { FALSE },
1353 self.m,
1354 g.as_mut_ptr(),
1355 self.user_data,
1356 )
1357 };
1358 ok != FALSE
1359 }
1360
1361 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
1362 if self.m == 0 || self.nele_jac == 0 {
1363 return true;
1364 }
1365 let Some(cb) = self.eval_jac_g else {
1366 return false;
1367 };
1368 let x_ptr = x
1369 .map(|s| s.as_ptr() as *mut Number)
1370 .unwrap_or(std::ptr::null_mut());
1371 let ok = match mode {
1372 SparsityRequest::Structure { irow, jcol } => unsafe {
1373 cb(
1374 self.n,
1375 x_ptr,
1376 if new_x { TRUE } else { FALSE },
1377 self.m,
1378 self.nele_jac,
1379 irow.as_mut_ptr(),
1380 jcol.as_mut_ptr(),
1381 std::ptr::null_mut(),
1382 self.user_data,
1383 )
1384 },
1385 SparsityRequest::Values { values } => unsafe {
1386 cb(
1387 self.n,
1388 x_ptr,
1389 if new_x { TRUE } else { FALSE },
1390 self.m,
1391 self.nele_jac,
1392 std::ptr::null_mut(),
1393 std::ptr::null_mut(),
1394 values.as_mut_ptr(),
1395 self.user_data,
1396 )
1397 },
1398 };
1399 ok != FALSE
1400 }
1401
1402 fn eval_h(
1403 &mut self,
1404 x: Option<&[Number]>,
1405 new_x: bool,
1406 obj_factor: Number,
1407 lambda: Option<&[Number]>,
1408 new_lambda: bool,
1409 mode: SparsityRequest<'_>,
1410 ) -> bool {
1411 let Some(cb) = self.eval_h else {
1412 return false;
1413 };
1414 if self.nele_hess == 0 {
1415 return true;
1416 }
1417 let x_ptr = x
1418 .map(|s| s.as_ptr() as *mut Number)
1419 .unwrap_or(std::ptr::null_mut());
1420 let lambda_ptr = lambda
1421 .map(|s| s.as_ptr() as *mut Number)
1422 .unwrap_or(std::ptr::null_mut());
1423 let ok = match mode {
1424 SparsityRequest::Structure { irow, jcol } => unsafe {
1425 cb(
1426 self.n,
1427 x_ptr,
1428 if new_x { TRUE } else { FALSE },
1429 obj_factor,
1430 self.m,
1431 lambda_ptr,
1432 if new_lambda { TRUE } else { FALSE },
1433 self.nele_hess,
1434 irow.as_mut_ptr(),
1435 jcol.as_mut_ptr(),
1436 std::ptr::null_mut(),
1437 self.user_data,
1438 )
1439 },
1440 SparsityRequest::Values { values } => unsafe {
1441 cb(
1442 self.n,
1443 x_ptr,
1444 if new_x { TRUE } else { FALSE },
1445 obj_factor,
1446 self.m,
1447 lambda_ptr,
1448 if new_lambda { TRUE } else { FALSE },
1449 self.nele_hess,
1450 std::ptr::null_mut(),
1451 std::ptr::null_mut(),
1452 values.as_mut_ptr(),
1453 self.user_data,
1454 )
1455 },
1456 };
1457 ok != FALSE
1458 }
1459
1460 fn intermediate_callback(
1461 &mut self,
1462 stats: pounce_nlp::tnlp::IterStats,
1463 _ip_data: &IpoptData,
1464 _ip_cq: &IpoptCq,
1465 ) -> bool {
1466 let Some(cb) = self.intermediate_cb else {
1467 return true;
1468 };
1469 let ok = unsafe {
1470 cb(
1471 stats.mode as Index,
1472 stats.iter as Index,
1473 stats.obj_value,
1474 stats.inf_pr,
1475 stats.inf_du,
1476 stats.mu,
1477 stats.d_norm,
1478 stats.regularization_size,
1479 stats.alpha_du,
1480 stats.alpha_pr,
1481 stats.ls_trials as Index,
1482 self.user_data,
1483 )
1484 };
1485 ok != FALSE
1486 }
1487
1488 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1489 self.final_status = Some(sol.status);
1490 if !sol.x.is_empty() {
1491 self.final_x.copy_from_slice(sol.x);
1492 }
1493 if !sol.z_l.is_empty() {
1494 self.final_z_l.copy_from_slice(sol.z_l);
1495 }
1496 if !sol.z_u.is_empty() {
1497 self.final_z_u.copy_from_slice(sol.z_u);
1498 }
1499 if !sol.g.is_empty() {
1500 self.final_g.copy_from_slice(sol.g);
1501 }
1502 if !sol.lambda.is_empty() {
1503 self.final_lambda.copy_from_slice(sol.lambda);
1504 }
1505 self.final_obj = sol.obj_value;
1506 }
1507}
1508
1509#[no_mangle]
1522pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
1523 if ipopt_problem.is_null() {
1524 return FALSE;
1525 }
1526 let info = unsafe { &mut *ipopt_problem };
1527 info.app.enable_iter_history();
1528 TRUE
1529}
1530
1531#[no_mangle]
1552pub unsafe extern "C" fn IpoptWriteSolveReport(
1553 ipopt_problem: IpoptProblem,
1554 path: *const c_char,
1555 detail: *const c_char,
1556) -> Bool {
1557 use pounce_solve_report::{
1558 status_to_solve_result_num, write_report_file, InputDescriptor, ReportBuilder, ReportDetail,
1559 };
1560
1561 if ipopt_problem.is_null() || path.is_null() {
1562 return FALSE;
1563 }
1564 let info = unsafe { &*ipopt_problem };
1565 let Some(last) = info.last_solve.as_ref() else {
1566 return FALSE;
1567 };
1568
1569 let Ok(path_str) = (unsafe { CStr::from_ptr(path) }).to_str() else {
1570 return FALSE;
1571 };
1572
1573 let detail_choice = if detail.is_null() {
1574 ReportDetail::Summary
1575 } else {
1576 let Ok(detail_str) = (unsafe { CStr::from_ptr(detail) }).to_str() else {
1577 return FALSE;
1578 };
1579 match ReportDetail::parse(detail_str) {
1580 Ok(d) => d,
1581 Err(_) => return FALSE,
1582 }
1583 };
1584
1585 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
1586 builder.problem.n_variables = info.n;
1587 builder.problem.n_constraints = info.m;
1588 builder.problem.n_objectives = 1;
1589 builder.problem.nnz_jac_g = Some(info.nele_jac);
1590 builder.problem.nnz_h_lag = Some(info.nele_hess);
1591
1592 builder.solution.status = last.status;
1593 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
1594 builder.solution.objective = last.final_obj;
1595 builder.solution.x = last.final_x.clone();
1596 builder.solution.lambda = last.final_lambda.clone();
1597
1598 builder.ingest_stats(&last.stats);
1599 if let Some(linsol) = last.linear_solver.clone() {
1600 builder.set_linear_solver_summary(linsol);
1601 }
1602
1603 let report = builder.finish();
1604 match write_report_file(std::path::Path::new(path_str), &report) {
1605 Ok(_) => TRUE,
1606 Err(_) => FALSE,
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612 use super::*;
1613 use std::ffi::CString;
1614
1615 unsafe extern "C" fn dummy_eval_f(
1616 _n: Index,
1617 _x: *const Number,
1618 _new_x: Bool,
1619 _obj_value: *mut Number,
1620 _user_data: *mut c_void,
1621 ) -> Bool {
1622 TRUE
1623 }
1624 unsafe extern "C" fn dummy_eval_grad_f(
1625 _n: Index,
1626 _x: *const Number,
1627 _new_x: Bool,
1628 _grad_f: *mut Number,
1629 _user_data: *mut c_void,
1630 ) -> Bool {
1631 TRUE
1632 }
1633
1634 fn create_unconstrained() -> IpoptProblem {
1635 let xl = [-1.0; 4];
1636 let xu = [1.0; 4];
1637 unsafe {
1638 CreateIpoptProblem(
1639 4,
1640 xl.as_ptr(),
1641 xu.as_ptr(),
1642 0,
1643 std::ptr::null(),
1644 std::ptr::null(),
1645 0,
1646 10,
1647 0,
1648 Some(dummy_eval_f),
1649 None,
1650 Some(dummy_eval_grad_f),
1651 None,
1652 None,
1653 )
1654 }
1655 }
1656
1657 #[test]
1658 fn create_succeeds_for_unconstrained_problem() {
1659 let p = create_unconstrained();
1660 assert!(!p.is_null());
1661 unsafe { FreeIpoptProblem(p) };
1662 }
1663
1664 #[test]
1665 fn create_returns_null_on_missing_required_callbacks() {
1666 let xl = [-1.0; 4];
1667 let xu = [1.0; 4];
1668 let p = unsafe {
1669 CreateIpoptProblem(
1670 4,
1671 xl.as_ptr(),
1672 xu.as_ptr(),
1673 0,
1674 std::ptr::null(),
1675 std::ptr::null(),
1676 0,
1677 10,
1678 0,
1679 None, None,
1681 Some(dummy_eval_grad_f),
1682 None,
1683 None,
1684 )
1685 };
1686 assert!(p.is_null());
1687 }
1688
1689 #[test]
1690 fn create_returns_null_on_negative_n() {
1691 let p = unsafe {
1692 CreateIpoptProblem(
1693 -1,
1694 std::ptr::null(),
1695 std::ptr::null(),
1696 0,
1697 std::ptr::null(),
1698 std::ptr::null(),
1699 0,
1700 10,
1701 0,
1702 Some(dummy_eval_f),
1703 None,
1704 Some(dummy_eval_grad_f),
1705 None,
1706 None,
1707 )
1708 };
1709 assert!(p.is_null());
1710 }
1711
1712 #[test]
1713 fn create_returns_null_on_invalid_index_style() {
1714 let xl = [0.0; 1];
1715 let xu = [1.0; 1];
1716 let p = unsafe {
1717 CreateIpoptProblem(
1718 1,
1719 xl.as_ptr(),
1720 xu.as_ptr(),
1721 0,
1722 std::ptr::null(),
1723 std::ptr::null(),
1724 0,
1725 1,
1726 2, Some(dummy_eval_f),
1728 None,
1729 Some(dummy_eval_grad_f),
1730 None,
1731 None,
1732 )
1733 };
1734 assert!(p.is_null());
1735 }
1736
1737 #[test]
1738 fn add_int_option_forwards_to_application() {
1739 let p = create_unconstrained();
1740 let key = CString::new("print_level").unwrap();
1741 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
1742 assert_eq!(ok, TRUE);
1743 let info = unsafe { &*p };
1744 let (level, found) = info
1745 .app
1746 .options()
1747 .get_integer_value("print_level", "")
1748 .unwrap();
1749 assert!(found);
1750 assert_eq!(level, 5);
1751 unsafe { FreeIpoptProblem(p) };
1752 }
1753
1754 #[test]
1755 fn add_str_option_with_invalid_key_returns_false() {
1756 let p = create_unconstrained();
1757 let key = CString::new("totally_unknown_option").unwrap();
1758 let val = CString::new("yes").unwrap();
1759 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
1760 assert_eq!(ok, FALSE);
1761 unsafe { FreeIpoptProblem(p) };
1762 }
1763
1764 #[test]
1765 fn add_options_on_null_problem_returns_false() {
1766 let key = CString::new("print_level").unwrap();
1767 let v = CString::new("yes").unwrap();
1768 unsafe {
1769 assert_eq!(
1770 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
1771 FALSE
1772 );
1773 assert_eq!(
1774 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
1775 FALSE
1776 );
1777 assert_eq!(
1778 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
1779 FALSE
1780 );
1781 }
1782 }
1783
1784 unsafe extern "C" fn dummy_intermediate(
1785 _alg_mod: Index,
1786 _iter_count: Index,
1787 _obj_value: Number,
1788 _inf_pr: Number,
1789 _inf_du: Number,
1790 _mu: Number,
1791 _d_norm: Number,
1792 _regularization_size: Number,
1793 _alpha_du: Number,
1794 _alpha_pr: Number,
1795 _ls_trials: Index,
1796 _user_data: *mut c_void,
1797 ) -> Bool {
1798 TRUE
1799 }
1800
1801 #[test]
1802 fn set_intermediate_callback_stores_pointer() {
1803 let p = create_unconstrained();
1804 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
1805 assert_eq!(ok, TRUE);
1806 let info = unsafe { &*p };
1807 assert!(info.intermediate_cb.is_some());
1808 unsafe { FreeIpoptProblem(p) };
1809 }
1810
1811 #[test]
1812 fn solve_returns_internal_error_on_null_problem() {
1813 let rc = unsafe {
1814 IpoptSolve(
1815 std::ptr::null_mut(),
1816 std::ptr::null_mut(),
1817 std::ptr::null_mut(),
1818 std::ptr::null_mut(),
1819 std::ptr::null_mut(),
1820 std::ptr::null_mut(),
1821 std::ptr::null_mut(),
1822 std::ptr::null_mut(),
1823 )
1824 };
1825 assert_eq!(rc, -199);
1826 }
1827
1828 #[test]
1829 fn free_null_is_safe() {
1830 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
1831 }
1832
1833 unsafe extern "C" fn quad_eval_f(
1839 _n: Index,
1840 x: *const Number,
1841 _new_x: Bool,
1842 obj_value: *mut Number,
1843 _user_data: *mut c_void,
1844 ) -> Bool {
1845 let v = *x.offset(0);
1846 *obj_value = (v - 2.0) * (v - 2.0);
1847 TRUE
1848 }
1849 unsafe extern "C" fn quad_eval_grad_f(
1850 _n: Index,
1851 x: *const Number,
1852 _new_x: Bool,
1853 grad: *mut Number,
1854 _user_data: *mut c_void,
1855 ) -> Bool {
1856 let v = *x.offset(0);
1857 *grad.offset(0) = 2.0 * (v - 2.0);
1858 TRUE
1859 }
1860 unsafe extern "C" fn quad_eval_h(
1861 _n: Index,
1862 _x: *const Number,
1863 _new_x: Bool,
1864 obj_factor: Number,
1865 _m: Index,
1866 _lambda: *const Number,
1867 _new_lambda: Bool,
1868 _nele_hess: Index,
1869 irow: *mut Index,
1870 jcol: *mut Index,
1871 values: *mut Number,
1872 _user_data: *mut c_void,
1873 ) -> Bool {
1874 if !irow.is_null() && !jcol.is_null() && values.is_null() {
1875 *irow.offset(0) = 0;
1876 *jcol.offset(0) = 0;
1877 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
1878 *values.offset(0) = 2.0 * obj_factor;
1879 } else {
1880 return FALSE;
1881 }
1882 TRUE
1883 }
1884
1885 #[test]
1886 fn solve_drives_unconstrained_quadratic_through_bridge() {
1887 let xl = [-1.0e20];
1890 let xu = [1.0e20];
1891 let p = unsafe {
1892 CreateIpoptProblem(
1893 1,
1894 xl.as_ptr(),
1895 xu.as_ptr(),
1896 0,
1897 std::ptr::null(),
1898 std::ptr::null(),
1899 0,
1900 1,
1901 0,
1902 Some(quad_eval_f),
1903 None,
1904 Some(quad_eval_grad_f),
1905 None,
1906 Some(quad_eval_h),
1907 )
1908 };
1909 assert!(!p.is_null());
1910 let mut x = [0.0_f64];
1911 let mut obj = 0.0_f64;
1912 let rc = unsafe {
1913 IpoptSolve(
1914 p,
1915 x.as_mut_ptr(),
1916 std::ptr::null_mut(),
1917 &mut obj,
1918 std::ptr::null_mut(),
1919 std::ptr::null_mut(),
1920 std::ptr::null_mut(),
1921 std::ptr::null_mut(),
1922 )
1923 };
1924 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
1925 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
1926 assert!(obj.abs() < 1e-10, "obj = {}", obj);
1927 unsafe { FreeIpoptProblem(p) };
1928 }
1929
1930 #[test]
1931 fn solve_invalid_problem_definition_when_x_null() {
1932 let p = create_unconstrained();
1933 let rc = unsafe {
1934 IpoptSolve(
1935 p,
1936 std::ptr::null_mut(), std::ptr::null_mut(),
1938 std::ptr::null_mut(),
1939 std::ptr::null_mut(),
1940 std::ptr::null_mut(),
1941 std::ptr::null_mut(),
1942 std::ptr::null_mut(),
1943 )
1944 };
1945 assert_eq!(
1946 rc,
1947 ApplicationReturnStatus::InvalidProblemDefinition as Index
1948 );
1949 unsafe { FreeIpoptProblem(p) };
1950 }
1951
1952 #[test]
1955 fn get_version_writes_pkg_version() {
1956 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
1957 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
1958 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1959 assert_eq!((mj, mn, pt), expected);
1960 }
1961
1962 #[test]
1963 fn get_version_tolerates_null_buffers() {
1964 unsafe {
1966 GetIpoptVersion(
1967 std::ptr::null_mut(),
1968 std::ptr::null_mut(),
1969 std::ptr::null_mut(),
1970 )
1971 };
1972 }
1973
1974 #[test]
1975 fn set_scaling_stores_user_supplied_arrays() {
1976 let p = create_unconstrained();
1977 let xs = [2.0, 3.0, 4.0, 5.0];
1978 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
1979 assert_eq!(ok, TRUE);
1980 let info = unsafe { &*p };
1981 let s = info.user_scaling.as_ref().unwrap();
1982 assert_eq!(s.obj_scaling, 7.0);
1983 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
1984 assert!(s.g_scaling.is_none());
1985 unsafe { FreeIpoptProblem(p) };
1986 }
1987
1988 #[test]
1989 fn set_scaling_on_null_problem_returns_false() {
1990 let ok = unsafe {
1991 SetIpoptProblemScaling(
1992 std::ptr::null_mut(),
1993 1.0,
1994 std::ptr::null(),
1995 std::ptr::null(),
1996 )
1997 };
1998 assert_eq!(ok, FALSE);
1999 }
2000
2001 #[test]
2002 fn open_output_file_writes_and_attaches_journal() {
2003 let p = create_unconstrained();
2004 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2005 let _ = std::fs::create_dir_all(&dir);
2006 let path = dir.join("output.log");
2007 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2008 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2009 assert_eq!(ok, TRUE);
2010 let info = unsafe { &*p };
2012 let (level, found) = info
2013 .app
2014 .options()
2015 .get_integer_value("file_print_level", "")
2016 .unwrap();
2017 assert!(found);
2018 assert_eq!(level, 5);
2019 unsafe { FreeIpoptProblem(p) };
2020 let _ = std::fs::remove_file(&path);
2021 }
2022
2023 #[test]
2024 fn open_output_file_with_null_inputs_returns_false() {
2025 let key = CString::new("nope").unwrap();
2026 unsafe {
2027 assert_eq!(
2028 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2029 FALSE
2030 );
2031 }
2032 let p = create_unconstrained();
2033 unsafe {
2034 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2035 FreeIpoptProblem(p);
2036 }
2037 }
2038
2039 #[test]
2040 fn get_current_iterate_returns_false_outside_callback() {
2041 let p = create_unconstrained();
2042 let rc = unsafe {
2043 GetIpoptCurrentIterate(
2044 p,
2045 FALSE,
2046 0,
2047 std::ptr::null_mut(),
2048 std::ptr::null_mut(),
2049 std::ptr::null_mut(),
2050 0,
2051 std::ptr::null_mut(),
2052 std::ptr::null_mut(),
2053 )
2054 };
2055 assert_eq!(rc, FALSE);
2056 unsafe { FreeIpoptProblem(p) };
2057 }
2058
2059 #[test]
2060 fn get_current_violations_returns_false_outside_callback() {
2061 let p = create_unconstrained();
2062 let rc = unsafe {
2063 GetIpoptCurrentViolations(
2064 p,
2065 FALSE,
2066 0,
2067 std::ptr::null_mut(),
2068 std::ptr::null_mut(),
2069 std::ptr::null_mut(),
2070 std::ptr::null_mut(),
2071 std::ptr::null_mut(),
2072 0,
2073 std::ptr::null_mut(),
2074 std::ptr::null_mut(),
2075 )
2076 };
2077 assert_eq!(rc, FALSE);
2078 unsafe { FreeIpoptProblem(p) };
2079 }
2080
2081 #[test]
2082 fn post_solve_stats_zero_before_solve() {
2083 let p = create_unconstrained();
2084 unsafe {
2085 assert_eq!(GetIpoptIterCount(p), 0);
2086 assert_eq!(GetIpoptSolveTime(p), 0.0);
2087 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2088 assert_eq!(GetIpoptDualInf(p), 0.0);
2089 assert_eq!(GetIpoptComplInf(p), 0.0);
2090 FreeIpoptProblem(p);
2091 }
2092 }
2093
2094 #[test]
2095 fn post_solve_stats_populated_after_solve() {
2096 let xl = [-1.0e20];
2098 let xu = [1.0e20];
2099 let p = unsafe {
2100 CreateIpoptProblem(
2101 1,
2102 xl.as_ptr(),
2103 xu.as_ptr(),
2104 0,
2105 std::ptr::null(),
2106 std::ptr::null(),
2107 0,
2108 1,
2109 0,
2110 Some(quad_eval_f),
2111 None,
2112 Some(quad_eval_grad_f),
2113 None,
2114 Some(quad_eval_h),
2115 )
2116 };
2117 let mut x = [0.0_f64];
2118 let mut obj = 0.0_f64;
2119 let rc = unsafe {
2120 IpoptSolve(
2121 p,
2122 x.as_mut_ptr(),
2123 std::ptr::null_mut(),
2124 &mut obj,
2125 std::ptr::null_mut(),
2126 std::ptr::null_mut(),
2127 std::ptr::null_mut(),
2128 std::ptr::null_mut(),
2129 )
2130 };
2131 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2132 unsafe {
2135 assert!(GetIpoptIterCount(p) >= 0);
2136 assert!(GetIpoptSolveTime(p) >= 0.0);
2137 assert!(GetIpoptPrimalInf(p).is_finite());
2138 assert!(GetIpoptDualInf(p).is_finite());
2139 assert!(GetIpoptComplInf(p).is_finite());
2140 FreeIpoptProblem(p);
2141 }
2142 }
2143
2144 #[test]
2145 fn write_solve_report_emits_v1_json_with_iter_history() {
2146 let xl = [-1.0e20];
2149 let xu = [1.0e20];
2150 let p = unsafe {
2151 CreateIpoptProblem(
2152 1,
2153 xl.as_ptr(),
2154 xu.as_ptr(),
2155 0,
2156 std::ptr::null(),
2157 std::ptr::null(),
2158 0,
2159 1,
2160 0,
2161 Some(quad_eval_f),
2162 None,
2163 Some(quad_eval_grad_f),
2164 None,
2165 Some(quad_eval_h),
2166 )
2167 };
2168
2169 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
2171 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
2172 assert_eq!(bad, FALSE);
2173
2174 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
2176 let mut x = [0.0_f64];
2177 let mut obj = 0.0_f64;
2178 let rc = unsafe {
2179 IpoptSolve(
2180 p,
2181 x.as_mut_ptr(),
2182 std::ptr::null_mut(),
2183 &mut obj,
2184 std::ptr::null_mut(),
2185 std::ptr::null_mut(),
2186 std::ptr::null_mut(),
2187 std::ptr::null_mut(),
2188 )
2189 };
2190 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2191
2192 let dir = std::env::temp_dir();
2193 let path = dir.join("pounce_cinterface_report.json");
2194 let cpath = CString::new(path.to_str().unwrap()).unwrap();
2195 let cdetail = CString::new("full").unwrap();
2196 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
2197 assert_eq!(ok, TRUE);
2198
2199 let txt = std::fs::read_to_string(&path).unwrap();
2202 assert!(
2203 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
2204 "{txt}"
2205 );
2206 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
2207 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
2208 assert_eq!(parsed.problem.n_variables, 1);
2209 assert_eq!(parsed.problem.n_constraints, 0);
2210
2211 let bad_detail = CString::new("verbose").unwrap();
2213 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
2214 assert_eq!(bad, FALSE);
2215
2216 let _ = std::fs::remove_file(&path);
2217 unsafe { FreeIpoptProblem(p) };
2218 }
2219
2220 unsafe extern "C" fn cb_quad_eval_g(
2227 _n: Index,
2228 x: *const Number,
2229 _new_x: Bool,
2230 _m: Index,
2231 g: *mut Number,
2232 _user_data: *mut c_void,
2233 ) -> Bool {
2234 *g.offset(0) = *x.offset(0);
2235 TRUE
2236 }
2237 unsafe extern "C" fn cb_quad_eval_jac_g(
2238 _n: Index,
2239 _x: *const Number,
2240 _new_x: Bool,
2241 _m: Index,
2242 nele_jac: Index,
2243 irow: *mut Index,
2244 jcol: *mut Index,
2245 values: *mut Number,
2246 _user_data: *mut c_void,
2247 ) -> Bool {
2248 assert_eq!(nele_jac, 1);
2249 if !irow.is_null() {
2250 *irow.offset(0) = 0;
2251 *jcol.offset(0) = 0;
2252 }
2253 if !values.is_null() {
2254 *values.offset(0) = 1.0;
2255 }
2256 TRUE
2257 }
2258 unsafe extern "C" fn cb_quad_eval_h(
2259 _n: Index,
2260 _x: *const Number,
2261 _new_x: Bool,
2262 obj_factor: Number,
2263 _m: Index,
2264 _lambda: *const Number,
2265 _new_lambda: Bool,
2266 _nele_hess: Index,
2267 irow: *mut Index,
2268 jcol: *mut Index,
2269 values: *mut Number,
2270 _user_data: *mut c_void,
2271 ) -> Bool {
2272 if !irow.is_null() {
2273 *irow.offset(0) = 0;
2274 *jcol.offset(0) = 0;
2275 }
2276 if !values.is_null() {
2277 *values.offset(0) = 2.0 * obj_factor;
2278 }
2279 TRUE
2280 }
2281
2282 fn create_callback_test_problem() -> IpoptProblem {
2283 let xl = [-1.0e20];
2285 let xu = [1.0e20];
2286 let gl = [-10.0];
2287 let gu = [10.0];
2288 unsafe {
2289 CreateIpoptProblem(
2290 1,
2291 xl.as_ptr(),
2292 xu.as_ptr(),
2293 1,
2294 gl.as_ptr(),
2295 gu.as_ptr(),
2296 1,
2297 1,
2298 0,
2299 Some(quad_eval_f),
2300 Some(cb_quad_eval_g),
2301 Some(quad_eval_grad_f),
2302 Some(cb_quad_eval_jac_g),
2303 Some(cb_quad_eval_h),
2304 )
2305 }
2306 }
2307
2308 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2309 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
2310 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
2311 std::sync::atomic::AtomicBool::new(false);
2312
2313 unsafe extern "C" fn counting_cb(
2314 _alg_mod: Index,
2315 iter_count: Index,
2316 _obj_value: Number,
2317 _inf_pr: Number,
2318 _inf_du: Number,
2319 _mu: Number,
2320 _d_norm: Number,
2321 _regularization_size: Number,
2322 _alpha_du: Number,
2323 _alpha_pr: Number,
2324 _ls_trials: Index,
2325 user_data: *mut c_void,
2326 ) -> Bool {
2327 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2328 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
2329 let problem = user_data as IpoptProblem;
2332 let mut x = [0.0_f64];
2333 let rc = GetIpoptCurrentIterate(
2334 problem,
2335 FALSE,
2336 1,
2337 x.as_mut_ptr(),
2338 std::ptr::null_mut(),
2339 std::ptr::null_mut(),
2340 1,
2341 std::ptr::null_mut(),
2342 std::ptr::null_mut(),
2343 );
2344 if rc == TRUE && x[0].is_finite() {
2345 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2346 }
2347 TRUE
2348 }
2349
2350 #[test]
2351 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
2352 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
2353 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
2354 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2355
2356 let p = create_callback_test_problem();
2357 assert!(!p.is_null());
2358 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
2359 assert_eq!(ok, TRUE);
2360 let mut x = [0.0_f64];
2361 let mut obj = 0.0_f64;
2362 let rc = unsafe {
2363 IpoptSolve(
2364 p,
2365 x.as_mut_ptr(),
2366 std::ptr::null_mut(),
2367 &mut obj,
2368 std::ptr::null_mut(),
2369 std::ptr::null_mut(),
2370 std::ptr::null_mut(),
2371 p as *mut c_void,
2372 )
2373 };
2374 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2375 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
2377 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
2378 assert!(
2379 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
2380 "last iter should be >= 1 after at least one accepted step"
2381 );
2382 assert!(
2383 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
2384 "GetIpoptCurrentIterate did not return a usable x"
2385 );
2386 unsafe { FreeIpoptProblem(p) };
2387 }
2388
2389 unsafe extern "C" fn user_stop_cb(
2390 _alg_mod: Index,
2391 _iter_count: Index,
2392 _obj_value: Number,
2393 _inf_pr: Number,
2394 _inf_du: Number,
2395 _mu: Number,
2396 _d_norm: Number,
2397 _regularization_size: Number,
2398 _alpha_du: Number,
2399 _alpha_pr: Number,
2400 _ls_trials: Index,
2401 _user_data: *mut c_void,
2402 ) -> Bool {
2403 FALSE
2404 }
2405
2406 #[test]
2407 fn intermediate_callback_false_surfaces_user_requested_stop() {
2408 let p = create_callback_test_problem();
2409 assert!(!p.is_null());
2410 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
2411 assert_eq!(ok, TRUE);
2412 let mut x = [0.0_f64];
2413 let rc = unsafe {
2414 IpoptSolve(
2415 p,
2416 x.as_mut_ptr(),
2417 std::ptr::null_mut(),
2418 std::ptr::null_mut(),
2419 std::ptr::null_mut(),
2420 std::ptr::null_mut(),
2421 std::ptr::null_mut(),
2422 std::ptr::null_mut(),
2423 )
2424 };
2425 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
2426 unsafe { FreeIpoptProblem(p) };
2427 }
2428
2429 #[test]
2430 fn parse_pkg_version_handles_missing_components() {
2431 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
2432 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
2433 assert_eq!(parse_pkg_version(""), (0, 0, 0));
2434 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
2435 }
2436
2437 use crate::solver::{
2440 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
2441 IpoptSolverSolve,
2442 };
2443
2444 #[test]
2445 fn solver_create_consumes_problem_handle() {
2446 let mut p = create_unconstrained();
2447 assert!(!p.is_null());
2448 let s = unsafe { IpoptCreateSolver(&mut p) };
2449 assert!(!s.is_null());
2450 assert!(
2451 p.is_null(),
2452 "IpoptCreateSolver should NULL out the caller's handle"
2453 );
2454 unsafe { IpoptFreeSolver(s) };
2455 }
2456
2457 #[test]
2458 fn solver_create_null_inputs_return_null() {
2459 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2461 assert!(s.is_null());
2462 let mut p: IpoptProblem = std::ptr::null_mut();
2464 let s = unsafe { IpoptCreateSolver(&mut p) };
2465 assert!(s.is_null());
2466 }
2467
2468 #[test]
2469 fn solver_free_null_is_safe() {
2470 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
2471 }
2472
2473 #[test]
2474 fn solver_solve_drives_quadratic_and_retains_factor() {
2475 let xl = [-1.0e20];
2476 let xu = [1.0e20];
2477 let mut p = unsafe {
2478 CreateIpoptProblem(
2479 1,
2480 xl.as_ptr(),
2481 xu.as_ptr(),
2482 0,
2483 std::ptr::null(),
2484 std::ptr::null(),
2485 0,
2486 1,
2487 0,
2488 Some(quad_eval_f),
2489 None,
2490 Some(quad_eval_grad_f),
2491 None,
2492 Some(quad_eval_h),
2493 )
2494 };
2495 assert!(!p.is_null());
2496 let s = unsafe { IpoptCreateSolver(&mut p) };
2497 assert!(!s.is_null());
2498 let mut x = [0.0_f64];
2499 let mut obj = 0.0_f64;
2500 let rc = unsafe {
2501 IpoptSolverSolve(
2502 s,
2503 x.as_mut_ptr(),
2504 std::ptr::null_mut(),
2505 &mut obj,
2506 std::ptr::null_mut(),
2507 std::ptr::null_mut(),
2508 std::ptr::null_mut(),
2509 std::ptr::null_mut(),
2510 )
2511 };
2512 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2513 assert!((x[0] - 2.0).abs() < 1e-6);
2514 assert!(obj.abs() < 1e-10);
2515
2516 let dim = unsafe { IpoptSolverGetKktDim(s) };
2519 assert!(dim > 0, "expected positive KKT dim, got {dim}");
2520 let rhs = vec![0.0_f64; dim as usize];
2521 let mut lhs = vec![1.0_f64; dim as usize];
2522 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
2523 assert_eq!(ok, TRUE);
2524 for (i, v) in lhs.iter().enumerate() {
2525 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
2526 }
2527 unsafe { IpoptFreeSolver(s) };
2528 }
2529
2530 #[test]
2531 fn solver_kkt_dim_minus_one_before_solve() {
2532 let mut p = create_unconstrained();
2533 let s = unsafe { IpoptCreateSolver(&mut p) };
2534 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
2535 unsafe { IpoptFreeSolver(s) };
2536 }
2537
2538 #[test]
2543 fn c_get_working_set_returns_false_before_any_solve() {
2544 let p = create_unconstrained();
2545 let mut bound_buf = [0; 4];
2546 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
2547 assert_eq!(rc, FALSE);
2548 unsafe { FreeIpoptProblem(p) };
2549 }
2550
2551 #[test]
2552 fn c_set_warm_start_with_both_null_returns_false() {
2553 let p = create_unconstrained();
2554 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
2555 assert_eq!(rc, FALSE);
2556 unsafe { FreeIpoptProblem(p) };
2557 }
2558
2559 #[test]
2560 fn c_set_warm_start_with_bad_status_code_returns_false() {
2561 let p = create_unconstrained();
2562 let bogus = [
2564 POUNCE_WS_INACTIVE,
2565 7,
2566 POUNCE_WS_AT_LOWER,
2567 POUNCE_WS_INACTIVE,
2568 ];
2569 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
2570 assert_eq!(rc, FALSE);
2571 unsafe { FreeIpoptProblem(p) };
2572 }
2573
2574 #[test]
2575 fn c_set_warm_start_then_clear_succeeds() {
2576 let p = create_unconstrained();
2577 let in_buf = [POUNCE_WS_INACTIVE; 4];
2578 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
2579 assert_eq!(set_rc, TRUE);
2580 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
2581 assert_eq!(clr_rc, TRUE);
2582 unsafe { FreeIpoptProblem(p) };
2583 }
2584
2585 #[test]
2586 fn c_set_warm_start_on_null_problem_returns_false() {
2587 let in_buf = [POUNCE_WS_INACTIVE; 1];
2588 let rc = unsafe {
2589 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
2590 };
2591 assert_eq!(rc, FALSE);
2592 }
2593
2594 #[test]
2595 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
2596 let p = create_callback_test_problem();
2602 let key = CString::new("algorithm").unwrap();
2603 let val = CString::new("active-set-sqp").unwrap();
2604 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2605 assert_eq!(ok, TRUE);
2606
2607 let mut x = [0.0_f64];
2608 let mut obj = 0.0_f64;
2609 let rc1 = unsafe {
2610 IpoptSolve(
2611 p,
2612 x.as_mut_ptr(),
2613 std::ptr::null_mut(),
2614 &mut obj,
2615 std::ptr::null_mut(),
2616 std::ptr::null_mut(),
2617 std::ptr::null_mut(),
2618 std::ptr::null_mut(),
2619 )
2620 };
2621 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
2622
2623 let mut bound_buf = [-1; 1];
2624 let mut cons_buf = [-1; 1];
2625 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
2626 assert_eq!(got, TRUE);
2627 assert!((0..=3).contains(&bound_buf[0]));
2629 assert!((0..=3).contains(&cons_buf[0]));
2630
2631 x[0] = 0.0;
2636 let mut obj2 = 0.0_f64;
2637 let mut bound_out = [-1; 1];
2638 let mut cons_out = [-1; 1];
2639 let rc2 = unsafe {
2640 IpoptSolveWarmStart(
2641 p,
2642 x.as_mut_ptr(),
2643 std::ptr::null_mut(),
2644 &mut obj2,
2645 std::ptr::null_mut(),
2646 std::ptr::null_mut(),
2647 std::ptr::null_mut(),
2648 bound_buf.as_ptr(),
2649 cons_buf.as_ptr(),
2650 bound_out.as_mut_ptr(),
2651 cons_out.as_mut_ptr(),
2652 std::ptr::null_mut(),
2653 )
2654 };
2655 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
2656 assert!((0..=3).contains(&bound_out[0]));
2657 assert!((0..=3).contains(&cons_out[0]));
2658
2659 unsafe { FreeIpoptProblem(p) };
2660 }
2661}