Skip to content

Commit 8ea75ab

Browse files
committed
adds constraint forward as well
1 parent cca4fbc commit 8ea75ab

7 files changed

Lines changed: 201 additions & 10 deletions

File tree

include/problem.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ typedef struct problem
1818
CSR_Matrix *stacked_jac;
1919
} problem;
2020

21-
/* Takes ownership of objective and constraints - caller should not free them */
21+
/* Retains objective and constraints (shared ownership with caller) */
2222
problem *new_problem(expr *objective, expr **constraints, int n_constraints);
2323
void problem_allocate(problem *prob, const double *u);
2424
void free_problem(problem *prob);
2525

2626
double problem_forward(problem *prob, const double *u);
27+
double *problem_constraint_forward(problem *prob, const double *u);
2728
double *problem_gradient(problem *prob, const double *u);
2829
CSR_Matrix *problem_jacobian(problem *prob, const double *u);
2930

python/bindings.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,56 @@ static PyObject *py_problem_forward(PyObject *self, PyObject *args)
388388
return Py_BuildValue("(dO)", obj_val, constraint_vals);
389389
}
390390

391+
static PyObject *py_problem_constraint_forward(PyObject *self, PyObject *args)
392+
{
393+
PyObject *prob_capsule;
394+
PyObject *u_obj;
395+
if (!PyArg_ParseTuple(args, "OO", &prob_capsule, &u_obj))
396+
{
397+
return NULL;
398+
}
399+
400+
problem *prob =
401+
(problem *) PyCapsule_GetPointer(prob_capsule, PROBLEM_CAPSULE_NAME);
402+
if (!prob)
403+
{
404+
PyErr_SetString(PyExc_ValueError, "invalid problem capsule");
405+
return NULL;
406+
}
407+
408+
PyArrayObject *u_array =
409+
(PyArrayObject *) PyArray_FROM_OTF(u_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
410+
if (!u_array)
411+
{
412+
return NULL;
413+
}
414+
415+
double *constraint_vals =
416+
problem_constraint_forward(prob, (const double *) PyArray_DATA(u_array));
417+
418+
PyObject *out = NULL;
419+
if (prob->total_constraint_size > 0)
420+
{
421+
npy_intp size = prob->total_constraint_size;
422+
out = PyArray_SimpleNew(1, &size, NPY_DOUBLE);
423+
if (!out)
424+
{
425+
Py_DECREF(u_array);
426+
return NULL;
427+
}
428+
memcpy(PyArray_DATA((PyArrayObject *) out), constraint_vals,
429+
size * sizeof(double));
430+
}
431+
else
432+
{
433+
npy_intp size = 0;
434+
out = PyArray_SimpleNew(1, &size, NPY_DOUBLE);
435+
}
436+
437+
Py_DECREF(u_array);
438+
return out;
439+
}
440+
391441
static PyObject *py_problem_gradient(PyObject *self, PyObject *args)
392442
{
393443
PyObject *prob_capsule;
@@ -500,6 +550,7 @@ static PyMethodDef DNLPMethods[] = {
500550
{"make_problem", py_make_problem, METH_VARARGS, "Create problem from objective and constraints"},
501551
{"problem_allocate", py_problem_allocate, METH_VARARGS, "Allocate problem resources"},
502552
{"problem_forward", py_problem_forward, METH_VARARGS, "Evaluate objective and constraints"},
553+
{"problem_constraint_forward", py_problem_constraint_forward, METH_VARARGS, "Evaluate constraints only"},
503554
{"problem_gradient", py_problem_gradient, METH_VARARGS, "Compute objective gradient"},
504555
{"problem_jacobian", py_problem_jacobian, METH_VARARGS, "Compute constraint jacobian"},
505556
{NULL, NULL, 0, NULL}};

python/convert.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ def forward(self, u: np.ndarray) -> tuple[float, np.ndarray]:
132132
"""Evaluate objective and constraints. Returns (obj_value, constraint_values)."""
133133
return diffengine.problem_forward(self._capsule, u)
134134

135+
def constraint_forward(self, u: np.ndarray) -> np.ndarray:
136+
"""Evaluate constraints only. Returns constraint_values array."""
137+
return diffengine.problem_constraint_forward(self._capsule, u)
138+
135139
def gradient(self, u: np.ndarray) -> np.ndarray:
136140
"""Compute gradient of objective. Returns gradient array."""
137141
return diffengine.problem_gradient(self._capsule, u)

python/tests/test_problem_convert.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ def test_problem_jacobian_lowlevel():
6767
assert np.allclose(jac.toarray(), expected_jac)
6868

6969

70+
def test_problem_constraint_forward_lowlevel():
71+
"""Test problem_constraint_forward for constraint values only (low-level)."""
72+
n_vars = 2
73+
x = diffengine.make_variable(n_vars, 1, 0, n_vars)
74+
75+
log_obj = diffengine.make_log(x)
76+
objective = diffengine.make_sum(log_obj, -1)
77+
78+
log_c = diffengine.make_log(x)
79+
exp_c = diffengine.make_exp(x)
80+
constraints = [log_c, exp_c]
81+
82+
prob = diffengine.make_problem(objective, constraints)
83+
u = np.array([2.0, 4.0])
84+
diffengine.problem_allocate(prob, u)
85+
86+
constraint_vals = diffengine.problem_constraint_forward(prob, u)
87+
88+
# Expected: [log(2), log(4), exp(2), exp(4)]
89+
expected = np.concatenate([np.log(u), np.exp(u)])
90+
assert np.allclose(constraint_vals, expected)
91+
92+
7093
def test_problem_no_constraints_lowlevel():
7194
"""Test Problem with no constraints (low-level)."""
7295
n_vars = 3
@@ -320,11 +343,37 @@ def test_problem_repeated_evaluations():
320343
assert np.allclose(grad2, 1.0 / u2)
321344

322345

346+
def test_problem_constraint_forward():
347+
"""Test Problem.constraint_forward for constraint values only."""
348+
x = cp.Variable(2)
349+
obj = cp.sum(cp.log(x))
350+
constraints = [
351+
cp.log(x),
352+
cp.exp(x),
353+
]
354+
355+
cvxpy_prob = cp.Problem(cp.Minimize(obj), constraints)
356+
prob = Problem(cvxpy_prob)
357+
358+
u = np.array([2.0, 4.0])
359+
prob.allocate(u)
360+
361+
# Test constraint_forward
362+
constraint_vals = prob.constraint_forward(u)
363+
expected = np.concatenate([np.log(u), np.exp(u)])
364+
assert np.allclose(constraint_vals, expected)
365+
366+
# Verify it gives same result as forward's constraint values
367+
_, forward_constraint_vals = prob.forward(u)
368+
assert np.allclose(constraint_vals, forward_constraint_vals)
369+
370+
323371
if __name__ == "__main__":
324372
# Low-level tests
325373
test_problem_forward_lowlevel()
326374
test_problem_gradient_lowlevel()
327375
test_problem_jacobian_lowlevel()
376+
test_problem_constraint_forward_lowlevel()
328377
test_problem_no_constraints_lowlevel()
329378
# Problem class tests
330379
test_problem_single_constraint()
@@ -334,4 +383,5 @@ def test_problem_repeated_evaluations():
334383
test_problem_no_constraints_convert()
335384
test_problem_larger_scale()
336385
test_problem_repeated_evaluations()
386+
test_problem_constraint_forward()
337387
print("All problem tests passed!")

src/problem.c

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,19 @@ problem *new_problem(expr *objective, expr **constraints, int n_constraints)
6363
problem *prob = (problem *) calloc(1, sizeof(problem));
6464
if (!prob) return NULL;
6565

66-
/* Take ownership of objective (no retain - caller transfers ownership) */
66+
/* Retain objective (shared ownership with caller) */
6767
prob->objective = objective;
68+
expr_retain(objective);
6869

69-
/* Copy constraints array (take ownership, no retain) */
70+
/* Copy and retain constraints array */
7071
prob->n_constraints = n_constraints;
7172
if (n_constraints > 0)
7273
{
7374
prob->constraints = (expr **) malloc(n_constraints * sizeof(expr *));
7475
for (int i = 0; i < n_constraints; i++)
7576
{
7677
prob->constraints[i] = constraints[i];
78+
expr_retain(constraints[i]);
7779
}
7880
}
7981
else
@@ -139,14 +141,11 @@ void free_problem(problem *prob)
139141
free(prob->gradient_values);
140142
free_csr_matrix(prob->stacked_jac);
141143

142-
/* Free expression trees with shared visited set to handle node sharing */
143-
VisitedSet visited;
144-
visited_init(&visited);
145-
146-
free_expr_tree_visited(prob->objective, &visited);
144+
/* Release expression references (decrements refcount) */
145+
free_expr(prob->objective);
147146
for (int i = 0; i < prob->n_constraints; i++)
148147
{
149-
free_expr_tree_visited(prob->constraints[i], &visited);
148+
free_expr(prob->constraints[i]);
150149
}
151150
free(prob->constraints);
152151

@@ -173,6 +172,21 @@ double problem_forward(problem *prob, const double *u)
173172
return obj_val;
174173
}
175174

175+
double *problem_constraint_forward(problem *prob, const double *u)
176+
{
177+
/* Evaluate constraints only and copy values */
178+
int offset = 0;
179+
for (int i = 0; i < prob->n_constraints; i++)
180+
{
181+
expr *c = prob->constraints[i];
182+
c->forward(c, u);
183+
memcpy(prob->constraint_values + offset, c->value, c->size * sizeof(double));
184+
offset += c->size;
185+
}
186+
187+
return prob->constraint_values;
188+
}
189+
176190
double *problem_gradient(problem *prob, const double *u)
177191
{
178192
/* Jacobian on objective */

tests/all_tests.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ int main(void)
125125
mu_run_test(test_problem_gradient, tests_run);
126126
mu_run_test(test_problem_jacobian, tests_run);
127127
mu_run_test(test_problem_jacobian_multi, tests_run);
128+
mu_run_test(test_problem_constraint_forward, tests_run);
128129

129130
printf("\n=== All %d tests passed ===\n", tests_run);
130131

tests/problem/test_problem.h

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ const char *test_problem_new_free(void)
3737
mu_assert("n_constraints wrong", prob->n_constraints == 1);
3838
mu_assert("total_constraint_size wrong", prob->total_constraint_size == 3);
3939

40-
/* Free problem (owns and frees all expressions) */
40+
/* Free problem and expressions (shared ownership) */
4141
free_problem(prob);
42+
free_expr(objective);
43+
free_expr(log_x);
44+
free_expr(x);
45+
free_expr(x_constraint);
4246

4347
return 0;
4448
}
@@ -73,6 +77,10 @@ const char *test_problem_forward(void)
7377
mu_assert("constraint[2] wrong", fabs(prob->constraint_values[2] - 3.0) < 1e-10);
7478

7579
free_problem(prob);
80+
free_expr(objective);
81+
free_expr(log_x);
82+
free_expr(x);
83+
free_expr(x_constraint);
7684

7785
return 0;
7886
}
@@ -99,6 +107,9 @@ const char *test_problem_gradient(void)
99107
mu_assert("grad[2] wrong", fabs(grad[2] - 0.25) < 1e-10);
100108

