Skip to content

Conversation

@balos1
Copy link
Member

@balos1 balos1 commented Nov 11, 2025

This PR adds sundials4py, which is a Python interface to SUNDIALS.

The generator code can be found here: https://github.com/sundials-codes/sundials4py-generate.

Rendered docs: https://sundials--796.org.readthedocs.build/en/796/.

Review Requests

@drreynolds:

  • doc/
  • bindings/sundials4py/arkode
  • bindings/sundials4py/idas
  • bindings/sundials4py/kinsol
  • bindings/sundials4py/sundomeigest
  • bindings/sundials4py/sunlinsol
  • bindings/sundials4py/sunmatrix
  • bindings/sundials4py/examples

@gardner48:

Everything but can skip:

  • _generated.hpp files (Steven and I will review these)
  • bindings/sundials4py/examples (Steven and Dan are both reviewing these)
  • bindings/sundials4py/arkode (Steven and Dan both reviewing these)
  • bindings/sundials4py/idas (Steven and Dan both reviewing these)

@Steven-Roberts

  • Skim _generated.hpp files in bindings/sundials4py
  • bindings/sundials4py/arkode
  • bindings/sundials4py/idas
  • bindings/sundials4py/include
  • bindings/sundials4py/sundials
  • bindings/sundials4py/nvector
  • bindings/sundials4py/sunadaptcontroller
  • bindings/sundials4py/sunadjointcheckpointscheme
  • bindings/sundials4py/sunnonlinsol
  • bindings/sundials4py/sunmemory
  • bindings/sundials4py/examples
  • https://github.com/sundials-codes/sundials4py-generate

Copy link
Collaborator

@Steven-Roberts Steven-Roberts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished first pass through the PR

Copy link
Member

@gardner48 gardner48 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished sundials directory

headers:
- ../../include/sundials/sundials_errors.h
enum_exclude_by_name__regex:
- "SUNErrCode_" # not needed in Python
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do users still have access to the SUN_ERR_* constants?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Unfortunately, the x macro we use in sundials_error.h makes it to where the generator is unable to parse them.

