Skip to main content

pounce_cinterface/
fortran.rs

1//! Fortran 77 ABI shim — port of `Interfaces/IpStdFInterface.c`.
2//!
3//! Exposes the gfortran-style `ip<name>_` symbols that the upstream
4//! Fortran example programs (`examples/hs071_f`) call. Each function
5//! receives all its arguments as pointers (the F77 ABI), translates
6//! to the C entry points in [`crate`], and translates back to the
7//! Fortran-side `OKRetVal = 0 / NotOKRetVal = 1` convention.
8//!
9//! Trailing `_` matches gfortran / clang-flang's `F77_FUNC` mangling
10//! when no underscores appear in the original name. Names with
11//! embedded underscores would need `__`; none of the names exposed
12//! here have any.
13//!
14//! Strings come in as `(char*, len_in_int)` pairs at the *end* of the
15//! call (clang-flang / gfortran convention) — Fortran callers must
16//! pass the lengths in the order the symbol declares. We accept the
17//! length as an extra trailing `c_int` per string argument and copy
18//! the buffer with trailing-space stripping ([`f2cstr`]).
19
20use crate::{
21    AddIpoptIntOption, AddIpoptNumOption, AddIpoptStrOption, CreateIpoptProblem, Eval_F_CB,
22    Eval_G_CB, Eval_Grad_F_CB, Eval_H_CB, Eval_Jac_G_CB, FreeIpoptProblem, Index, Intermediate_CB,
23    IpoptProblem, IpoptSolve, Number, SetIntermediateCallback,
24};
25use std::ffi::{c_char, c_int, c_void};
26
27/// Fortran-side OK status (matches `IpStdFInterface.c::OKRetVal`).
28const OK: Index = 0;
29/// Fortran-side error status (matches `IpStdFInterface.c::NotOKRetVal`).
30const NOT_OK: Index = 1;
31
32/// Holds the Fortran user-data table (`IDAT`, `DDAT` integer/double
33/// scratch buffers, plus the user's Fortran callbacks). This is what
34/// `IpStdFInterface.c::FUserData` carries; we keep an opaque
35/// `Box<FortranUserData>` that the C callbacks dereference.
36struct FortranUserData {
37    idat: *mut Index,
38    ddat: *mut Number,
39    eval_f: FEval_F_CB,
40    eval_g: Option<FEval_G_CB>,
41    eval_grad_f: FEval_Grad_F_CB,
42    eval_jac_g: Option<FEval_Jac_G_CB>,
43    eval_hess: Option<FEval_Hess_CB>,
44    intermediate_cb: Option<FIntermediate_CB>,
45    problem: IpoptProblem,
46}
47
48// Fortran callback function-pointer types. Each argument is by
49// reference, matching `IpStdFInterface.c`.
50
51pub type FEval_F_CB = unsafe extern "C" fn(
52    n: *const Index,
53    x: *mut Number,
54    new_x: *const Index,
55    obj_value: *mut Number,
56    idat: *mut Index,
57    ddat: *mut Number,
58    ierr: *mut Index,
59);
60
61pub type FEval_G_CB = unsafe extern "C" fn(
62    n: *const Index,
63    x: *mut Number,
64    new_x: *const Index,
65    m: *const Index,
66    g: *mut Number,
67    idat: *mut Index,
68    ddat: *mut Number,
69    ierr: *mut Index,
70);
71
72pub type FEval_Grad_F_CB = unsafe extern "C" fn(
73    n: *const Index,
74    x: *mut Number,
75    new_x: *const Index,
76    grad_f: *mut Number,
77    idat: *mut Index,
78    ddat: *mut Number,
79    ierr: *mut Index,
80);
81
82pub type FEval_Jac_G_CB = unsafe extern "C" fn(
83    task: *const Index,
84    n: *const Index,
85    x: *mut Number,
86    new_x: *const Index,
87    m: *const Index,
88    nnz_jac: *const Index,
89    irow: *mut Index,
90    jcol: *mut Index,
91    values: *mut Number,
92    idat: *mut Index,
93    ddat: *mut Number,
94    ierr: *mut Index,
95);
96
97pub type FEval_Hess_CB = unsafe extern "C" fn(
98    task: *const Index,
99    n: *const Index,
100    x: *mut Number,
101    new_x: *const Index,
102    obj_factor: *const Number,
103    m: *const Index,
104    lambda: *mut Number,
105    new_lambda: *const Index,
106    nnz_hess: *const Index,
107    irow: *mut Index,
108    jcol: *mut Index,
109    values: *mut Number,
110    idat: *mut Index,
111    ddat: *mut Number,
112    ierr: *mut Index,
113);
114
115pub type FIntermediate_CB = unsafe extern "C" fn(
116    alg_mode: *const Index,
117    iter_count: *const Index,
118    obj_value: *const Number,
119    inf_pr: *const Number,
120    inf_du: *const Number,
121    mu: *const Number,
122    d_norm: *const Number,
123    regu_size: *const Number,
124    alpha_du: *const Number,
125    alpha_pr: *const Number,
126    ls_trial: *const Index,
127    idat: *mut Index,
128    ddat: *mut Number,
129    istop: *mut Index,
130);
131
132// ------------------------------------------------------------------
133// C-side trampolines: these implement the C interface's `Eval_F_CB`
134// etc. and unpack the Fortran user_data to call back into Fortran.
135// ------------------------------------------------------------------
136
137unsafe extern "C" fn c_eval_f(
138    n: Index,
139    x: *const Number,
140    new_x: c_int,
141    obj_value: *mut Number,
142    user_data: *mut c_void,
143) -> c_int {
144    let fud = &mut *(user_data as *mut FortranUserData);
145    let mut ierr: Index = 0;
146    let n_local = n;
147    let new_x_i: Index = new_x as Index;
148    (fud.eval_f)(
149        &n_local,
150        x as *mut Number,
151        &new_x_i,
152        obj_value,
153        fud.idat,
154        fud.ddat,
155        &mut ierr,
156    );
157    if ierr == OK {
158        1
159    } else {
160        0
161    }
162}
163
164unsafe extern "C" fn c_eval_grad_f(
165    n: Index,
166    x: *const Number,
167    new_x: c_int,
168    grad_f: *mut Number,
169    user_data: *mut c_void,
170) -> c_int {
171    let fud = &mut *(user_data as *mut FortranUserData);
172    let mut ierr: Index = 0;
173    let n_local = n;
174    let new_x_i: Index = new_x as Index;
175    (fud.eval_grad_f)(
176        &n_local,
177        x as *mut Number,
178        &new_x_i,
179        grad_f,
180        fud.idat,
181        fud.ddat,
182        &mut ierr,
183    );
184    if ierr == OK {
185        1
186    } else {
187        0
188    }
189}
190
191unsafe extern "C" fn c_eval_g(
192    n: Index,
193    x: *const Number,
194    new_x: c_int,
195    m: Index,
196    g: *mut Number,
197    user_data: *mut c_void,
198) -> c_int {
199    let fud = &mut *(user_data as *mut FortranUserData);
200    let Some(cb) = fud.eval_g else {
201        return 0;
202    };
203    let mut ierr: Index = 0;
204    let n_local = n;
205    let m_local = m;
206    let new_x_i: Index = new_x as Index;
207    cb(
208        &n_local,
209        x as *mut Number,
210        &new_x_i,
211        &m_local,
212        g,
213        fud.idat,
214        fud.ddat,
215        &mut ierr,
216    );
217    if ierr == OK {
218        1
219    } else {
220        0
221    }
222}
223
224unsafe extern "C" fn c_eval_jac_g(
225    n: Index,
226    x: *const Number,
227    new_x: c_int,
228    m: Index,
229    nele_jac: Index,
230    irow: *mut Index,
231    jcol: *mut Index,
232    values: *mut Number,
233    user_data: *mut c_void,
234) -> c_int {
235    let fud = &mut *(user_data as *mut FortranUserData);
236    let Some(cb) = fud.eval_jac_g else {
237        return 0;
238    };
239    let task: Index = if !irow.is_null() && !jcol.is_null() && values.is_null() {
240        0
241    } else if irow.is_null() && jcol.is_null() && !values.is_null() {
242        1
243    } else {
244        return 0;
245    };
246    let mut ierr: Index = 0;
247    let n_local = n;
248    let m_local = m;
249    let nele_local = nele_jac;
250    let new_x_i: Index = new_x as Index;
251    cb(
252        &task,
253        &n_local,
254        x as *mut Number,
255        &new_x_i,
256        &m_local,
257        &nele_local,
258        irow,
259        jcol,
260        values,
261        fud.idat,
262        fud.ddat,
263        &mut ierr,
264    );
265    if ierr == OK {
266        1
267    } else {
268        0
269    }
270}
271
272#[allow(clippy::too_many_arguments)]
273unsafe extern "C" fn c_eval_h(
274    n: Index,
275    x: *const Number,
276    new_x: c_int,
277    obj_factor: Number,
278    m: Index,
279    lambda: *const Number,
280    new_lambda: c_int,
281    nele_hess: Index,
282    irow: *mut Index,
283    jcol: *mut Index,
284    values: *mut Number,
285    user_data: *mut c_void,
286) -> c_int {
287    let fud = &mut *(user_data as *mut FortranUserData);
288    let Some(cb) = fud.eval_hess else {
289        return 0;
290    };
291    let task: Index = if !irow.is_null() && !jcol.is_null() && values.is_null() {
292        0
293    } else if irow.is_null() && jcol.is_null() && !values.is_null() {
294        1
295    } else {
296        return 0;
297    };
298    let mut ierr: Index = 0;
299    let n_local = n;
300    let m_local = m;
301    let nele_local = nele_hess;
302    let new_x_i: Index = new_x as Index;
303    let new_lam_i: Index = new_lambda as Index;
304    cb(
305        &task,
306        &n_local,
307        x as *mut Number,
308        &new_x_i,
309        &obj_factor,
310        &m_local,
311        lambda as *mut Number,
312        &new_lam_i,
313        &nele_local,
314        irow,
315        jcol,
316        values,
317        fud.idat,
318        fud.ddat,
319        &mut ierr,
320    );
321    if ierr == OK {
322        1
323    } else {
324        0
325    }
326}
327
328#[allow(clippy::too_many_arguments)]
329unsafe extern "C" fn c_intermediate(
330    alg_mod: Index,
331    iter_count: Index,
332    obj_value: Number,
333    inf_pr: Number,
334    inf_du: Number,
335    mu: Number,
336    d_norm: Number,
337    regu_size: Number,
338    alpha_du: Number,
339    alpha_pr: Number,
340    ls_trials: Index,
341    user_data: *mut c_void,
342) -> c_int {
343    let fud = &mut *(user_data as *mut FortranUserData);
344    let Some(cb) = fud.intermediate_cb else {
345        return 1;
346    };
347    let mut istop: Index = 0;
348    cb(
349        &alg_mod,
350        &iter_count,
351        &obj_value,
352        &inf_pr,
353        &inf_du,
354        &mu,
355        &d_norm,
356        &regu_size,
357        &alpha_du,
358        &alpha_pr,
359        &ls_trials,
360        fud.idat,
361        fud.ddat,
362        &mut istop,
363    );
364    if istop == OK {
365        1
366    } else {
367        0
368    }
369}
370
371// ------------------------------------------------------------------
372// String marshalling. F77 passes (char*, len) where the buffer is
373// blank-padded to `len`; we strip trailing spaces and NUL-terminate.
374// ------------------------------------------------------------------
375
376fn f2cstr(buf: *const c_char, slen: c_int) -> Vec<u8> {
377    if buf.is_null() || slen <= 0 {
378        return vec![0];
379    }
380    // SAFETY: caller asserts (buf, slen) are a valid Fortran character
381    // slice of length `slen` bytes.
382    let bytes = unsafe { std::slice::from_raw_parts(buf as *const u8, slen as usize) };
383    let mut end = bytes.len();
384    while end > 0 && bytes[end - 1] == b' ' {
385        end -= 1;
386    }
387    let mut v = Vec::with_capacity(end + 1);
388    v.extend_from_slice(&bytes[..end]);
389    v.push(0);
390    v
391}
392
393// ------------------------------------------------------------------
394// Fortran entry points (gfortran trailing-underscore names).
395// ------------------------------------------------------------------
396
397/// `ipcreate_(N, X_L, X_U, M, G_L, G_U, NELE_JAC, NELE_HESS, IDX_STY,
398///            EVAL_F, EVAL_G, EVAL_GRAD_F, EVAL_JAC_G, EVAL_HESS) -> fptr`
399///
400/// Returns an opaque handle (a Box<FortranUserData>) cast to a
401/// pointer; pass back to [`ipfree_`] / [`ipsolve_`].
402///
403/// # Safety
404/// All pointer arguments must be valid for the lifetime of the
405/// returned handle. Bound arrays must hold `*N` / `*M` doubles.
406#[allow(clippy::too_many_arguments)]
407#[no_mangle]
408pub unsafe extern "C" fn ipcreate_(
409    n: *const Index,
410    x_l: *const Number,
411    x_u: *const Number,
412    m: *const Index,
413    g_l: *const Number,
414    g_u: *const Number,
415    nele_jac: *const Index,
416    nele_hess: *const Index,
417    idx_sty: *const Index,
418    eval_f: FEval_F_CB,
419    eval_g: Option<FEval_G_CB>,
420    eval_grad_f: FEval_Grad_F_CB,
421    eval_jac_g: Option<FEval_Jac_G_CB>,
422    eval_hess: Option<FEval_Hess_CB>,
423) -> *mut c_void {
424    let problem = CreateIpoptProblem(
425        *n,
426        x_l,
427        x_u,
428        *m,
429        g_l,
430        g_u,
431        *nele_jac,
432        *nele_hess,
433        *idx_sty,
434        Some(c_eval_f),
435        Some(c_eval_g),
436        Some(c_eval_grad_f),
437        Some(c_eval_jac_g),
438        Some(c_eval_h),
439    );
440    if problem.is_null() {
441        return std::ptr::null_mut();
442    }
443    let fud = Box::new(FortranUserData {
444        idat: std::ptr::null_mut(),
445        ddat: std::ptr::null_mut(),
446        eval_f,
447        eval_g,
448        eval_grad_f,
449        eval_jac_g,
450        eval_hess,
451        intermediate_cb: None,
452        problem,
453    });
454    Box::into_raw(fud) as *mut c_void
455}
456
457/// `ipfree_(FProblem)` — frees the handle and zeroes the user's
458/// pointer slot, mirroring `IpStdFInterface.c::F77_FUNC(ipfree)`.
459///
460/// # Safety
461/// `fproblem` must point to a slot that holds a handle previously
462/// returned by [`ipcreate_`], or NULL.
463#[no_mangle]
464pub unsafe extern "C" fn ipfree_(fproblem: *mut *mut c_void) {
465    if fproblem.is_null() || (*fproblem).is_null() {
466        return;
467    }
468    let raw = *fproblem as *mut FortranUserData;
469    let fud = Box::from_raw(raw);
470    FreeIpoptProblem(fud.problem);
471    drop(fud);
472    *fproblem = std::ptr::null_mut();
473}
474
475/// `ipsolve_(FProblem, X, G, OBJ_VAL, MULT_G, MULT_X_L, MULT_X_U, IDAT, DDAT) -> Index`.
476///
477/// # Safety
478/// All pointer arguments must satisfy the contracts documented on
479/// [`crate::IpoptSolve`]. `idat`/`ddat` are scratch arrays passed
480/// back to the user's Fortran callbacks.
481#[allow(clippy::too_many_arguments)]
482#[no_mangle]
483pub unsafe extern "C" fn ipsolve_(
484    fproblem: *mut *mut c_void,
485    x: *mut Number,
486    g: *mut Number,
487    obj_val: *mut Number,
488    mult_g: *mut Number,
489    mult_x_l: *mut Number,
490    mult_x_u: *mut Number,
491    idat: *mut Index,
492    ddat: *mut Number,
493) -> Index {
494    if fproblem.is_null() || (*fproblem).is_null() {
495        return -199;
496    }
497    let fud = &mut *(*fproblem as *mut FortranUserData);
498    fud.idat = idat;
499    fud.ddat = ddat;
500    let fud_ptr = (*fproblem) as *mut c_void;
501    IpoptSolve(
502        fud.problem,
503        x,
504        g,
505        obj_val,
506        mult_g,
507        mult_x_l,
508        mult_x_u,
509        fud_ptr,
510    )
511}
512
513/// `ipaddstroption_(FProblem, KEYWORD, VALUE, klen, vlen) -> Index`.
514/// Returns 0 (`OKRetVal`) on success, 1 on failure.
515///
516/// # Safety
517/// `fproblem` must be valid; the `(KEYWORD, klen)` and `(VALUE, vlen)`
518/// pairs must describe valid Fortran character slices.
519#[no_mangle]
520pub unsafe extern "C" fn ipaddstroption_(
521    fproblem: *mut *mut c_void,
522    keyword: *const c_char,
523    value: *const c_char,
524    klen: c_int,
525    vlen: c_int,
526) -> Index {
527    if fproblem.is_null() || (*fproblem).is_null() {
528        return NOT_OK;
529    }
530    let fud = &mut *(*fproblem as *mut FortranUserData);
531    let k = f2cstr(keyword, klen);
532    let v = f2cstr(value, vlen);
533    let ok = AddIpoptStrOption(
534        fud.problem,
535        k.as_ptr() as *const c_char,
536        v.as_ptr() as *const c_char,
537    );
538    if ok != 0 {
539        OK
540    } else {
541        NOT_OK
542    }
543}
544
545/// `ipaddnumoption_(FProblem, KEYWORD, VALUE, klen) -> Index`.
546///
547/// # Safety
548/// `fproblem` must be valid; the `(KEYWORD, klen)` pair must describe
549/// a valid Fortran character slice.
550#[no_mangle]
551pub unsafe extern "C" fn ipaddnumoption_(
552    fproblem: *mut *mut c_void,
553    keyword: *const c_char,
554    value: *const Number,
555    klen: c_int,
556) -> Index {
557    if fproblem.is_null() || (*fproblem).is_null() {
558        return NOT_OK;
559    }
560    let fud = &mut *(*fproblem as *mut FortranUserData);
561    let k = f2cstr(keyword, klen);
562    let ok = AddIpoptNumOption(fud.problem, k.as_ptr() as *const c_char, *value);
563    if ok != 0 {
564        OK
565    } else {
566        NOT_OK
567    }
568}
569
570/// `ipaddintoption_(FProblem, KEYWORD, VALUE, klen) -> Index`.
571///
572/// # Safety
573/// `fproblem` must be valid; `(KEYWORD, klen)` must describe a valid
574/// Fortran character slice.
575#[no_mangle]
576pub unsafe extern "C" fn ipaddintoption_(
577    fproblem: *mut *mut c_void,
578    keyword: *const c_char,
579    value: *const Index,
580    klen: c_int,
581) -> Index {
582    if fproblem.is_null() || (*fproblem).is_null() {
583        return NOT_OK;
584    }
585    let fud = &mut *(*fproblem as *mut FortranUserData);
586    let k = f2cstr(keyword, klen);
587    let ok = AddIpoptIntOption(fud.problem, k.as_ptr() as *const c_char, *value);
588    if ok != 0 {
589        OK
590    } else {
591        NOT_OK
592    }
593}
594
595/// `ipsetcallback_(FProblem, INTER_CB)` — install a Fortran-side
596/// intermediate callback.
597///
598/// # Safety
599/// `fproblem` must be valid; `inter_cb` must be a valid Fortran
600/// callback for the lifetime of the problem.
601#[no_mangle]
602pub unsafe extern "C" fn ipsetcallback_(fproblem: *mut *mut c_void, inter_cb: FIntermediate_CB) {
603    if fproblem.is_null() || (*fproblem).is_null() {
604        return;
605    }
606    let fud = &mut *(*fproblem as *mut FortranUserData);
607    fud.intermediate_cb = Some(inter_cb);
608    let _: Index = SetIntermediateCallback(fud.problem, Some(c_intermediate as Intermediate_CB));
609}
610
611/// `ipunsetcallback_(FProblem)` — remove the intermediate callback.
612///
613/// # Safety
614/// `fproblem` must be valid.
615#[no_mangle]
616pub unsafe extern "C" fn ipunsetcallback_(fproblem: *mut *mut c_void) {
617    if fproblem.is_null() || (*fproblem).is_null() {
618        return;
619    }
620    let fud = &mut *(*fproblem as *mut FortranUserData);
621    fud.intermediate_cb = None;
622    let _: Index = SetIntermediateCallback(fud.problem, None);
623}
624
625// Suppress unused import warning when the C ABI types aren't visible
626// to dead-code analysis (they're referenced through public type
627// aliases above).
628const _: Eval_F_CB = c_eval_f;
629const _: Eval_Grad_F_CB = c_eval_grad_f;
630const _: Eval_G_CB = c_eval_g;
631const _: Eval_Jac_G_CB = c_eval_jac_g;
632const _: Eval_H_CB = c_eval_h;
633const _: Intermediate_CB = c_intermediate;
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    #[test]
640    fn f2cstr_strips_trailing_spaces() {
641        let buf = b"hello    ";
642        let v = f2cstr(buf.as_ptr() as *const c_char, buf.len() as c_int);
643        assert_eq!(&v[..], b"hello\0");
644    }
645
646    #[test]
647    fn f2cstr_handles_null_buf() {
648        let v = f2cstr(std::ptr::null(), 5);
649        assert_eq!(&v[..], &[0]);
650    }
651
652    #[test]
653    fn f2cstr_keeps_embedded_spaces() {
654        let buf = b"a b c    ";
655        let v = f2cstr(buf.as_ptr() as *const c_char, buf.len() as c_int);
656        assert_eq!(&v[..], b"a b c\0");
657    }
658
659    /// Drive a 1-D unconstrained quadratic through the Fortran ABI
660    /// path: f(x) = (x - 3)^2.
661    unsafe extern "C" fn fquad_eval_f(
662        _n: *const Index,
663        x: *mut Number,
664        _new_x: *const Index,
665        obj: *mut Number,
666        _idat: *mut Index,
667        _ddat: *mut Number,
668        ierr: *mut Index,
669    ) {
670        let v = *x.offset(0);
671        *obj = (v - 3.0) * (v - 3.0);
672        *ierr = OK;
673    }
674    unsafe extern "C" fn fquad_eval_grad_f(
675        _n: *const Index,
676        x: *mut Number,
677        _new_x: *const Index,
678        grad: *mut Number,
679        _idat: *mut Index,
680        _ddat: *mut Number,
681        ierr: *mut Index,
682    ) {
683        let v = *x.offset(0);
684        *grad.offset(0) = 2.0 * (v - 3.0);
685        *ierr = OK;
686    }
687    unsafe extern "C" fn fquad_eval_hess(
688        task: *const Index,
689        _n: *const Index,
690        _x: *mut Number,
691        _new_x: *const Index,
692        obj_factor: *const Number,
693        _m: *const Index,
694        _lambda: *mut Number,
695        _new_lambda: *const Index,
696        _nnz_hess: *const Index,
697        irow: *mut Index,
698        jcol: *mut Index,
699        values: *mut Number,
700        _idat: *mut Index,
701        _ddat: *mut Number,
702        ierr: *mut Index,
703    ) {
704        if *task == 0 {
705            *irow.offset(0) = 0;
706            *jcol.offset(0) = 0;
707        } else {
708            *values.offset(0) = 2.0 * *obj_factor;
709        }
710        *ierr = OK;
711    }
712
713    #[test]
714    fn fortran_ipsolve_drives_quadratic() {
715        let n: Index = 1;
716        let m: Index = 0;
717        let nele_jac: Index = 0;
718        let nele_hess: Index = 1;
719        let idx_sty: Index = 0;
720        let xl = [-1.0e20];
721        let xu = [1.0e20];
722
723        let mut fp: *mut c_void = unsafe {
724            ipcreate_(
725                &n,
726                xl.as_ptr(),
727                xu.as_ptr(),
728                &m,
729                std::ptr::null(),
730                std::ptr::null(),
731                &nele_jac,
732                &nele_hess,
733                &idx_sty,
734                fquad_eval_f,
735                None,
736                fquad_eval_grad_f,
737                None,
738                Some(fquad_eval_hess),
739            )
740        };
741        assert!(!fp.is_null());
742
743        let mut x = [0.0_f64];
744        let mut obj: Number = 0.0;
745        let mut idat = [0_i32; 1];
746        let mut ddat = [0.0_f64; 1];
747        let rc = unsafe {
748            ipsolve_(
749                &mut fp,
750                x.as_mut_ptr(),
751                std::ptr::null_mut(),
752                &mut obj,
753                std::ptr::null_mut(),
754                std::ptr::null_mut(),
755                std::ptr::null_mut(),
756                idat.as_mut_ptr(),
757                ddat.as_mut_ptr(),
758            )
759        };
760        assert_eq!(rc, 0); // Solve_Succeeded
761        assert!((x[0] - 3.0).abs() < 1e-6, "x[0] = {}", x[0]);
762        unsafe { ipfree_(&mut fp) };
763        assert!(fp.is_null());
764    }
765}