From 15cb7c9fb5833403861098bea47941235c991c23 Mon Sep 17 00:00:00 2001 From: William Zijie Zhang Date: Fri, 27 Mar 2026 11:28:17 -0400 Subject: [PATCH 1/3] Add parameter support to Python bindings Expose the SparseDiffEngine parameter API to Python, enabling updatable problem parameters that can be changed between solves without reconstructing the expression tree. New bindings: make_parameter, make_param_scalar_mult, make_param_vector_mult, problem_register_params, problem_update_params. Existing constant/matmul bindings updated to use the unified parameter API internally (backward compatible). Also fixes stale elementwise_univariate.h includes renamed in the engine. Co-Authored-By: Claude Opus 4.6 (1M context) --- SparseDiffEngine | 2 +- .../_bindings/atoms/const_scalar_mult.h | 11 ++- .../_bindings/atoms/const_vector_mult.h | 12 ++- sparsediffpy/_bindings/atoms/constant.h | 4 +- sparsediffpy/_bindings/atoms/dense_matmul.h | 74 +++++++++++++++++-- sparsediffpy/_bindings/atoms/left_matmul.h | 50 ++++++++++++- sparsediffpy/_bindings/atoms/parameter.h | 24 ++++++ sparsediffpy/_bindings/atoms/right_matmul.h | 53 +++++++++++-- sparsediffpy/_bindings/atoms/scalar_mult.h | 45 +++++++++++ sparsediffpy/_bindings/atoms/vector_mult.h | 45 +++++++++++ sparsediffpy/_bindings/bindings.c | 15 ++++ .../_bindings/problem/register_params.h | 61 +++++++++++++++ .../_bindings/problem/update_params.h | 37 ++++++++++ 13 files changed, 412 insertions(+), 21 deletions(-) create mode 100644 sparsediffpy/_bindings/atoms/parameter.h create mode 100644 sparsediffpy/_bindings/atoms/scalar_mult.h create mode 100644 sparsediffpy/_bindings/atoms/vector_mult.h create mode 100644 sparsediffpy/_bindings/problem/register_params.h create mode 100644 sparsediffpy/_bindings/problem/update_params.h diff --git a/SparseDiffEngine b/SparseDiffEngine index b9d282f..85f456a 160000 --- a/SparseDiffEngine +++ b/SparseDiffEngine @@ -1 +1 @@ -Subproject commit b9d282f04d4347af2420e64a48e00c2c2e5da882 +Subproject commit 85f456aff2dab5bfdeed1cf636052d227c331018 diff --git a/sparsediffpy/_bindings/atoms/const_scalar_mult.h b/sparsediffpy/_bindings/atoms/const_scalar_mult.h index 121e0c7..521a78f 100644 --- a/sparsediffpy/_bindings/atoms/const_scalar_mult.h +++ b/sparsediffpy/_bindings/atoms/const_scalar_mult.h @@ -22,9 +22,18 @@ static PyObject *py_make_const_scalar_mult(PyObject *self, PyObject *args) return NULL; } - expr *node = new_const_scalar_mult(a, child); + expr *a_node = new_parameter(1, 1, PARAM_FIXED, child->n_vars, &a); + if (!a_node) + { + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for scalar"); + return NULL; + } + + expr *node = new_scalar_mult(a_node, child); if (!node) { + free_expr(a_node); PyErr_SetString(PyExc_RuntimeError, "failed to create const_scalar_mult node"); return NULL; diff --git a/sparsediffpy/_bindings/atoms/const_vector_mult.h b/sparsediffpy/_bindings/atoms/const_vector_mult.h index ddb34f7..33e8ae6 100644 --- a/sparsediffpy/_bindings/atoms/const_vector_mult.h +++ b/sparsediffpy/_bindings/atoms/const_vector_mult.h @@ -42,12 +42,22 @@ static PyObject *py_make_const_vector_mult(PyObject *self, PyObject *args) double *a_data = (double *) PyArray_DATA(a_array); - expr *node = new_const_vector_mult(a_data, child); + expr *a_node = + new_parameter(a_size, 1, PARAM_FIXED, child->n_vars, a_data); Py_DECREF(a_array); + if (!a_node) + { + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for vector"); + return NULL; + } + + expr *node = new_vector_mult(a_node, child); if (!node) { + free_expr(a_node); PyErr_SetString(PyExc_RuntimeError, "failed to create const_vector_mult node"); return NULL; diff --git a/sparsediffpy/_bindings/atoms/constant.h b/sparsediffpy/_bindings/atoms/constant.h index d5fcba3..d820f09 100644 --- a/sparsediffpy/_bindings/atoms/constant.h +++ b/sparsediffpy/_bindings/atoms/constant.h @@ -19,8 +19,8 @@ static PyObject *py_make_constant(PyObject *self, PyObject *args) return NULL; } - expr *node = - new_constant(d1, d2, n_vars, (const double *) PyArray_DATA(values_array)); + expr *node = new_parameter(d1, d2, PARAM_FIXED, n_vars, + (const double *) PyArray_DATA(values_array)); Py_DECREF(values_array); if (!node) diff --git a/sparsediffpy/_bindings/atoms/dense_matmul.h b/sparsediffpy/_bindings/atoms/dense_matmul.h index 48a8ba9..ee4e0d5 100644 --- a/sparsediffpy/_bindings/atoms/dense_matmul.h +++ b/sparsediffpy/_bindings/atoms/dense_matmul.h @@ -7,17 +7,20 @@ /* Dense left matrix multiplication: A @ f(x) where A is a dense matrix. * * Python signature: - * make_dense_left_matmul(child, A_data_flat, m, n) + * make_dense_left_matmul(param_or_none, child, A_data_flat, m, n) * + * - param_or_none: None for constant matrix, or a parameter capsule. * - child: the child expression capsule f(x). * - A_data_flat: contiguous row-major numpy float64 array of size m*n. * - m, n: dimensions of matrix A. */ static PyObject *py_make_dense_left_matmul(PyObject *self, PyObject *args) { + PyObject *param_obj; PyObject *child_capsule; PyObject *data_obj; int m, n; - if (!PyArg_ParseTuple(args, "OOii", &child_capsule, &data_obj, &m, &n)) + if (!PyArg_ParseTuple(args, "OOOii", ¶m_obj, &child_capsule, + &data_obj, &m, &n)) { return NULL; } @@ -38,11 +41,38 @@ static PyObject *py_make_dense_left_matmul(PyObject *self, PyObject *args) double *A_data = (double *) PyArray_DATA(data_array); - expr *node = new_left_matmul_dense(child, m, n, A_data); + /* Build the parameter node: use provided capsule or create PARAM_FIXED */ + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = + new_parameter(m * n, 1, PARAM_FIXED, child->n_vars, A_data); + if (!param_node) + { + Py_DECREF(data_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for dense matrix"); + return NULL; + } + } + else + { + 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; + } + } + + 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; @@ -54,17 +84,20 @@ static PyObject *py_make_dense_left_matmul(PyObject *self, PyObject *args) /* Dense right matrix multiplication: f(x) @ A where A is a dense matrix. * * Python signature: - * make_dense_right_matmul(child, A_data_flat, m, n) + * make_dense_right_matmul(param_or_none, child, A_data_flat, m, n) * + * - param_or_none: None for constant matrix, or a parameter capsule. * - child: the child expression capsule f(x). * - A_data_flat: contiguous row-major numpy float64 array of size m*n. * - m, n: dimensions of matrix A. */ static PyObject *py_make_dense_right_matmul(PyObject *self, PyObject *args) { + PyObject *param_obj; PyObject *child_capsule; PyObject *data_obj; int m, n; - if (!PyArg_ParseTuple(args, "OOii", &child_capsule, &data_obj, &m, &n)) + if (!PyArg_ParseTuple(args, "OOOii", ¶m_obj, &child_capsule, + &data_obj, &m, &n)) { return NULL; } @@ -85,11 +118,38 @@ static PyObject *py_make_dense_right_matmul(PyObject *self, PyObject *args) double *A_data = (double *) PyArray_DATA(data_array); - expr *node = new_right_matmul_dense(child, m, n, A_data); + /* Build the parameter node: use provided capsule or create PARAM_FIXED */ + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = + new_parameter(m * n, 1, PARAM_FIXED, child->n_vars, A_data); + if (!param_node) + { + Py_DECREF(data_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for dense matrix"); + return NULL; + } + } + else + { + 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; + } + } + + 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; @@ -98,4 +158,4 @@ static PyObject *py_make_dense_right_matmul(PyObject *self, PyObject *args) return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); } -#endif /* ATOM_DENSE_MATMUL_H */ \ No newline at end of file +#endif /* ATOM_DENSE_MATMUL_H */ diff --git a/sparsediffpy/_bindings/atoms/left_matmul.h b/sparsediffpy/_bindings/atoms/left_matmul.h index 1e013c9..0157e59 100644 --- a/sparsediffpy/_bindings/atoms/left_matmul.h +++ b/sparsediffpy/_bindings/atoms/left_matmul.h @@ -4,14 +4,24 @@ #include "bivariate_full_dom.h" #include "common.h" -/* Left matrix multiplication: A @ f(x) where A is a constant matrix */ +/* Left matrix multiplication: A @ f(x) where A is a constant or parameter + * sparse matrix. + * + * Python signature: + * make_sparse_left_matmul(param_or_none, child, data, indices, indptr, m, n) + * + * - param_or_none: None for constant matrix, or a parameter capsule. + * - child: the child expression capsule f(x). + * - data, indices, indptr: CSR arrays for matrix A. + * - m, n: dimensions of matrix A. */ static PyObject *py_make_sparse_left_matmul(PyObject *self, PyObject *args) { + PyObject *param_obj; PyObject *child_capsule; PyObject *data_obj, *indices_obj, *indptr_obj; int m, n; - if (!PyArg_ParseTuple(args, "OOOOii", &child_capsule, &data_obj, &indices_obj, - &indptr_obj, &m, &n)) + if (!PyArg_ParseTuple(args, "OOOOOii", ¶m_obj, &child_capsule, + &data_obj, &indices_obj, &indptr_obj, &m, &n)) { return NULL; } @@ -39,6 +49,37 @@ static PyObject *py_make_sparse_left_matmul(PyObject *self, PyObject *args) } int nnz = (int) PyArray_SIZE(data_array); + + /* Build the parameter node: use provided capsule or create PARAM_FIXED */ + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter(nnz, 1, PARAM_FIXED, child->n_vars, + (const double *) PyArray_DATA(data_array)); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for matrix"); + return NULL; + } + } + else + { + param_node = + (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); + return NULL; + } + } + CSR_Matrix *A = new_csr_matrix(m, n, nnz); memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); @@ -48,11 +89,12 @@ static PyObject *py_make_sparse_left_matmul(PyObject *self, PyObject *args) Py_DECREF(indices_array); Py_DECREF(indptr_array); - expr *node = new_left_matmul(child, A); + expr *node = new_left_matmul(param_node, child, A); free_csr_matrix(A); if (!node) { + if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, "failed to create left_matmul node"); return NULL; } diff --git a/sparsediffpy/_bindings/atoms/parameter.h b/sparsediffpy/_bindings/atoms/parameter.h new file mode 100644 index 0000000..940bf59 --- /dev/null +++ b/sparsediffpy/_bindings/atoms/parameter.h @@ -0,0 +1,24 @@ +#ifndef ATOM_PARAMETER_H +#define ATOM_PARAMETER_H + +#include "common.h" + +static PyObject *py_make_parameter(PyObject *self, PyObject *args) +{ + int d1, d2, param_id, n_vars; + if (!PyArg_ParseTuple(args, "iiii", &d1, &d2, ¶m_id, &n_vars)) + { + return NULL; + } + + expr *node = new_parameter(d1, d2, param_id, n_vars, NULL); + if (!node) + { + PyErr_SetString(PyExc_RuntimeError, "failed to create parameter node"); + return NULL; + } + expr_retain(node); /* Capsule owns a reference */ + return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); +} + +#endif /* ATOM_PARAMETER_H */ diff --git a/sparsediffpy/_bindings/atoms/right_matmul.h b/sparsediffpy/_bindings/atoms/right_matmul.h index 3547837..07ac68f 100644 --- a/sparsediffpy/_bindings/atoms/right_matmul.h +++ b/sparsediffpy/_bindings/atoms/right_matmul.h @@ -4,14 +4,24 @@ #include "bivariate_full_dom.h" #include "common.h" -/* Right matrix multiplication: f(x) @ A where A is a constant matrix */ +/* Right matrix multiplication: f(x) @ A where A is a constant or parameter + * sparse matrix. + * + * Python signature: + * make_sparse_right_matmul(param_or_none, child, data, indices, indptr, m, n) + * + * - param_or_none: None for constant matrix, or a parameter capsule. + * - child: the child expression capsule f(x). + * - data, indices, indptr: CSR arrays for matrix A. + * - m, n: dimensions of matrix A. */ static PyObject *py_make_sparse_right_matmul(PyObject *self, PyObject *args) { + PyObject *param_obj; PyObject *child_capsule; PyObject *data_obj, *indices_obj, *indptr_obj; int m, n; - if (!PyArg_ParseTuple(args, "OOOOii", &child_capsule, &data_obj, &indices_obj, - &indptr_obj, &m, &n)) + if (!PyArg_ParseTuple(args, "OOOOOii", ¶m_obj, &child_capsule, + &data_obj, &indices_obj, &indptr_obj, &m, &n)) { return NULL; } @@ -39,6 +49,37 @@ static PyObject *py_make_sparse_right_matmul(PyObject *self, PyObject *args) } int nnz = (int) PyArray_SIZE(data_array); + + /* Build the parameter node: use provided capsule or create PARAM_FIXED */ + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter(nnz, 1, PARAM_FIXED, child->n_vars, + (const double *) PyArray_DATA(data_array)); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node for matrix"); + return NULL; + } + } + else + { + param_node = + (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); + return NULL; + } + } + CSR_Matrix *A = new_csr_matrix(m, n, nnz); memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); @@ -48,12 +89,14 @@ static PyObject *py_make_sparse_right_matmul(PyObject *self, PyObject *args) Py_DECREF(indices_array); Py_DECREF(indptr_array); - expr *node = new_right_matmul(child, A); + expr *node = new_right_matmul(param_node, child, A); free_csr_matrix(A); if (!node) { - PyErr_SetString(PyExc_RuntimeError, "failed to create right_matmul node"); + if (param_obj == Py_None) free_expr(param_node); + PyErr_SetString(PyExc_RuntimeError, + "failed to create right_matmul node"); return NULL; } expr_retain(node); /* Capsule owns a reference */ diff --git a/sparsediffpy/_bindings/atoms/scalar_mult.h b/sparsediffpy/_bindings/atoms/scalar_mult.h new file mode 100644 index 0000000..133ca11 --- /dev/null +++ b/sparsediffpy/_bindings/atoms/scalar_mult.h @@ -0,0 +1,45 @@ +#ifndef ATOM_SCALAR_MULT_H +#define ATOM_SCALAR_MULT_H + +#include "common.h" + +/* Parameter scalar multiplication: a * f(x) where a comes from a parameter node + */ +static PyObject *py_make_param_scalar_mult(PyObject *self, PyObject *args) +{ + PyObject *param_capsule; + PyObject *child_capsule; + + if (!PyArg_ParseTuple(args, "OO", ¶m_capsule, &child_capsule)) + { + return NULL; + } + + expr *param_node = + (expr *) PyCapsule_GetPointer(param_capsule, EXPR_CAPSULE_NAME); + if (!param_node) + { + PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); + return NULL; + } + + expr *child = + (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); + if (!child) + { + PyErr_SetString(PyExc_ValueError, "invalid child capsule"); + return NULL; + } + + expr *node = new_scalar_mult(param_node, child); + if (!node) + { + PyErr_SetString(PyExc_RuntimeError, + "failed to create scalar_mult node"); + return NULL; + } + expr_retain(node); /* Capsule owns a reference */ + return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); +} + +#endif /* ATOM_SCALAR_MULT_H */ diff --git a/sparsediffpy/_bindings/atoms/vector_mult.h b/sparsediffpy/_bindings/atoms/vector_mult.h new file mode 100644 index 0000000..a34d831 --- /dev/null +++ b/sparsediffpy/_bindings/atoms/vector_mult.h @@ -0,0 +1,45 @@ +#ifndef ATOM_VECTOR_MULT_H +#define ATOM_VECTOR_MULT_H + +#include "common.h" + +/* Parameter vector elementwise multiplication: a . f(x) where a comes from a + * parameter node */ +static PyObject *py_make_param_vector_mult(PyObject *self, PyObject *args) +{ + PyObject *param_capsule; + PyObject *child_capsule; + + if (!PyArg_ParseTuple(args, "OO", ¶m_capsule, &child_capsule)) + { + return NULL; + } + + expr *param_node = + (expr *) PyCapsule_GetPointer(param_capsule, EXPR_CAPSULE_NAME); + if (!param_node) + { + PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); + return NULL; + } + + expr *child = + (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); + if (!child) + { + PyErr_SetString(PyExc_ValueError, "invalid child capsule"); + return NULL; + } + + expr *node = new_vector_mult(param_node, child); + if (!node) + { + PyErr_SetString(PyExc_RuntimeError, + "failed to create vector_mult node"); + return NULL; + } + expr_retain(node); /* Capsule owns a reference */ + return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); +} + +#endif /* ATOM_VECTOR_MULT_H */ diff --git a/sparsediffpy/_bindings/bindings.c b/sparsediffpy/_bindings/bindings.c index e7b30f5..d93f112 100644 --- a/sparsediffpy/_bindings/bindings.c +++ b/sparsediffpy/_bindings/bindings.c @@ -24,6 +24,7 @@ #include "atoms/matmul.h" #include "atoms/multiply.h" #include "atoms/neg.h" +#include "atoms/parameter.h" #include "atoms/normal_cdf.h" #include "atoms/power.h" #include "atoms/prod.h" @@ -37,6 +38,7 @@ #include "atoms/rel_entr_vector_scalar.h" #include "atoms/reshape.h" #include "atoms/right_matmul.h" +#include "atoms/scalar_mult.h" #include "atoms/sin.h" #include "atoms/sinh.h" #include "atoms/sum.h" @@ -45,6 +47,7 @@ #include "atoms/trace.h" #include "atoms/transpose.h" #include "atoms/variable.h" +#include "atoms/vector_mult.h" #include "atoms/xexp.h" /* Include problem bindings */ @@ -57,6 +60,8 @@ #include "problem/jacobian.h" #include "problem/make_problem.h" #include "problem/objective_forward.h" +#include "problem/register_params.h" +#include "problem/update_params.h" static int numpy_initialized = 0; @@ -70,6 +75,8 @@ static int ensure_numpy(void) static PyMethodDef DNLPMethods[] = { {"make_variable", py_make_variable, METH_VARARGS, "Create variable node"}, + {"make_parameter", py_make_parameter, METH_VARARGS, + "Create updatable parameter node"}, {"make_constant", py_make_constant, METH_VARARGS, "Create constant node"}, {"make_log", py_make_log, METH_VARARGS, "Create log node"}, {"make_exp", py_make_exp, METH_VARARGS, "Create exp node"}, @@ -92,6 +99,10 @@ static PyMethodDef DNLPMethods[] = { "Create constant scalar multiplication node (a * f(x))"}, {"make_const_vector_mult", py_make_const_vector_mult, METH_VARARGS, "Create constant vector multiplication node (a ∘ f(x))"}, + {"make_param_scalar_mult", py_make_param_scalar_mult, METH_VARARGS, + "Create parameter scalar multiplication node (a * f(x))"}, + {"make_param_vector_mult", py_make_param_vector_mult, METH_VARARGS, + "Create parameter vector elementwise multiplication node (a ∘ f(x))"}, {"make_power", py_make_power, METH_VARARGS, "Create power node"}, {"make_prod", py_make_prod, METH_VARARGS, "Create prod node"}, {"make_prod_axis_zero", py_make_prod_axis_zero, METH_VARARGS, @@ -168,6 +179,10 @@ static PyMethodDef DNLPMethods[] = { "Get Hessian sparsity in COO format (lower triangular)"}, {"problem_eval_hessian_vals_coo", py_problem_eval_hessian_vals_coo, METH_VARARGS, "Evaluate Hessian and return COO values array"}, + {"problem_register_params", py_problem_register_params, METH_VARARGS, + "Register parameter nodes with a problem"}, + {"problem_update_params", py_problem_update_params, METH_VARARGS, + "Update parameter values from theta array"}, {NULL, NULL, 0, NULL}}; static struct PyModuleDef sparsediffpy_module = { diff --git a/sparsediffpy/_bindings/problem/register_params.h b/sparsediffpy/_bindings/problem/register_params.h new file mode 100644 index 0000000..346eb33 --- /dev/null +++ b/sparsediffpy/_bindings/problem/register_params.h @@ -0,0 +1,61 @@ +#ifndef PROBLEM_REGISTER_PARAMS_H +#define PROBLEM_REGISTER_PARAMS_H + +#include "common.h" + +static PyObject *py_problem_register_params(PyObject *self, PyObject *args) +{ + PyObject *problem_capsule; + PyObject *param_list; + + if (!PyArg_ParseTuple(args, "OO", &problem_capsule, ¶m_list)) + { + return NULL; + } + + problem *prob = (problem *) PyCapsule_GetPointer(problem_capsule, + PROBLEM_CAPSULE_NAME); + if (!prob) + { + PyErr_SetString(PyExc_ValueError, "invalid problem capsule"); + return NULL; + } + + if (!PyList_Check(param_list)) + { + PyErr_SetString(PyExc_TypeError, + "second argument must be a list of parameter capsules"); + return NULL; + } + + Py_ssize_t n = PyList_Size(param_list); + expr **param_nodes = (expr **) malloc(n * sizeof(expr *)); + if (!param_nodes) + { + PyErr_SetString(PyExc_MemoryError, + "failed to allocate parameter node array"); + return NULL; + } + + for (Py_ssize_t i = 0; i < n; i++) + { + PyObject *capsule = PyList_GetItem(param_list, i); + expr *node = + (expr *) PyCapsule_GetPointer(capsule, EXPR_CAPSULE_NAME); + if (!node) + { + free(param_nodes); + PyErr_SetString(PyExc_ValueError, + "invalid parameter capsule in list"); + return NULL; + } + param_nodes[i] = node; + } + + problem_register_params(prob, param_nodes, (int) n); + free(param_nodes); + + Py_RETURN_NONE; +} + +#endif /* PROBLEM_REGISTER_PARAMS_H */ diff --git a/sparsediffpy/_bindings/problem/update_params.h b/sparsediffpy/_bindings/problem/update_params.h new file mode 100644 index 0000000..547de0c --- /dev/null +++ b/sparsediffpy/_bindings/problem/update_params.h @@ -0,0 +1,37 @@ +#ifndef PROBLEM_UPDATE_PARAMS_H +#define PROBLEM_UPDATE_PARAMS_H + +#include "common.h" + +static PyObject *py_problem_update_params(PyObject *self, PyObject *args) +{ + PyObject *problem_capsule; + PyObject *theta_obj; + + if (!PyArg_ParseTuple(args, "OO", &problem_capsule, &theta_obj)) + { + return NULL; + } + + problem *prob = (problem *) PyCapsule_GetPointer(problem_capsule, + PROBLEM_CAPSULE_NAME); + if (!prob) + { + PyErr_SetString(PyExc_ValueError, "invalid problem capsule"); + return NULL; + } + + PyArrayObject *theta_array = (PyArrayObject *) PyArray_FROM_OTF( + theta_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!theta_array) + { + return NULL; + } + + problem_update_params(prob, (const double *) PyArray_DATA(theta_array)); + Py_DECREF(theta_array); + + Py_RETURN_NONE; +} + +#endif /* PROBLEM_UPDATE_PARAMS_H */ From d3405f7721a1e2e0aea9e47c8b3ed45ee8dc0436 Mon Sep 17 00:00:00 2001 From: William Zijie Zhang Date: Sun, 29 Mar 2026 17:27:54 -0400 Subject: [PATCH 2/3] Simplify Python bindings: remove redundant wrappers and unify variants - Unify make_constant into make_parameter with optional values arg - Remove make_const_scalar_mult and make_const_vector_mult (use make_parameter + param variant) - Unify rel_entr/rel_entr_scalar_vector/rel_entr_vector_scalar into single make_rel_entr with auto-dispatch - Unify sparse/dense left/right matmul into make_left_matmul/make_right_matmul with format string Co-Authored-By: Claude Opus 4.6 (1M context) --- .../_bindings/atoms/const_scalar_mult.h | 45 ---- .../_bindings/atoms/const_vector_mult.h | 69 ------ sparsediffpy/_bindings/atoms/constant.h | 35 --- sparsediffpy/_bindings/atoms/dense_matmul.h | 161 ------------- sparsediffpy/_bindings/atoms/left_matmul.h | 222 +++++++++++++----- sparsediffpy/_bindings/atoms/parameter.h | 29 ++- sparsediffpy/_bindings/atoms/rel_entr.h | 24 +- .../_bindings/atoms/rel_entr_scalar_vector.h | 37 --- .../_bindings/atoms/rel_entr_vector_scalar.h | 37 --- sparsediffpy/_bindings/atoms/right_matmul.h | 218 ++++++++++++----- sparsediffpy/_bindings/bindings.c | 31 +-- 11 files changed, 369 insertions(+), 539 deletions(-) delete mode 100644 sparsediffpy/_bindings/atoms/const_scalar_mult.h delete mode 100644 sparsediffpy/_bindings/atoms/const_vector_mult.h delete mode 100644 sparsediffpy/_bindings/atoms/constant.h delete mode 100644 sparsediffpy/_bindings/atoms/dense_matmul.h delete mode 100644 sparsediffpy/_bindings/atoms/rel_entr_scalar_vector.h delete mode 100644 sparsediffpy/_bindings/atoms/rel_entr_vector_scalar.h diff --git a/sparsediffpy/_bindings/atoms/const_scalar_mult.h b/sparsediffpy/_bindings/atoms/const_scalar_mult.h deleted file mode 100644 index 521a78f..0000000 --- a/sparsediffpy/_bindings/atoms/const_scalar_mult.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef ATOM_CONST_SCALAR_MULT_H -#define ATOM_CONST_SCALAR_MULT_H - -#include "bivariate_full_dom.h" -#include "common.h" - -/* Constant scalar multiplication: a * f(x) where a is a constant double */ -static PyObject *py_make_const_scalar_mult(PyObject *self, PyObject *args) -{ - PyObject *child_capsule; - double a; - - if (!PyArg_ParseTuple(args, "Od", &child_capsule, &a)) - { - return NULL; - } - - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - return NULL; - } - - expr *a_node = new_parameter(1, 1, PARAM_FIXED, child->n_vars, &a); - if (!a_node) - { - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for scalar"); - return NULL; - } - - expr *node = new_scalar_mult(a_node, child); - if (!node) - { - free_expr(a_node); - PyErr_SetString(PyExc_RuntimeError, - "failed to create const_scalar_mult node"); - return NULL; - } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_CONST_SCALAR_MULT_H */ diff --git a/sparsediffpy/_bindings/atoms/const_vector_mult.h b/sparsediffpy/_bindings/atoms/const_vector_mult.h deleted file mode 100644 index 33e8ae6..0000000 --- a/sparsediffpy/_bindings/atoms/const_vector_mult.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef ATOM_CONST_VECTOR_MULT_H -#define ATOM_CONST_VECTOR_MULT_H - -#include "bivariate_full_dom.h" -#include "common.h" - -/* Constant vector elementwise multiplication: a ∘ f(x) where a is a constant vector - */ -static PyObject *py_make_const_vector_mult(PyObject *self, PyObject *args) -{ - PyObject *child_capsule; - PyObject *a_obj; - - if (!PyArg_ParseTuple(args, "OO", &child_capsule, &a_obj)) - { - return NULL; - } - - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - return NULL; - } - - PyArrayObject *a_array = - (PyArrayObject *) PyArray_FROM_OTF(a_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - - if (!a_array) - { - return NULL; - } - - /* Verify size matches child size */ - int a_size = (int) PyArray_SIZE(a_array); - if (a_size != child->size) - { - Py_DECREF(a_array); - PyErr_SetString(PyExc_ValueError, "vector 'a' size must match child size"); - return NULL; - } - - double *a_data = (double *) PyArray_DATA(a_array); - - expr *a_node = - new_parameter(a_size, 1, PARAM_FIXED, child->n_vars, a_data); - - Py_DECREF(a_array); - - if (!a_node) - { - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for vector"); - return NULL; - } - - expr *node = new_vector_mult(a_node, child); - if (!node) - { - free_expr(a_node); - PyErr_SetString(PyExc_RuntimeError, - "failed to create const_vector_mult node"); - return NULL; - } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_CONST_VECTOR_MULT_H */ diff --git a/sparsediffpy/_bindings/atoms/constant.h b/sparsediffpy/_bindings/atoms/constant.h deleted file mode 100644 index d820f09..0000000 --- a/sparsediffpy/_bindings/atoms/constant.h +++ /dev/null @@ -1,35 +0,0 @@ -#ifndef ATOM_CONSTANT_H -#define ATOM_CONSTANT_H - -#include "common.h" - -static PyObject *py_make_constant(PyObject *self, PyObject *args) -{ - int d1, d2, n_vars; - PyObject *values_obj; - if (!PyArg_ParseTuple(args, "iiiO", &d1, &d2, &n_vars, &values_obj)) - { - return NULL; - } - - PyArrayObject *values_array = (PyArrayObject *) PyArray_FROM_OTF( - values_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - if (!values_array) - { - return NULL; - } - - expr *node = new_parameter(d1, d2, PARAM_FIXED, n_vars, - (const double *) PyArray_DATA(values_array)); - Py_DECREF(values_array); - - if (!node) - { - PyErr_SetString(PyExc_RuntimeError, "failed to create constant node"); - return NULL; - } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_CONSTANT_H */ diff --git a/sparsediffpy/_bindings/atoms/dense_matmul.h b/sparsediffpy/_bindings/atoms/dense_matmul.h deleted file mode 100644 index ee4e0d5..0000000 --- a/sparsediffpy/_bindings/atoms/dense_matmul.h +++ /dev/null @@ -1,161 +0,0 @@ -#ifndef ATOM_DENSE_MATMUL_H -#define ATOM_DENSE_MATMUL_H - -#include "bivariate_full_dom.h" -#include "common.h" - -/* Dense left matrix multiplication: A @ f(x) where A is a dense matrix. - * - * Python signature: - * make_dense_left_matmul(param_or_none, child, A_data_flat, m, n) - * - * - param_or_none: None for constant matrix, or a parameter capsule. - * - child: the child expression capsule f(x). - * - A_data_flat: contiguous row-major numpy float64 array of size m*n. - * - m, n: dimensions of matrix A. */ -static PyObject *py_make_dense_left_matmul(PyObject *self, PyObject *args) -{ - PyObject *param_obj; - PyObject *child_capsule; - PyObject *data_obj; - int m, n; - if (!PyArg_ParseTuple(args, "OOOii", ¶m_obj, &child_capsule, - &data_obj, &m, &n)) - { - return NULL; - } - - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - 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); - - /* Build the parameter node: use provided capsule or create PARAM_FIXED */ - expr *param_node = NULL; - if (param_obj == Py_None) - { - param_node = - new_parameter(m * n, 1, PARAM_FIXED, child->n_vars, A_data); - if (!param_node) - { - Py_DECREF(data_array); - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for dense matrix"); - return NULL; - } - } - else - { - 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; - } - } - - 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; - } - expr_retain(node); - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -/* Dense right matrix multiplication: f(x) @ A where A is a dense matrix. - * - * Python signature: - * make_dense_right_matmul(param_or_none, child, A_data_flat, m, n) - * - * - param_or_none: None for constant matrix, or a parameter capsule. - * - child: the child expression capsule f(x). - * - A_data_flat: contiguous row-major numpy float64 array of size m*n. - * - m, n: dimensions of matrix A. */ -static PyObject *py_make_dense_right_matmul(PyObject *self, PyObject *args) -{ - PyObject *param_obj; - PyObject *child_capsule; - PyObject *data_obj; - int m, n; - if (!PyArg_ParseTuple(args, "OOOii", ¶m_obj, &child_capsule, - &data_obj, &m, &n)) - { - return NULL; - } - - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - 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); - - /* Build the parameter node: use provided capsule or create PARAM_FIXED */ - expr *param_node = NULL; - if (param_obj == Py_None) - { - param_node = - new_parameter(m * n, 1, PARAM_FIXED, child->n_vars, A_data); - if (!param_node) - { - Py_DECREF(data_array); - PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for dense matrix"); - return NULL; - } - } - else - { - 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; - } - } - - 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; - } - expr_retain(node); - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_DENSE_MATMUL_H */ diff --git a/sparsediffpy/_bindings/atoms/left_matmul.h b/sparsediffpy/_bindings/atoms/left_matmul.h index 0157e59..4ac4ecf 100644 --- a/sparsediffpy/_bindings/atoms/left_matmul.h +++ b/sparsediffpy/_bindings/atoms/left_matmul.h @@ -4,102 +4,200 @@ #include "bivariate_full_dom.h" #include "common.h" -/* Left matrix multiplication: A @ f(x) where A is a constant or parameter - * sparse matrix. +/* Left matrix multiplication: A @ f(x). * - * Python signature: - * make_sparse_left_matmul(param_or_none, child, data, indices, indptr, m, n) + * Python signatures: + * make_left_matmul(param_or_none, child, "sparse", data, indices, indptr, + * m, n) + * make_left_matmul(param_or_none, child, "dense", A_data_flat, m, n) * * - param_or_none: None for constant matrix, or a parameter capsule. - * - child: the child expression capsule f(x). - * - data, indices, indptr: CSR arrays for matrix A. - * - m, n: dimensions of matrix A. */ -static PyObject *py_make_sparse_left_matmul(PyObject *self, PyObject *args) + * - child: the child expression capsule f(x). */ +static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) { - PyObject *param_obj; - PyObject *child_capsule; - PyObject *data_obj, *indices_obj, *indptr_obj; - int m, n; - if (!PyArg_ParseTuple(args, "OOOOOii", ¶m_obj, &child_capsule, - &data_obj, &indices_obj, &indptr_obj, &m, &n)) + /* We need to parse the first 3 args to determine the format, then + * parse the rest accordingly. Use a tuple to get the format string. */ + Py_ssize_t nargs = PyTuple_Size(args); + if (nargs < 4) { + PyErr_SetString(PyExc_TypeError, + "make_left_matmul requires at least 4 arguments"); return NULL; } - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) + PyObject *param_obj = PyTuple_GetItem(args, 0); + PyObject *child_capsule = PyTuple_GetItem(args, 1); + PyObject *fmt_obj = PyTuple_GetItem(args, 2); + + if (!PyUnicode_Check(fmt_obj)) { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); + PyErr_SetString(PyExc_TypeError, + "third argument must be 'sparse' or 'dense'"); return NULL; } - PyArrayObject *data_array = - (PyArrayObject *) PyArray_FROM_OTF(data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - PyArrayObject *indices_array = (PyArrayObject *) PyArray_FROM_OTF( - indices_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); - PyArrayObject *indptr_array = (PyArrayObject *) PyArray_FROM_OTF( - indptr_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); - - if (!data_array || !indices_array || !indptr_array) + expr *child = + (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); + if (!child) { - Py_XDECREF(data_array); - Py_XDECREF(indices_array); - Py_XDECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, "invalid child capsule"); return NULL; } - int nnz = (int) PyArray_SIZE(data_array); + const char *fmt = PyUnicode_AsUTF8(fmt_obj); - /* Build the parameter node: use provided capsule or create PARAM_FIXED */ - expr *param_node = NULL; - if (param_obj == Py_None) + if (strcmp(fmt, "sparse") == 0) { - param_node = new_parameter(nnz, 1, PARAM_FIXED, child->n_vars, - (const double *) PyArray_DATA(data_array)); - if (!param_node) + /* Parse: param_or_none, child, "sparse", data, indices, indptr, m, n + */ + PyObject *data_obj, *indices_obj, *indptr_obj; + int m, n; + if (!PyArg_ParseTuple(args, "OOsOOOii", ¶m_obj, &child_capsule, + &fmt, &data_obj, &indices_obj, &indptr_obj, &m, + &n)) + { + return NULL; + } + + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + PyArrayObject *indices_array = (PyArrayObject *) PyArray_FROM_OTF( + indices_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); + PyArrayObject *indptr_array = (PyArrayObject *) PyArray_FROM_OTF( + indptr_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); + + if (!data_array || !indices_array || !indptr_array) + { + Py_XDECREF(data_array); + Py_XDECREF(indices_array); + Py_XDECREF(indptr_array); + return NULL; + } + + int nnz = (int) PyArray_SIZE(data_array); + + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter( + nnz, 1, PARAM_FIXED, child->n_vars, + (const double *) PyArray_DATA(data_array)); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString( + PyExc_RuntimeError, + "failed to create parameter node for matrix"); + return NULL; + } + } + else + { + param_node = (expr *) PyCapsule_GetPointer(param_obj, + EXPR_CAPSULE_NAME); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, + "invalid parameter capsule"); + return NULL; + } + } + + CSR_Matrix *A = new_csr_matrix(m, n, nnz); + memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); + memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); + memcpy(A->p, PyArray_DATA(indptr_array), (m + 1) * sizeof(int)); + + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + + expr *node = new_left_matmul(param_node, child, A); + free_csr_matrix(A); + + if (!node) { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); + if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for matrix"); + "failed to create left_matmul node"); return NULL; } + expr_retain(node); + return PyCapsule_New(node, EXPR_CAPSULE_NAME, + expr_capsule_destructor); } - else + else if (strcmp(fmt, "dense") == 0) { - param_node = - (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); - if (!param_node) + /* Parse: param_or_none, child, "dense", A_data_flat, m, n */ + PyObject *data_obj; + int m, n; + if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule, + &fmt, &data_obj, &m, &n)) + { + return NULL; + } + + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!data_array) { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); - PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); return NULL; } - } - CSR_Matrix *A = new_csr_matrix(m, n, nnz); - memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); - memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); - memcpy(A->p, PyArray_DATA(indptr_array), (m + 1) * sizeof(int)); + double *A_data = (double *) PyArray_DATA(data_array); - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter(m * n, 1, PARAM_FIXED, + child->n_vars, A_data); + if (!param_node) + { + Py_DECREF(data_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node"); + return NULL; + } + } + else + { + 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; + } + } - expr *node = new_left_matmul(param_node, child, A); - free_csr_matrix(A); + expr *node = + new_left_matmul_dense(param_node, child, m, n, A_data); + Py_DECREF(data_array); - if (!node) + if (!node) + { + if (param_obj == Py_None) free_expr(param_node); + PyErr_SetString(PyExc_RuntimeError, + "failed to create dense left_matmul node"); + return NULL; + } + expr_retain(node); + return PyCapsule_New(node, EXPR_CAPSULE_NAME, + expr_capsule_destructor); + } + else { - if (param_obj == Py_None) free_expr(param_node); - PyErr_SetString(PyExc_RuntimeError, "failed to create left_matmul node"); + PyErr_SetString(PyExc_ValueError, + "format must be 'sparse' or 'dense'"); return NULL; } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); } #endif /* ATOM_LEFT_MATMUL_H */ diff --git a/sparsediffpy/_bindings/atoms/parameter.h b/sparsediffpy/_bindings/atoms/parameter.h index 940bf59..9448993 100644 --- a/sparsediffpy/_bindings/atoms/parameter.h +++ b/sparsediffpy/_bindings/atoms/parameter.h @@ -3,15 +3,40 @@ #include "common.h" +/* Create a parameter node. + * + * Python signature: + * make_parameter(d1, d2, param_id, n_vars[, values]) + * + * - param_id >= 0: updatable parameter (values optional, NULL if omitted). + * - param_id == -1 (PARAM_FIXED): constant node (values required). */ static PyObject *py_make_parameter(PyObject *self, PyObject *args) { int d1, d2, param_id, n_vars; - if (!PyArg_ParseTuple(args, "iiii", &d1, &d2, ¶m_id, &n_vars)) + PyObject *values_obj = NULL; + if (!PyArg_ParseTuple(args, "iiii|O", &d1, &d2, ¶m_id, &n_vars, + &values_obj)) { return NULL; } - expr *node = new_parameter(d1, d2, param_id, n_vars, NULL); + const double *values = NULL; + PyArrayObject *values_array = NULL; + + if (values_obj && values_obj != Py_None) + { + values_array = (PyArrayObject *) PyArray_FROM_OTF( + values_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!values_array) + { + return NULL; + } + values = (const double *) PyArray_DATA(values_array); + } + + expr *node = new_parameter(d1, d2, param_id, n_vars, values); + Py_XDECREF(values_array); + if (!node) { PyErr_SetString(PyExc_RuntimeError, "failed to create parameter node"); diff --git a/sparsediffpy/_bindings/atoms/rel_entr.h b/sparsediffpy/_bindings/atoms/rel_entr.h index d7e8cc5..7389823 100644 --- a/sparsediffpy/_bindings/atoms/rel_entr.h +++ b/sparsediffpy/_bindings/atoms/rel_entr.h @@ -6,7 +6,11 @@ #include "bivariate_restricted_dom.h" #include "common.h" -/* rel_entr: rel_entr(x, y) = x * log(x/y) elementwise */ +/* rel_entr: rel_entr(x, y) = x * log(x/y) elementwise. + * Auto-dispatches based on argument sizes: + * - scalar x, vector y -> new_rel_entr_first_arg_scalar + * - vector x, scalar y -> new_rel_entr_second_arg_scalar + * - otherwise -> new_rel_entr_vector_args */ static PyObject *py_make_rel_entr(PyObject *self, PyObject *args) { (void) self; @@ -16,14 +20,28 @@ static PyObject *py_make_rel_entr(PyObject *self, PyObject *args) return NULL; } expr *left = (expr *) PyCapsule_GetPointer(left_capsule, EXPR_CAPSULE_NAME); - expr *right = (expr *) PyCapsule_GetPointer(right_capsule, EXPR_CAPSULE_NAME); + expr *right = + (expr *) PyCapsule_GetPointer(right_capsule, EXPR_CAPSULE_NAME); if (!left || !right) { PyErr_SetString(PyExc_ValueError, "invalid child capsule"); return NULL; } - expr *node = new_rel_entr_vector_args(left, right); + expr *node; + if (left->size == 1 && right->size > 1) + { + node = new_rel_entr_first_arg_scalar(left, right); + } + else if (left->size > 1 && right->size == 1) + { + node = new_rel_entr_second_arg_scalar(left, right); + } + else + { + node = new_rel_entr_vector_args(left, right); + } + if (!node) { PyErr_SetString(PyExc_RuntimeError, "failed to create rel_entr node"); diff --git a/sparsediffpy/_bindings/atoms/rel_entr_scalar_vector.h b/sparsediffpy/_bindings/atoms/rel_entr_scalar_vector.h deleted file mode 100644 index 65d1708..0000000 --- a/sparsediffpy/_bindings/atoms/rel_entr_scalar_vector.h +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#ifndef ATOM_REL_ENTR_SCALAR_VECTOR_H -#define ATOM_REL_ENTR_SCALAR_VECTOR_H - -#include "bivariate_restricted_dom.h" -#include "common.h" - -/* rel_entr_scalar_vector: rel_entr(x, y) where x is scalar, y is vector */ -static PyObject *py_make_rel_entr_scalar_vector(PyObject *self, PyObject *args) -{ - (void) self; - PyObject *left_capsule, *right_capsule; - if (!PyArg_ParseTuple(args, "OO", &left_capsule, &right_capsule)) - { - return NULL; - } - expr *left = (expr *) PyCapsule_GetPointer(left_capsule, EXPR_CAPSULE_NAME); - expr *right = (expr *) PyCapsule_GetPointer(right_capsule, EXPR_CAPSULE_NAME); - if (!left || !right) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - return NULL; - } - - expr *node = new_rel_entr_first_arg_scalar(left, right); - if (!node) - { - PyErr_SetString(PyExc_RuntimeError, - "failed to create rel_entr_scalar_vector node"); - return NULL; - } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_REL_ENTR_SCALAR_VECTOR_H */ diff --git a/sparsediffpy/_bindings/atoms/rel_entr_vector_scalar.h b/sparsediffpy/_bindings/atoms/rel_entr_vector_scalar.h deleted file mode 100644 index eb05c15..0000000 --- a/sparsediffpy/_bindings/atoms/rel_entr_vector_scalar.h +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#ifndef ATOM_REL_ENTR_VECTOR_SCALAR_H -#define ATOM_REL_ENTR_VECTOR_SCALAR_H - -#include "bivariate_restricted_dom.h" -#include "common.h" - -/* rel_entr_vector_scalar: rel_entr(x, y) where x is vector, y is scalar */ -static PyObject *py_make_rel_entr_vector_scalar(PyObject *self, PyObject *args) -{ - (void) self; - PyObject *left_capsule, *right_capsule; - if (!PyArg_ParseTuple(args, "OO", &left_capsule, &right_capsule)) - { - return NULL; - } - expr *left = (expr *) PyCapsule_GetPointer(left_capsule, EXPR_CAPSULE_NAME); - expr *right = (expr *) PyCapsule_GetPointer(right_capsule, EXPR_CAPSULE_NAME); - if (!left || !right) - { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); - return NULL; - } - - expr *node = new_rel_entr_second_arg_scalar(left, right); - if (!node) - { - PyErr_SetString(PyExc_RuntimeError, - "failed to create rel_entr_vector_scalar node"); - return NULL; - } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); -} - -#endif /* ATOM_REL_ENTR_VECTOR_SCALAR_H */ diff --git a/sparsediffpy/_bindings/atoms/right_matmul.h b/sparsediffpy/_bindings/atoms/right_matmul.h index 07ac68f..ebc5789 100644 --- a/sparsediffpy/_bindings/atoms/right_matmul.h +++ b/sparsediffpy/_bindings/atoms/right_matmul.h @@ -4,103 +4,195 @@ #include "bivariate_full_dom.h" #include "common.h" -/* Right matrix multiplication: f(x) @ A where A is a constant or parameter - * sparse matrix. +/* Right matrix multiplication: f(x) @ A. * - * Python signature: - * make_sparse_right_matmul(param_or_none, child, data, indices, indptr, m, n) + * Python signatures: + * make_right_matmul(param_or_none, child, "sparse", data, indices, indptr, + * m, n) + * make_right_matmul(param_or_none, child, "dense", A_data_flat, m, n) * * - param_or_none: None for constant matrix, or a parameter capsule. - * - child: the child expression capsule f(x). - * - data, indices, indptr: CSR arrays for matrix A. - * - m, n: dimensions of matrix A. */ -static PyObject *py_make_sparse_right_matmul(PyObject *self, PyObject *args) + * - child: the child expression capsule f(x). */ +static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) { - PyObject *param_obj; - PyObject *child_capsule; - PyObject *data_obj, *indices_obj, *indptr_obj; - int m, n; - if (!PyArg_ParseTuple(args, "OOOOOii", ¶m_obj, &child_capsule, - &data_obj, &indices_obj, &indptr_obj, &m, &n)) + Py_ssize_t nargs = PyTuple_Size(args); + if (nargs < 4) { + PyErr_SetString(PyExc_TypeError, + "make_right_matmul requires at least 4 arguments"); return NULL; } - expr *child = (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); - if (!child) + PyObject *param_obj = PyTuple_GetItem(args, 0); + PyObject *child_capsule = PyTuple_GetItem(args, 1); + PyObject *fmt_obj = PyTuple_GetItem(args, 2); + + if (!PyUnicode_Check(fmt_obj)) { - PyErr_SetString(PyExc_ValueError, "invalid child capsule"); + PyErr_SetString(PyExc_TypeError, + "third argument must be 'sparse' or 'dense'"); return NULL; } - PyArrayObject *data_array = - (PyArrayObject *) PyArray_FROM_OTF(data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); - PyArrayObject *indices_array = (PyArrayObject *) PyArray_FROM_OTF( - indices_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); - PyArrayObject *indptr_array = (PyArrayObject *) PyArray_FROM_OTF( - indptr_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); - - if (!data_array || !indices_array || !indptr_array) + expr *child = + (expr *) PyCapsule_GetPointer(child_capsule, EXPR_CAPSULE_NAME); + if (!child) { - Py_XDECREF(data_array); - Py_XDECREF(indices_array); - Py_XDECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, "invalid child capsule"); return NULL; } - int nnz = (int) PyArray_SIZE(data_array); + const char *fmt = PyUnicode_AsUTF8(fmt_obj); - /* Build the parameter node: use provided capsule or create PARAM_FIXED */ - expr *param_node = NULL; - if (param_obj == Py_None) + if (strcmp(fmt, "sparse") == 0) { - param_node = new_parameter(nnz, 1, PARAM_FIXED, child->n_vars, - (const double *) PyArray_DATA(data_array)); - if (!param_node) + PyObject *data_obj, *indices_obj, *indptr_obj; + int m, n; + if (!PyArg_ParseTuple(args, "OOsOOOii", ¶m_obj, &child_capsule, + &fmt, &data_obj, &indices_obj, &indptr_obj, &m, + &n)) + { + return NULL; + } + + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + PyArrayObject *indices_array = (PyArrayObject *) PyArray_FROM_OTF( + indices_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); + PyArrayObject *indptr_array = (PyArrayObject *) PyArray_FROM_OTF( + indptr_obj, NPY_INT32, NPY_ARRAY_IN_ARRAY); + + if (!data_array || !indices_array || !indptr_array) + { + Py_XDECREF(data_array); + Py_XDECREF(indices_array); + Py_XDECREF(indptr_array); + return NULL; + } + + int nnz = (int) PyArray_SIZE(data_array); + + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter( + nnz, 1, PARAM_FIXED, child->n_vars, + (const double *) PyArray_DATA(data_array)); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString( + PyExc_RuntimeError, + "failed to create parameter node for matrix"); + return NULL; + } + } + else + { + param_node = (expr *) PyCapsule_GetPointer(param_obj, + EXPR_CAPSULE_NAME); + if (!param_node) + { + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + PyErr_SetString(PyExc_ValueError, + "invalid parameter capsule"); + return NULL; + } + } + + CSR_Matrix *A = new_csr_matrix(m, n, nnz); + memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); + memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); + memcpy(A->p, PyArray_DATA(indptr_array), (m + 1) * sizeof(int)); + + Py_DECREF(data_array); + Py_DECREF(indices_array); + Py_DECREF(indptr_array); + + expr *node = new_right_matmul(param_node, child, A); + free_csr_matrix(A); + + if (!node) { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); + if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, - "failed to create parameter node for matrix"); + "failed to create right_matmul node"); return NULL; } + expr_retain(node); + return PyCapsule_New(node, EXPR_CAPSULE_NAME, + expr_capsule_destructor); } - else + else if (strcmp(fmt, "dense") == 0) { - param_node = - (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); - if (!param_node) + PyObject *data_obj; + int m, n; + if (!PyArg_ParseTuple(args, "OOsOii", ¶m_obj, &child_capsule, + &fmt, &data_obj, &m, &n)) + { + return NULL; + } + + PyArrayObject *data_array = (PyArrayObject *) PyArray_FROM_OTF( + data_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY); + if (!data_array) { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); - PyErr_SetString(PyExc_ValueError, "invalid parameter capsule"); return NULL; } - } - CSR_Matrix *A = new_csr_matrix(m, n, nnz); - memcpy(A->x, PyArray_DATA(data_array), nnz * sizeof(double)); - memcpy(A->i, PyArray_DATA(indices_array), nnz * sizeof(int)); - memcpy(A->p, PyArray_DATA(indptr_array), (m + 1) * sizeof(int)); + double *A_data = (double *) PyArray_DATA(data_array); - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); + expr *param_node = NULL; + if (param_obj == Py_None) + { + param_node = new_parameter(m * n, 1, PARAM_FIXED, + child->n_vars, A_data); + if (!param_node) + { + Py_DECREF(data_array); + PyErr_SetString(PyExc_RuntimeError, + "failed to create parameter node"); + return NULL; + } + } + else + { + 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; + } + } - expr *node = new_right_matmul(param_node, child, A); - free_csr_matrix(A); + expr *node = + new_right_matmul_dense(param_node, child, m, n, A_data); + Py_DECREF(data_array); - if (!node) + if (!node) + { + if (param_obj == Py_None) free_expr(param_node); + PyErr_SetString(PyExc_RuntimeError, + "failed to create dense right_matmul node"); + return NULL; + } + expr_retain(node); + return PyCapsule_New(node, EXPR_CAPSULE_NAME, + expr_capsule_destructor); + } + else { - if (param_obj == Py_None) free_expr(param_node); - PyErr_SetString(PyExc_RuntimeError, - "failed to create right_matmul node"); + PyErr_SetString(PyExc_ValueError, + "format must be 'sparse' or 'dense'"); return NULL; } - expr_retain(node); /* Capsule owns a reference */ - return PyCapsule_New(node, EXPR_CAPSULE_NAME, expr_capsule_destructor); } #endif /* ATOM_RIGHT_MATMUL_H */ diff --git a/sparsediffpy/_bindings/bindings.c b/sparsediffpy/_bindings/bindings.c index d93f112..f9b1ac9 100644 --- a/sparsediffpy/_bindings/bindings.c +++ b/sparsediffpy/_bindings/bindings.c @@ -7,11 +7,7 @@ #include "atoms/asinh.h" #include "atoms/atanh.h" #include "atoms/broadcast.h" -#include "atoms/const_scalar_mult.h" -#include "atoms/const_vector_mult.h" -#include "atoms/constant.h" #include "atoms/cos.h" -#include "atoms/dense_matmul.h" #include "atoms/diag_vec.h" #include "atoms/entr.h" #include "atoms/exp.h" @@ -34,8 +30,6 @@ #include "atoms/quad_form.h" #include "atoms/quad_over_lin.h" #include "atoms/rel_entr.h" -#include "atoms/rel_entr_scalar_vector.h" -#include "atoms/rel_entr_vector_scalar.h" #include "atoms/reshape.h" #include "atoms/right_matmul.h" #include "atoms/scalar_mult.h" @@ -76,8 +70,7 @@ static int ensure_numpy(void) static PyMethodDef DNLPMethods[] = { {"make_variable", py_make_variable, METH_VARARGS, "Create variable node"}, {"make_parameter", py_make_parameter, METH_VARARGS, - "Create updatable parameter node"}, - {"make_constant", py_make_constant, METH_VARARGS, "Create constant node"}, + "Create parameter node (param_id=-1 for constant, >=0 for updatable)"}, {"make_log", py_make_log, METH_VARARGS, "Create log node"}, {"make_exp", py_make_exp, METH_VARARGS, "Create exp node"}, {"make_index", py_make_index, METH_VARARGS, "Create index node"}, @@ -95,10 +88,6 @@ static PyMethodDef DNLPMethods[] = { "Create elementwise multiply node"}, {"make_matmul", py_make_matmul, METH_VARARGS, "Create matrix multiplication node (Z = X @ Y)"}, - {"make_const_scalar_mult", py_make_const_scalar_mult, METH_VARARGS, - "Create constant scalar multiplication node (a * f(x))"}, - {"make_const_vector_mult", py_make_const_vector_mult, METH_VARARGS, - "Create constant vector multiplication node (a ∘ f(x))"}, {"make_param_scalar_mult", py_make_param_scalar_mult, METH_VARARGS, "Create parameter scalar multiplication node (a * f(x))"}, {"make_param_vector_mult", py_make_param_vector_mult, METH_VARARGS, @@ -121,24 +110,16 @@ static PyMethodDef DNLPMethods[] = { {"make_entr", py_make_entr, METH_VARARGS, "Create entr node"}, {"make_logistic", py_make_logistic, METH_VARARGS, "Create logistic node"}, {"make_xexp", py_make_xexp, METH_VARARGS, "Create xexp node"}, - {"make_sparse_left_matmul", py_make_sparse_left_matmul, METH_VARARGS, - "Create sparse left matmul node (A @ f(x))"}, - {"make_dense_left_matmul", py_make_dense_left_matmul, METH_VARARGS, - "Create dense left matmul node (A @ f(x)) where A is dense"}, - {"make_sparse_right_matmul", py_make_sparse_right_matmul, METH_VARARGS, - "Create sparse right matmul node (f(x) @ A)"}, - {"make_dense_right_matmul", py_make_dense_right_matmul, METH_VARARGS, - "Create dense right matmul node (f(x) @ A) where A is dense"}, + {"make_left_matmul", py_make_left_matmul, METH_VARARGS, + "Create left matmul node A @ f(x) (format: 'sparse' or 'dense')"}, + {"make_right_matmul", py_make_right_matmul, METH_VARARGS, + "Create right matmul node f(x) @ A (format: 'sparse' or 'dense')"}, {"make_quad_form", py_make_quad_form, METH_VARARGS, "Create quadratic form node (x' * Q * x)"}, {"make_quad_over_lin", py_make_quad_over_lin, METH_VARARGS, "Create quad_over_lin node (sum(x^2) / y)"}, {"make_rel_entr", py_make_rel_entr, METH_VARARGS, - "Create rel_entr node: x * log(x/y) elementwise"}, - {"make_rel_entr_vector_scalar", py_make_rel_entr_vector_scalar, METH_VARARGS, - "Create rel_entr node with vector first arg, scalar second arg"}, - {"make_rel_entr_scalar_vector", py_make_rel_entr_scalar_vector, METH_VARARGS, - "Create rel_entr node with scalar first arg, vector second arg"}, + "Create rel_entr node: x * log(x/y), auto-dispatches scalar/vector"}, {"get_expr_dimensions", py_get_expr_dimensions, METH_VARARGS, "Get the dimensions (d1, d2) of an expression"}, {"get_expr_size", py_get_expr_size, METH_VARARGS, From 0145cad8134736641084e398bd6b21f0dca977ce Mon Sep 17 00:00:00 2001 From: William Zijie Zhang Date: Thu, 9 Apr 2026 15:50:10 -0400 Subject: [PATCH 3/3] update bindings for sparse matmul to pass None for param_source --- SparseDiffEngine | 2 +- sparsediffpy/_bindings/atoms/left_matmul.h | 22 ++++----------------- sparsediffpy/_bindings/atoms/right_matmul.h | 22 ++++----------------- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/SparseDiffEngine b/SparseDiffEngine index 85f456a..860dd5a 160000 --- a/SparseDiffEngine +++ b/SparseDiffEngine @@ -1 +1 @@ -Subproject commit 85f456aff2dab5bfdeed1cf636052d227c331018 +Subproject commit 860dd5a3113a8c2bf1ad2249dc0ae649a4375023 diff --git a/sparsediffpy/_bindings/atoms/left_matmul.h b/sparsediffpy/_bindings/atoms/left_matmul.h index 4ac4ecf..c871ff8 100644 --- a/sparsediffpy/_bindings/atoms/left_matmul.h +++ b/sparsediffpy/_bindings/atoms/left_matmul.h @@ -76,24 +76,11 @@ static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) int nnz = (int) PyArray_SIZE(data_array); + /* For sparse matrices, we don't create a PARAM_FIXED node when + param_obj is None — the CSR data is already copied into the + matrix struct. Only extract a real parameter capsule. */ expr *param_node = NULL; - if (param_obj == Py_None) - { - param_node = new_parameter( - nnz, 1, PARAM_FIXED, child->n_vars, - (const double *) PyArray_DATA(data_array)); - if (!param_node) - { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); - PyErr_SetString( - PyExc_RuntimeError, - "failed to create parameter node for matrix"); - return NULL; - } - } - else + if (param_obj != Py_None) { param_node = (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); @@ -122,7 +109,6 @@ static PyObject *py_make_left_matmul(PyObject *self, PyObject *args) if (!node) { - if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, "failed to create left_matmul node"); return NULL; diff --git a/sparsediffpy/_bindings/atoms/right_matmul.h b/sparsediffpy/_bindings/atoms/right_matmul.h index ebc5789..b3fa1cf 100644 --- a/sparsediffpy/_bindings/atoms/right_matmul.h +++ b/sparsediffpy/_bindings/atoms/right_matmul.h @@ -72,24 +72,11 @@ static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) int nnz = (int) PyArray_SIZE(data_array); + /* For sparse matrices, we don't create a PARAM_FIXED node when + param_obj is None — the CSR data is already copied into the + matrix struct. Only extract a real parameter capsule. */ expr *param_node = NULL; - if (param_obj == Py_None) - { - param_node = new_parameter( - nnz, 1, PARAM_FIXED, child->n_vars, - (const double *) PyArray_DATA(data_array)); - if (!param_node) - { - Py_DECREF(data_array); - Py_DECREF(indices_array); - Py_DECREF(indptr_array); - PyErr_SetString( - PyExc_RuntimeError, - "failed to create parameter node for matrix"); - return NULL; - } - } - else + if (param_obj != Py_None) { param_node = (expr *) PyCapsule_GetPointer(param_obj, EXPR_CAPSULE_NAME); @@ -118,7 +105,6 @@ static PyObject *py_make_right_matmul(PyObject *self, PyObject *args) if (!node) { - if (param_obj == Py_None) free_expr(param_node); PyErr_SetString(PyExc_RuntimeError, "failed to create right_matmul node"); return NULL;