From 503d3cbccbb5c00926f6264b951a938edaae247b Mon Sep 17 00:00:00 2001 From: William Zijie Zhang Date: Thu, 23 Apr 2026 15:01:08 -0400 Subject: [PATCH 1/2] sync SparseDiffEngine and adapt dense matmul bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump SparseDiffEngine submodule to origin/main (45f88d0), which pulls in the new convolve atom plus dance858's cleanup of the dense matmul constructor data-pointer convention. new_left_matmul_dense / new_right_matmul_dense now require exactly one of param_node / data to be non-NULL (constants: (NULL, data); parameters: (capsule, NULL)) — the engine fprintf/exit(1)s otherwise. Update the dense branches of make_left_matmul and make_right_matmul bindings to match: drop the PARAM_FIXED wrapper we were building for the constant case, and forward data=NULL when a real parameter capsule is supplied. DNLP's helpers.py still hands in A.flatten() in both cases; the binding absorbs the new convention so no DNLP-side change is needed. Verified: SparseDiffEngine ctest (267/267), DNLP nlp_tests (217 passed, 77 skipped), DNLP test_convolution + test_atoms (119 passed). Co-Authored-By: Claude Opus 4.7 (1M context) --- SparseDiffEngine | 2 +- sparsediffpy/_bindings/atoms/left_matmul.h | 42 +++++++++------------ sparsediffpy/_bindings/atoms/right_matmul.h | 41 +++++++++----------- 3 files changed, 35 insertions(+), 50 deletions(-) diff --git a/SparseDiffEngine b/SparseDiffEngine index 63a38d2..45f88d0 160000 --- a/SparseDiffEngine +++ b/SparseDiffEngine @@ -1 +1 @@ -Subproject commit 63a38d2054d7dd82e117b2cb7f4afa802be138b6 +Subproject commit 45f88d07c1a84fda4552247d29c2a03a5d59f961 diff --git a/sparsediffpy/_bindings/atoms/left_matmul.h b/sparsediffpy/_bindings/atoms/left_matmul.h index c871ff8..3ecaeaf 100644 --- a/sparsediffpy/_bindings/atoms/left_matmul.h +++ b/sparsediffpy/_bindings/atoms/left_matmul.h @@ -119,7 +119,13 @@ static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) } else if (strcmp(fmt, "dense") == 0) { - /* Parse: param_or_none, child, "dense", A_data_flat, m, n */ + /* Parse: param_or_none, child, "dense", A_data_flat, m, n. + * + * The engine's new_left_matmul_dense convention requires exactly + * one of param_node / data to be non-NULL: constants pass + * (NULL, data); parameters pass (capsule, NULL). DNLP always + * hands in a flat A for both cases, so when a parameter capsule + * is supplied we ignore the A_data payload. */ PyObject *data_obj; int m, n; if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule, @@ -128,48 +134,34 @@ static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) return NULL; } - PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( - data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - if (!data_array) - { - return NULL; - } - - double *A_data = (double *) PyArray_DATA(data_array); - - expr *param_node = NULL; + expr *node; if (param_obj == Py_None) { - param_node = new_parameter(m * n, 1, PARAM_FIXED, - child->n_vars, A_data); - if (!param_node) + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!data_array) { - Py_DECREF(data_array); - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node"); return NULL; } + double *A_data = (double *) PyArray_DATA(data_array); + node = new_left_matmul_dense(NULL, child, m, n, A_data); + Py_DECREF(data_array); } else { - param_node = (expr *) PyCapsule_GetPointer(param_obj, - EXPR_CAPSULE_NAME); + expr *param_node = (expr *) PyCapsule_GetPointer( + param_obj, EXPR_CAPSULE_NAME); if (!param_node) { - Py_DECREF(data_array); PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); return NULL; } + node = new_left_matmul_dense(param_node, child, m, n, NULL); } - expr *node = - new_left_matmul_dense(param_node, child, m, n, A_data); - Py_DECREF(data_array); - if (!node) { - if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, "failed to create dense left_matmul node"); return NULL; diff --git a/sparsediffpy/_bindings/atoms/right_matmul.h b/sparsediffpy/_bindings/atoms/right_matmul.h index b3fa1cf..aea01c1 100644 --- a/sparsediffpy/_bindings/atoms/right_matmul.h +++ b/sparsediffpy/_bindings/atoms/right_matmul.h @@ -115,6 +115,13 @@ static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) } else if (strcmp(fmt, "dense") == 0) { + /* Parse: param_or_none, child, "dense", A_data_flat, m, n. + * + * The engine's new_right_matmul_dense convention requires exactly + * one of param_node / data to be non-NULL: constants pass + * (NULL, data); parameters pass (capsule, NULL). DNLP always + * hands in a flat A for both cases, so when a parameter capsule + * is supplied we ignore the A_data payload. */ PyObject *data_obj; int m, n; if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule, @@ -123,48 +130,34 @@ static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) return NULL; } - PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( - data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - if (!data_array) - { - return NULL; - } - - double *A_data = (double *) PyArray_DATA(data_array); - - expr *param_node = NULL; + expr *node; if (param_obj == Py_None) { - param_node = new_parameter(m * n, 1, PARAM_FIXED, - child->n_vars, A_data); - if (!param_node) + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!data_array) { - Py_DECREF(data_array); - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node"); return NULL; } + double *A_data = (double *) PyArray_DATA(data_array); + node = new_right_matmul_dense(NULL, child, m, n, A_data); + Py_DECREF(data_array); } else { - param_node = (expr *) PyCapsule_GetPointer(param_obj, - EXPR_CAPSULE_NAME); + expr *param_node = (expr *) PyCapsule_GetPointer( + param_obj, EXPR_CAPSULE_NAME); if (!param_node) { - Py_DECREF(data_array); PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); return NULL; } + node = new_right_matmul_dense(param_node, child, m, n, NULL); } - expr *node = - new_right_matmul_dense(param_node, child, m, n, A_data); - Py_DECREF(data_array); - if (!node) { - if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, "failed to create dense right_matmul node"); return NULL; From a35102c503d381cc87d22d0889bfd4b9884ae16f Mon Sep 17 00:00:00 2001 From: William Zijie Zhang Date: Thu, 23 Apr 2026 15:03:48 -0400 Subject: [PATCH 2/2] trim comments on dense matmul bindings Co-Authored-By: Claude Opus 4.7 (1M context) --- sparsediffpy/_bindings/atoms/left_matmul.h | 9 ++------- sparsediffpy/_bindings/atoms/right_matmul.h | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/sparsediffpy/_bindings/atoms/left_matmul.h b/sparsediffpy/_bindings/atoms/left_matmul.h index 3ecaeaf..597aa46 100644 --- a/sparsediffpy/_bindings/atoms/left_matmul.h +++ b/sparsediffpy/_bindings/atoms/left_matmul.h @@ -119,13 +119,8 @@ static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) } else if (strcmp(fmt, "dense") == 0) { - /* Parse: param_or_none, child, "dense", A_data_flat, m, n. - * - * The engine's new_left_matmul_dense convention requires exactly - * one of param_node / data to be non-NULL: constants pass - * (NULL, data); parameters pass (capsule, NULL). DNLP always - * hands in a flat A for both cases, so when a parameter capsule - * is supplied we ignore the A_data payload. */ + /* Engine requires (param_node, data) to be mutually exclusive; + * drop the caller's A_data when a parameter capsule is supplied. */ PyObject *data_obj; int m, n; if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule, diff --git a/sparsediffpy/_bindings/atoms/right_matmul.h b/sparsediffpy/_bindings/atoms/right_matmul.h index aea01c1..86d7201 100644 --- a/sparsediffpy/_bindings/atoms/right_matmul.h +++ b/sparsediffpy/_bindings/atoms/right_matmul.h @@ -115,13 +115,8 @@ static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) } else if (strcmp(fmt, "dense") == 0) { - /* Parse: param_or_none, child, "dense", A_data_flat, m, n. - * - * The engine's new_right_matmul_dense convention requires exactly - * one of param_node / data to be non-NULL: constants pass - * (NULL, data); parameters pass (capsule, NULL). DNLP always - * hands in a flat A for both cases, so when a parameter capsule - * is supplied we ignore the A_data payload. */ + /* Engine requires (param_node, data) to be mutually exclusive; + * drop the caller's A_data when a parameter capsule is supplied. */ PyObject *data_obj; int m, n; if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule,