diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32b660c..6aa2425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ on: - ".github/ISSUE_TEMPLATE/**" pull_request: - branches: [main] paths-ignore: - "*.md" - "docs/**" @@ -33,7 +32,7 @@ jobs: defaults: run: - shell: bash -l {0} + shell: bash -el {0} steps: - name: Checkout repository @@ -65,7 +64,7 @@ jobs: defaults: run: - shell: bash -l {0} + shell: bash -el {0} steps: - name: Checkout repository diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c3e04..234392a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # scikit-SUNDAE Changelog +## [v1.1.1](https://github.com/NREL/scikit-sundae/tree/v1.1.1) + +### Bug Fixes +- Ensures exception propagations work correctly with numpy 2.4 releases ([#43](https://github.com/NREL/scikit-sundae/pull/43)) + ## [v1.1.0](https://github.com/NREL/scikit-sundae/tree/v1.1.0) ### New Features diff --git a/pyproject.toml b/pyproject.toml index f551d01..f4f615f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dynamic = ["version"] requires-python = ">=3.10,<3.15" license = "BSD-3-Clause" license-files = ["LICENSE*"] -description = "Python bindings to SUNDIALS differential aglebraic equation solvers." +description = "Python bindings to SUNDIALS differential algebraic equation solvers." keywords = ["sundials", "dae", "ode", "integrator", "ivp", "cvode", "ida"] authors = [ { name = "Corey R. Randall" }, diff --git a/src/sksundae/__init__.py b/src/sksundae/__init__.py index c1f501a..2263f88 100644 --- a/src/sksundae/__init__.py +++ b/src/sksundae/__init__.py @@ -62,4 +62,4 @@ __all__ = ['ida', 'utils', 'cvode', 'jacband', 'SUNDIALS_VERSION'] -__version__ = '1.1.0' +__version__ = '1.1.1' diff --git a/src/sksundae/_cy_common.pyx b/src/sksundae/_cy_common.pyx index 7a97771..23252c8 100644 --- a/src/sksundae/_cy_common.pyx +++ b/src/sksundae/_cy_common.pyx @@ -39,7 +39,7 @@ elif SUNDIALS_INT_TYPE == "long int": cdef svec2np(N_Vector nvec, np.ndarray[DTYPE_t, ndim=1] np_array): """Fill a numpy array with values from an N_Vector.""" - cdef sunrealtype* nvec_ptr + cdef sunrealtype* nv_ptr nv_ptr = N_VGetArrayPointer(nvec) ptr2np(nv_ptr, np_array) diff --git a/src/sksundae/_cy_cvode.pyx b/src/sksundae/_cy_cvode.pyx index 76d978a..2228c67 100644 --- a/src/sksundae/_cy_cvode.pyx +++ b/src/sksundae/_cy_cvode.pyx @@ -17,7 +17,13 @@ cimport numpy as np from scipy import sparse as sp from scipy.optimize._numdiff import group_columns -from cpython.exc cimport PyErr_CheckSignals, PyErr_Occurred +from cpython.exc cimport ( + PyErr_Fetch, PyErr_NormalizeException, + PyObject, PyErr_CheckSignals, PyErr_Occurred, # PyErr_GetRaisedException, +) + +# PyErr_Fetch and PyErr_NormalizeException are deprecated at 3.12. When support +# for <3.12 is dropped, replace with PyErr_GetRaisedException. # Extern cdef headers from .c_cvode cimport * @@ -235,8 +241,18 @@ cdef void _err_handler(int line, const char* func, const char* file, const char* msg, int err_code, void* err_user_data, SUNContext ctx) except *: """Custom error handler for shorter messages (no line or file).""" - - if not PyErr_Occurred(): + cdef PyObject *errtype, *errvalue, *errtraceback + + if PyErr_Occurred(): + aux = err_user_data + # aux.pyerr = PyErr_GetRaisedException() + + PyErr_Fetch(&errtype, &errvalue, &errtraceback) + PyErr_NormalizeException(&errtype, &errvalue, &errtraceback) + + aux.pyerr = errvalue + + else: decoded_func = func.decode("utf-8") decoded_msg = msg.decode("utf-8").replace(", ,", ",").strip() print(f"\n[{decoded_func}, Error: {err_code}] {decoded_msg}\n") @@ -262,6 +278,7 @@ cdef class AuxData: cdef bint with_userdata cdef bint is_constrained + cdef object pyerr # Exception cdef object rhsfn # Callable cdef object userdata # Any cdef object eventsfn # Callable @@ -272,6 +289,7 @@ cdef class AuxData: cdef object jactimes # CVODEJacTimes def __cinit__(self, sunindextype NEQ, object options): + self.pyerr = None self.np_yy = np.empty(NEQ, DTYPE) self.np_yp = np.empty(NEQ, DTYPE) @@ -740,7 +758,7 @@ cdef class CVODE: # 16) Set optional inputs SUNContext_ClearErrHandlers(self.ctx) - SUNContext_PushErrHandler(self.ctx, _err_handler, NULL) + SUNContext_PushErrHandler(self.ctx, _err_handler, self.aux) cdef sunrealtype first_step = self._options["first_step"] flag = CVodeSetInitStep(self.mem, first_step) @@ -921,10 +939,12 @@ cdef class CVODE: ind += 1 - if stop: - break + if self.aux.pyerr is not None: + raise self.aux.pyerr elif PyErr_CheckSignals() == -1: return + elif stop: + break if self.aux.eventsfn: i_ev, t_ev, y_ev = _collect_events(self.aux) @@ -1004,10 +1024,12 @@ cdef class CVODE: ind += 1 - if stop: - break + if self.aux.pyerr is not None: + raise self.aux.pyerr elif PyErr_CheckSignals() == -1: return + elif stop: + break if self.aux.eventsfn: i_ev, t_ev, y_ev = _collect_events(self.aux) diff --git a/src/sksundae/_cy_ida.pyx b/src/sksundae/_cy_ida.pyx index 995289d..32bafba 100644 --- a/src/sksundae/_cy_ida.pyx +++ b/src/sksundae/_cy_ida.pyx @@ -17,7 +17,13 @@ cimport numpy as np from scipy import sparse as sp from scipy.optimize._numdiff import group_columns -from cpython.exc cimport PyErr_CheckSignals, PyErr_Occurred +from cpython.exc cimport ( + PyErr_Fetch, PyErr_NormalizeException, + PyObject, PyErr_CheckSignals, PyErr_Occurred, # PyErr_GetRaisedException, +) + +# PyErr_Fetch and PyErr_NormalizeException are deprecated at 3.12. When support +# for <3.12 is dropped, replace with PyErr_GetRaisedException. # Extern cdef headers from .c_ida cimport * @@ -239,8 +245,18 @@ cdef void _err_handler(int line, const char* func, const char* file, const char* msg, int err_code, void* err_user_data, SUNContext ctx) except *: """Custom error handler for shorter messages (no line or file).""" - - if not PyErr_Occurred(): + cdef PyObject *errtype, *errvalue, *errtraceback + + if PyErr_Occurred(): + aux = err_user_data + # aux.pyerr = PyErr_GetRaisedException() + + PyErr_Fetch(&errtype, &errvalue, &errtraceback) + PyErr_NormalizeException(&errtype, &errvalue, &errtraceback) + + aux.pyerr = errvalue + + else: decoded_func = func.decode("utf-8") decoded_msg = msg.decode("utf-8").replace(", ,", ",").strip() print(f"\n[{decoded_func}, Error: {err_code}] {decoded_msg}\n") @@ -267,6 +283,7 @@ cdef class AuxData: cdef bint with_userdata cdef bint is_constrained + cdef object pyerr # Exception cdef object resfn # Callable cdef object userdata # Any cdef object eventsfn # Callable @@ -277,6 +294,7 @@ cdef class AuxData: cdef object jactimes # IDAJacTimes def __cinit__(self, sunindextype NEQ, object options): + self.pyerr = None self.np_yy = np.empty(NEQ, DTYPE) self.np_yp = np.empty(NEQ, DTYPE) self.np_rr = np.empty(NEQ, DTYPE) @@ -764,7 +782,9 @@ cdef class IDA: # 15) Set optional inputs SUNContext_ClearErrHandlers(self.ctx) - SUNContext_PushErrHandler(self.ctx, _err_handler, NULL) + SUNContext_PushErrHandler(self.ctx, _err_handler, self.aux) + + # Set algebraic variable indices np_algidx = np.ones(self.NEQ, DTYPE) if self._options["algebraic_idx"] is not None: @@ -992,10 +1012,12 @@ cdef class IDA: ind += 1 - if stop: - break + if self.aux.pyerr is not None: + raise self.aux.pyerr elif PyErr_CheckSignals() == -1: return + elif stop: + break if self.aux.eventsfn: i_ev, t_ev, y_ev, yp_ev = _collect_events(self.aux) @@ -1083,10 +1105,12 @@ cdef class IDA: ind += 1 - if stop: - break + if self.aux.pyerr is not None: + raise self.aux.pyerr elif PyErr_CheckSignals() == -1: return + elif stop: + break if self.aux.eventsfn: i_ev, t_ev, y_ev, yp_ev = _collect_events(self.aux)