{
if (!sunctx->python)
{
sunctx->python = SUNContextFunctionTable_Alloc();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we explicitly write the Create wrapper and always allocate the function table?

[](SUNContext sunctx,
std::function<std::remove_pointer_t<SUNErrHandlerFn>> err_fn)
{
if (!sunctx->python)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for NULL function

Suggested change
if (!sunctx->python)
if (!err_fn) { throw sundials4py::illegal_value("err_fn was null"); }
if (!sunctx->python)

{
auto fn_table = static_cast<SUNLinearSolverFunctionTable*>(
std::malloc(sizeof(SUNLinearSolverFunctionTable)));
std::memset(fn_table, 0, sizeof(SUNLinearSolverFunctionTable));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Steven's memset issue

m.def(
"SUNLogger_QueueMsg",
[](SUNLogger logger, SUNLogLevel lvl, const char* scope, const char* label,
const char* msg_txt) -> SUNErrCode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this handle the variadic inputs?

Copy link
Member

@gardner48 gardner48 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished sundials classes starting on test directory

# Programmer(s): Cody J. Balos @ LLNL
# -----------------------------------------------------------------
# SUNDIALS Copyright Start
# Copyright (c) 2002-2025, Lawrence Livermore National Security
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update copyright

# Programmer(s): Cody J. Balos @ LLNL
# -----------------------------------------------------------------
# SUNDIALS Copyright Start
# Copyright (c) 2002-2025, Lawrence Livermore National Security
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update copyright

# Programmer(s): Cody J. Balos @ LLNL
# -----------------------------------------------------------------
# SUNDIALS Copyright Start
# Copyright (c) 2002-2025, Lawrence Livermore National Security
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update copyright

- "^SUNBandMatrix_Data$"
# We don't interface these function in Python. Instead users can index the numpy array returned by Data.
- "^SUNBandMatrix_Cols$"
- "^SUNBandMatrix_Column$"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's not already we should note in the user guide which functions are not available from python

auto owner = nb::find(A);
auto ptr = SUNDenseMatrix_Data(A);
// SUNDenseMatrix_Data returns a column-major ordered array (i.e., Fortran style)
return nb::ndarray<sunrealtype, nb::numpy, nb::ndim<2>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning this as a 2D array contradicts the comment here about matching the C function. However, reshaping is more natural in the dense case than the banded.

rows, cols, nnz = 3, 3, 4
A = SUNSparseMatrix(rows, cols, nnz, SUN_CSR_MAT, sunctx)
ret = SUNMatZero(A)
assert ret == 0
Copy link
Member

@gardner48 gardner48 Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert ret == 0
assert ret == SUN_SUCCESS
dataA = SUNSparseMatrix_Data(A)
np.testing.assert_array_equal(dataA, np.zeros_like(dataA))

Comment on lines +64 to +66
assert np.allclose(dataB, dataA)
assert np.allclose(idx_valsB, idx_vals)
assert np.allclose(idx_ptrsB, idx_ptrs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert np.allclose(dataB, dataA)
assert np.allclose(idx_valsB, idx_vals)
assert np.allclose(idx_ptrsB, idx_ptrs)
np.testing.assert_allclose(dataB, dataA)
np.testing.assert_allclose(idx_valsB, idx_vals)
np.testing.assert_allclose(idx_ptrsB, idx_ptrs)

ret = SUNMatScaleAdd(3.0, A, B)
assert ret == 0
# 3*A + B = [3+2, 3+2, 3+2, 6+4] = [5, 5, 5, 10]
assert np.allclose(dataA, [5.0, 5.0, 5.0, 10.0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert np.allclose(dataA, [5.0, 5.0, 5.0, 10.0])
np.testing.assert_allclose(dataA, [5.0, 5.0, 5.0, 10.0])

Comment on lines +104 to +107
# Diagonal elements should be 3*2+1=7, off-diagonal unchanged
# So dataA = [7.0, 7.0, 7.0, 2.0] (assuming diagonal at idx_vals[0:3])
# But since idx_vals = [0,1,2,0], diagonal is at positions 0,1,2
assert np.allclose(dataA[:3], [7.0, 7.0, 7.0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guessing this is AI not quite getting it right

Suggested change
# Diagonal elements should be 3*2+1=7, off-diagonal unchanged
# So dataA = [7.0, 7.0, 7.0, 2.0] (assuming diagonal at idx_vals[0:3])
# But since idx_vals = [0,1,2,0], diagonal is at positions 0,1,2
assert np.allclose(dataA[:3], [7.0, 7.0, 7.0])
# Diagonal elements should be 3*2+1=7, off-diagonal should be 3*2=6
np.testing.assert_allclose(dataA, [7.0, 7.0, 7.0, 6.0])

# y[0] = 1.0*x[0] = 1.0
# y[1] = 1.0*x[1] = 1.0
# y[2] = 1.0*x[2] + 2.0*x[0] = 1.0 + 2.0 = 3.0
assert np.allclose(N_VGetArrayPointer(y), [1.0, 1.0, 3.0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert np.allclose(N_VGetArrayPointer(y), [1.0, 1.0, 3.0])
np.testing.assert_allclose(N_VGetArrayPointer(y), [1.0, 1.0, 3.0])

Copy link
Member

@gardner48 gardner48 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished all of the sundials4py directory. Only the doc directory to go but I probably won't get to that today, so feel free to start pushing updates.

sol = N_VClone(y)
ode_problem.solution(y, sol, tret)

assert np.allclose(N_VGetArrayPointer(sol), N_VGetArrayPointer(y), atol=1e-2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find and replace is probably easier than suggesting all the places this occurs. My understanding is using np.testing.assert_allclose has helpful error messages when the check fails.



@pytest.mark.skipif(
sunrealtype == np.float32, reason="Test not supported for sunrealtype=np.float32"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the tolerances are precision-dependent, what's the issue testing with single precision?

status = ARKodeSStolerances(ark.get(), SUNREALTYPE_RTOL, SUNREALTYPE_ATOL)
assert status == ARK_SUCCESS

status = ARKodeSetLinearSolver(ark.get(), ls, None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to have a direct solver version of this test as well to check attaching a matrix (and maybe a Jacobian function)

sol = N_VClone(y)
ode_problem.solution(y, sol, tret)

assert np.allclose(N_VGetArrayPointer(sol), N_VGetArrayPointer(y), atol=1e-2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the comparison tolerance be tied to the precision?


[project]
name = "sundials4py"
version = "7.6.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update scripts/updateVersion.sh to update this file

Copy link
Member

@gardner48 gardner48 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished all of the sundials4py directory. Only the doc directory to go but I probably won't get to that today, so feel free to start pushing updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants