Skip to content

Commit a2ebd92

Browse files
authored
Merge pull request #3 from dance858/tree-converter
Adds convert function from cvxpy problem to C
2 parents 4698872 + 624f5f5 commit a2ebd92

5 files changed

Lines changed: 843 additions & 10 deletions

File tree

docs/problem_struct_design.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Design: C Problem Struct
2+
3+
## Summary
4+
5+
Create a native C `problem` struct that encapsulates objective + constraints, with methods for:
6+
- `forward(u)` - evaluate objective and all constraints
7+
- `gradient(u)` - return objective gradient (jacobian.T)
8+
- `jacobian(u)` - return single stacked CSR matrix of constraint jacobians
9+
10+
## Files to Create/Modify
11+
12+
1. **`include/problem.h`** - New header defining problem struct
13+
2. **`src/problem.c`** - Implementation
14+
3. **`python/bindings.c`** - Python bindings for problem
15+
4. **`python/convert.py`** - Update to return problem capsule
16+
5. **`tests/problem/test_problem.h`** - C tests
17+
18+
---
19+
20+
## Step 1: Create `include/problem.h`
21+
22+
```c
23+
#ifndef PROBLEM_H
24+
#define PROBLEM_H
25+
26+
#include "expr.h"
27+
#include "utils/CSR_Matrix.h"
28+
29+
typedef struct problem
30+
{
31+
expr *objective; /* Objective expression (scalar) */
32+
expr **constraints; /* Array of constraint expressions */
33+
int n_constraints;
34+
int n_vars;
35+
int total_constraint_size; /* Sum of all constraint sizes */
36+
37+
/* Pre-allocated storage */
38+
double *constraint_values;
39+
double *gradient_values; /* Dense gradient array */
40+
CSR_Matrix *stacked_jac;
41+
} problem;
42+
43+
problem *new_problem(expr *objective, expr **constraints, int n_constraints);
44+
void problem_allocate(problem *prob, const double *u);
45+
void free_problem(problem *prob);
46+
double problem_forward(problem *prob, const double *u);
47+
double *problem_gradient(problem *prob, const double *u);
48+
CSR_Matrix *problem_jacobian(problem *prob, const double *u);
49+
50+
#endif
51+
```
52+
53+
---
54+
55+
## Step 2: Create `src/problem.c`
56+
57+
Key functions:
58+
59+
### `new_problem`
60+
- Retain (increment refcount) on objective and all constraints
61+
- Compute `total_constraint_size = sum(constraints[i]->size)`
62+
- Does NOT allocate storage arrays (use `problem_allocate` separately)
63+
64+
### `problem_allocate`
65+
Separate function to allocate memory for constraint values and jacobian:
66+
67+
```c
68+
void problem_allocate(problem *prob, const double *u)
69+
{
70+
/* 1. Allocate constraint values array */
71+
prob->constraint_values = malloc(prob->total_constraint_size * sizeof(double));
72+
73+
/* 2. Allocate jacobian:
74+
* - First, initialize all constraint jacobians
75+
* - Count total nnz across all constraints
76+
* - Allocate CSR matrix with this nnz (may be slight overestimate)
77+
*/
78+
int total_nnz = 0;
79+
for (int i = 0; i < prob->n_constraints; i++)
80+
{
81+
expr *c = prob->constraints[i];
82+
c->forward(c, u);
83+
c->jacobian_init(c);
84+
total_nnz += c->jacobian->nnz;
85+
}
86+
87+
/* Allocate stacked jacobian with total_constraint_size rows */
88+
prob->stacked_jac = alloc_csr(prob->total_constraint_size, prob->n_vars, total_nnz);
89+
90+
/* Note: The actual nnz may be smaller after evaluation due to
91+
* cancellations. Update stacked_jac->nnz after problem_jacobian(). */
92+
}
93+
```
94+
95+
### `free_problem`
96+
- Call `free_expr` on objective and all constraints (decrements refcount)
97+
- Free allocated arrays and stacked_jac
98+
99+
### `problem_forward`
100+
```c
101+
double problem_forward(problem *prob, const double *u)
102+
{
103+
prob->objective->forward(prob->objective, u);
104+
double obj_val = prob->objective->value[0];
105+
106+
int offset = 0;
107+
for (int i = 0; i < prob->n_constraints; i++)
108+
{
109+
expr *c = prob->constraints[i];
110+
c->forward(c, u);
111+
memcpy(prob->constraint_values + offset, c->value, c->size * sizeof(double));
112+
offset += c->size;
113+
}
114+
return obj_val;
115+
}
116+
```
117+
118+
### `problem_gradient`
119+
- Run forward pass on objective
120+
- Call jacobian_init + eval_jacobian
121+
- Objective jacobian is 1 x n_vars row vector
122+
- Copy sparse row to dense `gradient_values` array
123+
- Return pointer to internal array
124+
125+
### `problem_jacobian`
126+
- Forward + jacobian for each constraint
127+
- Stack CSR matrices vertically:
128+
- Total rows = `total_constraint_size`
129+
- Copy row pointers with offset, copy col indices and values
130+
- Lazy allocate/reallocate `stacked_jac` based on total nnz
131+
132+
---
133+
134+
## Step 3: Update `python/bindings.c`
135+
136+
Add capsule and functions:
137+
138+
```c
139+
#define PROBLEM_CAPSULE_NAME "DNLP_PROBLEM"
140+
141+
static void problem_capsule_destructor(PyObject *capsule) { ... }
142+
143+
static PyObject *py_make_problem(PyObject *self, PyObject *args)
144+
{
145+
PyObject *obj_capsule, *constraints_list;
146+
// Parse objective capsule and list of constraint capsules
147+
// Extract expr* pointers, call new_problem
148+
// Return PyCapsule
149+
}
150+
151+
static PyObject *py_problem_forward(PyObject *self, PyObject *args)
152+
{
153+
// Returns: (obj_value, constraint_values_array)
154+
}
155+
156+
static PyObject *py_problem_gradient(PyObject *self, PyObject *args)
157+
{
158+
// Returns: numpy array of size n_vars
159+
}
160+
161+
static PyObject *py_problem_jacobian(PyObject *self, PyObject *args)
162+
{
163+
// Returns: (data, indices, indptr, (m, n)) for scipy CSR
164+
}
165+
```
166+
167+
Add to DNLPMethods:
168+
```c
169+
{"make_problem", py_make_problem, METH_VARARGS, "Create problem"},
170+
{"problem_forward", py_problem_forward, METH_VARARGS, "Evaluate problem"},
171+
{"problem_gradient", py_problem_gradient, METH_VARARGS, "Compute gradient"},
172+
{"problem_jacobian", py_problem_jacobian, METH_VARARGS, "Compute constraint jacobian"},
173+
```
174+
175+
---
176+
177+
## Step 4: Update `python/convert.py`
178+
179+
```python
180+
def convert_problem(problem: cp.Problem):
181+
"""Convert CVXPY Problem to C problem struct."""
182+
var_dict = build_variable_dict(problem.variables())
183+
184+
c_objective = _convert_expr(problem.objective.expr, var_dict)
185+
c_constraints = [_convert_expr(c.expr, var_dict) for c in problem.constraints]
186+
187+
return diffengine.make_problem(c_objective, c_constraints)
188+
189+
190+
class Problem:
191+
"""Wrapper for C problem struct with clean Python API."""
192+
193+
def __init__(self, cvxpy_problem: cp.Problem):
194+
self._capsule = convert_problem(cvxpy_problem)
195+
196+
def forward(self, u: np.ndarray) -> tuple[float, np.ndarray]:
197+
return diffengine.problem_forward(self._capsule, u)
198+
199+
def gradient(self, u: np.ndarray) -> np.ndarray:
200+
return diffengine.problem_gradient(self._capsule, u)
201+
202+
def jacobian(self, u: np.ndarray) -> sparse.csr_matrix:
203+
data, indices, indptr, shape = diffengine.problem_jacobian(self._capsule, u)
204+
return sparse.csr_matrix((data, indices, indptr), shape=shape)
205+
```
206+
207+
---
208+
209+
## Step 5: Add Tests
210+
211+
### C tests in `tests/problem/test_problem.h`:
212+
- `test_problem_forward` - verify objective and constraint values
213+
- `test_problem_gradient` - verify gradient matches manual calculation
214+
- `test_problem_jacobian_stacking` - verify stacked matrix structure
215+
216+
### Python tests in `convert.py`:
217+
- `test_problem_forward` - compare with numpy
218+
- `test_problem_gradient` - gradient of sum(log(x)) = 1/x
219+
- `test_problem_jacobian` - verify stacked jacobian shape and values
220+
221+
---
222+
223+
## Implementation Order
224+
225+
1. Create `include/problem.h`
226+
2. Create `src/problem.c` with new_problem, free_problem, problem_forward
227+
3. Add problem_gradient and problem_jacobian
228+
4. Add Python bindings to `bindings.c`
229+
5. Rebuild: `cmake --build build`
230+
6. Update `convert.py` with Problem class
231+
7. Add and run tests
232+
233+
## Key Design Notes
234+
235+
- **Memory**: Uses expr refcounting - new_problem retains, free_problem releases
236+
- **Two-phase init**: `new_problem` creates struct, `problem_allocate` allocates arrays
237+
- Constraint values array: size = `total_constraint_size`
238+
- Jacobian: initialize all constraint jacobians first, count total nnz, allocate CSR
239+
- The allocated nnz may be a slight overestimate; update `stacked_jac->nnz` after evaluation
240+
- **Hessian**: Deferred - not allocated in this design (to be added later)
241+
- **API**: Returns internal pointers (caller should NOT free)

0 commit comments

Comments
 (0)