Skip to content

Commit aedaafd

Browse files
Transurgeonclaude
andcommitted
Change gradient, jacobian, constraint_forward to return void
Values are now accessed directly from the problem struct: - prob->constraint_values for constraint forward results - prob->gradient_values for objective gradient - prob->stacked_jac for constraint jacobian Also updated Python bindings and tests to use the new API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5b27f25 commit aedaafd

10 files changed

Lines changed: 106 additions & 127 deletions

File tree

include/problem.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ typedef struct problem
1212
int n_vars;
1313
int total_constraint_size;
1414

15-
/* Allocated by problem_init_derivatives */
15+
/* Allocated by new_problem */
1616
double *constraint_values;
1717
double *gradient_values;
18+
19+
/* Allocated by problem_init_derivatives */
1820
CSR_Matrix *stacked_jac;
1921
} problem;
2022

@@ -24,8 +26,8 @@ void problem_init_derivatives(problem *prob);
2426
void free_problem(problem *prob);
2527

2628
double problem_objective_forward(problem *prob, const double *u);
27-
double *problem_constraint_forward(problem *prob, const double *u);
28-
double *problem_gradient(problem *prob, const double *u);
29-
CSR_Matrix *problem_jacobian(problem *prob, const double *u);
29+
void problem_constraint_forward(problem *prob, const double *u);
30+
void problem_gradient(problem *prob);
31+
void problem_jacobian(problem *prob);
3032

3133
#endif

python/convert.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ def constraint_forward(self, u: np.ndarray) -> np.ndarray:
151151
"""Evaluate constraints only. Returns constraint_values array."""
152152
return diffengine.problem_constraint_forward(self._capsule, u)
153153

154-
def gradient(self, u: np.ndarray) -> np.ndarray:
155-
"""Compute gradient of objective. Returns gradient array."""
156-
return diffengine.problem_gradient(self._capsule, u)
154+
def gradient(self) -> np.ndarray:
155+
"""Compute gradient of objective. Call objective_forward first. Returns gradient array."""
156+
return diffengine.problem_gradient(self._capsule)
157157

158-
def jacobian(self, u: np.ndarray) -> sparse.csr_matrix:
159-
"""Compute jacobian of constraints. Returns scipy CSR matrix."""
160-
data, indices, indptr, shape = diffengine.problem_jacobian(self._capsule, u)
158+
def jacobian(self) -> sparse.csr_matrix:
159+
"""Compute jacobian of constraints. Call constraint_forward first. Returns scipy CSR matrix."""
160+
data, indices, indptr, shape = diffengine.problem_jacobian(self._capsule)
161161
return sparse.csr_matrix((data, indices, indptr), shape=shape)

python/problem/constraint_forward.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ static PyObject *py_problem_constraint_forward(PyObject *self, PyObject *args)
2727
return NULL;
2828
}
2929

30-
double *constraint_vals =
31-
problem_constraint_forward(prob, (const double *) PyArray_DATA(u_array));
30+
problem_constraint_forward(prob, (const double *) PyArray_DATA(u_array));
31+
Py_DECREF(u_array);
3232

3333
PyObject *out = NULL;
3434
if (prob->total_constraint_size > 0)
@@ -37,10 +37,9 @@ static PyObject *py_problem_constraint_forward(PyObject *self, PyObject *args)
3737
out = PyArray_SimpleNew(1, &size, NPY_DOUBLE);
3838
if (!out)
3939
{
40-
Py_DECREF(u_array);
4140
return NULL;
4241
}
43-
memcpy(PyArray_DATA((PyArrayObject *) out), constraint_vals,
42+
memcpy(PyArray_DATA((PyArrayObject *) out), prob->constraint_values,
4443
size * sizeof(double));
4544
}
4645
else
@@ -49,7 +48,6 @@ static PyObject *py_problem_constraint_forward(PyObject *self, PyObject *args)
4948
out = PyArray_SimpleNew(1, &size, NPY_DOUBLE);
5049
}
5150

52-
Py_DECREF(u_array);
5351
return out;
5452
}
5553

python/problem/gradient.h

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
static PyObject *py_problem_gradient(PyObject *self, PyObject *args)
77
{
88
PyObject *prob_capsule;
9-
PyObject *u_obj;
10-
if (!PyArg_ParseTuple(args, "OO", &prob_capsule, &u_obj))
9+
if (!PyArg_ParseTuple(args, "O", &prob_capsule))
1110
{
1211
return NULL;
1312
}
@@ -20,25 +19,16 @@ static PyObject *py_problem_gradient(PyObject *self, PyObject *args)
2019
return NULL;
2120
}
2221

