Skip to main content

diffsol_c/
ode_c.rs

1#[cfg(any(
2    feature = "diffsl-external-dynamic",
3    feature = "diffsl-cranelift",
4    feature = "diffsl-llvm",
5))]
6use std::ffi::CStr;
7#[cfg(any(
8    feature = "diffsl-external-dynamic",
9    feature = "diffsl-cranelift",
10    feature = "diffsl-llvm",
11))]
12use std::os::raw::c_char;
13#[cfg(feature = "diffsl-external-dynamic")]
14use std::path::PathBuf;
15use std::ptr;
16
17use crate::c_api_utils::{valid_f64_ptr, DIFFSOL_BAD_ARG, DIFFSOL_ERR, DIFFSOL_OK};
18use crate::host_array::HostArray;
19#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
20use crate::jit_c::jit_backend_from_i32;
21use crate::linear_solver_type_c::{linear_solver_from_i32, linear_solver_to_i32};
22use crate::matrix_type_c::matrix_type_from_i32;
23use crate::matrix_type_c::matrix_type_to_i32;
24use crate::ode::OdeWrapper;
25use crate::ode_solver_type_c::{ode_solver_from_i32, ode_solver_to_i32};
26use crate::scalar_type::ScalarType;
27use crate::solution_wrapper::SolutionWrapper;
28use crate::{c_error, c_invalid_arg};
29
30fn boxed_host_array(array: HostArray) -> *mut HostArray {
31    Box::into_raw(Box::new(array))
32}
33
34#[cfg(any(feature = "external", feature = "diffsl-external-dynamic"))]
35#[repr(C)]
36#[derive(Clone, Copy, Debug)]
37/// C-compatible dependency pair used by the FFI constructors.
38///
39/// Memory layout guarantees:
40/// - `#[repr(C)]` preserves field order exactly as declared.
41/// - The first machine word is `row`, followed by `col`.
42/// - Each field is a `usize`, so size/alignment are target-dependent
43///   (32-bit targets: 4 bytes, 64-bit targets: 8 bytes).
44/// - Arrays of `DiffsolDepPair` are contiguous in memory and can be passed as
45///   pointer/length (`*const DiffsolDepPair`, `usize`) to this API.
46pub struct DiffsolDepPair {
47    pub row: usize,
48    pub col: usize,
49}
50
51fn parse_ode_new_common_args(
52    matrix_type: i32,
53    linear_solver: i32,
54    ode_solver: i32,
55) -> Option<(
56    crate::matrix_type::MatrixType,
57    crate::linear_solver_type::LinearSolverType,
58    crate::ode_solver_type::OdeSolverType,
59)> {
60    let matrix_type = match matrix_type_from_i32(matrix_type) {
61        Some(value) => value,
62        None => {
63            c_invalid_arg!("invalid matrix_type");
64            return None;
65        }
66    };
67    let linear_solver = match linear_solver_from_i32(linear_solver) {
68        Some(value) => value,
69        None => {
70            c_invalid_arg!("invalid linear_solver");
71            return None;
72        }
73    };
74    let ode_solver = match ode_solver_from_i32(ode_solver) {
75        Some(value) => value,
76        None => {
77            c_invalid_arg!("invalid ode_solver");
78            return None;
79        }
80    };
81    Some((matrix_type, linear_solver, ode_solver))
82}
83
84#[cfg(any(feature = "external", feature = "diffsl-external-dynamic"))]
85unsafe fn dependency_pairs_from_raw_parts(
86    deps_ptr: *const DiffsolDepPair,
87    deps_len: usize,
88) -> Vec<(usize, usize)> {
89    if deps_ptr.is_null() || deps_len == 0 {
90        Vec::new()
91    } else {
92        unsafe { std::slice::from_raw_parts(deps_ptr, deps_len) }
93            .iter()
94            .map(|pair| (pair.row, pair.col))
95            .collect()
96    }
97}
98
99#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
100fn parse_ode_new_jit_args(
101    code: *const c_char,
102    matrix_type: i32,
103    linear_solver: i32,
104    ode_solver: i32,
105) -> Option<(
106    String,
107    crate::matrix_type::MatrixType,
108    crate::linear_solver_type::LinearSolverType,
109    crate::ode_solver_type::OdeSolverType,
110)> {
111    if code.is_null() {
112        c_invalid_arg!("code is null");
113        return None;
114    }
115    let code = unsafe { CStr::from_ptr(code) };
116    let code = match code.to_str() {
117        Ok(value) => value.to_owned(),
118        Err(_) => {
119            c_error!("code is not valid UTF-8");
120            return None;
121        }
122    };
123    let (matrix_type, linear_solver, ode_solver) =
124        parse_ode_new_common_args(matrix_type, linear_solver, ode_solver)?;
125    Some((code, matrix_type, linear_solver, ode_solver))
126}
127
128#[cfg(feature = "diffsl-external-dynamic")]
129fn parse_ode_new_external_dynamic_args(
130    path: *const c_char,
131    matrix_type: i32,
132    linear_solver: i32,
133    ode_solver: i32,
134) -> Option<(
135    PathBuf,
136    crate::matrix_type::MatrixType,
137    crate::linear_solver_type::LinearSolverType,
138    crate::ode_solver_type::OdeSolverType,
139)> {
140    if path.is_null() {
141        c_invalid_arg!("path is null");
142        return None;
143    }
144    let path = unsafe { CStr::from_ptr(path) };
145    let path = match path.to_str() {
146        Ok(value) => PathBuf::from(value),
147        Err(_) => {
148            c_error!("path is not valid UTF-8");
149            return None;
150        }
151    };
152    let (matrix_type, linear_solver, ode_solver) =
153        parse_ode_new_common_args(matrix_type, linear_solver, ode_solver)?;
154    Some((path, matrix_type, linear_solver, ode_solver))
155}
156
157/// Free a list of host arrays previously returned by this library.
158///
159/// # Safety
160/// `list` must be either null or a pointer returned by this library for a list
161/// of length `len`. Each pointed-to element remains owned separately.
162#[unsafe(no_mangle)]
163pub unsafe extern "C" fn diffsol_host_array_list_free(list: *mut *mut HostArray, len: usize) {
164    if list.is_null() {
165        c_invalid_arg!("host array list is null");
166        return;
167    }
168    unsafe {
169        drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(list, len)));
170    }
171}
172
173#[cfg(feature = "external")]
174/// Construct an external-backed ODE wrapper.
175///
176/// # Safety
177/// Dependency pointers must be either null with length `0` or point to valid
178/// memory containing [`DiffsolDepPair`] values for the specified lengths for the
179/// duration of this call.
180#[unsafe(no_mangle)]
181pub unsafe extern "C" fn diffsol_ode_new_external(
182    matrix_type: i32,
183    linear_solver: i32,
184    ode_solver: i32,
185    rhs_state_deps_ptr: *const DiffsolDepPair,
186    rhs_state_deps_len: usize,
187    rhs_input_deps_ptr: *const DiffsolDepPair,
188    rhs_input_deps_len: usize,
189    mass_state_deps_ptr: *const DiffsolDepPair,
190    mass_state_deps_len: usize,
191) -> *mut OdeWrapper {
192    let Some((matrix_type, linear_solver, ode_solver)) =
193        parse_ode_new_common_args(matrix_type, linear_solver, ode_solver)
194    else {
195        return ptr::null_mut();
196    };
197
198    let rhs_state_deps =
199        unsafe { dependency_pairs_from_raw_parts(rhs_state_deps_ptr, rhs_state_deps_len) };
200    let rhs_input_deps =
201        unsafe { dependency_pairs_from_raw_parts(rhs_input_deps_ptr, rhs_input_deps_len) };
202    let mass_state_deps =
203        unsafe { dependency_pairs_from_raw_parts(mass_state_deps_ptr, mass_state_deps_len) };
204
205    let scalar_type = ScalarType::F64;
206    match OdeWrapper::new_external(
207        rhs_state_deps,
208        rhs_input_deps,
209        mass_state_deps,
210        scalar_type,
211        matrix_type,
212        linear_solver,
213        ode_solver,
214    ) {
215        Ok(ode) => Box::into_raw(Box::new(ode)),
216        Err(err) => {
217            c_error!(&format!("{}", err));
218            ptr::null_mut()
219        }
220    }
221}
222
223#[cfg(feature = "diffsl-external-dynamic")]
224/// Construct a dynamic-library-backed ODE wrapper.
225///
226/// # Safety
227/// `path` must be a valid, null-terminated UTF-8 string for the duration of
228/// this call. Dependency pointers must be either null with length `0` or point
229/// to valid memory containing [`DiffsolDepPair`] values for the specified lengths
230/// for the duration of this call.
231#[unsafe(no_mangle)]
232pub unsafe extern "C" fn diffsol_ode_new_external_dynamic(
233    path: *const c_char,
234    matrix_type: i32,
235    linear_solver: i32,
236    ode_solver: i32,
237    rhs_state_deps_ptr: *const DiffsolDepPair,
238    rhs_state_deps_len: usize,
239    rhs_input_deps_ptr: *const DiffsolDepPair,
240    rhs_input_deps_len: usize,
241    mass_state_deps_ptr: *const DiffsolDepPair,
242    mass_state_deps_len: usize,
243) -> *mut OdeWrapper {
244    let Some((path, matrix_type, linear_solver, ode_solver)) =
245        parse_ode_new_external_dynamic_args(path, matrix_type, linear_solver, ode_solver)
246    else {
247        return ptr::null_mut();
248    };
249
250    let rhs_state_deps =
251        unsafe { dependency_pairs_from_raw_parts(rhs_state_deps_ptr, rhs_state_deps_len) };
252    let rhs_input_deps =
253        unsafe { dependency_pairs_from_raw_parts(rhs_input_deps_ptr, rhs_input_deps_len) };
254    let mass_state_deps =
255        unsafe { dependency_pairs_from_raw_parts(mass_state_deps_ptr, mass_state_deps_len) };
256
257    let scalar_type = ScalarType::F64;
258    match OdeWrapper::new_external_dynamic(
259        path,
260        rhs_state_deps,
261        rhs_input_deps,
262        mass_state_deps,
263        scalar_type,
264        matrix_type,
265        linear_solver,
266        ode_solver,
267    ) {
268        Ok(ode) => Box::into_raw(Box::new(ode)),
269        Err(err) => {
270            c_error!(&format!("{}", err));
271            ptr::null_mut()
272        }
273    }
274}
275
276#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
277/// Construct a JIT-backed ODE wrapper from DiffSL source code.
278///
279/// # Safety
280/// `code` must be a valid, null-terminated UTF-8 string for the duration of
281/// this call. The backend and solver enum values must be valid values defined by
282/// this library.
283#[unsafe(no_mangle)]
284pub unsafe extern "C" fn diffsol_ode_new_jit(
285    code: *const c_char,
286    jit_backend: i32,
287    matrix_type: i32,
288    linear_solver: i32,
289    ode_solver: i32,
290) -> *mut OdeWrapper {
291    let Some((code, matrix_type, linear_solver, ode_solver)) =
292        parse_ode_new_jit_args(code, matrix_type, linear_solver, ode_solver)
293    else {
294        return ptr::null_mut();
295    };
296    let jit_backend = match jit_backend_from_i32(jit_backend) {
297        Some(value) => value,
298        None => {
299            c_invalid_arg!("invalid jit_backend_type");
300            return ptr::null_mut();
301        }
302    };
303    let scalar_type = ScalarType::F64;
304    match OdeWrapper::new_jit(
305        &code,
306        jit_backend,
307        scalar_type,
308        matrix_type,
309        linear_solver,
310        ode_solver,
311    ) {
312        Ok(ode) => Box::into_raw(Box::new(ode)),
313        Err(err) => {
314            c_error!(&format!("{}", err));
315            ptr::null_mut()
316        }
317    }
318}
319
320/// Free an ODE wrapper previously returned by this library.
321///
322/// # Safety
323/// `ode` must be either null or a pointer returned by this library that has not
324/// already been freed.
325#[unsafe(no_mangle)]
326pub unsafe extern "C" fn diffsol_ode_free(ode: *mut OdeWrapper) {
327    if ode.is_null() {
328        c_invalid_arg!("ode is null");
329        return;
330    }
331    unsafe {
332        drop(Box::from_raw(ode));
333    }
334}
335
336/// Return a handle to the initial-condition solver options for an ODE.
337///
338/// # Safety
339/// `ode` must be a valid pointer created by this library. `out_options` must be
340/// a valid, writable pointer to receive ownership of the returned options
341/// object.
342#[unsafe(no_mangle)]
343pub unsafe extern "C" fn diffsol_ode_get_ic_options(
344    ode: *const OdeWrapper,
345    out_options: *mut *mut crate::initial_condition_options::InitialConditionSolverOptions,
346) -> i32 {
347    if ode.is_null() || out_options.is_null() {
348        return c_invalid_arg!("invalid arguments to diffsol_ode_get_ic_options");
349    }
350    let ode = unsafe { &*ode };
351    let options = ode.get_ic_options();
352    let boxed = Box::new(options);
353    unsafe {
354        *out_options = Box::into_raw(boxed);
355    }
356    DIFFSOL_OK
357}
358
359/// Return a handle to the ODE solver options for an ODE.
360///
361/// # Safety
362/// `ode` must be a valid pointer created by this library. `out_options` must be
363/// a valid, writable pointer to receive ownership of the returned options
364/// object.
365#[unsafe(no_mangle)]
366pub unsafe extern "C" fn diffsol_ode_get_options(
367    ode: *const OdeWrapper,
368    out_options: *mut *mut crate::ode_options::OdeSolverOptions,
369) -> i32 {
370    if ode.is_null() || out_options.is_null() {
371        return c_invalid_arg!("invalid arguments to diffsol_ode_get_options");
372    }
373    let ode = unsafe { &*ode };
374    let options = ode.get_options();
375    let boxed = Box::new(options);
376    unsafe {
377        *out_options = Box::into_raw(boxed);
378    }
379    DIFFSOL_OK
380}
381
382/// Evaluate the initial condition vector for an ODE.
383///
384/// # Safety
385/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
386/// must be either null with `params_len == 0` or point to `params_len`
387/// readable `f64` values. `out_array` must be a valid, writable pointer.
388#[unsafe(no_mangle)]
389pub unsafe extern "C" fn diffsol_ode_y0(
390    ode: *mut OdeWrapper,
391    params_ptr: *const f64,
392    params_len: usize,
393    out_array: *mut *mut HostArray,
394) -> i32 {
395    if ode.is_null() || out_array.is_null() || !valid_f64_ptr(params_ptr, params_len) {
396        c_invalid_arg!("invalid arguments to diffsol_ode_y0");
397        return DIFFSOL_BAD_ARG;
398    }
399    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
400    let ode = unsafe { &mut *ode };
401    match ode.y0(params) {
402        Ok(array) => {
403            let boxed = boxed_host_array(array);
404            unsafe {
405                *out_array = boxed;
406            }
407            DIFFSOL_OK
408        }
409        Err(err) => {
410            c_error!(&format!("{}", err));
411            DIFFSOL_ERR
412        }
413    }
414}
415
416/// Evaluate the ODE right-hand side at a given time and state.
417///
418/// # Safety
419/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
420/// and `y_ptr` must point to readable `f64` buffers of the specified lengths,
421/// unless the corresponding length is zero. `out_array` must be writable.
422#[unsafe(no_mangle)]
423pub unsafe extern "C" fn diffsol_ode_rhs(
424    ode: *mut OdeWrapper,
425    params_ptr: *const f64,
426    params_len: usize,
427    t: f64,
428    y_ptr: *const f64,
429    y_len: usize,
430    out_array: *mut *mut HostArray,
431) -> i32 {
432    if ode.is_null()
433        || out_array.is_null()
434        || !valid_f64_ptr(params_ptr, params_len)
435        || !valid_f64_ptr(y_ptr, y_len)
436    {
437        c_invalid_arg!("invalid arguments to diffsol_ode_rhs");
438        return DIFFSOL_BAD_ARG;
439    }
440    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
441    let y = HostArray::new_vector(y_ptr as *mut u8, y_len, ScalarType::F64);
442    let ode = unsafe { &mut *ode };
443    match ode.rhs(params, t, y) {
444        Ok(array) => {
445            let boxed = boxed_host_array(array);
446            unsafe {
447                *out_array = boxed;
448            }
449            DIFFSOL_OK
450        }
451        Err(err) => {
452            c_error!(&format!("{}", err));
453            DIFFSOL_ERR
454        }
455    }
456}
457
458/// Evaluate the ODE Jacobian-vector product at a given time and state.
459///
460/// # Safety
461/// `ode` must be a valid mutable pointer created by this library. `params_ptr`,
462/// `y_ptr`, and `v_ptr` must point to readable `f64` buffers of the specified
463/// lengths, unless the corresponding length is zero. `out_array` must be
464/// writable.
465#[unsafe(no_mangle)]
466pub unsafe extern "C" fn diffsol_ode_rhs_jac_mul(
467    ode: *mut OdeWrapper,
468    params_ptr: *const f64,
469    params_len: usize,
470    t: f64,
471    y_ptr: *const f64,
472    y_len: usize,
473    v_ptr: *const f64,
474    v_len: usize,
475    out_array: *mut *mut HostArray,
476) -> i32 {
477    if ode.is_null()
478        || out_array.is_null()
479        || !valid_f64_ptr(params_ptr, params_len)
480        || !valid_f64_ptr(y_ptr, y_len)
481        || !valid_f64_ptr(v_ptr, v_len)
482    {
483        c_invalid_arg!("invalid arguments to diffsol_ode_rhs_jac_mul");
484        return DIFFSOL_BAD_ARG;
485    }
486    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
487    let y = HostArray::new_vector(y_ptr as *mut u8, y_len, ScalarType::F64);
488    let v = HostArray::new_vector(v_ptr as *mut u8, v_len, ScalarType::F64);
489    let ode = unsafe { &mut *ode };
490    match ode.rhs_jac_mul(params, t, y, v) {
491        Ok(array) => {
492            let boxed = boxed_host_array(array);
493            unsafe {
494                *out_array = boxed;
495            }
496            DIFFSOL_OK
497        }
498        Err(err) => {
499            c_error!(&format!("{}", err));
500            DIFFSOL_ERR
501        }
502    }
503}
504
505/// Solve an ODE up to a final time.
506///
507/// # Safety
508/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
509/// must point to `params_len` readable `f64` values unless `params_len == 0`.
510/// `out_solution` must be a valid, writable pointer.
511#[unsafe(no_mangle)]
512pub unsafe extern "C" fn diffsol_ode_solve(
513    ode: *mut OdeWrapper,
514    params_ptr: *const f64,
515    params_len: usize,
516    final_time: f64,
517    out_solution: *mut *mut SolutionWrapper,
518) -> i32 {
519    if ode.is_null() || out_solution.is_null() || !valid_f64_ptr(params_ptr, params_len) {
520        c_invalid_arg!("invalid arguments to diffsol_ode_solve");
521        return DIFFSOL_BAD_ARG;
522    }
523    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
524    let ode = unsafe { &mut *ode };
525    match ode.solve(params, final_time) {
526        Ok(new_solution) => {
527            unsafe {
528                *out_solution = Box::into_raw(Box::new(new_solution));
529            }
530            DIFFSOL_OK
531        }
532        Err(err) => {
533            c_error!(&format!("{}", err));
534            DIFFSOL_ERR
535        }
536    }
537}
538
539/// Solve a hybrid ODE up to a final time, automatically applying resets after roots.
540///
541/// # Safety
542/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
543/// must point to `params_len` readable `f64` values unless `params_len == 0`.
544/// `out_solution` must be a valid, writable pointer.
545#[unsafe(no_mangle)]
546pub unsafe extern "C" fn diffsol_ode_solve_hybrid(
547    ode: *mut OdeWrapper,
548    params_ptr: *const f64,
549    params_len: usize,
550    final_time: f64,
551    out_solution: *mut *mut SolutionWrapper,
552) -> i32 {
553    if ode.is_null() || out_solution.is_null() || !valid_f64_ptr(params_ptr, params_len) {
554        c_invalid_arg!("invalid arguments to diffsol_ode_solve_hybrid");
555        return DIFFSOL_BAD_ARG;
556    }
557    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
558    let ode = unsafe { &mut *ode };
559    match ode.solve_hybrid(params, final_time) {
560        Ok(new_solution) => {
561            unsafe {
562                *out_solution = Box::into_raw(Box::new(new_solution));
563            }
564            DIFFSOL_OK
565        }
566        Err(err) => {
567            c_error!(&format!("{}", err));
568            DIFFSOL_ERR
569        }
570    }
571}
572
573/// Solve an ODE and sample the solution at requested times.
574///
575/// # Safety
576/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
577/// and `t_eval_ptr` must point to readable `f64` buffers of the specified
578/// lengths, unless the corresponding length is zero. `out_solution` must be writable.
579#[unsafe(no_mangle)]
580pub unsafe extern "C" fn diffsol_ode_solve_dense(
581    ode: *mut OdeWrapper,
582    params_ptr: *const f64,
583    params_len: usize,
584    t_eval_ptr: *const f64,
585    t_eval_len: usize,
586    out_solution: *mut *mut SolutionWrapper,
587) -> i32 {
588    if ode.is_null()
589        || out_solution.is_null()
590        || !valid_f64_ptr(params_ptr, params_len)
591        || !valid_f64_ptr(t_eval_ptr, t_eval_len)
592    {
593        c_invalid_arg!("invalid arguments to diffsol_ode_solve_dense");
594        return DIFFSOL_BAD_ARG;
595    }
596    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
597    let t_eval = HostArray::new_vector(t_eval_ptr as *mut u8, t_eval_len, ScalarType::F64);
598    let ode = unsafe { &mut *ode };
599    match ode.solve_dense(params, t_eval) {
600        Ok(new_solution) => {
601            unsafe {
602                *out_solution = Box::into_raw(Box::new(new_solution));
603            }
604            DIFFSOL_OK
605        }
606        Err(err) => {
607            c_error!(&format!("{}", err));
608            DIFFSOL_ERR
609        }
610    }
611}
612
613/// Solve a hybrid ODE and sample the solution at requested times.
614///
615/// # Safety
616/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
617/// and `t_eval_ptr` must point to readable `f64` buffers of the specified
618/// lengths, unless the corresponding length is zero. `out_solution` must be writable.
619#[unsafe(no_mangle)]
620pub unsafe extern "C" fn diffsol_ode_solve_hybrid_dense(
621    ode: *mut OdeWrapper,
622    params_ptr: *const f64,
623    params_len: usize,
624    t_eval_ptr: *const f64,
625    t_eval_len: usize,
626    out_solution: *mut *mut SolutionWrapper,
627) -> i32 {
628    if ode.is_null()
629        || out_solution.is_null()
630        || !valid_f64_ptr(params_ptr, params_len)
631        || !valid_f64_ptr(t_eval_ptr, t_eval_len)
632    {
633        c_invalid_arg!("invalid arguments to diffsol_ode_solve_hybrid_dense");
634        return DIFFSOL_BAD_ARG;
635    }
636    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
637    let t_eval = HostArray::new_vector(t_eval_ptr as *mut u8, t_eval_len, ScalarType::F64);
638    let ode = unsafe { &mut *ode };
639    match ode.solve_hybrid_dense(params, t_eval) {
640        Ok(new_solution) => {
641            unsafe {
642                *out_solution = Box::into_raw(Box::new(new_solution));
643            }
644            DIFFSOL_OK
645        }
646        Err(err) => {
647            c_error!(&format!("{}", err));
648            DIFFSOL_ERR
649        }
650    }
651}
652
653/// Solve an ODE and sample forward sensitivities at requested times.
654///
655/// # Safety
656/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
657/// and `t_eval_ptr` must point to readable `f64` buffers of the specified
658/// lengths, unless the corresponding length is zero. `out_solution` must be writable.
659#[unsafe(no_mangle)]
660pub unsafe extern "C" fn diffsol_ode_solve_fwd_sens(
661    ode: *mut OdeWrapper,
662    params_ptr: *const f64,
663    params_len: usize,
664    t_eval_ptr: *const f64,
665    t_eval_len: usize,
666    out_solution: *mut *mut SolutionWrapper,
667) -> i32 {
668    if ode.is_null()
669        || out_solution.is_null()
670        || !valid_f64_ptr(params_ptr, params_len)
671        || !valid_f64_ptr(t_eval_ptr, t_eval_len)
672    {
673        c_invalid_arg!("invalid arguments to diffsol_ode_solve_fwd_sens");
674        return DIFFSOL_BAD_ARG;
675    }
676    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
677    let t_eval = HostArray::new_vector(t_eval_ptr as *mut u8, t_eval_len, ScalarType::F64);
678    let ode = unsafe { &mut *ode };
679    match ode.solve_fwd_sens(params, t_eval) {
680        Ok(new_solution) => {
681            unsafe {
682                *out_solution = Box::into_raw(Box::new(new_solution));
683            }
684            DIFFSOL_OK
685        }
686        Err(err) => {
687            c_error!(&format!("{}", err));
688            DIFFSOL_ERR
689        }
690    }
691}
692
693/// Solve a hybrid ODE with forward sensitivities at requested times.
694///
695/// # Safety
696/// `ode` must be a valid mutable pointer created by this library. `params_ptr`
697/// and `t_eval_ptr` must point to readable `f64` buffers of the specified
698/// lengths, unless the corresponding length is zero. `out_solution` must be writable.
699#[unsafe(no_mangle)]
700pub unsafe extern "C" fn diffsol_ode_solve_hybrid_fwd_sens(
701    ode: *mut OdeWrapper,
702    params_ptr: *const f64,
703    params_len: usize,
704    t_eval_ptr: *const f64,
705    t_eval_len: usize,
706    out_solution: *mut *mut SolutionWrapper,
707) -> i32 {
708    if ode.is_null()
709        || out_solution.is_null()
710        || !valid_f64_ptr(params_ptr, params_len)
711        || !valid_f64_ptr(t_eval_ptr, t_eval_len)
712    {
713        c_invalid_arg!("invalid arguments to diffsol_ode_solve_hybrid_fwd_sens");
714        return DIFFSOL_BAD_ARG;
715    }
716    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
717    let t_eval = HostArray::new_vector(t_eval_ptr as *mut u8, t_eval_len, ScalarType::F64);
718    let ode = unsafe { &mut *ode };
719    match ode.solve_hybrid_fwd_sens(params, t_eval) {
720        Ok(new_solution) => {
721            unsafe {
722                *out_solution = Box::into_raw(Box::new(new_solution));
723            }
724            DIFFSOL_OK
725        }
726        Err(err) => {
727            c_error!(&format!("{}", err));
728            DIFFSOL_ERR
729        }
730    }
731}
732
733/// Solve the sum-of-squares adjoint problem for an ODE.
734///
735/// # Safety
736/// `ode` must be a valid mutable pointer created by this library. `params_ptr`,
737/// `data_ptr`, and `t_eval_ptr` must point to readable buffers matching the
738/// provided dimensions. `out_value` and `out_sens` must be valid, writable
739/// pointers.
740#[unsafe(no_mangle)]
741pub unsafe extern "C" fn diffsol_ode_solve_sum_squares_adj(
742    ode: *mut OdeWrapper,
743    params_ptr: *const f64,
744    params_len: usize,
745    data_ptr: *const f64,
746    data_rows: usize,
747    data_cols: usize,
748    data_row_stride: usize,
749    data_col_stride: usize,
750    t_eval_ptr: *const f64,
751    t_eval_len: usize,
752    out_value: *mut f64,
753    out_sens: *mut *mut HostArray,
754) -> i32 {
755    if ode.is_null()
756        || out_value.is_null()
757        || out_sens.is_null()
758        || data_ptr.is_null()
759        || !valid_f64_ptr(params_ptr, params_len)
760        || !valid_f64_ptr(t_eval_ptr, t_eval_len)
761    {
762        c_invalid_arg!("invalid arguments to diffsol_ode_solve_sum_squares_adj");
763        return DIFFSOL_BAD_ARG;
764    }
765    let params = HostArray::new_vector(params_ptr as *mut u8, params_len, ScalarType::F64);
766    let t_eval = HostArray::new_vector(t_eval_ptr as *mut u8, t_eval_len, ScalarType::F64);
767    let data = HostArray::new_col_major(
768        data_ptr as *mut u8,
769        data_rows,
770        data_cols,
771        data_row_stride as isize,
772        data_col_stride as isize,
773        ScalarType::F64,
774    );
775    let ode = unsafe { &mut *ode };
776    match ode.solve_sum_squares_adj(params, data, t_eval) {
777        Ok((value, sens)) => {
778            let sens_boxed = boxed_host_array(sens);
779            unsafe {
780                *out_value = value;
781                *out_sens = sens_boxed;
782            }
783            DIFFSOL_OK
784        }
785        Err(err) => {
786            c_error!(&format!("{}", err));
787            DIFFSOL_ERR
788        }
789    }
790}
791
792/// Return the matrix type configured for an ODE.
793///
794/// # Safety
795/// `ode` must be a valid pointer created by this library.
796#[unsafe(no_mangle)]
797pub unsafe extern "C" fn diffsol_ode_get_matrix_type(ode: *const OdeWrapper) -> i32 {
798    if ode.is_null() {
799        c_invalid_arg!("ode is null");
800        return -1;
801    }
802    let ode = unsafe { &*ode };
803    match ode.get_matrix_type() {
804        Ok(value) => matrix_type_to_i32(value),
805        Err(err) => {
806            c_error!(&format!("{}", err));
807            -1
808        }
809    }
810}
811
812/// Return the ODE solver enum configured for an ODE.
813///
814/// # Safety
815/// `ode` must be a valid pointer created by this library.
816#[unsafe(no_mangle)]
817pub unsafe extern "C" fn diffsol_ode_get_ode_solver(ode: *const OdeWrapper) -> i32 {
818    if ode.is_null() {
819        c_invalid_arg!("ode is null");
820        return -1;
821    }
822    let ode = unsafe { &*ode };
823    match ode.get_ode_solver() {
824        Ok(value) => ode_solver_to_i32(value),
825        Err(err) => {
826            c_error!(&format!("{}", err));
827            -1
828        }
829    }
830}
831
832/// Set the ODE solver enum for an ODE.
833///
834/// # Safety
835/// `ode` must be a valid mutable pointer created by this library.
836#[unsafe(no_mangle)]
837pub unsafe extern "C" fn diffsol_ode_set_ode_solver(ode: *mut OdeWrapper, value: i32) -> i32 {
838    if ode.is_null() {
839        c_invalid_arg!("ode is null");
840        return DIFFSOL_BAD_ARG;
841    }
842    let value = match ode_solver_from_i32(value) {
843        Some(v) => v,
844        None => {
845            c_invalid_arg!("invalid ode_solver");
846            return DIFFSOL_BAD_ARG;
847        }
848    };
849    let ode = unsafe { &mut *ode };
850    match ode.set_ode_solver(value) {
851        Ok(()) => DIFFSOL_OK,
852        Err(err) => c_error!(&format!("{}", err)),
853    }
854}
855
856/// Return the linear solver enum configured for an ODE.
857///
858/// # Safety
859/// `ode` must be a valid pointer created by this library.
860#[unsafe(no_mangle)]
861pub unsafe extern "C" fn diffsol_ode_get_linear_solver(ode: *const OdeWrapper) -> i32 {
862    if ode.is_null() {
863        c_invalid_arg!("ode is null");
864        return -1;
865    }
866    let ode = unsafe { &*ode };
867    match ode.get_linear_solver() {
868        Ok(value) => linear_solver_to_i32(value),
869        Err(err) => {
870            c_error!(&format!("{}", err));
871            -1
872        }
873    }
874}
875
876/// Set the linear solver enum for an ODE.
877///
878/// # Safety
879/// `ode` must be a valid mutable pointer created by this library.
880#[unsafe(no_mangle)]
881pub unsafe extern "C" fn diffsol_ode_set_linear_solver(ode: *mut OdeWrapper, value: i32) -> i32 {
882    if ode.is_null() {
883        c_invalid_arg!("ode is null");
884        return DIFFSOL_BAD_ARG;
885    }
886    let value = match linear_solver_from_i32(value) {
887        Some(v) => v,
888        None => {
889            c_invalid_arg!("invalid linear_solver");
890            return DIFFSOL_BAD_ARG;
891        }
892    };
893    let ode = unsafe { &mut *ode };
894    match ode.set_linear_solver(value) {
895        Ok(()) => DIFFSOL_OK,
896        Err(err) => c_error!(&format!("{}", err)),
897    }
898}
899
900/// Return the relative tolerance configured for an ODE.
901///
902/// # Safety
903/// `ode` must be a valid pointer created by this library. `out_value` must be a
904/// valid, writable pointer.
905#[unsafe(no_mangle)]
906pub unsafe extern "C" fn diffsol_ode_get_rtol(ode: *const OdeWrapper, out_value: *mut f64) -> i32 {
907    if ode.is_null() || out_value.is_null() {
908        c_invalid_arg!("invalid arguments to diffsol_ode_get_rtol");
909        return DIFFSOL_BAD_ARG;
910    }
911    let ode = unsafe { &*ode };
912    match ode.get_rtol() {
913        Ok(value) => {
914            unsafe {
915                *out_value = value;
916            }
917            DIFFSOL_OK
918        }
919        Err(err) => c_error!(&format!("{}", err)),
920    }
921}
922
923/// Set the relative tolerance for an ODE.
924///
925/// # Safety
926/// `ode` must be a valid mutable pointer created by this library.
927#[unsafe(no_mangle)]
928pub unsafe extern "C" fn diffsol_ode_set_rtol(ode: *mut OdeWrapper, value: f64) -> i32 {
929    if ode.is_null() {
930        c_invalid_arg!("ode is null");
931        return DIFFSOL_BAD_ARG;
932    }
933    let ode = unsafe { &mut *ode };
934    match ode.set_rtol(value) {
935        Ok(()) => DIFFSOL_OK,
936        Err(err) => c_error!(&format!("{}", err)),
937    }
938}
939
940/// Return the absolute tolerance configured for an ODE.
941///
942/// # Safety
943/// `ode` must be a valid pointer created by this library. `out_value` must be a
944/// valid, writable pointer.
945#[unsafe(no_mangle)]
946pub unsafe extern "C" fn diffsol_ode_get_atol(ode: *const OdeWrapper, out_value: *mut f64) -> i32 {
947    if ode.is_null() || out_value.is_null() {
948        c_invalid_arg!("invalid arguments to diffsol_ode_get_atol");
949        return DIFFSOL_BAD_ARG;
950    }
951    let ode = unsafe { &*ode };
952    match ode.get_atol() {
953        Ok(value) => {
954            unsafe {
955                *out_value = value;
956            }
957            DIFFSOL_OK
958        }
959        Err(err) => c_error!(&format!("{}", err)),
960    }
961}
962
963/// Set the absolute tolerance for an ODE.
964///
965/// # Safety
966/// `ode` must be a valid mutable pointer created by this library.
967#[unsafe(no_mangle)]
968pub unsafe extern "C" fn diffsol_ode_set_atol(ode: *mut OdeWrapper, value: f64) -> i32 {
969    if ode.is_null() {
970        c_invalid_arg!("ode is null");
971        return DIFFSOL_BAD_ARG;
972    }
973    let ode = unsafe { &mut *ode };
974    match ode.set_atol(value) {
975        Ok(()) => DIFFSOL_OK,
976        Err(err) => c_error!(&format!("{}", err)),
977    }
978}
979
980#[cfg(all(test, feature = "diffsl-external-f64"))]
981mod tests {
982    use std::ptr;
983
984    use crate::initial_condition_options::InitialConditionSolverOptions;
985    use crate::linear_solver_type::LinearSolverType;
986    use crate::linear_solver_type_c::{
987        diffsol_linear_solver_type_count, diffsol_linear_solver_type_is_valid,
988        diffsol_linear_solver_type_name, linear_solver_to_i32,
989    };
990    use crate::matrix_type::MatrixType;
991    use crate::ode_options::OdeSolverOptions;
992    use crate::ode_options_c::{
993        diffsol_ode_options_free, diffsol_ode_options_get_max_nonlinear_solver_iterations,
994        diffsol_ode_options_get_min_timestep,
995        diffsol_ode_options_set_max_nonlinear_solver_iterations,
996        diffsol_ode_options_set_min_timestep,
997    };
998    use crate::ode_solver_type::OdeSolverType;
999    use crate::ode_solver_type_c::{
1000        diffsol_ode_solver_type_count, diffsol_ode_solver_type_is_valid,
1001        diffsol_ode_solver_type_name, ode_solver_to_i32,
1002    };
1003    use crate::scalar_type::ScalarType;
1004    use crate::scalar_type_c::{
1005        diffsol_scalar_type_count, diffsol_scalar_type_is_valid, diffsol_scalar_type_name,
1006        scalar_type_to_i32,
1007    };
1008    use crate::solution_wrapper_c::{
1009        diffsol_solution_wrapper_get_sens, diffsol_solution_wrapper_get_ts,
1010        diffsol_solution_wrapper_get_ys,
1011    };
1012    use crate::test_support::{
1013        assert_close, assert_last_error_contains, c_string, clear_last_error, ffi_free_solution,
1014        ffi_read_host_array_list_matrices, ffi_read_host_array_matrix, ffi_read_host_array_vector,
1015        find_time_window, logistic_state, logistic_state_dr, mass_state_deps, rhs_input_deps,
1016        rhs_state_deps, ASSERT_TOL, LOGISTIC_X0,
1017    };
1018    use crate::{
1019        initial_condition_options_c::{
1020            diffsol_ic_options_free, diffsol_ic_options_get_max_linesearch_iterations,
1021            diffsol_ic_options_get_use_linesearch,
1022            diffsol_ic_options_set_max_linesearch_iterations,
1023            diffsol_ic_options_set_use_linesearch,
1024        },
1025        matrix_type_c::{
1026            diffsol_matrix_type_count, diffsol_matrix_type_is_valid, diffsol_matrix_type_name,
1027            matrix_type_to_i32,
1028        },
1029    };
1030
1031    use super::*;
1032
1033    fn to_dep_pairs(values: &[(usize, usize)]) -> Vec<DiffsolDepPair> {
1034        values
1035            .iter()
1036            .map(|&(row, col)| DiffsolDepPair { row, col })
1037            .collect()
1038    }
1039
1040    unsafe fn make_ode_ptr(
1041        matrix_type: i32,
1042        linear_solver: i32,
1043        ode_solver: i32,
1044    ) -> *mut OdeWrapper {
1045        let rhs_state_deps = to_dep_pairs(&rhs_state_deps());
1046        let rhs_input_deps = to_dep_pairs(&rhs_input_deps());
1047        let mass_state_deps = to_dep_pairs(&mass_state_deps());
1048        unsafe {
1049            diffsol_ode_new_external(
1050                matrix_type,
1051                linear_solver,
1052                ode_solver,
1053                rhs_state_deps.as_ptr(),
1054                rhs_state_deps.len(),
1055                rhs_input_deps.as_ptr(),
1056                rhs_input_deps.len(),
1057                mass_state_deps.as_ptr(),
1058                mass_state_deps.len(),
1059            )
1060        }
1061    }
1062
1063    #[test]
1064    fn c_api_reports_enum_metadata() {
1065        clear_last_error();
1066        unsafe {
1067            assert_eq!(diffsol_matrix_type_count(), 3);
1068            assert_eq!(diffsol_ode_solver_type_count(), 4);
1069            assert_eq!(diffsol_linear_solver_type_count(), 3);
1070            assert_eq!(diffsol_scalar_type_count(), 2);
1071
1072            assert_eq!(
1073                c_string(diffsol_matrix_type_name(matrix_type_to_i32(
1074                    MatrixType::NalgebraDense
1075                ))),
1076                "nalgebra_dense"
1077            );
1078            assert_eq!(
1079                c_string(diffsol_ode_solver_type_name(ode_solver_to_i32(
1080                    OdeSolverType::Bdf
1081                ))),
1082                "bdf"
1083            );
1084            assert_eq!(
1085                c_string(diffsol_linear_solver_type_name(linear_solver_to_i32(
1086                    LinearSolverType::Default
1087                ))),
1088                "default"
1089            );
1090            assert_eq!(
1091                c_string(diffsol_scalar_type_name(scalar_type_to_i32(
1092                    ScalarType::F64
1093                ))),
1094                "f64"
1095            );
1096        }
1097    }
1098
1099    #[test]
1100    fn c_api_invalid_enums_set_last_error() {
1101        clear_last_error();
1102        unsafe {
1103            assert_eq!(diffsol_matrix_type_is_valid(99), 0);
1104            assert_last_error_contains("invalid matrix_type");
1105            clear_last_error();
1106
1107            assert_eq!(diffsol_ode_solver_type_is_valid(99), 0);
1108            assert_last_error_contains("invalid ode_solver_type");
1109            clear_last_error();
1110
1111            assert_eq!(diffsol_linear_solver_type_is_valid(99), 0);
1112            assert_last_error_contains("invalid linear_solver_type");
1113            clear_last_error();
1114
1115            assert_eq!(diffsol_scalar_type_is_valid(99), 0);
1116            assert_last_error_contains("invalid scalar_type");
1117        }
1118    }
1119
1120    #[test]
1121    fn c_api_rejects_invalid_ode_arguments() {
1122        clear_last_error();
1123        unsafe {
1124            let mut out_array = ptr::null_mut();
1125            let status = diffsol_ode_y0(ptr::null_mut(), ptr::null(), 0, &mut out_array);
1126            assert_eq!(status, DIFFSOL_BAD_ARG);
1127            assert!(out_array.is_null());
1128            assert_last_error_contains("invalid arguments to diffsol_ode_y0");
1129            clear_last_error();
1130
1131            let ode = make_ode_ptr(
1132                99,
1133                linear_solver_to_i32(LinearSolverType::Default),
1134                ode_solver_to_i32(OdeSolverType::Bdf),
1135            );
1136            assert!(ode.is_null());
1137            assert_last_error_contains("invalid matrix_type");
1138        }
1139    }
1140
1141    #[test]
1142    fn c_api_full_lifecycle_matches_external_logistic_model() {
1143        clear_last_error();
1144        unsafe {
1145            let ode = make_ode_ptr(
1146                matrix_type_to_i32(MatrixType::NalgebraDense),
1147                linear_solver_to_i32(LinearSolverType::Default),
1148                ode_solver_to_i32(OdeSolverType::Bdf),
1149            );
1150            assert!(!ode.is_null());
1151
1152            assert_eq!(
1153                diffsol_ode_get_matrix_type(ode),
1154                matrix_type_to_i32(MatrixType::NalgebraDense)
1155            );
1156            assert_eq!(
1157                diffsol_ode_get_ode_solver(ode),
1158                ode_solver_to_i32(OdeSolverType::Bdf)
1159            );
1160            assert_eq!(
1161                diffsol_ode_get_linear_solver(ode),
1162                linear_solver_to_i32(LinearSolverType::Default)
1163            );
1164
1165            assert_eq!(
1166                diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Tsit45)),
1167                DIFFSOL_OK
1168            );
1169            assert_eq!(
1170                diffsol_ode_get_ode_solver(ode),
1171                ode_solver_to_i32(OdeSolverType::Tsit45)
1172            );
1173            assert_eq!(
1174                diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Bdf)),
1175                DIFFSOL_OK
1176            );
1177
1178            assert_eq!(diffsol_ode_set_rtol(ode, 1e-8), DIFFSOL_OK);
1179            assert_eq!(diffsol_ode_set_atol(ode, 1e-8), DIFFSOL_OK);
1180            let mut rtol = 0.0;
1181            let mut atol = 0.0;
1182            assert_eq!(diffsol_ode_get_rtol(ode, &mut rtol), DIFFSOL_OK);
1183            assert_eq!(diffsol_ode_get_atol(ode, &mut atol), DIFFSOL_OK);
1184            assert_close(rtol, 1e-8, ASSERT_TOL, "rtol roundtrip");
1185            assert_close(atol, 1e-8, ASSERT_TOL, "atol roundtrip");
1186
1187            let mut ic_options: *mut InitialConditionSolverOptions = ptr::null_mut();
1188            assert_eq!(diffsol_ode_get_ic_options(ode, &mut ic_options), DIFFSOL_OK);
1189            assert!(!ic_options.is_null());
1190            let mut use_linesearch = 0;
1191            let mut max_linesearch_iterations = 0usize;
1192            assert_eq!(
1193                diffsol_ic_options_get_use_linesearch(ic_options, &mut use_linesearch),
1194                DIFFSOL_OK
1195            );
1196            assert_eq!(
1197                diffsol_ic_options_set_use_linesearch(ic_options, 1),
1198                DIFFSOL_OK
1199            );
1200            assert_eq!(
1201                diffsol_ic_options_get_use_linesearch(ic_options, &mut use_linesearch),
1202                DIFFSOL_OK
1203            );
1204            assert_eq!(use_linesearch, 1);
1205            assert_eq!(
1206                diffsol_ic_options_set_max_linesearch_iterations(ic_options, 23),
1207                DIFFSOL_OK
1208            );
1209            assert_eq!(
1210                diffsol_ic_options_get_max_linesearch_iterations(
1211                    ic_options,
1212                    &mut max_linesearch_iterations
1213                ),
1214                DIFFSOL_OK
1215            );
1216            assert_eq!(max_linesearch_iterations, 23);
1217            diffsol_ic_options_free(ic_options);
1218
1219            let mut ode_options: *mut OdeSolverOptions = ptr::null_mut();
1220            assert_eq!(diffsol_ode_get_options(ode, &mut ode_options), DIFFSOL_OK);
1221            assert!(!ode_options.is_null());
1222            let mut max_nonlinear_iterations = 0usize;
1223            let mut min_timestep = 0.0;
1224            assert_eq!(
1225                diffsol_ode_options_set_max_nonlinear_solver_iterations(ode_options, 17),
1226                DIFFSOL_OK
1227            );
1228            assert_eq!(
1229                diffsol_ode_options_get_max_nonlinear_solver_iterations(
1230                    ode_options,
1231                    &mut max_nonlinear_iterations
1232                ),
1233                DIFFSOL_OK
1234            );
1235            assert_eq!(max_nonlinear_iterations, 17);
1236            assert_eq!(
1237                diffsol_ode_options_set_min_timestep(ode_options, 1e-4),
1238                DIFFSOL_OK
1239            );
1240            assert_eq!(
1241                diffsol_ode_options_get_min_timestep(ode_options, &mut min_timestep),
1242                DIFFSOL_OK
1243            );
1244            assert_close(min_timestep, 1e-4, ASSERT_TOL, "min_timestep roundtrip");
1245            diffsol_ode_options_free(ode_options);
1246
1247            let params = [2.0f64];
1248            let y = [0.25f64];
1249            let v = [3.0f64];
1250
1251            let mut y0_ptr = ptr::null_mut();
1252            assert_eq!(
1253                diffsol_ode_y0(ode, params.as_ptr(), params.len(), &mut y0_ptr),
1254                DIFFSOL_OK
1255            );
1256            assert_eq!(ffi_read_host_array_vector(y0_ptr), vec![LOGISTIC_X0]);
1257
1258            let mut rhs_ptr = ptr::null_mut();
1259            assert_eq!(
1260                diffsol_ode_rhs(
1261                    ode,
1262                    params.as_ptr(),
1263                    params.len(),
1264                    0.0,
1265                    y.as_ptr(),
1266                    y.len(),
1267                    &mut rhs_ptr,
1268                ),
1269                DIFFSOL_OK
1270            );
1271            assert_close(
1272                ffi_read_host_array_vector(rhs_ptr)[0],
1273                0.375,
1274                ASSERT_TOL,
1275                "ffi rhs",
1276            );
1277
1278            let mut rhs_jac_mul_ptr = ptr::null_mut();
1279            assert_eq!(
1280                diffsol_ode_rhs_jac_mul(
1281                    ode,
1282                    params.as_ptr(),
1283                    params.len(),
1284                    0.0,
1285                    y.as_ptr(),
1286                    y.len(),
1287                    v.as_ptr(),
1288                    v.len(),
1289                    &mut rhs_jac_mul_ptr,
1290                ),
1291                DIFFSOL_OK
1292            );
1293            assert_close(
1294                ffi_read_host_array_vector(rhs_jac_mul_ptr)[0],
1295                3.0,
1296                ASSERT_TOL,
1297                "ffi rhs_jac_mul",
1298            );
1299
1300            let mut solve_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1301            assert_eq!(
1302                diffsol_ode_solve(
1303                    ode,
1304                    params.as_ptr(),
1305                    params.len(),
1306                    1e-9,
1307                    &mut solve_solution_ptr
1308                ),
1309                DIFFSOL_OK
1310            );
1311            assert!(!solve_solution_ptr.is_null());
1312
1313            let mut solve_ys_ptr = ptr::null_mut();
1314            let mut solve_ts_ptr = ptr::null_mut();
1315            assert_eq!(
1316                diffsol_solution_wrapper_get_ys(solve_solution_ptr, &mut solve_ys_ptr),
1317                DIFFSOL_OK
1318            );
1319            assert_eq!(
1320                diffsol_solution_wrapper_get_ts(solve_solution_ptr, &mut solve_ts_ptr),
1321                DIFFSOL_OK
1322            );
1323            let (solve_rows, solve_cols, solve_ys) = ffi_read_host_array_matrix(solve_ys_ptr);
1324            let solve_ts = ffi_read_host_array_vector(solve_ts_ptr);
1325            assert_eq!(solve_rows, 1);
1326            assert_eq!(solve_cols, solve_ts.len());
1327            assert!(!solve_ts.is_empty());
1328            assert_close(
1329                *solve_ts.last().unwrap(),
1330                1e-9,
1331                ASSERT_TOL,
1332                "ffi solve final time",
1333            );
1334            assert_close(
1335                *solve_ys.last().unwrap(),
1336                logistic_state(LOGISTIC_X0, 2.0, 1e-9),
1337                ASSERT_TOL,
1338                "ffi solve final value",
1339            );
1340            ffi_free_solution(solve_solution_ptr);
1341
1342            let mut solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1343            assert_eq!(
1344                diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Tsit45)),
1345                DIFFSOL_OK
1346            );
1347
1348            let t_eval = [0.25f64, 0.5f64, 1.0f64];
1349            assert_eq!(
1350                diffsol_ode_solve_dense(
1351                    ode,
1352                    params.as_ptr(),
1353                    params.len(),
1354                    t_eval.as_ptr(),
1355                    t_eval.len(),
1356                    &mut solution_ptr,
1357                ),
1358                DIFFSOL_OK
1359            );
1360            let mut ys_ptr = ptr::null_mut();
1361            let mut ts_ptr = ptr::null_mut();
1362            assert_eq!(
1363                diffsol_solution_wrapper_get_ys(solution_ptr, &mut ys_ptr),
1364                DIFFSOL_OK
1365            );
1366            assert_eq!(
1367                diffsol_solution_wrapper_get_ts(solution_ptr, &mut ts_ptr),
1368                DIFFSOL_OK
1369            );
1370            let (rows, cols, ys) = ffi_read_host_array_matrix(ys_ptr);
1371            let ts = ffi_read_host_array_vector(ts_ptr);
1372            assert_eq!(rows, 1);
1373            assert_eq!(cols, ts.len());
1374            let start = find_time_window(&ts, &t_eval, ASSERT_TOL);
1375            for (i, &t) in t_eval.iter().enumerate() {
1376                assert_close(ts[start + i], t, ASSERT_TOL, "ffi solution time");
1377                assert_close(
1378                    ys[start + i],
1379                    logistic_state(0.1, 2.0, t),
1380                    5e-4,
1381                    "ffi solution value",
1382                );
1383            }
1384            assert_eq!(
1385                diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Bdf)),
1386                DIFFSOL_OK
1387            );
1388
1389            let hybrid_t_eval = [0.5f64, 1.0, 1.25, 1.5, 2.0];
1390            let hybrid_ode = make_ode_ptr(
1391                matrix_type_to_i32(MatrixType::NalgebraDense),
1392                linear_solver_to_i32(LinearSolverType::Default),
1393                ode_solver_to_i32(OdeSolverType::Bdf),
1394            );
1395            assert!(!hybrid_ode.is_null());
1396            let mut hybrid_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1397            assert_eq!(
1398                diffsol_ode_solve_hybrid_dense(
1399                    hybrid_ode,
1400                    params.as_ptr(),
1401                    params.len(),
1402                    hybrid_t_eval.as_ptr(),
1403                    hybrid_t_eval.len(),
1404                    &mut hybrid_solution_ptr,
1405                ),
1406                DIFFSOL_OK
1407            );
1408            let mut hybrid_ys_ptr = ptr::null_mut();
1409            let mut hybrid_ts_ptr = ptr::null_mut();
1410            assert_eq!(
1411                diffsol_solution_wrapper_get_ys(hybrid_solution_ptr, &mut hybrid_ys_ptr),
1412                DIFFSOL_OK
1413            );
1414            assert_eq!(
1415                diffsol_solution_wrapper_get_ts(hybrid_solution_ptr, &mut hybrid_ts_ptr),
1416                DIFFSOL_OK
1417            );
1418            let (hybrid_rows, hybrid_cols, hybrid_ys) = ffi_read_host_array_matrix(hybrid_ys_ptr);
1419            let hybrid_ts = ffi_read_host_array_vector(hybrid_ts_ptr);
1420            assert_eq!(hybrid_rows, 1);
1421            assert_eq!(hybrid_cols, hybrid_t_eval.len());
1422            assert_eq!(hybrid_ts, hybrid_t_eval);
1423            assert_close(
1424                hybrid_ys[0],
1425                logistic_state(LOGISTIC_X0, 2.0, hybrid_t_eval[0]),
1426                5e-4,
1427                "ffi hybrid dense pre-root value",
1428            );
1429            assert_close(
1430                hybrid_ys[1],
1431                logistic_state(LOGISTIC_X0, 2.0, hybrid_t_eval[1]),
1432                5e-4,
1433                "ffi hybrid dense near-root value",
1434            );
1435            for (i, value) in hybrid_ys.iter().enumerate().skip(2) {
1436                assert_close(
1437                    *value,
1438                    1.0,
1439                    5e-4,
1440                    &format!("ffi hybrid dense post-root value[{i}]"),
1441                );
1442            }
1443            ffi_free_solution(hybrid_solution_ptr);
1444            diffsol_ode_free(hybrid_ode);
1445
1446            let analysis_ode = make_ode_ptr(
1447                matrix_type_to_i32(MatrixType::NalgebraDense),
1448                linear_solver_to_i32(LinearSolverType::Default),
1449                ode_solver_to_i32(OdeSolverType::Bdf),
1450            );
1451            assert!(!analysis_ode.is_null());
1452
1453            let mut sens_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1454            assert_eq!(
1455                diffsol_ode_solve_fwd_sens(
1456                    analysis_ode,
1457                    params.as_ptr(),
1458                    params.len(),
1459                    t_eval.as_ptr(),
1460                    t_eval.len(),
1461                    &mut sens_solution_ptr,
1462                ),
1463                DIFFSOL_OK
1464            );
1465            let mut sens_list = ptr::null_mut();
1466            let mut sens_len = 0usize;
1467            assert_eq!(
1468                diffsol_solution_wrapper_get_sens(sens_solution_ptr, &mut sens_list, &mut sens_len),
1469                DIFFSOL_OK
1470            );
1471            let sens_values = ffi_read_host_array_list_matrices(sens_list, sens_len);
1472            assert_eq!(sens_values.len(), 1);
1473            assert_eq!(sens_values[0].0, 1);
1474            assert_eq!(sens_values[0].1, t_eval.len());
1475            for (i, (&value, &t)) in sens_values[0].2.iter().zip(t_eval.iter()).enumerate() {
1476                assert_close(
1477                    value,
1478                    logistic_state_dr(LOGISTIC_X0, 2.0, t),
1479                    ASSERT_TOL,
1480                    &format!("ffi sensitivity[{i}]"),
1481                );
1482            }
1483
1484            let adjoint_t_eval = [0.0f64, 0.25f64, 0.5f64, 1.0f64];
1485            let adjoint_data: Vec<f64> = adjoint_t_eval
1486                .iter()
1487                .map(|&t| logistic_state(LOGISTIC_X0, 2.0, t))
1488                .collect();
1489            let mut objective = 0.0;
1490            let mut adjoint_grad_ptr = ptr::null_mut();
1491            assert_eq!(
1492                diffsol_ode_solve_sum_squares_adj(
1493                    analysis_ode,
1494                    params.as_ptr(),
1495                    params.len(),
1496                    adjoint_data.as_ptr(),
1497                    1,
1498                    adjoint_t_eval.len(),
1499                    1,
1500                    1,
1501                    adjoint_t_eval.as_ptr(),
1502                    adjoint_t_eval.len(),
1503                    &mut objective,
1504                    &mut adjoint_grad_ptr,
1505                ),
1506                DIFFSOL_OK
1507            );
1508            assert_close(objective, 0.0, ASSERT_TOL, "ffi adjoint objective");
1509            let grad = ffi_read_host_array_vector(adjoint_grad_ptr);
1510            assert_eq!(grad.len(), 1);
1511            assert_close(grad[0], 0.0, ASSERT_TOL, "ffi adjoint gradient");
1512
1513            ffi_free_solution(sens_solution_ptr);
1514            diffsol_ode_free(analysis_ode);
1515            ffi_free_solution(solution_ptr);
1516            diffsol_ode_free(ode);
1517        }
1518    }
1519}
1520
1521#[cfg(all(test, feature = "diffsl-external-dynamic"))]
1522mod external_dynamic_tests {
1523    use std::ffi::CString;
1524    use std::os::raw::c_char;
1525    use std::ptr;
1526
1527    use crate::linear_solver_type::LinearSolverType;
1528    use crate::linear_solver_type_c::linear_solver_to_i32;
1529    use crate::matrix_type::MatrixType;
1530    use crate::matrix_type_c::matrix_type_to_i32;
1531    use crate::ode_solver_type::OdeSolverType;
1532    use crate::ode_solver_type_c::ode_solver_to_i32;
1533    use crate::test_support::{
1534        assert_last_error_contains, assert_last_error_set, clear_last_error,
1535        external_dynamic_fixture_path, ffi_read_host_array_vector, mass_state_deps, rhs_input_deps,
1536        rhs_state_deps, LOGISTIC_X0,
1537    };
1538
1539    use super::*;
1540
1541    fn to_dep_pairs(values: &[(usize, usize)]) -> Vec<DiffsolDepPair> {
1542        values
1543            .iter()
1544            .map(|&(row, col)| DiffsolDepPair { row, col })
1545            .collect()
1546    }
1547
1548    unsafe fn make_ode_ptr(
1549        matrix_type: i32,
1550        linear_solver: i32,
1551        ode_solver: i32,
1552    ) -> *mut OdeWrapper {
1553        let path = CString::new(
1554            external_dynamic_fixture_path()
1555                .to_string_lossy()
1556                .into_owned(),
1557        )
1558        .unwrap();
1559        let rhs_state_deps = to_dep_pairs(&rhs_state_deps());
1560        let rhs_input_deps = to_dep_pairs(&rhs_input_deps());
1561        let mass_state_deps = to_dep_pairs(&mass_state_deps());
1562        unsafe {
1563            diffsol_ode_new_external_dynamic(
1564                path.as_ptr(),
1565                matrix_type,
1566                linear_solver,
1567                ode_solver,
1568                rhs_state_deps.as_ptr(),
1569                rhs_state_deps.len(),
1570                rhs_input_deps.as_ptr(),
1571                rhs_input_deps.len(),
1572                mass_state_deps.as_ptr(),
1573                mass_state_deps.len(),
1574            )
1575        }
1576    }
1577
1578    #[test]
1579    fn c_api_constructs_dynamic_external_ode() {
1580        clear_last_error();
1581        unsafe {
1582            let ode = make_ode_ptr(
1583                matrix_type_to_i32(MatrixType::NalgebraDense),
1584                linear_solver_to_i32(LinearSolverType::Default),
1585                ode_solver_to_i32(OdeSolverType::Bdf),
1586            );
1587            assert!(!ode.is_null());
1588            assert_eq!(
1589                diffsol_ode_get_matrix_type(ode),
1590                matrix_type_to_i32(MatrixType::NalgebraDense)
1591            );
1592            let params = [2.0_f64];
1593            let mut y0_ptr = ptr::null_mut();
1594            assert_eq!(
1595                diffsol_ode_y0(ode, params.as_ptr(), params.len(), &mut y0_ptr),
1596                DIFFSOL_OK
1597            );
1598            assert_eq!(ffi_read_host_array_vector(y0_ptr), vec![LOGISTIC_X0]);
1599            diffsol_ode_free(ode);
1600        }
1601    }
1602
1603    #[test]
1604    fn c_api_rejects_null_dynamic_library_path() {
1605        clear_last_error();
1606        unsafe {
1607            let ode = diffsol_ode_new_external_dynamic(
1608                ptr::null(),
1609                matrix_type_to_i32(MatrixType::NalgebraDense),
1610                linear_solver_to_i32(LinearSolverType::Default),
1611                ode_solver_to_i32(OdeSolverType::Bdf),
1612                ptr::null(),
1613                0,
1614                ptr::null(),
1615                0,
1616                ptr::null(),
1617                0,
1618            );
1619            assert!(ode.is_null());
1620            assert_last_error_contains("path is null");
1621        }
1622    }
1623
1624    #[test]
1625    fn c_api_rejects_non_utf8_dynamic_library_path() {
1626        clear_last_error();
1627        unsafe {
1628            let invalid_utf8 = [0xff_u8, 0];
1629            let ode = diffsol_ode_new_external_dynamic(
1630                invalid_utf8.as_ptr() as *const c_char,
1631                matrix_type_to_i32(MatrixType::NalgebraDense),
1632                linear_solver_to_i32(LinearSolverType::Default),
1633                ode_solver_to_i32(OdeSolverType::Bdf),
1634                ptr::null(),
1635                0,
1636                ptr::null(),
1637                0,
1638                ptr::null(),
1639                0,
1640            );
1641            assert!(ode.is_null());
1642            assert_last_error_contains("path is not valid UTF-8");
1643        }
1644    }
1645
1646    #[test]
1647    fn c_api_reports_missing_dynamic_library_path() {
1648        clear_last_error();
1649        unsafe {
1650            let missing_path = external_dynamic_fixture_path().with_file_name("does-not-exist");
1651            let missing_path = CString::new(missing_path.to_string_lossy().into_owned()).unwrap();
1652            let ode = diffsol_ode_new_external_dynamic(
1653                missing_path.as_ptr(),
1654                matrix_type_to_i32(MatrixType::NalgebraDense),
1655                linear_solver_to_i32(LinearSolverType::Default),
1656                ode_solver_to_i32(OdeSolverType::Bdf),
1657                ptr::null(),
1658                0,
1659                ptr::null(),
1660                0,
1661                ptr::null(),
1662                0,
1663            );
1664            assert!(ode.is_null());
1665            assert_last_error_set();
1666        }
1667    }
1668}
1669
1670#[cfg(all(test, any(feature = "diffsl-cranelift", feature = "diffsl-llvm")))]
1671mod jit_tests {
1672    use std::ffi::{CStr, CString};
1673    use std::ptr;
1674
1675    use crate::error_c::{diffsol_error_code, diffsol_last_error_message};
1676    use crate::initial_condition_options_c::diffsol_ic_options_free;
1677    use crate::jit::JitBackendType;
1678    use crate::jit_c::jit_backend_to_i32;
1679    use crate::linear_solver_type::LinearSolverType;
1680    use crate::linear_solver_type_c::linear_solver_to_i32;
1681    use crate::matrix_type::MatrixType;
1682    use crate::matrix_type_c::matrix_type_to_i32;
1683    use crate::ode_options_c::diffsol_ode_options_free;
1684    use crate::ode_solver_type::OdeSolverType;
1685    use crate::ode_solver_type_c::ode_solver_to_i32;
1686    #[cfg(feature = "diffsl-llvm")]
1687    use crate::solution_wrapper_c::diffsol_solution_wrapper_get_sens;
1688    use crate::solution_wrapper_c::{
1689        diffsol_solution_wrapper_get_ts, diffsol_solution_wrapper_get_ys,
1690    };
1691    #[cfg(feature = "diffsl-llvm")]
1692    use crate::test_support::ffi_read_host_array_list_matrices;
1693    use crate::test_support::{
1694        assert_close, available_jit_backends, clear_last_error, ffi_free_solution,
1695        ffi_read_host_array_matrix, ffi_read_host_array_vector, find_time_window,
1696        hybrid_logistic_diffsl_code, hybrid_logistic_state, logistic_diffsl_code_cstring,
1697        logistic_state, ASSERT_TOL, LOGISTIC_X0,
1698    };
1699    #[cfg(feature = "diffsl-llvm")]
1700    use crate::test_support::{hybrid_logistic_state_dr, logistic_state_dr};
1701
1702    use super::*;
1703
1704    unsafe fn make_ode_ptr(
1705        jit_backend: JitBackendType,
1706        matrix_type: i32,
1707        linear_solver: i32,
1708        ode_solver: i32,
1709    ) -> *mut OdeWrapper {
1710        let code = logistic_diffsl_code_cstring();
1711        unsafe {
1712            make_ode_ptr_with_code(
1713                jit_backend,
1714                code.as_ptr(),
1715                matrix_type,
1716                linear_solver,
1717                ode_solver,
1718            )
1719        }
1720    }
1721
1722    unsafe fn make_ode_ptr_with_code(
1723        jit_backend: JitBackendType,
1724        code: *const std::os::raw::c_char,
1725        matrix_type: i32,
1726        linear_solver: i32,
1727        ode_solver: i32,
1728    ) -> *mut OdeWrapper {
1729        unsafe {
1730            diffsol_ode_new_jit(
1731                code,
1732                jit_backend_to_i32(jit_backend),
1733                matrix_type,
1734                linear_solver,
1735                ode_solver,
1736            )
1737        }
1738    }
1739
1740    unsafe fn last_error_message() -> String {
1741        let ptr = unsafe { diffsol_last_error_message() };
1742        assert_eq!(unsafe { diffsol_error_code() }, 1);
1743        assert!(!ptr.is_null());
1744        unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_owned()
1745    }
1746
1747    #[test]
1748    fn c_api_full_lifecycle_matches_jit_logistic_model() {
1749        clear_last_error();
1750        for jit_backend in available_jit_backends() {
1751            unsafe {
1752                let ode = make_ode_ptr(
1753                    jit_backend,
1754                    matrix_type_to_i32(MatrixType::NalgebraDense),
1755                    linear_solver_to_i32(LinearSolverType::Default),
1756                    ode_solver_to_i32(OdeSolverType::Bdf),
1757                );
1758                assert!(!ode.is_null());
1759
1760                assert_eq!(
1761                    diffsol_ode_get_matrix_type(ode),
1762                    matrix_type_to_i32(MatrixType::NalgebraDense)
1763                );
1764                assert_eq!(
1765                    diffsol_ode_get_ode_solver(ode),
1766                    ode_solver_to_i32(OdeSolverType::Bdf)
1767                );
1768                assert_eq!(
1769                    diffsol_ode_get_linear_solver(ode),
1770                    linear_solver_to_i32(LinearSolverType::Default)
1771                );
1772
1773                let params = [2.0f64];
1774                let y = [0.25f64];
1775                let v = [3.0f64];
1776
1777                let mut y0_ptr = ptr::null_mut();
1778                assert_eq!(
1779                    diffsol_ode_y0(ode, params.as_ptr(), params.len(), &mut y0_ptr),
1780                    DIFFSOL_OK
1781                );
1782                assert_eq!(ffi_read_host_array_vector(y0_ptr), vec![LOGISTIC_X0]);
1783
1784                let mut rhs_ptr = ptr::null_mut();
1785                assert_eq!(
1786                    diffsol_ode_rhs(
1787                        ode,
1788                        params.as_ptr(),
1789                        params.len(),
1790                        0.0,
1791                        y.as_ptr(),
1792                        y.len(),
1793                        &mut rhs_ptr,
1794                    ),
1795                    DIFFSOL_OK
1796                );
1797                assert_close(
1798                    ffi_read_host_array_vector(rhs_ptr)[0],
1799                    0.375,
1800                    ASSERT_TOL,
1801                    "jit ffi rhs",
1802                );
1803
1804                let mut rhs_jac_mul_ptr = ptr::null_mut();
1805                assert_eq!(
1806                    diffsol_ode_rhs_jac_mul(
1807                        ode,
1808                        params.as_ptr(),
1809                        params.len(),
1810                        0.0,
1811                        y.as_ptr(),
1812                        y.len(),
1813                        v.as_ptr(),
1814                        v.len(),
1815                        &mut rhs_jac_mul_ptr,
1816                    ),
1817                    DIFFSOL_OK
1818                );
1819                assert_close(
1820                    ffi_read_host_array_vector(rhs_jac_mul_ptr)[0],
1821                    3.0,
1822                    ASSERT_TOL,
1823                    "jit ffi rhs_jac_mul",
1824                );
1825
1826                let mut solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1827                let t_eval = [0.25f64, 0.5f64, 1.0f64];
1828                assert_eq!(
1829                    diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Tsit45)),
1830                    DIFFSOL_OK
1831                );
1832                assert_eq!(
1833                    diffsol_ode_solve_dense(
1834                        ode,
1835                        params.as_ptr(),
1836                        params.len(),
1837                        t_eval.as_ptr(),
1838                        t_eval.len(),
1839                        &mut solution_ptr,
1840                    ),
1841                    DIFFSOL_OK
1842                );
1843                let mut ys_ptr = ptr::null_mut();
1844                let mut ts_ptr = ptr::null_mut();
1845                assert_eq!(
1846                    diffsol_solution_wrapper_get_ys(solution_ptr, &mut ys_ptr),
1847                    DIFFSOL_OK
1848                );
1849                assert_eq!(
1850                    diffsol_solution_wrapper_get_ts(solution_ptr, &mut ts_ptr),
1851                    DIFFSOL_OK
1852                );
1853                let (rows, cols, ys) = ffi_read_host_array_matrix(ys_ptr);
1854                let ts = ffi_read_host_array_vector(ts_ptr);
1855                assert_eq!(rows, 1);
1856                assert_eq!(cols, ts.len());
1857                let start = find_time_window(&ts, &t_eval, ASSERT_TOL);
1858                for (i, &t) in t_eval.iter().enumerate() {
1859                    assert_close(ts[start + i], t, ASSERT_TOL, "jit ffi solution time");
1860                    assert_close(
1861                        ys[start + i],
1862                        logistic_state(LOGISTIC_X0, 2.0, t),
1863                        5e-4,
1864                        "jit ffi solution value",
1865                    );
1866                }
1867                assert_eq!(
1868                    diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Bdf)),
1869                    DIFFSOL_OK
1870                );
1871
1872                #[cfg(feature = "diffsl-llvm")]
1873                {
1874                    let analysis_code = logistic_diffsl_code_cstring();
1875                    let analysis_ode = make_ode_ptr_with_code(
1876                        JitBackendType::Llvm,
1877                        analysis_code.as_ptr(),
1878                        matrix_type_to_i32(MatrixType::NalgebraDense),
1879                        linear_solver_to_i32(LinearSolverType::Default),
1880                        ode_solver_to_i32(OdeSolverType::Bdf),
1881                    );
1882                    assert!(!analysis_ode.is_null());
1883
1884                    let mut sens_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
1885                    assert_eq!(
1886                        diffsol_ode_solve_fwd_sens(
1887                            analysis_ode,
1888                            params.as_ptr(),
1889                            params.len(),
1890                            t_eval.as_ptr(),
1891                            t_eval.len(),
1892                            &mut sens_solution_ptr,
1893                        ),
1894                        DIFFSOL_OK
1895                    );
1896                    let mut sens_list = ptr::null_mut();
1897                    let mut sens_len = 0usize;
1898                    assert_eq!(
1899                        diffsol_solution_wrapper_get_sens(
1900                            sens_solution_ptr,
1901                            &mut sens_list,
1902                            &mut sens_len
1903                        ),
1904                        DIFFSOL_OK
1905                    );
1906                    let sens_values = ffi_read_host_array_list_matrices(sens_list, sens_len);
1907                    assert_eq!(sens_values.len(), 1);
1908                    assert_eq!(sens_values[0].0, 1);
1909                    assert_eq!(sens_values[0].1, t_eval.len());
1910                    for (i, (&value, &t)) in sens_values[0].2.iter().zip(t_eval.iter()).enumerate()
1911                    {
1912                        assert_close(
1913                            value,
1914                            logistic_state_dr(LOGISTIC_X0, 2.0, t),
1915                            ASSERT_TOL,
1916                            &format!("jit ffi sensitivity[{i}]"),
1917                        );
1918                    }
1919
1920                    let adjoint_t_eval = [0.0f64, 0.25f64, 0.5f64, 1.0f64];
1921                    let adjoint_data: Vec<f64> = adjoint_t_eval
1922                        .iter()
1923                        .map(|&t| logistic_state(LOGISTIC_X0, 2.0, t))
1924                        .collect();
1925                    let mut objective = 0.0;
1926                    let mut adjoint_grad_ptr = ptr::null_mut();
1927                    assert_eq!(
1928                        diffsol_ode_solve_sum_squares_adj(
1929                            analysis_ode,
1930                            params.as_ptr(),
1931                            params.len(),
1932                            adjoint_data.as_ptr(),
1933                            1,
1934                            adjoint_t_eval.len(),
1935                            1,
1936                            1,
1937                            adjoint_t_eval.as_ptr(),
1938                            adjoint_t_eval.len(),
1939                            &mut objective,
1940                            &mut adjoint_grad_ptr,
1941                        ),
1942                        DIFFSOL_OK
1943                    );
1944                    assert_close(objective, 0.0, ASSERT_TOL, "jit ffi adjoint objective");
1945                    let grad = ffi_read_host_array_vector(adjoint_grad_ptr);
1946                    assert_eq!(grad.len(), 1);
1947                    assert!(
1948                        grad[0].is_finite(),
1949                        "jit ffi adjoint gradient should be finite"
1950                    );
1951
1952                    ffi_free_solution(sens_solution_ptr);
1953                    diffsol_ode_free(analysis_ode);
1954                }
1955                ffi_free_solution(solution_ptr);
1956                diffsol_ode_free(ode);
1957            }
1958        }
1959    }
1960
1961    #[test]
1962    fn c_api_rejects_invalid_jit_arguments() {
1963        unsafe {
1964            clear_last_error();
1965            assert!(diffsol_ode_new_jit(
1966                ptr::null(),
1967                jit_backend_to_i32(available_jit_backends()[0]),
1968                matrix_type_to_i32(MatrixType::NalgebraDense),
1969                linear_solver_to_i32(LinearSolverType::Default),
1970                ode_solver_to_i32(OdeSolverType::Bdf),
1971            )
1972            .is_null());
1973            assert!(last_error_message().contains("code is null"));
1974
1975            clear_last_error();
1976            let invalid_utf8 = CString::from_vec_with_nul(vec![0xff, 0]).unwrap();
1977            assert!(diffsol_ode_new_jit(
1978                invalid_utf8.as_ptr(),
1979                jit_backend_to_i32(available_jit_backends()[0]),
1980                matrix_type_to_i32(MatrixType::NalgebraDense),
1981                linear_solver_to_i32(LinearSolverType::Default),
1982                ode_solver_to_i32(OdeSolverType::Bdf),
1983            )
1984            .is_null());
1985            assert!(last_error_message().contains("valid UTF-8"));
1986
1987            clear_last_error();
1988            let code = logistic_diffsl_code_cstring();
1989            assert!(diffsol_ode_new_jit(
1990                code.as_ptr(),
1991                99,
1992                matrix_type_to_i32(MatrixType::NalgebraDense),
1993                linear_solver_to_i32(LinearSolverType::Default),
1994                ode_solver_to_i32(OdeSolverType::Bdf),
1995            )
1996            .is_null());
1997            assert!(last_error_message().contains("invalid jit_backend_type"));
1998
1999            clear_last_error();
2000            assert!(diffsol_ode_new_jit(
2001                code.as_ptr(),
2002                jit_backend_to_i32(available_jit_backends()[0]),
2003                99,
2004                linear_solver_to_i32(LinearSolverType::Default),
2005                ode_solver_to_i32(OdeSolverType::Bdf),
2006            )
2007            .is_null());
2008            assert!(last_error_message().contains("invalid matrix_type"));
2009
2010            clear_last_error();
2011            assert!(diffsol_ode_new_jit(
2012                code.as_ptr(),
2013                jit_backend_to_i32(available_jit_backends()[0]),
2014                matrix_type_to_i32(MatrixType::NalgebraDense),
2015                99,
2016                ode_solver_to_i32(OdeSolverType::Bdf),
2017            )
2018            .is_null());
2019            assert!(last_error_message().contains("invalid linear_solver"));
2020
2021            clear_last_error();
2022            assert!(diffsol_ode_new_jit(
2023                code.as_ptr(),
2024                jit_backend_to_i32(available_jit_backends()[0]),
2025                matrix_type_to_i32(MatrixType::NalgebraDense),
2026                linear_solver_to_i32(LinearSolverType::Default),
2027                99,
2028            )
2029            .is_null());
2030            assert!(last_error_message().contains("invalid ode_solver"));
2031
2032            clear_last_error();
2033            let invalid_code = CString::new("not valid diffsl").unwrap();
2034            assert!(diffsol_ode_new_jit(
2035                invalid_code.as_ptr(),
2036                jit_backend_to_i32(available_jit_backends()[0]),
2037                matrix_type_to_i32(MatrixType::NalgebraDense),
2038                linear_solver_to_i32(LinearSolverType::Default),
2039                ode_solver_to_i32(OdeSolverType::Bdf),
2040            )
2041            .is_null());
2042            assert!(diffsol_error_code() != 0);
2043
2044            let mut ic_options = ptr::null_mut();
2045            assert_eq!(
2046                diffsol_ode_get_ic_options(ptr::null_mut(), &mut ic_options),
2047                DIFFSOL_BAD_ARG
2048            );
2049            let mut ode_options = ptr::null_mut();
2050            assert_eq!(
2051                diffsol_ode_get_options(ptr::null_mut(), &mut ode_options),
2052                DIFFSOL_BAD_ARG
2053            );
2054
2055            let mut out_array = ptr::null_mut();
2056            assert_eq!(
2057                diffsol_ode_y0(ptr::null_mut(), ptr::null(), 0, &mut out_array),
2058                DIFFSOL_BAD_ARG
2059            );
2060            assert_eq!(
2061                diffsol_ode_rhs(
2062                    ptr::null_mut(),
2063                    ptr::null(),
2064                    0,
2065                    0.0,
2066                    ptr::null(),
2067                    0,
2068                    &mut out_array,
2069                ),
2070                DIFFSOL_BAD_ARG
2071            );
2072            assert_eq!(
2073                diffsol_ode_rhs_jac_mul(
2074                    ptr::null_mut(),
2075                    ptr::null(),
2076                    0,
2077                    0.0,
2078                    ptr::null(),
2079                    0,
2080                    ptr::null(),
2081                    0,
2082                    &mut out_array,
2083                ),
2084                DIFFSOL_BAD_ARG
2085            );
2086
2087            clear_last_error();
2088            diffsol_ode_free(ptr::null_mut());
2089            assert!(last_error_message().contains("ode is null"));
2090
2091            clear_last_error();
2092            diffsol_host_array_list_free(ptr::null_mut(), 0);
2093            assert!(last_error_message().contains("host array list is null"));
2094        }
2095    }
2096
2097    #[test]
2098    fn c_api_jit_wrapper_branches_cover_runtime_success_and_errors() {
2099        for jit_backend in available_jit_backends() {
2100            unsafe {
2101                let ode = make_ode_ptr(
2102                    jit_backend,
2103                    matrix_type_to_i32(MatrixType::NalgebraDense),
2104                    linear_solver_to_i32(LinearSolverType::Default),
2105                    ode_solver_to_i32(OdeSolverType::Bdf),
2106                );
2107                assert!(!ode.is_null());
2108
2109                let mut ic_options = ptr::null_mut();
2110                let mut ode_options = ptr::null_mut();
2111                assert_eq!(diffsol_ode_get_ic_options(ode, &mut ic_options), DIFFSOL_OK);
2112                assert_eq!(diffsol_ode_get_options(ode, &mut ode_options), DIFFSOL_OK);
2113                diffsol_ic_options_free(ic_options);
2114                diffsol_ode_options_free(ode_options);
2115
2116                let mut out_value = 0.0;
2117                assert_eq!(diffsol_ode_get_rtol(ode, &mut out_value), DIFFSOL_OK);
2118                assert_close(out_value, 1e-6, ASSERT_TOL, "jit ffi default rtol");
2119                assert_eq!(diffsol_ode_set_rtol(ode, 1e-4), DIFFSOL_OK);
2120                assert_eq!(diffsol_ode_get_rtol(ode, &mut out_value), DIFFSOL_OK);
2121                assert_close(out_value, 1e-4, ASSERT_TOL, "jit ffi updated rtol");
2122
2123                assert_eq!(diffsol_ode_get_atol(ode, &mut out_value), DIFFSOL_OK);
2124                assert_close(out_value, 1e-6, ASSERT_TOL, "jit ffi default atol");
2125                assert_eq!(diffsol_ode_set_atol(ode, 1e-5), DIFFSOL_OK);
2126                assert_eq!(diffsol_ode_get_atol(ode, &mut out_value), DIFFSOL_OK);
2127                assert_close(out_value, 1e-5, ASSERT_TOL, "jit ffi updated atol");
2128
2129                assert_eq!(
2130                    diffsol_ode_set_linear_solver(ode, linear_solver_to_i32(LinearSolverType::Lu)),
2131                    DIFFSOL_OK
2132                );
2133                assert_eq!(
2134                    diffsol_ode_get_linear_solver(ode),
2135                    linear_solver_to_i32(LinearSolverType::Lu)
2136                );
2137                assert_eq!(
2138                    diffsol_ode_set_ode_solver(ode, ode_solver_to_i32(OdeSolverType::Tsit45)),
2139                    DIFFSOL_OK
2140                );
2141                assert_eq!(
2142                    diffsol_ode_get_ode_solver(ode),
2143                    ode_solver_to_i32(OdeSolverType::Tsit45)
2144                );
2145                assert_eq!(
2146                    diffsol_ode_get_matrix_type(ode),
2147                    matrix_type_to_i32(MatrixType::NalgebraDense)
2148                );
2149
2150                let params = [2.0f64];
2151                let mut solution_ptr: *mut SolutionWrapper = ptr::null_mut();
2152                assert_eq!(
2153                    diffsol_ode_solve(ode, params.as_ptr(), params.len(), 1.0, &mut solution_ptr),
2154                    DIFFSOL_OK
2155                );
2156                ffi_free_solution(solution_ptr);
2157
2158                let t_eval = [0.25f64, 0.5f64, 1.0f64];
2159                let mut dense_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
2160                assert_eq!(
2161                    diffsol_ode_solve_dense(
2162                        ode,
2163                        params.as_ptr(),
2164                        params.len(),
2165                        t_eval.as_ptr(),
2166                        t_eval.len(),
2167                        &mut dense_solution_ptr,
2168                    ),
2169                    DIFFSOL_OK
2170                );
2171                ffi_free_solution(dense_solution_ptr);
2172
2173                let no_params: [f64; 0] = [];
2174                let y = [0.25f64];
2175                let v = [3.0f64];
2176                let mut out_array = ptr::null_mut();
2177                assert_eq!(
2178                    diffsol_ode_y0(ode, no_params.as_ptr(), no_params.len(), &mut out_array),
2179                    DIFFSOL_ERR
2180                );
2181                assert_eq!(
2182                    diffsol_ode_rhs(
2183                        ode,
2184                        no_params.as_ptr(),
2185                        no_params.len(),
2186                        0.0,
2187                        y.as_ptr(),
2188                        y.len(),
2189                        &mut out_array,
2190                    ),
2191                    DIFFSOL_ERR
2192                );
2193                assert_eq!(
2194                    diffsol_ode_rhs_jac_mul(
2195                        ode,
2196                        no_params.as_ptr(),
2197                        no_params.len(),
2198                        0.0,
2199                        y.as_ptr(),
2200                        y.len(),
2201                        v.as_ptr(),
2202                        v.len(),
2203                        &mut out_array,
2204                    ),
2205                    DIFFSOL_ERR
2206                );
2207
2208                let mut err_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
2209                assert_eq!(
2210                    diffsol_ode_solve(
2211                        ode,
2212                        no_params.as_ptr(),
2213                        no_params.len(),
2214                        1.0,
2215                        &mut err_solution_ptr,
2216                    ),
2217                    DIFFSOL_ERR
2218                );
2219                assert_eq!(
2220                    diffsol_ode_solve_hybrid(
2221                        ode,
2222                        no_params.as_ptr(),
2223                        no_params.len(),
2224                        1.0,
2225                        &mut err_solution_ptr,
2226                    ),
2227                    DIFFSOL_ERR
2228                );
2229                assert_eq!(
2230                    diffsol_ode_solve_dense(
2231                        ode,
2232                        no_params.as_ptr(),
2233                        no_params.len(),
2234                        t_eval.as_ptr(),
2235                        t_eval.len(),
2236                        &mut err_solution_ptr,
2237                    ),
2238                    DIFFSOL_ERR
2239                );
2240                assert_eq!(
2241                    diffsol_ode_solve_hybrid_dense(
2242                        ode,
2243                        no_params.as_ptr(),
2244                        no_params.len(),
2245                        t_eval.as_ptr(),
2246                        t_eval.len(),
2247                        &mut err_solution_ptr,
2248                    ),
2249                    DIFFSOL_ERR
2250                );
2251
2252                #[cfg(feature = "diffsl-llvm")]
2253                if matches!(jit_backend, JitBackendType::Llvm) {
2254                    assert_eq!(
2255                        diffsol_ode_solve_fwd_sens(
2256                            ode,
2257                            no_params.as_ptr(),
2258                            no_params.len(),
2259                            t_eval.as_ptr(),
2260                            t_eval.len(),
2261                            &mut err_solution_ptr,
2262                        ),
2263                        DIFFSOL_ERR
2264                    );
2265                    assert_eq!(
2266                        diffsol_ode_solve_hybrid_fwd_sens(
2267                            ode,
2268                            no_params.as_ptr(),
2269                            no_params.len(),
2270                            t_eval.as_ptr(),
2271                            t_eval.len(),
2272                            &mut err_solution_ptr,
2273                        ),
2274                        DIFFSOL_ERR
2275                    );
2276
2277                    let adjoint_data: Vec<f64> = t_eval
2278                        .iter()
2279                        .map(|&t| logistic_state(LOGISTIC_X0, 2.0, t))
2280                        .collect();
2281                    let mut objective = 0.0;
2282                    let mut sens_ptr = ptr::null_mut();
2283                    assert_eq!(
2284                        diffsol_ode_solve_sum_squares_adj(
2285                            ode,
2286                            no_params.as_ptr(),
2287                            no_params.len(),
2288                            adjoint_data.as_ptr(),
2289                            1,
2290                            t_eval.len(),
2291                            1,
2292                            1,
2293                            t_eval.as_ptr(),
2294                            t_eval.len(),
2295                            &mut objective,
2296                            &mut sens_ptr,
2297                        ),
2298                        DIFFSOL_ERR
2299                    );
2300                }
2301
2302                assert_eq!(diffsol_ode_get_matrix_type(ptr::null()), -1);
2303                assert_eq!(diffsol_ode_get_ode_solver(ptr::null()), -1);
2304                assert_eq!(diffsol_ode_get_linear_solver(ptr::null()), -1);
2305                assert_eq!(
2306                    diffsol_ode_set_ode_solver(ptr::null_mut(), 0),
2307                    DIFFSOL_BAD_ARG
2308                );
2309                assert_eq!(
2310                    diffsol_ode_set_linear_solver(ptr::null_mut(), 0),
2311                    DIFFSOL_BAD_ARG
2312                );
2313                assert_eq!(diffsol_ode_set_ode_solver(ode, 99), DIFFSOL_BAD_ARG);
2314                assert_eq!(diffsol_ode_set_linear_solver(ode, 99), DIFFSOL_BAD_ARG);
2315                assert_eq!(
2316                    diffsol_ode_get_rtol(ptr::null(), &mut out_value),
2317                    DIFFSOL_BAD_ARG
2318                );
2319                assert_eq!(diffsol_ode_get_rtol(ode, ptr::null_mut()), DIFFSOL_BAD_ARG);
2320                assert_eq!(diffsol_ode_set_rtol(ptr::null_mut(), 1e-3), DIFFSOL_BAD_ARG);
2321                assert_eq!(
2322                    diffsol_ode_get_atol(ptr::null(), &mut out_value),
2323                    DIFFSOL_BAD_ARG
2324                );
2325                assert_eq!(diffsol_ode_get_atol(ode, ptr::null_mut()), DIFFSOL_BAD_ARG);
2326                assert_eq!(diffsol_ode_set_atol(ptr::null_mut(), 1e-3), DIFFSOL_BAD_ARG);
2327                assert_eq!(
2328                    diffsol_ode_solve(ode, params.as_ptr(), params.len(), 1.0, ptr::null_mut()),
2329                    DIFFSOL_BAD_ARG
2330                );
2331                assert_eq!(
2332                    diffsol_ode_solve_hybrid(
2333                        ode,
2334                        params.as_ptr(),
2335                        params.len(),
2336                        1.0,
2337                        ptr::null_mut(),
2338                    ),
2339                    DIFFSOL_BAD_ARG
2340                );
2341                assert_eq!(
2342                    diffsol_ode_solve_dense(
2343                        ode,
2344                        params.as_ptr(),
2345                        params.len(),
2346                        t_eval.as_ptr(),
2347                        t_eval.len(),
2348                        ptr::null_mut(),
2349                    ),
2350                    DIFFSOL_BAD_ARG
2351                );
2352                assert_eq!(
2353                    diffsol_ode_solve_hybrid_dense(
2354                        ode,
2355                        params.as_ptr(),
2356                        params.len(),
2357                        t_eval.as_ptr(),
2358                        t_eval.len(),
2359                        ptr::null_mut(),
2360                    ),
2361                    DIFFSOL_BAD_ARG
2362                );
2363                #[cfg(feature = "diffsl-llvm")]
2364                if matches!(jit_backend, JitBackendType::Llvm) {
2365                    assert_eq!(
2366                        diffsol_ode_solve_fwd_sens(
2367                            ode,
2368                            params.as_ptr(),
2369                            params.len(),
2370                            t_eval.as_ptr(),
2371                            t_eval.len(),
2372                            ptr::null_mut(),
2373                        ),
2374                        DIFFSOL_BAD_ARG
2375                    );
2376                    assert_eq!(
2377                        diffsol_ode_solve_hybrid_fwd_sens(
2378                            ode,
2379                            params.as_ptr(),
2380                            params.len(),
2381                            t_eval.as_ptr(),
2382                            t_eval.len(),
2383                            ptr::null_mut(),
2384                        ),
2385                        DIFFSOL_BAD_ARG
2386                    );
2387                    let mut objective = 0.0;
2388                    let mut sens_ptr = ptr::null_mut();
2389                    assert_eq!(
2390                        diffsol_ode_solve_sum_squares_adj(
2391                            ode,
2392                            params.as_ptr(),
2393                            params.len(),
2394                            t_eval.as_ptr(),
2395                            1,
2396                            t_eval.len(),
2397                            1,
2398                            1,
2399                            t_eval.as_ptr(),
2400                            t_eval.len(),
2401                            ptr::null_mut(),
2402                            &mut sens_ptr,
2403                        ),
2404                        DIFFSOL_BAD_ARG
2405                    );
2406                    assert_eq!(
2407                        diffsol_ode_solve_sum_squares_adj(
2408                            ode,
2409                            params.as_ptr(),
2410                            params.len(),
2411                            t_eval.as_ptr(),
2412                            1,
2413                            t_eval.len(),
2414                            1,
2415                            1,
2416                            t_eval.as_ptr(),
2417                            t_eval.len(),
2418                            &mut objective,
2419                            ptr::null_mut(),
2420                        ),
2421                        DIFFSOL_BAD_ARG
2422                    );
2423                }
2424
2425                diffsol_ode_free(ode);
2426            }
2427        }
2428    }
2429
2430    #[test]
2431    fn c_api_hybrid_jit_solver_paths_match_expected_values() {
2432        for jit_backend in available_jit_backends() {
2433            unsafe {
2434                let code = CString::new(hybrid_logistic_diffsl_code()).unwrap();
2435                let ode = make_ode_ptr_with_code(
2436                    jit_backend,
2437                    code.as_ptr(),
2438                    matrix_type_to_i32(MatrixType::NalgebraDense),
2439                    linear_solver_to_i32(LinearSolverType::Default),
2440                    ode_solver_to_i32(OdeSolverType::Bdf),
2441                );
2442                assert!(!ode.is_null());
2443
2444                let params = [2.0f64];
2445                let mut solution_ptr: *mut SolutionWrapper = ptr::null_mut();
2446                assert_eq!(
2447                    diffsol_ode_solve_hybrid(
2448                        ode,
2449                        params.as_ptr(),
2450                        params.len(),
2451                        2.0,
2452                        &mut solution_ptr
2453                    ),
2454                    DIFFSOL_OK
2455                );
2456                let mut ys_ptr = ptr::null_mut();
2457                let mut ts_ptr = ptr::null_mut();
2458                assert_eq!(
2459                    diffsol_solution_wrapper_get_ys(solution_ptr, &mut ys_ptr),
2460                    DIFFSOL_OK
2461                );
2462                assert_eq!(
2463                    diffsol_solution_wrapper_get_ts(solution_ptr, &mut ts_ptr),
2464                    DIFFSOL_OK
2465                );
2466                let (_rows, cols, ys) = ffi_read_host_array_matrix(ys_ptr);
2467                let ts = ffi_read_host_array_vector(ts_ptr);
2468                assert!(cols >= 1);
2469                assert_close(*ts.last().unwrap(), 2.0, 5e-4, "jit hybrid solve time");
2470                assert_close(
2471                    *ys.last().unwrap(),
2472                    hybrid_logistic_state(2.0, 2.0),
2473                    5e-4,
2474                    "jit hybrid solve value",
2475                );
2476                ffi_free_solution(solution_ptr);
2477
2478                #[cfg(feature = "diffsl-llvm")]
2479                if matches!(jit_backend, JitBackendType::Llvm) {
2480                    let t_eval = [0.25f64, 0.5f64, 1.0f64];
2481                    let mut sens_solution_ptr: *mut SolutionWrapper = ptr::null_mut();
2482                    assert_eq!(
2483                        diffsol_ode_solve_hybrid_fwd_sens(
2484                            ode,
2485                            params.as_ptr(),
2486                            params.len(),
2487                            t_eval.as_ptr(),
2488                            t_eval.len(),
2489                            &mut sens_solution_ptr,
2490                        ),
2491                        DIFFSOL_OK
2492                    );
2493                    let mut sens_list = ptr::null_mut();
2494                    let mut sens_len = 0usize;
2495                    assert_eq!(
2496                        diffsol_solution_wrapper_get_sens(
2497                            sens_solution_ptr,
2498                            &mut sens_list,
2499                            &mut sens_len
2500                        ),
2501                        DIFFSOL_OK
2502                    );
2503                    let sens_values = ffi_read_host_array_list_matrices(sens_list, sens_len);
2504                    for (i, (&value, &t)) in sens_values[0].2.iter().zip(t_eval.iter()).enumerate()
2505                    {
2506                        assert_close(
2507                            value,
2508                            hybrid_logistic_state_dr(2.0, t),
2509                            5e-4,
2510                            &format!("jit hybrid sensitivity[{i}]"),
2511                        );
2512                    }
2513                    ffi_free_solution(sens_solution_ptr);
2514                }
2515
2516                diffsol_ode_free(ode);
2517            }
2518        }
2519    }
2520}