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 let feral_cfg = feral_cfg.clone();
583 Box::new(move || default_backend_factory(feral_cfg.clone()))
584 };
585 let resto_provider = make_default_restoration_factory_provider(
586 RestoAlgorithmBuilder::new(),
587 info.app.algorithm_builder_from_options(),
588 bff_mint,
589 );
590 info.app.set_restoration_factory_provider(resto_provider);
591
592 let bridge_for_solve: Rc<RefCell<dyn TNLP>> = bridge.clone();
593 let status = info.app.optimize_tnlp(bridge_for_solve);
594 let bridge_ref = bridge.borrow();
595 info.last_solve = Some(LastSolve {
596 stats: info.app.statistics(),
597 status,
598 linear_solver: info.app.linear_solver_summary(),
599 final_x: bridge_ref.final_x.clone(),
600 final_lambda: bridge_ref.final_lambda.clone(),
601 final_obj: bridge_ref.final_obj,
602 });
603 if !x.is_null() && n_us > 0 {
604 std::ptr::copy_nonoverlapping(bridge_ref.final_x.as_ptr(), x, n_us);
605 }
606 if !g.is_null() && m_us > 0 {
607 std::ptr::copy_nonoverlapping(bridge_ref.final_g.as_ptr(), g, m_us);
608 }
609 if !obj_val.is_null() {
610 *obj_val = bridge_ref.final_obj;
611 }
612 if !mult_g.is_null() && m_us > 0 {
613 std::ptr::copy_nonoverlapping(bridge_ref.final_lambda.as_ptr(), mult_g, m_us);
614 }
615 if !mult_x_L.is_null() && n_us > 0 {
616 std::ptr::copy_nonoverlapping(bridge_ref.final_z_l.as_ptr(), mult_x_L, n_us);
617 }
618 if !mult_x_U.is_null() && n_us > 0 {
619 std::ptr::copy_nonoverlapping(bridge_ref.final_z_u.as_ptr(), mult_x_U, n_us);
620 }
621 status as Index
622}
623
624#[no_mangle]
630pub unsafe extern "C" fn SetIntermediateCallback(
631 ipopt_problem: IpoptProblem,
632 intermediate_cb: Option<Intermediate_CB>,
633) -> Bool {
634 if ipopt_problem.is_null() {
635 return FALSE;
636 }
637 let info = &mut *ipopt_problem;
638 info.intermediate_cb = intermediate_cb;
639 TRUE
640}
641
642#[allow(clippy::too_many_arguments)]
665#[no_mangle]
666pub unsafe extern "C" fn GetIpoptCurrentIterate(
667 ipopt_problem: IpoptProblem,
668 _scaled: Bool,
669 n: Index,
670 x: *mut Number,
671 z_l: *mut Number,
672 z_u: *mut Number,
673 m: Index,
674 g: *mut Number,
675 lambda: *mut Number,
676) -> Bool {
677 if ipopt_problem.is_null() {
678 return FALSE;
679 }
680 let info = &*ipopt_problem;
681 if n != info.n || m != info.m {
682 return FALSE;
683 }
684 let result = ip_intermediate::with_current(|ctx| {
685 let data = ctx.data.borrow();
686 let Some(curr) = data.curr.as_ref() else {
687 return false;
688 };
689 let nlp = ctx.nlp.borrow();
690 let n_us = n as usize;
691 let m_us = m as usize;
692 if !x.is_null() && n_us > 0 {
693 let full_x = nlp.lift_x_to_full(&*curr.x);
694 if full_x.len() != n_us {
695 return false;
696 }
697 std::ptr::copy_nonoverlapping(full_x.as_ptr(), x, n_us);
698 }
699 if !z_l.is_null() && n_us > 0 {
700 let full = nlp.pack_z_l_for_user(&*curr.z_l);
701 if full.len() != n_us {
702 return false;
703 }
704 std::ptr::copy_nonoverlapping(full.as_ptr(), z_l, n_us);
705 }
706 if !z_u.is_null() && n_us > 0 {
707 let full = nlp.pack_z_u_for_user(&*curr.z_u);
708 if full.len() != n_us {
709 return false;
710 }
711 std::ptr::copy_nonoverlapping(full.as_ptr(), z_u, n_us);
712 }
713 if !g.is_null() && m_us > 0 {
714 let cq = ctx.cq.borrow();
715 let full = nlp.pack_g_for_user(&*cq.curr_c(), &*cq.curr_d());
716 if full.len() != m_us {
717 return false;
718 }
719 std::ptr::copy_nonoverlapping(full.as_ptr(), g, m_us);
720 }
721 if !lambda.is_null() && m_us > 0 {
722 let full = nlp.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
723 if full.len() != m_us {
724 return false;
725 }
726 std::ptr::copy_nonoverlapping(full.as_ptr(), lambda, m_us);
727 }
728 true
729 });
730 if result.unwrap_or(false) {
731 TRUE
732 } else {
733 FALSE
734 }
735}
736
737#[allow(clippy::too_many_arguments)]
752#[no_mangle]
753pub unsafe extern "C" fn GetIpoptCurrentViolations(
754 ipopt_problem: IpoptProblem,
755 _scaled: Bool,
756 n: Index,
757 x_l_violation: *mut Number,
758 x_u_violation: *mut Number,
759 compl_x_l: *mut Number,
760 compl_x_u: *mut Number,
761 grad_lag_x: *mut Number,
762 m: Index,
763 nlp_constraint_violation: *mut Number,
764 compl_g: *mut Number,
765) -> Bool {
766 if ipopt_problem.is_null() {
767 return FALSE;
768 }
769 let info = &*ipopt_problem;
770 if n != info.n || m != info.m {
771 return FALSE;
772 }
773 let result = ip_intermediate::with_current(|ctx| {
774 let data = ctx.data.borrow();
775 let Some(_curr) = data.curr.as_ref() else {
776 return false;
777 };
778 drop(data);
779 let nlp = ctx.nlp.borrow();
780 let cq = ctx.cq.borrow();
781 let n_us = n as usize;
782 let m_us = m as usize;
783 if !x_l_violation.is_null() && n_us > 0 {
789 let mut v = vec![0.0; n_us];
790 let slack = cq.curr_slack_x_l();
791 let z_l_full = nlp.pack_z_l_for_user(&*slack);
792 for (i, s) in z_l_full.iter().enumerate() {
797 v[i] = (-s).max(0.0);
798 }
799 std::ptr::copy_nonoverlapping(v.as_ptr(), x_l_violation, n_us);
800 }
801 if !x_u_violation.is_null() && n_us > 0 {
802 let mut v = vec![0.0; n_us];
803 let slack = cq.curr_slack_x_u();
804 let s_full = nlp.pack_z_u_for_user(&*slack);
805 for (i, s) in s_full.iter().enumerate() {
806 v[i] = (-s).max(0.0);
807 }
808 std::ptr::copy_nonoverlapping(v.as_ptr(), x_u_violation, n_us);
809 }
810 if !compl_x_l.is_null() && n_us > 0 {
811 let v = nlp.pack_z_l_for_user(&*cq.curr_compl_x_l());
812 if v.len() != n_us {
813 return false;
814 }
815 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_l, n_us);
816 }
817 if !compl_x_u.is_null() && n_us > 0 {
818 let v = nlp.pack_z_u_for_user(&*cq.curr_compl_x_u());
819 if v.len() != n_us {
820 return false;
821 }
822 std::ptr::copy_nonoverlapping(v.as_ptr(), compl_x_u, n_us);
823 }
824 if !grad_lag_x.is_null() && n_us > 0 {
825 let glx = cq.curr_grad_lag_x();
826 let full = nlp.lift_x_to_full(&*glx);
830 if full.len() != n_us {
831 return false;
832 }
833 std::ptr::copy_nonoverlapping(full.as_ptr(), grad_lag_x, n_us);
834 }
835 if !nlp_constraint_violation.is_null() && m_us > 0 {
836 let zero = vec![0.0; m_us];
842 std::ptr::copy_nonoverlapping(zero.as_ptr(), nlp_constraint_violation, m_us);
843 }
844 if !compl_g.is_null() && m_us > 0 {
845 let zero = vec![0.0; m_us];
848 std::ptr::copy_nonoverlapping(zero.as_ptr(), compl_g, m_us);
849 }
850 true
851 });
852 if result.unwrap_or(false) {
853 TRUE
854 } else {
855 FALSE
856 }
857}
858
859#[no_mangle]
867pub unsafe extern "C" fn GetIpoptVersion(
868 major: *mut c_int,
869 minor: *mut c_int,
870 release: *mut c_int,
871) {
872 let (mj, mn, pt) = parse_pkg_version(env!("CARGO_PKG_VERSION"));
877 if !major.is_null() {
878 *major = mj;
879 }
880 if !minor.is_null() {
881 *minor = mn;
882 }
883 if !release.is_null() {
884 *release = pt;
885 }
886}
887
888fn parse_pkg_version(v: &str) -> (c_int, c_int, c_int) {
889 let mut it = v.split('.').map(|s| s.parse::<c_int>().unwrap_or(0));
890 (
891 it.next().unwrap_or(0),
892 it.next().unwrap_or(0),
893 it.next().unwrap_or(0),
894 )
895}
896
897#[no_mangle]
914pub unsafe extern "C" fn GetIpoptIterCount(ipopt_problem: IpoptProblem) -> Index {
915 last_stat(ipopt_problem, |s| s.iteration_count).unwrap_or(0)
916}
917
918#[no_mangle]
925pub unsafe extern "C" fn GetIpoptSolveTime(ipopt_problem: IpoptProblem) -> Number {
926 last_stat(ipopt_problem, |s| s.total_wallclock_time_secs).unwrap_or(0.0)
927}
928
929#[no_mangle]
936pub unsafe extern "C" fn GetIpoptPrimalInf(ipopt_problem: IpoptProblem) -> Number {
937 last_stat(ipopt_problem, |s| s.final_constr_viol).unwrap_or(0.0)
938}
939
940#[no_mangle]
947pub unsafe extern "C" fn GetIpoptDualInf(ipopt_problem: IpoptProblem) -> Number {
948 last_stat(ipopt_problem, |s| s.final_dual_inf).unwrap_or(0.0)
949}
950
951#[no_mangle]
957pub unsafe extern "C" fn GetIpoptComplInf(ipopt_problem: IpoptProblem) -> Number {
958 last_stat(ipopt_problem, |s| s.final_compl).unwrap_or(0.0)
959}
960
961unsafe fn last_stat<T, F>(ipopt_problem: IpoptProblem, f: F) -> Option<T>
962where
963 F: FnOnce(&SolveStatistics) -> T,
964{
965 if ipopt_problem.is_null() {
966 return None;
967 }
968 (*ipopt_problem).last_solve.as_ref().map(|ls| f(&ls.stats))
969}
970
971fn bound_status_to_int(s: pounce_qp::BoundStatus) -> c_int {
981 use pounce_qp::BoundStatus::*;
982 match s {
983 Inactive => POUNCE_WS_INACTIVE,
984 AtLower => POUNCE_WS_AT_LOWER,
985 AtUpper => POUNCE_WS_AT_UPPER,
986 Fixed => POUNCE_WS_FIXED_OR_EQ,
987 }
988}
989
990fn int_to_bound_status(v: c_int) -> Option<pounce_qp::BoundStatus> {
991 use pounce_qp::BoundStatus::*;
992 match v {
993 POUNCE_WS_INACTIVE => Some(Inactive),
994 POUNCE_WS_AT_LOWER => Some(AtLower),
995 POUNCE_WS_AT_UPPER => Some(AtUpper),
996 POUNCE_WS_FIXED_OR_EQ => Some(Fixed),
997 _ => None,
998 }
999}
1000
1001fn cons_status_to_int(s: pounce_qp::ConsStatus) -> c_int {
1002 use pounce_qp::ConsStatus::*;
1003 match s {
1004 Inactive => POUNCE_WS_INACTIVE,
1005 AtLower => POUNCE_WS_AT_LOWER,
1006 AtUpper => POUNCE_WS_AT_UPPER,
1007 Equality => POUNCE_WS_FIXED_OR_EQ,
1008 }
1009}
1010
1011fn int_to_cons_status(v: c_int) -> Option<pounce_qp::ConsStatus> {
1012 use pounce_qp::ConsStatus::*;
1013 match v {
1014 POUNCE_WS_INACTIVE => Some(Inactive),
1015 POUNCE_WS_AT_LOWER => Some(AtLower),
1016 POUNCE_WS_AT_UPPER => Some(AtUpper),
1017 POUNCE_WS_FIXED_OR_EQ => Some(Equality),
1018 _ => None,
1019 }
1020}
1021
1022#[no_mangle]
1038pub unsafe extern "C" fn IpoptGetWorkingSet(
1039 ipopt_problem: IpoptProblem,
1040 bound_status_out: *mut IpoptBoundStatus,
1041 cons_status_out: *mut IpoptConsStatus,
1042) -> Bool {
1043 if ipopt_problem.is_null() {
1044 return FALSE;
1045 }
1046 let info = &*ipopt_problem;
1047 let ws = match info.app.last_sqp_working_set() {
1048 Some(w) => w,
1049 None => return FALSE,
1050 };
1051 if !bound_status_out.is_null() {
1052 for (i, &s) in ws.bounds.iter().enumerate() {
1053 *bound_status_out.add(i) = bound_status_to_int(s);
1054 }
1055 }
1056 if !cons_status_out.is_null() {
1057 for (i, &s) in ws.constraints.iter().enumerate() {
1058 *cons_status_out.add(i) = cons_status_to_int(s);
1059 }
1060 }
1061 TRUE
1062}
1063
1064#[no_mangle]
1080pub unsafe extern "C" fn IpoptSetWarmStartWorkingSet(
1081 ipopt_problem: IpoptProblem,
1082 bound_status_in: *const IpoptBoundStatus,
1083 cons_status_in: *const IpoptConsStatus,
1084) -> Bool {
1085 if ipopt_problem.is_null() {
1086 return FALSE;
1087 }
1088 if bound_status_in.is_null() && cons_status_in.is_null() {
1089 return FALSE;
1090 }
1091 let info = &mut *ipopt_problem;
1092 let n = info.n.max(0) as usize;
1093 let m = info.m.max(0) as usize;
1094 let mut bounds = vec![pounce_qp::BoundStatus::Inactive; n];
1095 if !bound_status_in.is_null() {
1096 for i in 0..n {
1097 let v = *bound_status_in.add(i);
1098 match int_to_bound_status(v) {
1099 Some(s) => bounds[i] = s,
1100 None => return FALSE,
1101 }
1102 }
1103 }
1104 let mut constraints = vec![pounce_qp::ConsStatus::Inactive; m];
1105 if !cons_status_in.is_null() {
1106 for i in 0..m {
1107 let v = *cons_status_in.add(i);
1108 match int_to_cons_status(v) {
1109 Some(s) => constraints[i] = s,
1110 None => return FALSE,
1111 }
1112 }
1113 }
1114 info.app
1122 .set_sqp_warm_start(pounce_algorithm::sqp::SqpIterates {
1123 x: vec![0.0; n],
1124 lambda_g: vec![0.0; m],
1125 lambda_x: vec![0.0; n],
1126 working: Some(pounce_qp::WorkingSet {
1127 bounds,
1128 constraints,
1129 }),
1130 });
1131 TRUE
1132}
1133
1134#[no_mangle]
1141pub unsafe extern "C" fn IpoptClearWarmStartWorkingSet(ipopt_problem: IpoptProblem) -> Bool {
1142 if ipopt_problem.is_null() {
1143 return FALSE;
1144 }
1145 (*ipopt_problem).app.clear_sqp_warm_start();
1146 TRUE
1147}
1148
1149#[allow(clippy::too_many_arguments)]
1165#[no_mangle]
1166pub unsafe extern "C" fn IpoptSolveWarmStart(
1167 ipopt_problem: IpoptProblem,
1168 x: *mut Number,
1169 g: *mut Number,
1170 obj_val: *mut Number,
1171 mult_g: *mut Number,
1172 mult_x_L: *mut Number,
1173 mult_x_U: *mut Number,
1174 bound_status_in: *const IpoptBoundStatus,
1175 cons_status_in: *const IpoptConsStatus,
1176 bound_status_out: *mut IpoptBoundStatus,
1177 cons_status_out: *mut IpoptConsStatus,
1178 user_data: *mut c_void,
1179) -> Index {
1180 if ipopt_problem.is_null() {
1181 return ApplicationReturnStatus::InternalError as Index;
1182 }
1183 if !bound_status_in.is_null() || !cons_status_in.is_null() {
1188 let _ = IpoptSetWarmStartWorkingSet(ipopt_problem, bound_status_in, cons_status_in);
1189 }
1190 let status = IpoptSolve(
1191 ipopt_problem,
1192 x,
1193 g,
1194 obj_val,
1195 mult_g,
1196 mult_x_L,
1197 mult_x_U,
1198 user_data,
1199 );
1200 let _ = IpoptGetWorkingSet(ipopt_problem, bound_status_out, cons_status_out);
1201 status
1202}
1203
1204pub(crate) struct CCallbackTnlp {
1215 pub(crate) n: Index,
1216 pub(crate) m: Index,
1217 pub(crate) nele_jac: Index,
1218 pub(crate) nele_hess: Index,
1219 pub(crate) index_style: Index,
1220 pub(crate) x_l: Vec<Number>,
1221 pub(crate) x_u: Vec<Number>,
1222 pub(crate) g_l: Vec<Number>,
1223 pub(crate) g_u: Vec<Number>,
1224 pub(crate) initial_x: Vec<Number>,
1225 pub(crate) eval_f: Option<Eval_F_CB>,
1226 pub(crate) eval_grad_f: Option<Eval_Grad_F_CB>,
1227 pub(crate) eval_g: Option<Eval_G_CB>,
1228 pub(crate) eval_jac_g: Option<Eval_Jac_G_CB>,
1229 pub(crate) eval_h: Option<Eval_H_CB>,
1230 pub(crate) user_data: *mut c_void,
1231 pub(crate) intermediate_cb: Option<Intermediate_CB>,
1234 pub(crate) user_scaling: Option<UserScaling>,
1236 pub(crate) final_status: Option<pounce_nlp::alg_types::SolverReturn>,
1237 pub(crate) final_x: Vec<Number>,
1238 pub(crate) final_z_l: Vec<Number>,
1239 pub(crate) final_z_u: Vec<Number>,
1240 pub(crate) final_g: Vec<Number>,
1241 pub(crate) final_lambda: Vec<Number>,
1242 pub(crate) final_obj: Number,
1243}
1244
1245impl TNLP for CCallbackTnlp {
1246 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1247 Some(NlpInfo {
1248 n: self.n as pounce_common::types::Index,
1249 m: self.m as pounce_common::types::Index,
1250 nnz_jac_g: self.nele_jac as pounce_common::types::Index,
1251 nnz_h_lag: self.nele_hess as pounce_common::types::Index,
1252 index_style: if self.index_style == 1 {
1253 IndexStyle::Fortran
1254 } else {
1255 IndexStyle::C
1256 },
1257 })
1258 }
1259
1260 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1261 if !self.x_l.is_empty() {
1262 b.x_l.copy_from_slice(&self.x_l);
1263 }
1264 if !self.x_u.is_empty() {
1265 b.x_u.copy_from_slice(&self.x_u);
1266 }
1267 if !self.g_l.is_empty() {
1268 b.g_l.copy_from_slice(&self.g_l);
1269 }
1270 if !self.g_u.is_empty() {
1271 b.g_u.copy_from_slice(&self.g_u);
1272 }
1273 true
1274 }
1275
1276 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1277 if !self.initial_x.is_empty() {
1278 sp.x.copy_from_slice(&self.initial_x);
1279 }
1280 true
1281 }
1282
1283 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1284 let Some(s) = self.user_scaling.as_ref() else {
1285 return false;
1286 };
1287 *req.obj_scaling = s.obj_scaling;
1288 if let Some(x) = s.x_scaling.as_ref() {
1289 if x.len() == req.x_scaling.len() {
1290 req.x_scaling.copy_from_slice(x);
1291 *req.use_x_scaling = true;
1292 }
1293 } else {
1294 *req.use_x_scaling = false;
1295 }
1296 if let Some(g) = s.g_scaling.as_ref() {
1297 if g.len() == req.g_scaling.len() {
1298 req.g_scaling.copy_from_slice(g);
1299 *req.use_g_scaling = true;
1300 }
1301 } else {
1302 *req.use_g_scaling = false;
1303 }
1304 true
1305 }
1306
1307 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
1308 let cb = self.eval_f?;
1309 let mut obj = 0.0;
1310 let ok = unsafe {
1311 cb(
1312 self.n,
1313 x.as_ptr() as *mut Number,
1314 if new_x { TRUE } else { FALSE },
1315 &mut obj,
1316 self.user_data,
1317 )
1318 };
1319 if ok != FALSE {
1320 Some(obj)
1321 } else {
1322 None
1323 }
1324 }
1325
1326 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
1327 let Some(cb) = self.eval_grad_f else {
1328 return false;
1329 };
1330 let ok = unsafe {
1331 cb(
1332 self.n,
1333 x.as_ptr() as *mut Number,
1334 if new_x { TRUE } else { FALSE },
1335 grad_f.as_mut_ptr(),
1336 self.user_data,
1337 )
1338 };
1339 ok != FALSE
1340 }
1341
1342 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
1343 if self.m == 0 {
1344 return true;
1345 }
1346 let Some(cb) = self.eval_g else {
1347 return false;
1348 };
1349 let ok = unsafe {
1350 cb(
1351 self.n,
1352 x.as_ptr() as *mut Number,
1353 if new_x { TRUE } else { FALSE },
1354 self.m,
1355 g.as_mut_ptr(),
1356 self.user_data,
1357 )
1358 };
1359 ok != FALSE
1360 }
1361
1362 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
1363 if self.m == 0 || self.nele_jac == 0 {
1364 return true;
1365 }
1366 let Some(cb) = self.eval_jac_g else {
1367 return false;
1368 };
1369 let x_ptr = x
1370 .map(|s| s.as_ptr() as *mut Number)
1371 .unwrap_or(std::ptr::null_mut());
1372 let ok = match mode {
1373 SparsityRequest::Structure { irow, jcol } => unsafe {
1374 cb(
1375 self.n,
1376 x_ptr,
1377 if new_x { TRUE } else { FALSE },
1378 self.m,
1379 self.nele_jac,
1380 irow.as_mut_ptr(),
1381 jcol.as_mut_ptr(),
1382 std::ptr::null_mut(),
1383 self.user_data,
1384 )
1385 },
1386 SparsityRequest::Values { values } => unsafe {
1387 cb(
1388 self.n,
1389 x_ptr,
1390 if new_x { TRUE } else { FALSE },
1391 self.m,
1392 self.nele_jac,
1393 std::ptr::null_mut(),
1394 std::ptr::null_mut(),
1395 values.as_mut_ptr(),
1396 self.user_data,
1397 )
1398 },
1399 };
1400 ok != FALSE
1401 }
1402
1403 fn eval_h(
1404 &mut self,
1405 x: Option<&[Number]>,
1406 new_x: bool,
1407 obj_factor: Number,
1408 lambda: Option<&[Number]>,
1409 new_lambda: bool,
1410 mode: SparsityRequest<'_>,
1411 ) -> bool {
1412 let Some(cb) = self.eval_h else {
1413 return false;
1414 };
1415 if self.nele_hess == 0 {
1416 return true;
1417 }
1418 let x_ptr = x
1419 .map(|s| s.as_ptr() as *mut Number)
1420 .unwrap_or(std::ptr::null_mut());
1421 let lambda_ptr = lambda
1422 .map(|s| s.as_ptr() as *mut Number)
1423 .unwrap_or(std::ptr::null_mut());
1424 let ok = match mode {
1425 SparsityRequest::Structure { irow, jcol } => unsafe {
1426 cb(
1427 self.n,
1428 x_ptr,
1429 if new_x { TRUE } else { FALSE },
1430 obj_factor,
1431 self.m,
1432 lambda_ptr,
1433 if new_lambda { TRUE } else { FALSE },
1434 self.nele_hess,
1435 irow.as_mut_ptr(),
1436 jcol.as_mut_ptr(),
1437 std::ptr::null_mut(),
1438 self.user_data,
1439 )
1440 },
1441 SparsityRequest::Values { values } => unsafe {
1442 cb(
1443 self.n,
1444 x_ptr,
1445 if new_x { TRUE } else { FALSE },
1446 obj_factor,
1447 self.m,
1448 lambda_ptr,
1449 if new_lambda { TRUE } else { FALSE },
1450 self.nele_hess,
1451 std::ptr::null_mut(),
1452 std::ptr::null_mut(),
1453 values.as_mut_ptr(),
1454 self.user_data,
1455 )
1456 },
1457 };
1458 ok != FALSE
1459 }
1460
1461 fn intermediate_callback(
1462 &mut self,
1463 stats: pounce_nlp::tnlp::IterStats,
1464 _ip_data: &IpoptData,
1465 _ip_cq: &IpoptCq,
1466 ) -> bool {
1467 let Some(cb) = self.intermediate_cb else {
1468 return true;
1469 };
1470 let ok = unsafe {
1471 cb(
1472 stats.mode as Index,
1473 stats.iter as Index,
1474 stats.obj_value,
1475 stats.inf_pr,
1476 stats.inf_du,
1477 stats.mu,
1478 stats.d_norm,
1479 stats.regularization_size,
1480 stats.alpha_du,
1481 stats.alpha_pr,
1482 stats.ls_trials as Index,
1483 self.user_data,
1484 )
1485 };
1486 ok != FALSE
1487 }
1488
1489 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1490 self.final_status = Some(sol.status);
1491 if !sol.x.is_empty() {
1492 self.final_x.copy_from_slice(sol.x);
1493 }
1494 if !sol.z_l.is_empty() {
1495 self.final_z_l.copy_from_slice(sol.z_l);
1496 }
1497 if !sol.z_u.is_empty() {
1498 self.final_z_u.copy_from_slice(sol.z_u);
1499 }
1500 if !sol.g.is_empty() {
1501 self.final_g.copy_from_slice(sol.g);
1502 }
1503 if !sol.lambda.is_empty() {
1504 self.final_lambda.copy_from_slice(sol.lambda);
1505 }
1506 self.final_obj = sol.obj_value;
1507 }
1508}
1509
1510#[no_mangle]
1523pub unsafe extern "C" fn IpoptEnableIterHistory(ipopt_problem: IpoptProblem) -> Bool {
1524 if ipopt_problem.is_null() {
1525 return FALSE;
1526 }
1527 let info = unsafe { &mut *ipopt_problem };
1528 info.app.enable_iter_history();
1529 TRUE
1530}
1531
1532#[no_mangle]
1553pub unsafe extern "C" fn IpoptWriteSolveReport(
1554 ipopt_problem: IpoptProblem,
1555 path: *const c_char,
1556 detail: *const c_char,
1557) -> Bool {
1558 use pounce_solve_report::{
1559 status_to_solve_result_num, write_report_file, InputDescriptor, ReportBuilder, ReportDetail,
1560 };
1561
1562 if ipopt_problem.is_null() || path.is_null() {
1563 return FALSE;
1564 }
1565 let info = unsafe { &*ipopt_problem };
1566 let Some(last) = info.last_solve.as_ref() else {
1567 return FALSE;
1568 };
1569
1570 let Ok(path_str) = (unsafe { CStr::from_ptr(path) }).to_str() else {
1571 return FALSE;
1572 };
1573
1574 let detail_choice = if detail.is_null() {
1575 ReportDetail::Summary
1576 } else {
1577 let Ok(detail_str) = (unsafe { CStr::from_ptr(detail) }).to_str() else {
1578 return FALSE;
1579 };
1580 match ReportDetail::parse(detail_str) {
1581 Ok(d) => d,
1582 Err(_) => return FALSE,
1583 }
1584 };
1585
1586 let mut builder = ReportBuilder::new(detail_choice, InputDescriptor::TnlpDirect);
1587 builder.problem.n_variables = info.n;
1588 builder.problem.n_constraints = info.m;
1589 builder.problem.n_objectives = 1;
1590 builder.problem.nnz_jac_g = Some(info.nele_jac);
1591 builder.problem.nnz_h_lag = Some(info.nele_hess);
1592
1593 builder.solution.status = last.status;
1594 builder.solution.solve_result_num = status_to_solve_result_num(last.status);
1595 builder.solution.objective = last.final_obj;
1596 builder.solution.x = last.final_x.clone();
1597 builder.solution.lambda = last.final_lambda.clone();
1598
1599 builder.ingest_stats(&last.stats);
1600 if let Some(linsol) = last.linear_solver.clone() {
1601 builder.set_linear_solver_summary(linsol);
1602 }
1603
1604 let report = builder.finish();
1605 match write_report_file(std::path::Path::new(path_str), &report) {
1606 Ok(_) => TRUE,
1607 Err(_) => FALSE,
1608 }
1609}
1610
1611#[cfg(test)]
1612mod tests {
1613 use super::*;
1614 use std::ffi::CString;
1615
1616 unsafe extern "C" fn dummy_eval_f(
1617 _n: Index,
1618 _x: *const Number,
1619 _new_x: Bool,
1620 _obj_value: *mut Number,
1621 _user_data: *mut c_void,
1622 ) -> Bool {
1623 TRUE
1624 }
1625 unsafe extern "C" fn dummy_eval_grad_f(
1626 _n: Index,
1627 _x: *const Number,
1628 _new_x: Bool,
1629 _grad_f: *mut Number,
1630 _user_data: *mut c_void,
1631 ) -> Bool {
1632 TRUE
1633 }
1634
1635 fn create_unconstrained() -> IpoptProblem {
1636 let xl = [-1.0; 4];
1637 let xu = [1.0; 4];
1638 unsafe {
1639 CreateIpoptProblem(
1640 4,
1641 xl.as_ptr(),
1642 xu.as_ptr(),
1643 0,
1644 std::ptr::null(),
1645 std::ptr::null(),
1646 0,
1647 10,
1648 0,
1649 Some(dummy_eval_f),
1650 None,
1651 Some(dummy_eval_grad_f),
1652 None,
1653 None,
1654 )
1655 }
1656 }
1657
1658 #[test]
1659 fn create_succeeds_for_unconstrained_problem() {
1660 let p = create_unconstrained();
1661 assert!(!p.is_null());
1662 unsafe { FreeIpoptProblem(p) };
1663 }
1664
1665 #[test]
1666 fn create_returns_null_on_missing_required_callbacks() {
1667 let xl = [-1.0; 4];
1668 let xu = [1.0; 4];
1669 let p = unsafe {
1670 CreateIpoptProblem(
1671 4,
1672 xl.as_ptr(),
1673 xu.as_ptr(),
1674 0,
1675 std::ptr::null(),
1676 std::ptr::null(),
1677 0,
1678 10,
1679 0,
1680 None, None,
1682 Some(dummy_eval_grad_f),
1683 None,
1684 None,
1685 )
1686 };
1687 assert!(p.is_null());
1688 }
1689
1690 #[test]
1691 fn create_returns_null_on_negative_n() {
1692 let p = unsafe {
1693 CreateIpoptProblem(
1694 -1,
1695 std::ptr::null(),
1696 std::ptr::null(),
1697 0,
1698 std::ptr::null(),
1699 std::ptr::null(),
1700 0,
1701 10,
1702 0,
1703 Some(dummy_eval_f),
1704 None,
1705 Some(dummy_eval_grad_f),
1706 None,
1707 None,
1708 )
1709 };
1710 assert!(p.is_null());
1711 }
1712
1713 #[test]
1714 fn create_returns_null_on_invalid_index_style() {
1715 let xl = [0.0; 1];
1716 let xu = [1.0; 1];
1717 let p = unsafe {
1718 CreateIpoptProblem(
1719 1,
1720 xl.as_ptr(),
1721 xu.as_ptr(),
1722 0,
1723 std::ptr::null(),
1724 std::ptr::null(),
1725 0,
1726 1,
1727 2, Some(dummy_eval_f),
1729 None,
1730 Some(dummy_eval_grad_f),
1731 None,
1732 None,
1733 )
1734 };
1735 assert!(p.is_null());
1736 }
1737
1738 #[test]
1739 fn add_int_option_forwards_to_application() {
1740 let p = create_unconstrained();
1741 let key = CString::new("print_level").unwrap();
1742 let ok = unsafe { AddIpoptIntOption(p, key.as_ptr(), 5) };
1743 assert_eq!(ok, TRUE);
1744 let info = unsafe { &*p };
1745 let (level, found) = info
1746 .app
1747 .options()
1748 .get_integer_value("print_level", "")
1749 .unwrap();
1750 assert!(found);
1751 assert_eq!(level, 5);
1752 unsafe { FreeIpoptProblem(p) };
1753 }
1754
1755 #[test]
1756 fn add_str_option_with_invalid_key_returns_false() {
1757 let p = create_unconstrained();
1758 let key = CString::new("totally_unknown_option").unwrap();
1759 let val = CString::new("yes").unwrap();
1760 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
1761 assert_eq!(ok, FALSE);
1762 unsafe { FreeIpoptProblem(p) };
1763 }
1764
1765 #[test]
1766 fn add_options_on_null_problem_returns_false() {
1767 let key = CString::new("print_level").unwrap();
1768 let v = CString::new("yes").unwrap();
1769 unsafe {
1770 assert_eq!(
1771 AddIpoptIntOption(std::ptr::null_mut(), key.as_ptr(), 5),
1772 FALSE
1773 );
1774 assert_eq!(
1775 AddIpoptNumOption(std::ptr::null_mut(), key.as_ptr(), 1.0),
1776 FALSE
1777 );
1778 assert_eq!(
1779 AddIpoptStrOption(std::ptr::null_mut(), key.as_ptr(), v.as_ptr()),
1780 FALSE
1781 );
1782 }
1783 }
1784
1785 unsafe extern "C" fn dummy_intermediate(
1786 _alg_mod: Index,
1787 _iter_count: Index,
1788 _obj_value: Number,
1789 _inf_pr: Number,
1790 _inf_du: Number,
1791 _mu: Number,
1792 _d_norm: Number,
1793 _regularization_size: Number,
1794 _alpha_du: Number,
1795 _alpha_pr: Number,
1796 _ls_trials: Index,
1797 _user_data: *mut c_void,
1798 ) -> Bool {
1799 TRUE
1800 }
1801
1802 #[test]
1803 fn set_intermediate_callback_stores_pointer() {
1804 let p = create_unconstrained();
1805 let ok = unsafe { SetIntermediateCallback(p, Some(dummy_intermediate)) };
1806 assert_eq!(ok, TRUE);
1807 let info = unsafe { &*p };
1808 assert!(info.intermediate_cb.is_some());
1809 unsafe { FreeIpoptProblem(p) };
1810 }
1811
1812 #[test]
1813 fn solve_returns_internal_error_on_null_problem() {
1814 let rc = unsafe {
1815 IpoptSolve(
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 std::ptr::null_mut(),
1824 )
1825 };
1826 assert_eq!(rc, -199);
1827 }
1828
1829 #[test]
1830 fn free_null_is_safe() {
1831 unsafe { FreeIpoptProblem(std::ptr::null_mut()) };
1832 }
1833
1834 unsafe extern "C" fn quad_eval_f(
1840 _n: Index,
1841 x: *const Number,
1842 _new_x: Bool,
1843 obj_value: *mut Number,
1844 _user_data: *mut c_void,
1845 ) -> Bool {
1846 let v = *x.offset(0);
1847 *obj_value = (v - 2.0) * (v - 2.0);
1848 TRUE
1849 }
1850 unsafe extern "C" fn quad_eval_grad_f(
1851 _n: Index,
1852 x: *const Number,
1853 _new_x: Bool,
1854 grad: *mut Number,
1855 _user_data: *mut c_void,
1856 ) -> Bool {
1857 let v = *x.offset(0);
1858 *grad.offset(0) = 2.0 * (v - 2.0);
1859 TRUE
1860 }
1861 unsafe extern "C" fn quad_eval_h(
1862 _n: Index,
1863 _x: *const Number,
1864 _new_x: Bool,
1865 obj_factor: Number,
1866 _m: Index,
1867 _lambda: *const Number,
1868 _new_lambda: Bool,
1869 _nele_hess: Index,
1870 irow: *mut Index,
1871 jcol: *mut Index,
1872 values: *mut Number,
1873 _user_data: *mut c_void,
1874 ) -> Bool {
1875 if !irow.is_null() && !jcol.is_null() && values.is_null() {
1876 *irow.offset(0) = 0;
1877 *jcol.offset(0) = 0;
1878 } else if irow.is_null() && jcol.is_null() && !values.is_null() {
1879 *values.offset(0) = 2.0 * obj_factor;
1880 } else {
1881 return FALSE;
1882 }
1883 TRUE
1884 }
1885
1886 #[test]
1887 fn solve_drives_unconstrained_quadratic_through_bridge() {
1888 let xl = [-1.0e20];
1891 let xu = [1.0e20];
1892 let p = unsafe {
1893 CreateIpoptProblem(
1894 1,
1895 xl.as_ptr(),
1896 xu.as_ptr(),
1897 0,
1898 std::ptr::null(),
1899 std::ptr::null(),
1900 0,
1901 1,
1902 0,
1903 Some(quad_eval_f),
1904 None,
1905 Some(quad_eval_grad_f),
1906 None,
1907 Some(quad_eval_h),
1908 )
1909 };
1910 assert!(!p.is_null());
1911 let mut x = [0.0_f64];
1912 let mut obj = 0.0_f64;
1913 let rc = unsafe {
1914 IpoptSolve(
1915 p,
1916 x.as_mut_ptr(),
1917 std::ptr::null_mut(),
1918 &mut obj,
1919 std::ptr::null_mut(),
1920 std::ptr::null_mut(),
1921 std::ptr::null_mut(),
1922 std::ptr::null_mut(),
1923 )
1924 };
1925 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
1926 assert!((x[0] - 2.0).abs() < 1e-6, "x[0] = {}", x[0]);
1927 assert!(obj.abs() < 1e-10, "obj = {}", obj);
1928 unsafe { FreeIpoptProblem(p) };
1929 }
1930
1931 #[test]
1932 fn solve_invalid_problem_definition_when_x_null() {
1933 let p = create_unconstrained();
1934 let rc = unsafe {
1935 IpoptSolve(
1936 p,
1937 std::ptr::null_mut(), 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 std::ptr::null_mut(),
1944 )
1945 };
1946 assert_eq!(
1947 rc,
1948 ApplicationReturnStatus::InvalidProblemDefinition as Index
1949 );
1950 unsafe { FreeIpoptProblem(p) };
1951 }
1952
1953 #[test]
1956 fn get_version_writes_pkg_version() {
1957 let (mut mj, mut mn, mut pt) = (-1, -1, -1);
1958 unsafe { GetIpoptVersion(&mut mj, &mut mn, &mut pt) };
1959 let expected = parse_pkg_version(env!("CARGO_PKG_VERSION"));
1960 assert_eq!((mj, mn, pt), expected);
1961 }
1962
1963 #[test]
1964 fn get_version_tolerates_null_buffers() {
1965 unsafe {
1967 GetIpoptVersion(
1968 std::ptr::null_mut(),
1969 std::ptr::null_mut(),
1970 std::ptr::null_mut(),
1971 )
1972 };
1973 }
1974
1975 #[test]
1976 fn set_scaling_stores_user_supplied_arrays() {
1977 let p = create_unconstrained();
1978 let xs = [2.0, 3.0, 4.0, 5.0];
1979 let ok = unsafe { SetIpoptProblemScaling(p, 7.0, xs.as_ptr(), std::ptr::null()) };
1980 assert_eq!(ok, TRUE);
1981 let info = unsafe { &*p };
1982 let s = info.user_scaling.as_ref().unwrap();
1983 assert_eq!(s.obj_scaling, 7.0);
1984 assert_eq!(s.x_scaling.as_deref(), Some(&xs[..]));
1985 assert!(s.g_scaling.is_none());
1986 unsafe { FreeIpoptProblem(p) };
1987 }
1988
1989 #[test]
1990 fn set_scaling_on_null_problem_returns_false() {
1991 let ok = unsafe {
1992 SetIpoptProblemScaling(
1993 std::ptr::null_mut(),
1994 1.0,
1995 std::ptr::null(),
1996 std::ptr::null(),
1997 )
1998 };
1999 assert_eq!(ok, FALSE);
2000 }
2001
2002 #[test]
2003 fn open_output_file_writes_and_attaches_journal() {
2004 let p = create_unconstrained();
2005 let dir = std::env::temp_dir().join("pounce-cinterface-test");
2006 let _ = std::fs::create_dir_all(&dir);
2007 let path = dir.join("output.log");
2008 let cstr = CString::new(path.to_string_lossy().as_bytes()).unwrap();
2009 let ok = unsafe { OpenIpoptOutputFile(p, cstr.as_ptr(), 5) };
2010 assert_eq!(ok, TRUE);
2011 let info = unsafe { &*p };
2013 let (level, found) = info
2014 .app
2015 .options()
2016 .get_integer_value("file_print_level", "")
2017 .unwrap();
2018 assert!(found);
2019 assert_eq!(level, 5);
2020 unsafe { FreeIpoptProblem(p) };
2021 let _ = std::fs::remove_file(&path);
2022 }
2023
2024 #[test]
2025 fn open_output_file_with_null_inputs_returns_false() {
2026 let key = CString::new("nope").unwrap();
2027 unsafe {
2028 assert_eq!(
2029 OpenIpoptOutputFile(std::ptr::null_mut(), key.as_ptr(), 0),
2030 FALSE
2031 );
2032 }
2033 let p = create_unconstrained();
2034 unsafe {
2035 assert_eq!(OpenIpoptOutputFile(p, std::ptr::null(), 0), FALSE);
2036 FreeIpoptProblem(p);
2037 }
2038 }
2039
2040 #[test]
2041 fn get_current_iterate_returns_false_outside_callback() {
2042 let p = create_unconstrained();
2043 let rc = unsafe {
2044 GetIpoptCurrentIterate(
2045 p,
2046 FALSE,
2047 0,
2048 std::ptr::null_mut(),
2049 std::ptr::null_mut(),
2050 std::ptr::null_mut(),
2051 0,
2052 std::ptr::null_mut(),
2053 std::ptr::null_mut(),
2054 )
2055 };
2056 assert_eq!(rc, FALSE);
2057 unsafe { FreeIpoptProblem(p) };
2058 }
2059
2060 #[test]
2061 fn get_current_violations_returns_false_outside_callback() {
2062 let p = create_unconstrained();
2063 let rc = unsafe {
2064 GetIpoptCurrentViolations(
2065 p,
2066 FALSE,
2067 0,
2068 std::ptr::null_mut(),
2069 std::ptr::null_mut(),
2070 std::ptr::null_mut(),
2071 std::ptr::null_mut(),
2072 std::ptr::null_mut(),
2073 0,
2074 std::ptr::null_mut(),
2075 std::ptr::null_mut(),
2076 )
2077 };
2078 assert_eq!(rc, FALSE);
2079 unsafe { FreeIpoptProblem(p) };
2080 }
2081
2082 #[test]
2083 fn post_solve_stats_zero_before_solve() {
2084 let p = create_unconstrained();
2085 unsafe {
2086 assert_eq!(GetIpoptIterCount(p), 0);
2087 assert_eq!(GetIpoptSolveTime(p), 0.0);
2088 assert_eq!(GetIpoptPrimalInf(p), 0.0);
2089 assert_eq!(GetIpoptDualInf(p), 0.0);
2090 assert_eq!(GetIpoptComplInf(p), 0.0);
2091 FreeIpoptProblem(p);
2092 }
2093 }
2094
2095 #[test]
2096 fn post_solve_stats_populated_after_solve() {
2097 let xl = [-1.0e20];
2099 let xu = [1.0e20];
2100 let p = unsafe {
2101 CreateIpoptProblem(
2102 1,
2103 xl.as_ptr(),
2104 xu.as_ptr(),
2105 0,
2106 std::ptr::null(),
2107 std::ptr::null(),
2108 0,
2109 1,
2110 0,
2111 Some(quad_eval_f),
2112 None,
2113 Some(quad_eval_grad_f),
2114 None,
2115 Some(quad_eval_h),
2116 )
2117 };
2118 let mut x = [0.0_f64];
2119 let mut obj = 0.0_f64;
2120 let rc = unsafe {
2121 IpoptSolve(
2122 p,
2123 x.as_mut_ptr(),
2124 std::ptr::null_mut(),
2125 &mut obj,
2126 std::ptr::null_mut(),
2127 std::ptr::null_mut(),
2128 std::ptr::null_mut(),
2129 std::ptr::null_mut(),
2130 )
2131 };
2132 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2133 unsafe {
2136 assert!(GetIpoptIterCount(p) >= 0);
2137 assert!(GetIpoptSolveTime(p) >= 0.0);
2138 assert!(GetIpoptPrimalInf(p).is_finite());
2139 assert!(GetIpoptDualInf(p).is_finite());
2140 assert!(GetIpoptComplInf(p).is_finite());
2141 FreeIpoptProblem(p);
2142 }
2143 }
2144
2145 #[test]
2146 fn write_solve_report_emits_v1_json_with_iter_history() {
2147 let xl = [-1.0e20];
2150 let xu = [1.0e20];
2151 let p = unsafe {
2152 CreateIpoptProblem(
2153 1,
2154 xl.as_ptr(),
2155 xu.as_ptr(),
2156 0,
2157 std::ptr::null(),
2158 std::ptr::null(),
2159 0,
2160 1,
2161 0,
2162 Some(quad_eval_f),
2163 None,
2164 Some(quad_eval_grad_f),
2165 None,
2166 Some(quad_eval_h),
2167 )
2168 };
2169
2170 let cpath = CString::new("/tmp/pounce_cinterface_no_solve.json").unwrap();
2172 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), std::ptr::null()) };
2173 assert_eq!(bad, FALSE);
2174
2175 assert_eq!(unsafe { IpoptEnableIterHistory(p) }, TRUE);
2177 let mut x = [0.0_f64];
2178 let mut obj = 0.0_f64;
2179 let rc = unsafe {
2180 IpoptSolve(
2181 p,
2182 x.as_mut_ptr(),
2183 std::ptr::null_mut(),
2184 &mut obj,
2185 std::ptr::null_mut(),
2186 std::ptr::null_mut(),
2187 std::ptr::null_mut(),
2188 std::ptr::null_mut(),
2189 )
2190 };
2191 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2192
2193 let dir = std::env::temp_dir();
2194 let path = dir.join("pounce_cinterface_report.json");
2195 let cpath = CString::new(path.to_str().unwrap()).unwrap();
2196 let cdetail = CString::new("full").unwrap();
2197 let ok = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), cdetail.as_ptr()) };
2198 assert_eq!(ok, TRUE);
2199
2200 let txt = std::fs::read_to_string(&path).unwrap();
2203 assert!(
2204 txt.contains("\"schema\": \"pounce.solve-report/v1\""),
2205 "{txt}"
2206 );
2207 assert!(txt.contains("\"kind\": \"tnlp-direct\""));
2208 let parsed: pounce_solve_report::SolveReport = serde_json::from_str(&txt).unwrap();
2209 assert_eq!(parsed.problem.n_variables, 1);
2210 assert_eq!(parsed.problem.n_constraints, 0);
2211
2212 let bad_detail = CString::new("verbose").unwrap();
2214 let bad = unsafe { IpoptWriteSolveReport(p, cpath.as_ptr(), bad_detail.as_ptr()) };
2215 assert_eq!(bad, FALSE);
2216
2217 let _ = std::fs::remove_file(&path);
2218 unsafe { FreeIpoptProblem(p) };
2219 }
2220
2221 unsafe extern "C" fn cb_quad_eval_g(
2228 _n: Index,
2229 x: *const Number,
2230 _new_x: Bool,
2231 _m: Index,
2232 g: *mut Number,
2233 _user_data: *mut c_void,
2234 ) -> Bool {
2235 *g.offset(0) = *x.offset(0);
2236 TRUE
2237 }
2238 unsafe extern "C" fn cb_quad_eval_jac_g(
2239 _n: Index,
2240 _x: *const Number,
2241 _new_x: Bool,
2242 _m: Index,
2243 nele_jac: Index,
2244 irow: *mut Index,
2245 jcol: *mut Index,
2246 values: *mut Number,
2247 _user_data: *mut c_void,
2248 ) -> Bool {
2249 assert_eq!(nele_jac, 1);
2250 if !irow.is_null() {
2251 *irow.offset(0) = 0;
2252 *jcol.offset(0) = 0;
2253 }
2254 if !values.is_null() {
2255 *values.offset(0) = 1.0;
2256 }
2257 TRUE
2258 }
2259 unsafe extern "C" fn cb_quad_eval_h(
2260 _n: Index,
2261 _x: *const Number,
2262 _new_x: Bool,
2263 obj_factor: Number,
2264 _m: Index,
2265 _lambda: *const Number,
2266 _new_lambda: Bool,
2267 _nele_hess: Index,
2268 irow: *mut Index,
2269 jcol: *mut Index,
2270 values: *mut Number,
2271 _user_data: *mut c_void,
2272 ) -> Bool {
2273 if !irow.is_null() {
2274 *irow.offset(0) = 0;
2275 *jcol.offset(0) = 0;
2276 }
2277 if !values.is_null() {
2278 *values.offset(0) = 2.0 * obj_factor;
2279 }
2280 TRUE
2281 }
2282
2283 fn create_callback_test_problem() -> IpoptProblem {
2284 let xl = [-1.0e20];
2286 let xu = [1.0e20];
2287 let gl = [-10.0];
2288 let gu = [10.0];
2289 unsafe {
2290 CreateIpoptProblem(
2291 1,
2292 xl.as_ptr(),
2293 xu.as_ptr(),
2294 1,
2295 gl.as_ptr(),
2296 gu.as_ptr(),
2297 1,
2298 1,
2299 0,
2300 Some(quad_eval_f),
2301 Some(cb_quad_eval_g),
2302 Some(quad_eval_grad_f),
2303 Some(cb_quad_eval_jac_g),
2304 Some(cb_quad_eval_h),
2305 )
2306 }
2307 }
2308
2309 static CB_ITER_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
2310 static CB_LAST_ITER: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
2311 static CB_INSPECTOR_OK: std::sync::atomic::AtomicBool =
2312 std::sync::atomic::AtomicBool::new(false);
2313
2314 unsafe extern "C" fn counting_cb(
2315 _alg_mod: Index,
2316 iter_count: Index,
2317 _obj_value: Number,
2318 _inf_pr: Number,
2319 _inf_du: Number,
2320 _mu: Number,
2321 _d_norm: Number,
2322 _regularization_size: Number,
2323 _alpha_du: Number,
2324 _alpha_pr: Number,
2325 _ls_trials: Index,
2326 user_data: *mut c_void,
2327 ) -> Bool {
2328 CB_ITER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2329 CB_LAST_ITER.store(iter_count, std::sync::atomic::Ordering::SeqCst);
2330 let problem = user_data as IpoptProblem;
2333 let mut x = [0.0_f64];
2334 let rc = GetIpoptCurrentIterate(
2335 problem,
2336 FALSE,
2337 1,
2338 x.as_mut_ptr(),
2339 std::ptr::null_mut(),
2340 std::ptr::null_mut(),
2341 1,
2342 std::ptr::null_mut(),
2343 std::ptr::null_mut(),
2344 );
2345 if rc == TRUE && x[0].is_finite() {
2346 CB_INSPECTOR_OK.store(true, std::sync::atomic::Ordering::SeqCst);
2347 }
2348 TRUE
2349 }
2350
2351 #[test]
2352 fn intermediate_callback_fires_per_iteration_and_inspector_reads_x() {
2353 CB_ITER_COUNTER.store(0, std::sync::atomic::Ordering::SeqCst);
2354 CB_LAST_ITER.store(-1, std::sync::atomic::Ordering::SeqCst);
2355 CB_INSPECTOR_OK.store(false, std::sync::atomic::Ordering::SeqCst);
2356
2357 let p = create_callback_test_problem();
2358 assert!(!p.is_null());
2359 let ok = unsafe { SetIntermediateCallback(p, Some(counting_cb)) };
2360 assert_eq!(ok, TRUE);
2361 let mut x = [0.0_f64];
2362 let mut obj = 0.0_f64;
2363 let rc = unsafe {
2364 IpoptSolve(
2365 p,
2366 x.as_mut_ptr(),
2367 std::ptr::null_mut(),
2368 &mut obj,
2369 std::ptr::null_mut(),
2370 std::ptr::null_mut(),
2371 std::ptr::null_mut(),
2372 p as *mut c_void,
2373 )
2374 };
2375 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2376 let n_fires = CB_ITER_COUNTER.load(std::sync::atomic::Ordering::SeqCst);
2378 assert!(n_fires >= 2, "callback fired {n_fires} times, want >=2");
2379 assert!(
2380 CB_LAST_ITER.load(std::sync::atomic::Ordering::SeqCst) >= 1,
2381 "last iter should be >= 1 after at least one accepted step"
2382 );
2383 assert!(
2384 CB_INSPECTOR_OK.load(std::sync::atomic::Ordering::SeqCst),
2385 "GetIpoptCurrentIterate did not return a usable x"
2386 );
2387 unsafe { FreeIpoptProblem(p) };
2388 }
2389
2390 unsafe extern "C" fn user_stop_cb(
2391 _alg_mod: Index,
2392 _iter_count: Index,
2393 _obj_value: Number,
2394 _inf_pr: Number,
2395 _inf_du: Number,
2396 _mu: Number,
2397 _d_norm: Number,
2398 _regularization_size: Number,
2399 _alpha_du: Number,
2400 _alpha_pr: Number,
2401 _ls_trials: Index,
2402 _user_data: *mut c_void,
2403 ) -> Bool {
2404 FALSE
2405 }
2406
2407 #[test]
2408 fn intermediate_callback_false_surfaces_user_requested_stop() {
2409 let p = create_callback_test_problem();
2410 assert!(!p.is_null());
2411 let ok = unsafe { SetIntermediateCallback(p, Some(user_stop_cb)) };
2412 assert_eq!(ok, TRUE);
2413 let mut x = [0.0_f64];
2414 let rc = unsafe {
2415 IpoptSolve(
2416 p,
2417 x.as_mut_ptr(),
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 std::ptr::null_mut(),
2424 )
2425 };
2426 assert_eq!(rc, ApplicationReturnStatus::UserRequestedStop as Index);
2427 unsafe { FreeIpoptProblem(p) };
2428 }
2429
2430 #[test]
2431 fn parse_pkg_version_handles_missing_components() {
2432 assert_eq!(parse_pkg_version("1.2.3"), (1, 2, 3));
2433 assert_eq!(parse_pkg_version("4.5"), (4, 5, 0));
2434 assert_eq!(parse_pkg_version(""), (0, 0, 0));
2435 assert_eq!(parse_pkg_version("1.x.3"), (1, 0, 3));
2436 }
2437
2438 use crate::solver::{
2441 IpoptCreateSolver, IpoptFreeSolver, IpoptSolverGetKktDim, IpoptSolverKktSolve,
2442 IpoptSolverSolve,
2443 };
2444
2445 #[test]
2446 fn solver_create_consumes_problem_handle() {
2447 let mut p = create_unconstrained();
2448 assert!(!p.is_null());
2449 let s = unsafe { IpoptCreateSolver(&mut p) };
2450 assert!(!s.is_null());
2451 assert!(
2452 p.is_null(),
2453 "IpoptCreateSolver should NULL out the caller's handle"
2454 );
2455 unsafe { IpoptFreeSolver(s) };
2456 }
2457
2458 #[test]
2459 fn solver_create_null_inputs_return_null() {
2460 let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2462 assert!(s.is_null());
2463 let mut p: IpoptProblem = std::ptr::null_mut();
2465 let s = unsafe { IpoptCreateSolver(&mut p) };
2466 assert!(s.is_null());
2467 }
2468
2469 #[test]
2470 fn solver_free_null_is_safe() {
2471 unsafe { IpoptFreeSolver(std::ptr::null_mut()) };
2472 }
2473
2474 #[test]
2475 fn solver_solve_drives_quadratic_and_retains_factor() {
2476 let xl = [-1.0e20];
2477 let xu = [1.0e20];
2478 let mut p = unsafe {
2479 CreateIpoptProblem(
2480 1,
2481 xl.as_ptr(),
2482 xu.as_ptr(),
2483 0,
2484 std::ptr::null(),
2485 std::ptr::null(),
2486 0,
2487 1,
2488 0,
2489 Some(quad_eval_f),
2490 None,
2491 Some(quad_eval_grad_f),
2492 None,
2493 Some(quad_eval_h),
2494 )
2495 };
2496 assert!(!p.is_null());
2497 let s = unsafe { IpoptCreateSolver(&mut p) };
2498 assert!(!s.is_null());
2499 let mut x = [0.0_f64];
2500 let mut obj = 0.0_f64;
2501 let rc = unsafe {
2502 IpoptSolverSolve(
2503 s,
2504 x.as_mut_ptr(),
2505 std::ptr::null_mut(),
2506 &mut obj,
2507 std::ptr::null_mut(),
2508 std::ptr::null_mut(),
2509 std::ptr::null_mut(),
2510 std::ptr::null_mut(),
2511 )
2512 };
2513 assert_eq!(rc, ApplicationReturnStatus::SolveSucceeded as Index);
2514 assert!((x[0] - 2.0).abs() < 1e-6);
2515 assert!(obj.abs() < 1e-10);
2516
2517 let dim = unsafe { IpoptSolverGetKktDim(s) };
2520 assert!(dim > 0, "expected positive KKT dim, got {dim}");
2521 let rhs = vec![0.0_f64; dim as usize];
2522 let mut lhs = vec![1.0_f64; dim as usize];
2523 let ok = unsafe { IpoptSolverKktSolve(s, rhs.as_ptr(), lhs.as_mut_ptr()) };
2524 assert_eq!(ok, TRUE);
2525 for (i, v) in lhs.iter().enumerate() {
2526 assert!(v.abs() < 1e-10, "lhs[{i}] = {v} not ~0");
2527 }
2528 unsafe { IpoptFreeSolver(s) };
2529 }
2530
2531 #[test]
2532 fn solver_kkt_dim_minus_one_before_solve() {
2533 let mut p = create_unconstrained();
2534 let s = unsafe { IpoptCreateSolver(&mut p) };
2535 assert_eq!(unsafe { IpoptSolverGetKktDim(s) }, -1);
2536 unsafe { IpoptFreeSolver(s) };
2537 }
2538
2539 #[test]
2544 fn c_get_working_set_returns_false_before_any_solve() {
2545 let p = create_unconstrained();
2546 let mut bound_buf = [0; 4];
2547 let rc = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), std::ptr::null_mut()) };
2548 assert_eq!(rc, FALSE);
2549 unsafe { FreeIpoptProblem(p) };
2550 }
2551
2552 #[test]
2553 fn c_set_warm_start_with_both_null_returns_false() {
2554 let p = create_unconstrained();
2555 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, std::ptr::null(), std::ptr::null()) };
2556 assert_eq!(rc, FALSE);
2557 unsafe { FreeIpoptProblem(p) };
2558 }
2559
2560 #[test]
2561 fn c_set_warm_start_with_bad_status_code_returns_false() {
2562 let p = create_unconstrained();
2563 let bogus = [
2565 POUNCE_WS_INACTIVE,
2566 7,
2567 POUNCE_WS_AT_LOWER,
2568 POUNCE_WS_INACTIVE,
2569 ];
2570 let rc = unsafe { IpoptSetWarmStartWorkingSet(p, bogus.as_ptr(), std::ptr::null()) };
2571 assert_eq!(rc, FALSE);
2572 unsafe { FreeIpoptProblem(p) };
2573 }
2574
2575 #[test]
2576 fn c_set_warm_start_then_clear_succeeds() {
2577 let p = create_unconstrained();
2578 let in_buf = [POUNCE_WS_INACTIVE; 4];
2579 let set_rc = unsafe { IpoptSetWarmStartWorkingSet(p, in_buf.as_ptr(), std::ptr::null()) };
2580 assert_eq!(set_rc, TRUE);
2581 let clr_rc = unsafe { IpoptClearWarmStartWorkingSet(p) };
2582 assert_eq!(clr_rc, TRUE);
2583 unsafe { FreeIpoptProblem(p) };
2584 }
2585
2586 #[test]
2587 fn c_set_warm_start_on_null_problem_returns_false() {
2588 let in_buf = [POUNCE_WS_INACTIVE; 1];
2589 let rc = unsafe {
2590 IpoptSetWarmStartWorkingSet(std::ptr::null_mut(), in_buf.as_ptr(), std::ptr::null())
2591 };
2592 assert_eq!(rc, FALSE);
2593 }
2594
2595 #[test]
2596 fn c_solve_warm_start_round_trips_working_set_on_sqp_path() {
2597 let p = create_callback_test_problem();
2603 let key = CString::new("algorithm").unwrap();
2604 let val = CString::new("active-set-sqp").unwrap();
2605 let ok = unsafe { AddIpoptStrOption(p, key.as_ptr(), val.as_ptr()) };
2606 assert_eq!(ok, TRUE);
2607
2608 let mut x = [0.0_f64];
2609 let mut obj = 0.0_f64;
2610 let rc1 = unsafe {
2611 IpoptSolve(
2612 p,
2613 x.as_mut_ptr(),
2614 std::ptr::null_mut(),
2615 &mut obj,
2616 std::ptr::null_mut(),
2617 std::ptr::null_mut(),
2618 std::ptr::null_mut(),
2619 std::ptr::null_mut(),
2620 )
2621 };
2622 assert_eq!(rc1, ApplicationReturnStatus::SolveSucceeded as Index);
2623
2624 let mut bound_buf = [-1; 1];
2625 let mut cons_buf = [-1; 1];
2626 let got = unsafe { IpoptGetWorkingSet(p, bound_buf.as_mut_ptr(), cons_buf.as_mut_ptr()) };
2627 assert_eq!(got, TRUE);
2628 assert!((0..=3).contains(&bound_buf[0]));
2630 assert!((0..=3).contains(&cons_buf[0]));
2631
2632 x[0] = 0.0;
2637 let mut obj2 = 0.0_f64;
2638 let mut bound_out = [-1; 1];
2639 let mut cons_out = [-1; 1];
2640 let rc2 = unsafe {
2641 IpoptSolveWarmStart(
2642 p,
2643 x.as_mut_ptr(),
2644 std::ptr::null_mut(),
2645 &mut obj2,
2646 std::ptr::null_mut(),
2647 std::ptr::null_mut(),
2648 std::ptr::null_mut(),
2649 bound_buf.as_ptr(),
2650 cons_buf.as_ptr(),
2651 bound_out.as_mut_ptr(),
2652 cons_out.as_mut_ptr(),
2653 std::ptr::null_mut(),
2654 )
2655 };
2656 assert_eq!(rc2, ApplicationReturnStatus::SolveSucceeded as Index);
2657 assert!((0..=3).contains(&bound_out[0]));
2658 assert!((0..=3).contains(&cons_out[0]));
2659
2660 unsafe { FreeIpoptProblem(p) };
2661 }
2662}