23-
PyArrayObject *u_array =
24-
(PyArrayObject *) PyArray_FROM_OTF(u_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
25-
if (!u_array)
26-
{
27-
return NULL;
28-
}
29-
30-
double *grad = problem_gradient(prob, (const double *) PyArray_DATA(u_array));
22+
problem_gradient(prob);
3123

3224
npy_intp size = prob->n_vars;
3325
PyObject *out = PyArray_SimpleNew(1, &size, NPY_DOUBLE);
3426
if (!out)
3527
{
36-
Py_DECREF(u_array);
3728
return NULL;
3829
}
39-
memcpy(PyArray_DATA((PyArrayObject *) out), grad, size * sizeof(double));
30+
memcpy(PyArray_DATA((PyArrayObject *) out), prob->gradient_values, size * sizeof(double));
4031

41-
Py_DECREF(u_array);
4232
return out;
4333
}
4434

python/problem/jacobian.h

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
static PyObject *py_problem_jacobian(PyObject *self, PyObject *args)
77
{
88
PyObject *prob_capsule;
9-
PyObject *u_obj;
10-
if (!PyArg_ParseTuple(args, "OO", &prob_capsule, &u_obj))
9+
if (!PyArg_ParseTuple(args, "O", &prob_capsule))
1110
{
1211
return NULL;
1312
}
@@ -32,15 +31,9 @@ static PyObject *py_problem_jacobian(PyObject *self, PyObject *args)
3231
return Py_BuildValue("(OOO(ii))", data, indices, indptr, 0, prob->n_vars);
3332
}
3433

35-
PyArrayObject *u_array =
36-
(PyArrayObject *) PyArray_FROM_OTF(u_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
37-
if (!u_array)
38-
{
39-
return NULL;
40-
}
41-
42-
CSR_Matrix *jac = problem_jacobian(prob, (const double *) PyArray_DATA(u_array));
34+
problem_jacobian(prob);
4335

36+
CSR_Matrix *jac = prob->stacked_jac;
4437
npy_intp nnz = jac->nnz;
4538
npy_intp m_plus_1 = jac->m + 1;
4639

@@ -53,15 +46,13 @@ static PyObject *py_problem_jacobian(PyObject *self, PyObject *args)
5346
Py_XDECREF(data);
5447
Py_XDECREF(indices);
5548
Py_XDECREF(indptr);
56-
Py_DECREF(u_array);
5749
return NULL;
5850
}
5951

6052
memcpy(PyArray_DATA((PyArrayObject *) data), jac->x, nnz * sizeof(double));
6153
memcpy(PyArray_DATA((PyArrayObject *) indices), jac->i, nnz * sizeof(int));
6254
memcpy(PyArray_DATA((PyArrayObject *) indptr), jac->p, m_plus_1 * sizeof(int));
6355

64-
Py_DECREF(u_array);
6556
return Py_BuildValue("(OOO(ii))", data, indices, indptr, jac->m, jac->n);
6657
}
6758

python/tests/test_constrained.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ def test_single_constraint():
1717
prob = C_problem(problem)
1818

1919
u = np.array([1.0, 2.0, 3.0])
20-
prob.allocate(u)
20+
prob.init_derivatives()
2121

2222
# Test constraint_forward: constr.expr = -log(x)
2323
constraint_vals = prob.constraint_forward(u)
2424
assert np.allclose(constraint_vals, -np.log(u))
2525

2626
# Test jacobian: d/dx(-log(x)) = -1/x
27-
jac = prob.jacobian(u)
27+
jac = prob.jacobian()
2828
expected_jac = np.diag(-1.0 / u)
2929
assert np.allclose(jac.toarray(), expected_jac)
3030

@@ -42,15 +42,15 @@ def test_two_constraints():
4242
prob = C_problem(problem)
4343

4444
u = np.array([1.0, 2.0])
45-
prob.allocate(u)
45+
prob.init_derivatives()
4646

4747
# Test constraint_forward: [-log(u), -exp(u)]
4848
expected_constraint_vals = np.concatenate([-np.log(u), -np.exp(u)])
4949
constraint_vals = prob.constraint_forward(u)
5050
assert np.allclose(constraint_vals, expected_constraint_vals)
5151

5252
# Test jacobian - stacked vertically
53-
jac = prob.jacobian(u)
53+
jac = prob.jacobian()
5454
assert jac.shape == (4, 2)
5555
expected_jac = np.vstack([np.diag(-1.0 / u), np.diag(-np.exp(u))])
5656
assert np.allclose(jac.toarray(), expected_jac)
@@ -70,7 +70,7 @@ def test_three_constraints_different_sizes():
7070
prob = C_problem(problem)
7171

7272
u = np.array([1.0, 2.0, 3.0])
73-
prob.allocate(u)
73+
prob.init_derivatives()
7474

