Skip to content

Commit 61d657f

Browse files
Transurgeonclaude
andcommitted
Address review comments on parameter-support-v2
- Remove redundant refresh_param_values calls from eval_jacobian and eval_wsum_hess in left_matmul (forward always runs first) - Use memcpy in problem_register_params for pointer array copy - Add PARAM_FIXED guard in problem_update_params to skip fixed constants - Remove unused right_matmul_expr struct from subexpr.h - Add test_param_fixed_skip_in_update covering mixed fixed/updatable params - Add CLAUDE.md for Claude Code guidance Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe8cf7b commit 61d657f

6 files changed

Lines changed: 169 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
SparseDiffEngine is a C library implementing the differentiation engine for CVXPY's DNLP extension. It computes sparse Jacobians and Hessians for nonlinear programming solvers. Used as a backend via Python bindings in the parent SparseDiffPy package.
8+
9+
## Build & Test Commands
10+
11+
```bash
12+
# Build standalone (CMake)
13+
mkdir -p build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug && make
14+
15+
# Run all tests
16+
cd build && ctest --output-on-failure
17+
18+
# Run tests directly (more verbose output)
19+
cd build && ./all_tests
20+
21+
# Rebuild after changes (from build/)
22+
make && ctest --output-on-failure
23+
24+
# Format code
25+
clang-format -i src/**/*.c include/**/*.h
26+
```
27+
28+
**Platform dependencies**: macOS uses Accelerate framework (automatic), Linux needs `libopenblas-dev`, Windows uses OpenBLAS via vcpkg.
29+
30+
## Architecture
31+
32+
### Expression DAG with Function-Pointer Polymorphism
33+
34+
Every node is an `expr` struct (`include/expr.h`) containing function pointers that act as a vtable:
35+
- `forward()` — evaluate node given variable values `u`
36+
- `jacobian_init()` / `eval_jacobian()` — sparsity pattern allocation and numeric fill
37+
- `wsum_hess_init()` / `eval_wsum_hess()` — weighted-sum Hessian
38+
- `is_affine()` — used for caching optimizations
39+
- `free_type_data()` — type-specific cleanup
40+
41+
Type-specific nodes use C struct inheritance: embed `expr base` as the first field, then cast. Defined in `include/subexpr.h` (e.g., `parameter_expr`, `scalar_mult_expr`).
42+
43+
All node constructors call `init_expr()` to wire up the dispatch table. Children are owned via `left`/`right` pointers with reference counting (`expr_retain`/`free_expr`).
44+
45+
### Parameter System
46+
47+
`parameter_expr` (in `subexpr.h`) unifies constants and updatable parameters:
48+
- `param_id == PARAM_FIXED (-1)`: fixed constant, not updatable
49+
- `param_id >= 0`: offset into the parameter vector `theta`
50+
51+
Bivariate ops that involve parameters (scalar_mult, vector_mult, left_matmul, right_matmul) store a `param_source` pointer and read values during forward/jacobian/hessian evaluation.
52+
53+
**Workflow**: `new_parameter()``problem_register_params()``problem_update_params(prob, theta)` copies from `theta` into each parameter node's `value` array using `param_id` as offset.
54+
55+
### Problem Interface (`include/problem.h`)
56+
57+
The `problem` struct owns an objective, constraints array, and parameter nodes. Key lifecycle:
58+
1. `new_problem()` — allocates problem, output arrays
59+
2. `problem_register_params()` — registers parameter nodes
60+
3. `problem_init_derivatives()` — allocates sparsity patterns (Jacobian + Hessian)
61+
4. `problem_objective_forward()` / `problem_constraint_forward()` — evaluate
62+
5. `problem_gradient()` / `problem_jacobian()` / `problem_hessian()` — compute derivatives
63+
6. `problem_update_params()` — update parameter values, reset caches
64+
65+
### Sparse Matrix Formats (`include/utils/`)
66+
67+
CSR is primary. CSC is intermediate during computation. COO for Python interop; lower-triangular COO for symmetric Hessians.
68+
69+
## Adding a New Operation
70+
71+
1. Create `src/<category>/new_op.c` implementing the function pointer interface
72+
2. Add constructor declaration to the appropriate `include/<category>.h`
73+
3. Write test as a `.h` file in `tests/<test_category>/`
74+
4. Register test in `tests/all_tests.c` with `mu_run_test(test_name, tests_run)`
75+
76+
## Test Framework
77+
78+
Uses minunit.h (header-only). Tests are `const char *test_name(void)` functions returning NULL on success. Tolerance: `ABS_TOL=1e-6, REL_TOL=1e-6` via `cmp_double_array()` in `test_helpers.c`. No built-in test filtering — all tests run via single `all_tests` binary.
79+
80+
## C Code Style
81+
82+
C99, Allman braces, 4-space indent, 85-column limit, right-aligned pointers (`int *ptr`). Enforced by `.clang-format`. Strict warnings: `-Wall -Wextra -Wpedantic -Wshadow -Wformat=2 -Wcast-qual -Wcast-align -Wdouble-promotion -Wnull-dereference`.