101109
free_problem(prob);
110+
free_expr(objective);
111+
free_expr(log_x);
112+
free_expr(x);
102113

103114
return 0;
104115
}
@@ -146,6 +157,11 @@ const char *test_problem_jacobian(void)
146157
mu_assert("jac->x[1] wrong", fabs(jac->x[1] - 0.25) < 1e-10);
147158

148159
free_problem(prob);
160+
free_expr(objective);
161+
free_expr(log_obj);
162+
free_expr(x_obj);
163+
free_expr(log_c1);
164+
free_expr(x_c1);
149165

150166
return 0;
151167
}
@@ -221,6 +237,60 @@ const char *test_problem_jacobian_multi(void)
221237
mu_assert("jac->x[3] wrong", fabs(jac->x[3] - exp(4.0)) < 1e-10);
222238

223239
free_problem(prob);
240+
free_expr(objective);
241+
free_expr(log_obj);
242+
free_expr(log_c1);
243+
free_expr(exp_c2);
244+
free_expr(x);
245+
246+
return 0;
247+
}
248+
249+
/*
250+
* Test problem_constraint_forward: evaluate constraints only
251+
* Constraint 1: log(x) -> [log(2), log(4)]
252+
* Constraint 2: exp(x) -> [exp(2), exp(4)]
253+
*/
254+
const char *test_problem_constraint_forward(void)
255+
{
256+
int n_vars = 2;
257+
258+
/* Shared variable */
259+
expr *x = new_variable(2, 1, 0, n_vars);
260+
261+
/* Objective: sum(log(x)) */
262+
expr *log_obj = new_log(x);
263+
expr *objective = new_sum(log_obj, -1);
264+
265+
/* Constraint 1: log(x) */
266+
expr *log_c1 = new_log(x);
267+
268+
/* Constraint 2: exp(x) */
269+
expr *exp_c2 = new_exp(x);
270+
271+
expr *constraints[2] = {log_c1, exp_c2};
272+
273+
problem *prob = new_problem(objective, constraints, 2);
274+
275+
double u[2] = {2.0, 4.0};
276+
problem_allocate(prob, u);
277+
278+
double *constraint_vals = problem_constraint_forward(prob, u);
279+
280+
/* Check constraint values:
281+
* [log(2), log(4), exp(2), exp(4)]
282+
*/
283+
mu_assert("constraint[0] wrong", fabs(constraint_vals[0] - log(2.0)) < 1e-10);
284+
mu_assert("constraint[1] wrong", fabs(constraint_vals[1] - log(4.0)) < 1e-10);
285+
mu_assert("constraint[2] wrong", fabs(constraint_vals[2] - exp(2.0)) < 1e-10);
286+
mu_assert("constraint[3] wrong", fabs(constraint_vals[3] - exp(4.0)) < 1e-10);
287+
288+
free_problem(prob);
289+
free_expr(objective);
290+
free_expr(log_obj);
291+
free_expr(log_c1);
292+
free_expr(exp_c2);
293+
free_expr(x);
224294

225295
return 0;
226296
}

0 commit comments

Comments
 (0)