7575
# Test constraint_forward
7676
expected_constraint_vals = np.concatenate([
@@ -82,7 +82,7 @@ def test_three_constraints_different_sizes():
8282
assert np.allclose(constraint_vals, expected_constraint_vals)
8383

8484
# Test jacobian shape and values
85-
jac = prob.jacobian(u)
85+
jac = prob.jacobian()
8686
assert jac.shape == (7, 3)
8787
# First 3 rows: -diag(1/u), next 3 rows: -diag(exp(u)), last row: -1/u
8888
expected_jac = np.zeros((7, 3))
@@ -108,15 +108,15 @@ def test_multiple_variables():
108108
x_vals = np.array([1.0, 2.0])
109109
y_vals = np.array([0.5, 1.0])
110110
u = np.concatenate([x_vals, y_vals])
111-
prob.allocate(u)
111+
prob.init_derivatives()
112112

113113
# Test constraint_forward
114114
expected_constraint_vals = np.concatenate([-np.log(x_vals), -np.exp(y_vals)])
115115
constraint_vals = prob.constraint_forward(u)
116116
assert np.allclose(constraint_vals, expected_constraint_vals)
117117

118118
# Test jacobian
119-
jac = prob.jacobian(u)
119+
jac = prob.jacobian()
120120
assert jac.shape == (4, 4)
121121
expected_jac = np.zeros((4, 4))
122122
expected_jac[0, 0] = -1.0 / x_vals[0]
@@ -142,7 +142,7 @@ def test_larger_scale():
142142
prob = C_problem(problem)
143143

144144
u = np.linspace(1.0, 5.0, n)
145-
prob.allocate(u)
145+
prob.init_derivatives()
146146

147147
# Test constraint_forward
148148
expected_constraint_vals = np.concatenate([
@@ -155,7 +155,7 @@ def test_larger_scale():
155155
assert np.allclose(constraint_vals, expected_constraint_vals)
156156

157157
# Test jacobian shape
158-
jac = prob.jacobian(u)
158+
jac = prob.jacobian()
159159
assert jac.shape == (n + n + 1 + 1, n)
160160

161161

@@ -169,16 +169,16 @@ def test_repeated_evaluations():
169169
prob = C_problem(problem)
170170

171171
u1 = np.array([1.0, 2.0, 3.0])
172-
prob.allocate(u1)
172+
prob.init_derivatives()
173173

174174
# First evaluation
175175
constraint_vals1 = prob.constraint_forward(u1)
176-
jac1 = prob.jacobian(u1)
176+
jac1 = prob.jacobian()
177177

178178
# Second evaluation at different point
179179
u2 = np.array([2.0, 3.0, 4.0])
180180
constraint_vals2 = prob.constraint_forward(u2)
181-
jac2 = prob.jacobian(u2)
181+
jac2 = prob.jacobian()
182182

183183
assert np.allclose(constraint_vals1, -np.exp(u1))
184184
assert np.allclose(constraint_vals2, -np.exp(u2))

python/tests/test_problem_native.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_problem_gradient():
5858
diffengine.problem_init_derivatives(prob)
5959

6060
diffengine.problem_objective_forward(prob, u)
61-
grad = diffengine.problem_gradient(prob, u)
61+
grad = diffengine.problem_gradient(prob)
6262
expected_grad = 1.0 / u
6363
assert np.allclose(grad, expected_grad)
6464

@@ -76,7 +76,7 @@ def test_problem_jacobian():
7676
diffengine.problem_init_derivatives(prob)
7777

7878
diffengine.problem_constraint_forward(prob, u)
79-
data, indices, indptr, shape = diffengine.problem_jacobian(prob, u)
79+
data, indices, indptr, shape = diffengine.problem_jacobian(prob)
8080
jac = sparse.csr_matrix((data, indices, indptr), shape=shape)
8181

8282
expected_jac = np.diag(1.0 / u)
@@ -100,11 +100,11 @@ def test_problem_no_constraints():
100100
assert len(constraint_vals) == 0
101101

102102
diffengine.problem_objective_forward(prob, u)
103-
grad = diffengine.problem_gradient(prob, u)
103+
grad = diffengine.problem_gradient(prob)
104104
assert np.allclose(grad, 1.0 / u)
105105

106106
diffengine.problem_constraint_forward(prob, u)
107-
data, indices, indptr, shape = diffengine.problem_jacobian(prob, u)
107+
data, indices, indptr, shape = diffengine.problem_jacobian(prob)
108108
jac = sparse.csr_matrix((data, indices, indptr), shape=shape)
109109
assert jac.shape == (0, 3)
110110

@@ -138,13 +138,13 @@ def test_problem_multiple_constraints():
138138

139139
# Test gradient
140140
diffengine.problem_objective_forward(prob, u)
141-
grad = diffengine.problem_gradient(prob, u)
141+
grad = diffengine.problem_gradient(prob)
142142
expected_grad = 1.0 / u
143143
assert np.allclose(grad, expected_grad)
144144

145145
# Test Jacobian
146146
diffengine.problem_constraint_forward(prob, u)
147-
data, indices, indptr, shape = diffengine.problem_jacobian(prob, u)
147+
data, indices, indptr, shape = diffengine.problem_jacobian(prob)
148148
jac = sparse.csr_matrix((data, indices, indptr), shape=shape)
149149

150150
# Expected Jacobian:

0 commit comments

Comments
 (0)