include/subexpr.h

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,6 @@ typedef struct left_matmul_expr
130130
void (*refresh_param_values)(struct left_matmul_expr *);
131131
} left_matmul_expr;
132132

133-
/* Right matrix multiplication: y = f(x) * A where f(x) is an expression.
134-
* f(x) has shape p x n, A has shape n x q, output y has shape p x q.
135-
* Uses vec(y) = B * vec(f(x)) where B = A^T kron I_p. */
136-
typedef struct right_matmul_expr
137-
{
138-
expr base;
139-
CSR_Matrix *B; /* B = A^T kron I_p */
140-
CSR_Matrix *BT; /* B^T for backpropagating Hessian weights */
141-
CSC_Matrix *CSC_work;
142-
} right_matmul_expr;
143-
144133
/* Scalar multiplication: y = a * child where a comes from param_source */
145134
typedef struct scalar_mult_expr
146135
{

src/bivariate/left_matmul.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ static void jacobian_init(expr *node)
122122
static void eval_jacobian(expr *node)
123123
{
124124
left_matmul_expr *lnode = (left_matmul_expr *) node;
125-
refresh_param_values(lnode);
126125
expr *x = node->left;
127126

128127
CSC_Matrix *Jchild_CSC = lnode->Jchild_CSC;
@@ -157,7 +156,6 @@ static void wsum_hess_init(expr *node)
157156
static void eval_wsum_hess(expr *node, const double *w)
158157
{
159158
left_matmul_expr *lnode = (left_matmul_expr *) node;
160-
refresh_param_values(lnode);
161159

162160
/* compute A^T w*/
163161
Matrix *AT = lnode->AT;

src/problem.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,11 @@ void problem_register_params(problem *prob, expr **param_nodes, int n_param_node
335335
{
336336
prob->n_param_nodes = n_param_nodes;
337337
prob->param_nodes = (expr **) malloc(n_param_nodes * sizeof(expr *));
338-
prob->total_parameter_size = 0;
338+
memcpy(prob->param_nodes, param_nodes, n_param_nodes * sizeof(expr *));
339339

340+
prob->total_parameter_size = 0;
340341
for (int i = 0; i < n_param_nodes; i++)
341342
{
342-
prob->param_nodes[i] = param_nodes[i];
343343
prob->total_parameter_size += param_nodes[i]->size;
344344
}
345345
}
@@ -350,6 +350,8 @@ void problem_update_params(problem *prob, const double *theta)
350350
{
351351
expr *pnode = prob->param_nodes[i];
352352
parameter_expr *param = (parameter_expr *) pnode;
353+
if (param->param_id == PARAM_FIXED)
354+
continue;
353355
int offset = param->param_id;
354356
memcpy(pnode->value, theta + offset, pnode->size * sizeof(double));
355357
param->has_been_refreshed = false;

tests/all_tests.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ int main(void)
303303
mu_run_test(test_param_vector_mult_problem, tests_run);
304304
mu_run_test(test_param_left_matmul_problem, tests_run);
305305
mu_run_test(test_param_right_matmul_problem, tests_run);
306+
mu_run_test(test_param_fixed_skip_in_update, tests_run);
306307
#endif /* PROFILE_ONLY */
307308

308309
#ifdef PROFILE_ONLY

tests/problem/test_param_prob.h

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,4 +345,86 @@ const char *test_param_right_matmul_problem(void)
345345
return 0;
346346
}
347347

348+
/*
349+
* Test 5: PARAM_FIXED params are skipped by problem_update_params
350+
*
351+
* Problem: minimize a * sum(log(x)) + b * sum(x), no constraints, x size 2
352+
* a is a FIXED scalar parameter (param_id=PARAM_FIXED, value=2.0)
353+
* b is an updatable scalar parameter (param_id=0)
354+
*
355+
* At x=[1,2], a=2, b=3:
356+
* obj = 2*(log(1)+log(2)) + 3*(1+2) = 2*log(2) + 9
357+
* gradient = [2/1 + 3, 2/2 + 3] = [5.0, 4.0]
358+
*
359+
* After update theta={5.0} (only b changes to 5, a stays 2):
360+
* obj = 2*log(2) + 5*3 = 2*log(2) + 15
361+
* gradient = [2/1 + 5, 2/2 + 5] = [7.0, 6.0]
362+
*/
363+
const char *test_param_fixed_skip_in_update(void)
364+
{
365+
int n_vars = 2;
366+
367+
/* Build tree: a * sum(log(x)) + b * sum(x) */
368+
expr *x1 = new_variable(2, 1, 0, n_vars);
369+
expr *log_x = new_log(x1);
370+
double a_val = 2.0;
371+
expr *a_param = new_parameter(1, 1, PARAM_FIXED, n_vars, &a_val);
372+
expr *a_log = new_scalar_mult(a_param, log_x);
373+
expr *sum_a_log = new_sum(a_log, -1);
374+
375+
expr *x2 = new_variable(2, 1, 0, n_vars);
376+
expr *b_param = new_parameter(1, 1, 0, n_vars, NULL);
377+
expr *b_x = new_scalar_mult(b_param, x2);
378+
expr *sum_b_x = new_sum(b_x, -1);
379+
380+
expr *objective = new_add(sum_a_log, sum_b_x);
381+
382+
/* Create problem and register BOTH params */
383+
problem *prob = new_problem(objective, NULL, 0, false);
384+
385+
expr *param_nodes[2] = {a_param, b_param};
386+
problem_register_params(prob, param_nodes, 2);
387+
problem_init_derivatives(prob);
388+
389+
/* Set b=3 and evaluate at x=[1,2] */
390+
double theta[1] = {3.0};
391+
problem_update_params(prob, theta);
392+
393+
/* Verify a is still 2.0 (not overwritten) */
394+
mu_assert("a_param changed after update",
395+
fabs(a_param->value[0] - 2.0) < 1e-10);
396+
397+
double u[2] = {1.0, 2.0};
398+
double obj_val = problem_objective_forward(prob, u);
399+
problem_gradient(prob);
400+
401+
double expected_obj = 2.0 * log(2.0) + 9.0;
402+
mu_assert("obj wrong (b=3)", fabs(obj_val - expected_obj) < 1e-10);
403+
404+
double expected_grad[2] = {5.0, 4.0};
405+
mu_assert("gradient wrong (b=3)",
406+
cmp_double_array(prob->gradient_values, expected_grad, 2));
407+
408+
/* Update b=5, a should stay 2 */
409+
theta[0] = 5.0;
410+
problem_update_params(prob, theta);
411+
412+
mu_assert("a_param changed after second update",
413+
fabs(a_param->value[0] - 2.0) < 1e-10);
414+
415+
obj_val = problem_objective_forward(prob, u);
416+
problem_gradient(prob);
417+
418+
double expected_obj2 = 2.0 * log(2.0) + 15.0;
419+
mu_assert("obj wrong (b=5)", fabs(obj_val - expected_obj2) < 1e-10);
420+
421+
double expected_grad2[2] = {7.0, 6.0};
422+
mu_assert("gradient wrong (b=5)",
423+
cmp_double_array(prob->gradient_values, expected_grad2, 2));
424+
425+
free_problem(prob);
426+
427+
return 0;
428+
}
429+
348430
#endif /* TEST_PARAM_PROB_H */

0 commit comments

Comments
 (0)