Skip to main content

pounce_cinterface/
lib.rs

1//! POUNCE C ABI — port of `Interfaces/IpStdCInterface.{h,cpp}`.
2//!
3//! Provides the `CreateIpoptProblem / IpoptSolve / FreeIpoptProblem` C
4//! entry points that existing PyIpopt / cyipopt / JuMP wrappers link
5//! against. Function names and signatures match upstream Ipopt 3.14.x
6//! exactly so consumers can swap `libipopt.{dylib,so}` for
7//! `libpounce_cinterface` without rebuilding.
8//!
9//! Surface area (in `IpStdCInterface.h` order):
10//!
11//! * Lifecycle: [`CreateIpoptProblem`], [`FreeIpoptProblem`].
12//! * Options: [`AddIpoptStrOption`], [`AddIpoptNumOption`],
13//!   [`AddIpoptIntOption`], [`OpenIpoptOutputFile`],
14//!   [`SetIpoptProblemScaling`].
15//! * Callbacks: [`SetIntermediateCallback`].
16//! * Solve: [`IpoptSolve`].
17//! * Introspection (only valid inside an intermediate callback):
18//!   [`GetIpoptCurrentIterate`], [`GetIpoptCurrentViolations`].
19//! * Library info: [`GetIpoptVersion`].
20//!
21//! Pounce extensions for post-solve stats (not present in upstream
22//! Ipopt's C API): [`GetIpoptIterCount`], [`GetIpoptSolveTime`],
23//! [`GetIpoptPrimalInf`], [`GetIpoptDualInf`], [`GetIpoptComplInf`].
24//!
25//! All entry points are `extern "C"` and `#[no_mangle]`. Pointers are
26//! raw and the caller is responsible for lifetime; the `IpoptProblem`
27//! handle is opaque (`*mut c_void` from C's perspective). The Fortran
28//! 77 ABI shim lives in [`fortran`].
29
30#![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
55/// Mirrors C `Number` typedef in `IpStdCInterface.h`.
56pub type Number = f64;
57/// Mirrors C `Index`.
58pub type Index = c_int;
59/// Mirrors C `Bool`.
60pub type Bool = c_int;
61
62const TRUE: Bool = 1;
63const FALSE: Bool = 0;
64
65/// C-ABI encoding of [`pounce_qp::BoundStatus`] (§7.2 of the
66/// active-set-SQP design note). Stable values:
67/// `0 = Inactive`, `1 = AtLower`, `2 = AtUpper`, `3 = Fixed`.
68pub type IpoptBoundStatus = c_int;
69/// C-ABI encoding of [`pounce_qp::ConsStatus`] (§7.2). Stable values:
70/// `0 = Inactive`, `1 = AtLower`, `2 = AtUpper`, `3 = Equality`.
71pub 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
78/// Internal owned state behind the opaque `IpoptProblem` handle.
79/// `#[repr(C)]` is unnecessary because C only sees the pointer.
80pub 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    /// User-provided scaling installed by [`SetIpoptProblemScaling`].
98    /// `obj_scaling` defaults to `1.0`. `x_scaling`/`g_scaling` are
99    /// `None` when the user passed NULL.
100    pub(crate) user_scaling: Option<UserScaling>,
101    /// Final iterate and stats from the most recent [`IpoptSolve`].
102    /// Used by `GetIpopt{IterCount,SolveTime,...}` accessors. Reset
103    /// (cleared) by the next `IpoptSolve` call.
104    pub(crate) last_solve: Option<LastSolve>,
105}
106
107/// User-provided NLP scaling stored on the problem until
108/// [`IpoptSolve`] copies it into the [`CCallbackTnlp`] bridge.
109#[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/// Stats and final-iterate snapshot retained between
117/// [`IpoptSolve`] and the post-solve accessors. Everything needed to
118/// reconstruct a `pounce.solve-report/v1` JSON file lives here so
119/// [`IpoptWriteSolveReport`] doesn't have to ask the caller to thread
120/// `x`/`lambda`/`obj` back in.
121#[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
146// User-callback function pointer types — match
147// `IpStdCInterface.h:Eval_F_CB` etc. byte for byte.
148
149pub 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/// Port of `IpStdCInterface.cpp:CreateIpoptProblem`. Returns NULL on
217/// invalid arguments (negative n/m, missing required callbacks, NULL
218/// bound pointers when the corresponding dimension is positive).
219///
220/// # Safety
221///
222/// `x_L`, `x_U` must be valid pointers to `n` `Number`s when `n > 0`.
223/// `g_L`, `g_U` must be valid pointers to `m` `Number`s when `m > 0`.
224/// The callback function pointers must be valid for the lifetime of
225/// the returned [`IpoptProblem`].
226#[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    // Install the tracing subscriber on first use so C consumers
244    // (cyipopt, AMPL, …) get logging and the iteration collector that
245    // backs `IpoptEnableIterHistory` (pounce#71). Idempotent.
246    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/// Port of `IpStdCInterface.cpp:FreeIpoptProblem`.
312///
313/// # Safety
314///
315/// `ipopt_problem` must be a pointer previously returned by
316/// [`CreateIpoptProblem`] and not yet freed, or NULL.
317#[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/// Port of `IpStdCInterface.cpp:AddIpoptStrOption`.
333///
334/// # Safety
335///
336/// `ipopt_problem` must be a valid `IpoptProblem`. `keyword` and `val`
337/// must be valid NUL-terminated strings.
338#[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/// Port of `AddIpoptNumOption`.
364///
365/// # Safety
366///
367/// `keyword` must be a valid NUL-terminated string and
368/// `ipopt_problem` must be a valid `IpoptProblem`.
369#[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/// Port of `AddIpoptIntOption`.
393///
394/// # Safety
395///
396/// `keyword` must be a valid NUL-terminated string and
397/// `ipopt_problem` must be a valid `IpoptProblem`.
398#[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/// Port of `IpStdCInterface.cpp:OpenIpoptOutputFile`. Opens `file_name`
423/// at `print_level` and attaches a journalist `FileJournal` so all
424/// solver output is mirrored to disk. Equivalent to setting
425/// `output_file` + `file_print_level` options and triggering
426/// `IpoptApplication::Initialize`.
427///
428/// Returns `TRUE` (1) on success, `FALSE` (0) if the file could not
429/// be opened or the option store rejected the value.
430///
431/// # Safety
432///
433/// `ipopt_problem` must be a valid `IpoptProblem`. `file_name` must
434/// be a valid NUL-terminated string.
435#[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/// Port of `IpStdCInterface.cpp:SetIpoptProblemScaling`. Stores
456/// user-provided NLP scaling on the problem; the scaling is forwarded
457/// to the solver via [`TNLP::get_scaling_parameters`] when the option
458/// `nlp_scaling_method=user-scaling` is set. Passing NULL for
459/// `x_scaling` / `g_scaling` disables scaling on that axis.
460///
461/// Always returns `TRUE`.
462///
463/// # Safety
464///
465/// `ipopt_problem` must be a valid `IpoptProblem`. When non-NULL,
466/// `x_scaling` must point to `n` doubles and `g_scaling` to `m`
467/// doubles; both arrays are copied internally.
468#[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/// Port of `IpStdCInterface.cpp:IpoptSolve`. Returns the
500/// `ApplicationReturnStatus` integer.
501///
502/// Builds a [`CCallbackTnlp`] from the user-supplied callback table
503/// and bounds, runs it through [`IpoptApplication::optimize_tnlp`],
504/// and writes back the final iterate.
505///
506/// # Safety
507///
508/// All pointer arguments are read/written per the
509/// `IpStdCInterface.h` contract: `x` is in/out (size `n`); `g`,
510/// `mult_g`, `mult_x_L`, `mult_x_U` are out-only (sizes `m, m, n, n`)
511/// and may be NULL when the corresponding output is not desired.
512#[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    // Wire the restoration phase fresh for this solve. Without it, any
572    // line-search failure surfaces as `RestorationFailure` instead of
573    // falling back into the ℓ1-feasibility sub-IPM — exactly what the
574    // CLI driver does. Re-wire per `IpoptSolve` to stay correct across
575    // repeated solves on the same `IpoptProblem`. The feral config is
576    // snapshot from the now-fully-populated options so `feral_*`
577    // overrides flow into the restoration sub-IPM too. Use the multi-pass
578    // provider so the ℓ₁ wrapper / auto-fallback don't panic on the
579    // second inner solve (pounce#10 Phase 3 / pounce#24).
580    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/// Port of `SetIntermediateCallback`.
624///
625/// # Safety
626///
627/// `ipopt_problem` must be valid.
628#[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/// Port of `IpStdCInterface.cpp:GetIpoptCurrentIterate` (Ipopt 3.14+).
642/// Designed to be called from inside an intermediate callback to
643/// inspect `x`, the bound multipliers `z_L/z_U`, the constraint values
644/// `g`, and the constraint multipliers `lambda` at the current
645/// iterate.
646///
647/// All output buffers are optional — pass NULL to skip. `n` and `m`
648/// must match the dimensions the problem was created with; mismatched
649/// sizes cause the function to return `FALSE` without writing.
650///
651/// `scaled` is currently ignored — quantities are reported in the
652/// user TNLP's unscaled space (matching upstream Ipopt's default
653/// caller behavior when scaling is unused). Honoring `scaled` for the
654/// `gradient-based` scaler is a follow-up.
655///
656/// Returns `FALSE` when called outside an active intermediate
657/// callback (no live iterate to inspect).
658///
659/// # Safety
660///
661/// `ipopt_problem` must be a valid `IpoptProblem`. Each output buffer,
662/// when non-NULL, must hold at least the declared length.
663#[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/// Port of `IpStdCInterface.cpp:GetIpoptCurrentViolations` (Ipopt 3.14+).
737/// Same contract as [`GetIpoptCurrentIterate`]; returns `FALSE` when
738/// called outside an active intermediate callback.
739///
740/// `scaled` is currently ignored — see [`GetIpoptCurrentIterate`].
741/// Violations and complementarities are reported in the compressed
742/// algorithm-side space scattered out to full-`n`/`m`; this is the
743/// shape upstream callers consume (zero-fill for free positions /
744/// no-bound positions).
745///
746/// # Safety
747///
748/// `ipopt_problem` must be a valid `IpoptProblem`. Each output buffer,
749/// when non-NULL, must hold at least the declared length.
750#[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        // x_L / x_U violations: scatter the compressed slack-shortfalls
783        // up to full-`n`. Upstream defines `x_L_violation_i = max(0, x_L_i
784        // - x_i)`; the algorithm tracks `slack_x_l = P_L^T x - x_L`
785        // (always non-negative at feasible iterates), so reverse the
786        // sign and clamp.
787        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            // pack_z_l_for_user scatters by the same x_L mapping; the
792            // returned vector at full-x positions holds `slack_x_l[i]`
793            // which is `x_i - x_L_i`. Clamp the *negative* part to get
794            // the violation `max(0, x_L_i - x_i)`.
795            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            // Scatter compressed x-var → full-x via lift_x_to_full
826            // (treats `glx` as if it were an x-vector). Fixed-variable
827            // slots remain zero.
828            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            // Per-row equality and range violation reconstruction in
836            // full-g coordinates is a follow-up. The scalar
837            // `curr_primal_infeasibility_max` (== `inf_pr` reported in
838            // `IterStats`) is the outer summary; populate per-row
839            // detail as a future refinement and zero-fill for now.
840            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            // Per-row constraint complementarity (`v_L .* s_L` /
845            // `v_U .* s_U` mapped back to full-g) is also a follow-up.
846            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/// Port of `IpStdCInterface.cpp:GetIpoptVersion` (Ipopt 3.14.18+).
859/// Writes the pounce crate's `major.minor.patch` into the buffers.
860/// Any pointer may be NULL to skip that component.
861///
862/// # Safety
863///
864/// Each non-NULL pointer must point at a writable `int`.
865#[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    // Read from Cargo at compile time so the symbol always matches the
872    // shipped binary. `unwrap_or(0)` keeps the function infallible if a
873    // component is missing from the manifest (shouldn't happen in
874    // practice — workspace manifest requires SemVer triples).
875    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// ----------------------------------------------------------------------
897// Pounce extensions: post-solve statistics accessors.
898//
899// Convenience accessors not present in upstream Ipopt's C API. Valid
900// only after [`IpoptSolve`] has returned; calling them on a
901// never-solved problem yields zero. They expose the same
902// `SolveStatistics` data the Rust API surfaces via
903// [`IpoptApplication::statistics`].
904// ----------------------------------------------------------------------
905
906/// Number of IPM iterations in the most recent solve, or `0` if the
907/// problem has not been solved yet.
908///
909/// # Safety
910///
911/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
912#[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/// Wall-clock solve time in seconds for the most recent solve, or
918/// `0.0` if the problem has not been solved yet.
919///
920/// # Safety
921///
922/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
923#[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/// Final primal infeasibility (max constraint violation) for the most
929/// recent solve.
930///
931/// # Safety
932///
933/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
934#[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/// Final dual infeasibility (max gradient-of-Lagrangian norm) for the
940/// most recent solve.
941///
942/// # Safety
943///
944/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
945#[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/// Final complementarity error for the most recent solve.
951///
952/// # Safety
953///
954/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
955#[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
970// ─────────────────────────────────────────────────────────────
971// Pounce extension: SQP working-set warm-start C ABI (§7.2 of
972// `docs/research/active-set-sqp-warm-start.md`).
973//
974// Three new entry points; all backward-compatible additions.
975// No existing signature changes — existing cyipopt / JuMP /
976// AMPL clients are unaffected.
977// ─────────────────────────────────────────────────────────────
978
979fn 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/// Retrieve the working set produced by the most recent SQP solve
1022/// (`algorithm = active-set-sqp`). Buffer sizes are `n` for
1023/// `bound_status_out` and `m` for `cons_status_out`. Pass `NULL`
1024/// for either to skip that side.
1025///
1026/// Returns `TRUE` (1) on success, `FALSE` (0) if there is no
1027/// working set to retrieve (e.g. no SQP solve has run, the IPM
1028/// path was used, or the very first KKT check declared
1029/// optimality before solving any QP).
1030///
1031/// # Safety
1032///
1033/// `ipopt_problem` must be a valid `IpoptProblem`. Output
1034/// buffers (when non-NULL) must be sized at least `n` and `m`
1035/// respectively.
1036#[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/// Supply a warm-start working set consumed by the next
1064/// [`IpoptSolve`] on this problem. Pass `NULL` for either side to
1065/// cold-start it. The caller-owned buffers are copied; reuse
1066/// across calls is safe.
1067///
1068/// Returns `TRUE` on success, `FALSE` on (a) NULL problem, (b)
1069/// an out-of-range status code in one of the buffers, or
1070/// (c) both inputs NULL (which would equal a no-op
1071/// — call [`IpoptClearWarmStartWorkingSet`] instead).
1072///
1073/// # Safety
1074///
1075/// `ipopt_problem` must be valid. `bound_status_in` (when
1076/// non-NULL) must be sized `n`; `cons_status_in` (when non-NULL)
1077/// must be sized `m`.
1078#[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    // We do *not* know the primal/dual iterate here — the caller
1114    // either left them at default zeros (cold) or already wrote
1115    // them into `x` before calling `IpoptSolve`. We seed
1116    // `SqpIterates` with zeros; `IpoptSolve` will use its `x`
1117    // argument as the starting point (the SqpProblemSpec adapter
1118    // wraps `IpoptNlp::get_starting_x`, which the C path
1119    // initializes from the user-supplied `x` buffer).
1120    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/// Drop any pending warm-start working set without solving. The
1134/// next [`IpoptSolve`] will cold-start.
1135///
1136/// # Safety
1137///
1138/// `ipopt_problem` must be a valid `IpoptProblem` or NULL.
1139#[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/// Convenience one-shot: equivalent to
1149/// `IpoptSetWarmStartWorkingSet` + `IpoptSolve` +
1150/// `IpoptGetWorkingSet` in sequence. The input/output working-set
1151/// buffers are independent (so a caller can read back the new
1152/// working set into the same array used as input). Pass `NULL`
1153/// for any in/out buffer to skip that side.
1154///
1155/// Returns the `ApplicationReturnStatus` integer, identical to
1156/// [`IpoptSolve`].
1157///
1158/// # Safety
1159///
1160/// All pointer arguments follow the same contract as
1161/// `IpoptSolve` plus the working-set buffer sizes documented on
1162/// `IpoptSetWarmStartWorkingSet` / `IpoptGetWorkingSet`.
1163#[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    // Best-effort set. Errors here (e.g. bad status code) are
1183    // silently treated as cold-start; the caller can probe via
1184    // `IpoptSetWarmStartWorkingSet` directly if they need to
1185    // validate the input.
1186    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
1203/// Adapter that bridges the user-supplied C callback table to the
1204/// in-crate [`TNLP`] trait. Mirrors `Interfaces/IpStdInterfaceTNLP.cpp`
1205/// (`StdInterfaceTNLP`); each TNLP method forwards to the matching
1206/// `Eval_*_CB` and propagates `false` returns up so the algorithm
1207/// layer can map them to `Invalid_Number_Detected`.
1208///
1209/// Holds a snapshot of bounds and the initial `x`. After `optimize_tnlp`
1210/// finishes, `finalize_solution` is called by the algorithm layer; the
1211/// adapter records the final iterate in `final_*` fields, which the
1212/// outer [`IpoptSolve`] copies back into the caller's buffers.
1213pub(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    /// User-installed intermediate callback, copied at solve time so the
1231    /// TNLP-trait `intermediate_callback` impl can forward through to it.
1232    pub(crate) intermediate_cb: Option<Intermediate_CB>,
1233    /// Snapshot of user-provided scaling captured at solve time.
1234    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/// Enable per-iteration history capture on the underlying
1510/// `IpoptApplication`. Must be called *before* [`IpoptSolve`] for the
1511/// trajectory to appear in the report written by
1512/// [`IpoptWriteSolveReport`]. Off by default — capturing each iterate
1513/// has a small per-iter cost the IPM core skips otherwise.
1514///
1515/// Returns `TRUE` on success, `FALSE` if `ipopt_problem` is NULL.
1516///
1517/// # Safety
1518///
1519/// `ipopt_problem` must be a valid handle returned by
1520/// [`CreateIpoptProblem`] (or `NULL`).
1521#[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/// Write a `pounce.solve-report/v1` JSON file capturing the most
1532/// recent [`IpoptSolve`] result. `path` is a NUL-terminated UTF-8
1533/// filesystem path. `detail` is one of `"summary"` or `"full"`
1534/// (NUL-terminated); pass `NULL` for the default (`"summary"`).
1535///
1536/// When `detail = "full"` and [`IpoptEnableIterHistory`] was called
1537/// pre-solve, the per-iteration trajectory is embedded so that
1538/// downstream tools (`diagnose`, `find_stalls`, `convergence_trace`)
1539/// see the same trace the `pounce` CLI's `--json-output` path
1540/// produces. The input descriptor is recorded as `tnlp-direct`
1541/// because the cinterface receives callbacks rather than a file.
1542///
1543/// Returns `TRUE` on a successful write, `FALSE` for NULL handle,
1544/// no prior solve, an invalid `detail`, a bad path, or an I/O error.
1545///
1546/// # Safety
1547///
1548/// `ipopt_problem` must be a valid handle; `path` must be a valid
1549/// NUL-terminated UTF-8 string; `detail` must be NULL or a valid
1550/// NUL-terminated UTF-8 string.
1551#[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, // missing eval_f
1680                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, // valid values are 0 and 1
1727                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    // ---- End-to-end bridge: 1-D unconstrained quadratic ----
1834    //
1835    // f(x) = (x - 2)^2, no bounds, no constraints. Newton driver
1836    // converges in one step.
1837
1838    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        // Bounds wide open (kappa1 push won't move us off 0.0 since
1888        // |0| < 1e19, but the Newton step lands us at 2.0 anyway).
1889        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(), // x null but n > 0
1937                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    // ---- New entry points (issue #19) ----
1953
1954    #[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        // None of these should crash.
1965        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        // Option should be reflected in the app.
2011        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        // Reuse the same quadratic as the end-to-end solve test.
2097        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        // After a successful solve, iter count is recorded (>= 0) and
2133        // wall time is non-negative; primal/dual/compl norms exist.
2134        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        // Quadratic — Newton driver, single iter; just exercises the
2147        // post-solve report path end-to-end.
2148        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        // Write before any solve must fail.
2170        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        // Enable per-iter capture, solve, then write at detail = full.
2175        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        // Read it back and check the schema tag + that it parses with
2200        // the same struct shape pounce-cli uses.
2201        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        // Invalid detail string is rejected.
2212        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    // --- Intermediate-callback wiring (issue #19, follow-up) ---
2221    //
2222    // The callback only fires on the IPM path (`optimize_constrained`).
2223    // Unconstrained problems short-circuit through the Newton driver,
2224    // so these tests use a single-inequality problem to force the IPM.
2225
2226    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        // min (x - 2)^2  s.t.  -10 <= x <= 10 (single inequality).
2284        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        // user_data carries the IpoptProblem so we can exercise the
2330        // inspector from inside the callback.
2331        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        // At least the iter-0 fire happened, plus one per accepted step.
2376        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    // ---- Solver-session C ABI (crate::solver) ----
2438
2439    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        // NULL pointer-to-handle.
2460        let s = unsafe { IpoptCreateSolver(std::ptr::null_mut()) };
2461        assert!(s.is_null());
2462        // Pointer to a NULL handle.
2463        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        // After convergence the factor is retained — kkt_dim is positive
2517        // and a zero RHS back-solves to zero.
2518        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    // ─────────────────────────────────────────────────────────
2539    // §7.2 SQP working-set warm-start C ABI tests.
2540    // ─────────────────────────────────────────────────────────
2541
2542    #[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        // Length n = 4; '7' is out of range (valid: 0..=3).
2563        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        // Use the 1-D `(x − 2)²` quadratic from
2597        // `create_callback_test_problem`. Set `algorithm
2598        // active-set-sqp`, solve, then read the working set
2599        // through `IpoptGetWorkingSet`. Pass it back via
2600        // `IpoptSolveWarmStart` for a second solve.
2601        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        // Status codes must be in 0..=3.
2628        assert!((0..=3).contains(&bound_buf[0]));
2629        assert!((0..=3).contains(&cons_buf[0]));
2630
2631        // Second solve with the just-retrieved working set as
2632        // input. Resets x to a non-optimal starting point so the
2633        // SQP loop actually has work to do; the warm-start
2634        // should still converge to the optimum.
2635        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}