Skip to content

Commit 9b4076e

Browse files
committed
adding docs for problem struct
1 parent ca03bcc commit 9b4076e

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

docs/problem_struct_design.md

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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 free_problem(problem *prob);
45+
double problem_forward(problem *prob, const double *u);
46+
double *problem_gradient(problem *prob, const double *u);
47+
CSR_Matrix *problem_jacobian(problem *prob, const double *u);
48+
49+
#endif
50+
```
51+
52+
---
53+
54+
## Step 2: Create `src/problem.c`
55+
56+
Key functions:
57+
58+
### `new_problem`
59+
- Retain (increment refcount) on objective and all constraints
60+
- Compute `total_constraint_size = sum(constraints[i]->size)`
61+
- Pre-allocate `constraint_values` and `gradient_values` arrays
62+
63+
### `free_problem`
64+
- Call `free_expr` on objective and all constraints (decrements refcount)
65+
- Free allocated arrays and stacked_jac
66+
67+
### `problem_forward`
68+
```c
69+
double problem_forward(problem *prob, const double *u)
70+
{
71+
prob->objective->forward(prob->objective, u);
72+
double obj_val = prob->objective->value[0];
73+
74+
int offset = 0;
75+
for (int i = 0; i < prob->n_constraints; i++)
76+
{
77+
expr *c = prob->constraints[i];
78+
c->forward(c, u);
79+
memcpy(prob->constraint_values + offset, c->value, c->size * sizeof(double));
80+
offset += c->size;
81+
}
82+
return obj_val;
83+
}
84+
```
85+
86+
### `problem_gradient`
87+
- Run forward pass on objective
88+
- Call jacobian_init + eval_jacobian
89+
- Objective jacobian is 1 x n_vars row vector
90+
- Copy sparse row to dense `gradient_values` array
91+
- Return pointer to internal array
92+
93+
### `problem_jacobian`
94+
- Forward + jacobian for each constraint
95+
- Stack CSR matrices vertically:
96+
- Total rows = `total_constraint_size`
97+
- Copy row pointers with offset, copy col indices and values
98+
- Lazy allocate/reallocate `stacked_jac` based on total nnz
99+
100+
---
101+
102+
## Step 3: Update `python/bindings.c`
103+
104+
Add capsule and functions:
105+
106+
```c
107+
#define PROBLEM_CAPSULE_NAME "DNLP_PROBLEM"
108+
109+
static void problem_capsule_destructor(PyObject *capsule) { ... }
110+
111+
static PyObject *py_make_problem(PyObject *self, PyObject *args)
112+
{
113+
PyObject *obj_capsule, *constraints_list;
114+
// Parse objective capsule and list of constraint capsules
115+
// Extract expr* pointers, call new_problem
116+
// Return PyCapsule
117+
}
118+
119+
static PyObject *py_problem_forward(PyObject *self, PyObject *args)
120+
{
121+
// Returns: (obj_value, constraint_values_array)
122+
}
123+
124+
static PyObject *py_problem_gradient(PyObject *self, PyObject *args)
125+
{
126+
// Returns: numpy array of size n_vars
127+
}
128+
129+
static PyObject *py_problem_jacobian(PyObject *self, PyObject *args)
130+
{
131+
// Returns: (data, indices, indptr, (m, n)) for scipy CSR
132+
}
133+
```
134+
135+
Add to DNLPMethods:
136+
```c
137+
{"make_problem", py_make_problem, METH_VARARGS, "Create problem"},
138+
{"problem_forward", py_problem_forward, METH_VARARGS, "Evaluate problem"},
139+
{"problem_gradient", py_problem_gradient, METH_VARARGS, "Compute gradient"},
140+
{"problem_jacobian", py_problem_jacobian, METH_VARARGS, "Compute constraint jacobian"},
141+
```
142+
143+
---
144+
145+
## Step 4: Update `python/convert.py`
146+
147+
```python
148+
def convert_problem(problem: cp.Problem):
149+
"""Convert CVXPY Problem to C problem struct."""
150+
var_dict = build_variable_dict(problem.variables())
151+
152+
c_objective = _convert_expr(problem.objective.expr, var_dict)
153+
c_constraints = [_convert_expr(c.expr, var_dict) for c in problem.constraints]
154+
155+
return diffengine.make_problem(c_objective, c_constraints)
156+
157+
158+
class Problem:
159+
"""Wrapper for C problem struct with clean Python API."""
160+
161+
def __init__(self, cvxpy_problem: cp.Problem):
162+
self._capsule = convert_problem(cvxpy_problem)
163+
164+
def forward(self, u: np.ndarray) -> tuple[float, np.ndarray]:
165+
return diffengine.problem_forward(self._capsule, u)
166+
167+
def gradient(self, u: np.ndarray) -> np.ndarray:
168+
return diffengine.problem_gradient(self._capsule, u)
169+
170+
def jacobian(self, u: np.ndarray) -> sparse.csr_matrix:
171+
data, indices, indptr, shape = diffengine.problem_jacobian(self._capsule, u)
172+
return sparse.csr_matrix((data, indices, indptr), shape=shape)
173+
```
174+
175+
---
176+
177+
## Step 5: Add Tests
178+
179+
### C tests in `tests/problem/test_problem.h`:
180+
- `test_problem_forward` - verify objective and constraint values
181+
- `test_problem_gradient` - verify gradient matches manual calculation
182+
- `test_problem_jacobian_stacking` - verify stacked matrix structure
183+
184+
### Python tests in `convert.py`:
185+
- `test_problem_forward` - compare with numpy
186+
- `test_problem_gradient` - gradient of sum(log(x)) = 1/x
187+
- `test_problem_jacobian` - verify stacked jacobian shape and values
188+
189+
---
190+
191+
## Implementation Order
192+
193+
1. Create `include/problem.h`
194+
2. Create `src/problem.c` with new_problem, free_problem, problem_forward
195+
3. Add problem_gradient and problem_jacobian
196+
4. Add Python bindings to `bindings.c`
197+
5. Rebuild: `cmake --build build`
198+
6. Update `convert.py` with Problem class
199+
7. Add and run tests
200+
201+
## Key Design Notes
202+
203+
- **Memory**: Uses expr refcounting - new_problem retains, free_problem releases
204+
- **Efficiency**: Pre-allocates arrays, lazy-allocates stacked jacobian
205+
- **API**: Returns internal pointers (caller should NOT free)

0 commit comments

Comments
 (0)