Skip to content

Commit 23d6cef

Browse files
Transurgeonclaude
andcommitted
Merge origin/main into parameter-support-v2 and add CLAUDE.md
Resolve merge conflicts in parameter.c and scalar_mult.c: keep parameter-support-v2 features (parameter_expr, param_id, free_type_data) while adopting _impl suffix naming from main. Fix old 2-arg new_left_matmul calls in test_chain_rule_jacobian.h to use new 3-arg signature with NULL param_node. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2 parents 4353001 + 60ea4b0 commit 23d6cef

89 files changed

Lines changed: 547 additions & 288 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build & Test
6+
7+
```bash
8+
# Build (from repo root)
9+
mkdir -p build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug && make
10+
11+
# Run all tests
12+
./build/all_tests
13+
14+
# Run tests via CTest
15+
cd build && ctest --output-on-failure
16+
17+
# Format check (requires clang-format)
18+
find src include -name '*.c' -o -name '*.h' | xargs clang-format --dry-run -Werror
19+
20+
# Build with sanitizers (for debugging)
21+
cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_FLAGS="-fsanitize=address,undefined" && make
22+
```
23+
24+
There is no way to run a single test in isolation — `all_tests` runs the full suite. To test selectively, temporarily comment out `mu_run_test()` calls in `tests/all_tests.c`.
25+
26+
## Architecture
27+
28+
### Expr Node (Virtual Dispatch via Function Pointers)
29+
30+
Every operation is an `expr` node (`include/expr.h`). Polymorphism is achieved through function pointers set at construction time via `init_expr()`:
31+
32+
- `forward` — evaluate node value from input vector `u`
33+
- `jacobian_init` / `eval_jacobian` — allocate then fill sparse Jacobian (CSR)
34+
- `wsum_hess_init` / `eval_wsum_hess` — allocate then fill weighted-sum Hessian (CSR)
35+
- `is_affine` — used to skip zero Hessian computation
36+
- `local_jacobian` / `local_wsum_hess` — element-wise ops only: diagonal f'(g(x)) and f''(g(x))
37+
- `free_type_data` — cleanup for type-specific allocations (e.g., param_source pointers)
38+
39+
Type-specific data uses C struct inheritance: cast the first field (`expr base`) to a subtype defined in `include/subexpr.h` (e.g., `parameter_expr`, `scalar_mult_expr`, `left_matmul_expr`).
40+
41+
### Operation Categories
42+
43+
| Directory | Description | Chain rule pattern |
44+
|-----------|-------------|-------------------|
45+
| `src/affine/` | Linear ops (variable, parameter, add, index, matmul, etc.) | J = structural combination of child Jacobians |
46+
| `src/elementwise_full_dom/` | Univariate ops (exp, sin, log, power, etc.) | J = diag(f'(g(x))) · J_child; common chain rule in `common.c` |
47+
| `src/elementwise_restricted_dom/` | Univariate ops with domain restrictions (log, entropy) | Same pattern as full_dom |
48+
| `src/bivariate_full_dom/` | Two-arg ops (elementwise multiply) | Product rule on two children |
49+
| `src/bivariate_restricted_dom/` | Two-arg restricted domain (quad_over_lin, rel_entr) | Custom chain rules |
50+
| `src/other/` | Special ops (prod, quad_form) | Custom implementations |
51+
52+
### Init vs Eval Split
53+
54+
Derivative computation is split into two phases:
55+
1. **Init** (`jacobian_init`, `wsum_hess_init`) — allocates CSR matrices and sets sparsity patterns. Called once per expression tree. Guarded by `jacobian_init()` / `wsum_hess_init()` wrappers in `expr.c` to handle DAGs where nodes are visited multiple times.
56+
2. **Eval** (`eval_jacobian`, `eval_wsum_hess`) — fills numerical values into pre-allocated structures. Called each time input changes.
57+
58+
### Elementwise Common Chain Rule
59+
60+
`src/elementwise_full_dom/common.c` and `src/elementwise_restricted_dom/common.c` implement shared chain rule logic for all univariate ops. Each op only provides `local_jacobian` (diagonal of f') and `local_wsum_hess` (diagonal of f''). The common code handles:
61+
- Variable child: Jacobian is diagonal
62+
- Composite child: J = diag(f') · J_child; H = J_child^T · diag(w·f'') · J_child + f' · H_child
63+
64+
### Parameter System
65+
66+
Parameters (`src/affine/parameter.c`) unify constants and updatable values:
67+
- `param_id == PARAM_FIXED` (-1): fixed constant, value set at construction
68+
- `param_id >= 0`: updatable parameter, indexed into a global theta array
69+
70+
Problem-level API (`src/problem.c`):
71+
- `problem_register_params()` — register parameter nodes
72+
- `problem_update_params()` — update all parameter values from theta vector
73+
74+
Operations that use parameters (scalar_mult, vector_mult, left/right_matmul) store a `param_source` pointer to the parameter node and read its value during forward/jacobian/hessian evaluation.
75+
76+
### Workspace (`Expr_Work`)
77+
78+
Each node has a `work` pointer for cached intermediate results: CSC conversion of Jacobian, local Jacobian diagonal, Hessian term1/term2 matrices. Allocated during init, reused across evals.
79+
80+
## Adding a New Operation
81+
82+
1. If needed, add a subtype struct in `include/subexpr.h`
83+
2. Declare the constructor in the appropriate header (`include/affine.h`, `include/elementwise_full_dom.h`, etc.)
84+
3. Implement in `src/<category>/<op_name>.c` with static functions for forward, jacobian_init_impl, eval_jacobian, wsum_hess_init_impl, eval_wsum_hess, is_affine
85+
4. For elementwise univariate ops: only implement `local_jacobian` and `local_wsum_hess`, then use `common_jacobian_init`/`common_eval_jacobian`/etc. from `common.c`
86+
5. Add tests in `tests/forward_pass/`, `tests/jacobian_tests/`, `tests/wsum_hess/` and register them in `tests/all_tests.c`
87+
6. Use `check_jacobian()` and `check_wsum_hess()` from `tests/numerical_diff.h` to validate against finite differences
88+
89+
## Code Style
90+
91+
C99. Enforced by `.clang-format`: Allman braces, 4-space indent, 85-column limit, right-aligned pointers. Run `clang-format -i <file>` before committing.
92+
93+
## CI Workflows (`.github/workflows/`)
94+
95+
- `cmake.yml` — build and test on Linux, macOS, Windows
96+
- `formatting.yml` — clang-format check
97+
- `sanitizer.yml` — ASan, MSan, UBSan
98+
- `valgrind.yml` — memory leak detection
99+
- `release.yml` — release packaging
100+
101+
## Platform Notes
102+
103+
- **macOS**: Links Accelerate for BLAS
104+
- **Linux**: Links OpenBLAS (`libopenblas-dev`)
105+
- **Windows**: Links OpenBLAS via vcpkg; no dense matmul in some configurations
106+
107+
## Test Tolerances
108+
109+
`ABS_TOL = 1e-6`, `REL_TOL = 1e-6` (defined in test headers). Numerical differentiation step size: `NUMERICAL_DIFF_DEFAULT_H = 1e-7`.

include/expr.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ typedef struct expr
7373
CSR_Matrix *jacobian;
7474
CSR_Matrix *wsum_hess;
7575
forward_fn forward;
76-
jacobian_init_fn jacobian_init;
77-
wsum_hess_init_fn wsum_hess_init;
76+
jacobian_init_fn jacobian_init_impl;
77+
wsum_hess_init_fn wsum_hess_init_impl;
7878
eval_jacobian_fn eval_jacobian;
7979
wsum_hess_fn eval_wsum_hess;
8080

@@ -99,6 +99,11 @@ void init_expr(expr *node, int d1, int d2, int n_vars, forward_fn forward,
9999

100100
void free_expr(expr *node);
101101

102+
/* Guarded init: skips if already initialized (safe for DAGs
103+
* where a node may be visited through multiple parents). */
104+
void jacobian_init(expr *node);
105+
void wsum_hess_init(expr *node);
106+
102107
/* Initialize CSC form of the Jacobian from the CSR Jacobian.
103108
* Must be called after jacobian_init. */
104109
void jacobian_csc_init(expr *node);

src/affine/add.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ static void forward(expr *node, const double *u)
3434
}
3535
}
3636

37-
static void jacobian_init(expr *node)
37+
static void jacobian_init_impl(expr *node)
3838
{
3939
/* initialize children's jacobians */
40-
node->left->jacobian_init(node->left);
41-
node->right->jacobian_init(node->right);
40+
jacobian_init(node->left);
41+
jacobian_init(node->right);
4242

4343
/* we never have to store more than the sum of children's nnz */
4444
int nnz_max = node->left->jacobian->nnz + node->right->jacobian->nnz;
@@ -60,11 +60,11 @@ static void eval_jacobian(expr *node)
6060
node->jacobian);
6161
}
6262

63-
static void wsum_hess_init(expr *node)
63+
static void wsum_hess_init_impl(expr *node)
6464
{
6565
/* initialize children's wsum_hess */
66-
node->left->wsum_hess_init(node->left);
67-
node->right->wsum_hess_init(node->right);
66+
wsum_hess_init(node->left);
67+
wsum_hess_init(node->right);
6868

6969
/* we never have to store more than the sum of children's nnz */
7070
int nnz_max = node->left->wsum_hess->nnz + node->right->wsum_hess->nnz;
@@ -95,8 +95,8 @@ expr *new_add(expr *left, expr *right)
9595
{
9696
assert(left->d1 == right->d1 && left->d2 == right->d2);
9797
expr *node = (expr *) calloc(1, sizeof(expr));
98-
init_expr(node, left->d1, left->d2, left->n_vars, forward, jacobian_init,
99-
eval_jacobian, is_affine, wsum_hess_init, eval_wsum_hess, NULL);
98+
init_expr(node, left->d1, left->d2, left->n_vars, forward, jacobian_init_impl,
99+
eval_jacobian, is_affine, wsum_hess_init_impl, eval_wsum_hess, NULL);
100100
node->left = left;
101101
node->right = right;
102102
expr_retain(left);

src/affine/broadcast.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ static void forward(expr *node, const double *u)
6666
}
6767
}
6868

69-
static void jacobian_init(expr *node)
69+
static void jacobian_init_impl(expr *node)
7070
{
7171
expr *x = node->left;
72-
x->jacobian_init(x);
72+
jacobian_init(x);
7373
broadcast_expr *bcast = (broadcast_expr *) node;
7474
int total_nnz;
7575

@@ -185,10 +185,10 @@ static void eval_jacobian(expr *node)
185185
}
186186
}
187187

188-
static void wsum_hess_init(expr *node)
188+
static void wsum_hess_init_impl(expr *node)
189189
{
190190
expr *x = node->left;
191-
x->wsum_hess_init(x);
191+
wsum_hess_init(x);
192192

193193
/* Same sparsity as child - weights get summed */
194194
node->wsum_hess = new_csr_copy_sparsity(x->wsum_hess);
@@ -279,8 +279,8 @@ expr *new_broadcast(expr *child, int d1, int d2)
279279
// --------------------------------------------------------------------------
280280
// initialize the rest of the expression
281281
// --------------------------------------------------------------------------
282-
init_expr(node, d1, d2, child->n_vars, forward, jacobian_init, eval_jacobian,
283-
is_affine, wsum_hess_init, eval_wsum_hess, NULL);
282+
init_expr(node, d1, d2, child->n_vars, forward, jacobian_init_impl,
283+
eval_jacobian, is_affine, wsum_hess_init_impl, eval_wsum_hess, NULL);
284284
node->left = child;
285285
expr_retain(child);
286286
bcast->type = type;

src/affine/diag_vec.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ static void forward(expr *node, const double *u)
4444
}
4545
}
4646

47-
static void jacobian_init(expr *node)
47+
static void jacobian_init_impl(expr *node)
4848
{
4949
expr *x = node->left;
5050
int n = x->size;
51-
x->jacobian_init(x);
51+
jacobian_init(x);
5252

5353
CSR_Matrix *Jx = x->jacobian;
5454
CSR_Matrix *J = new_csr_matrix(node->size, node->n_vars, Jx->nnz);
@@ -92,12 +92,12 @@ static void eval_jacobian(expr *node)
9292
}
9393
}
9494

95-
static void wsum_hess_init(expr *node)
95+
static void wsum_hess_init_impl(expr *node)
9696
{
9797
expr *x = node->left;
9898

9999
/* initialize child's wsum_hess */
100-
x->wsum_hess_init(x);
100+
wsum_hess_init(x);
101101

102102
/* workspace for extracting diagonal weights */
103103
node->work->dwork = (double *) calloc(x->size, sizeof(double));
@@ -137,8 +137,8 @@ expr *new_diag_vec(expr *child)
137137
/* n is the number of elements (works for both row and column vectors) */
138138
int n = child->size;
139139
expr *node = (expr *) calloc(1, sizeof(expr));
140-
init_expr(node, n, n, child->n_vars, forward, jacobian_init, eval_jacobian,
141-
is_affine, wsum_hess_init, eval_wsum_hess, NULL);
140+
init_expr(node, n, n, child->n_vars, forward, jacobian_init_impl, eval_jacobian,
141+
is_affine, wsum_hess_init_impl, eval_wsum_hess, NULL);
142142
node->left = child;
143143
expr_retain(child);
144144

src/affine/hstack.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ static void forward(expr *node, const double *u)
4242
}
4343
}
4444

45-
static void jacobian_init(expr *node)
45+
static void jacobian_init_impl(expr *node)
4646
{
4747
hstack_expr *hnode = (hstack_expr *) node;
4848

@@ -51,7 +51,7 @@ static void jacobian_init(expr *node)
5151
for (int i = 0; i < hnode->n_args; i++)
5252
{
5353
assert(hnode->args[i] != NULL);
54-
hnode->args[i]->jacobian_init(hnode->args[i]);
54+
jacobian_init(hnode->args[i]);
5555
nnz += hnode->args[i]->jacobian->nnz;
5656
}
5757

@@ -100,14 +100,14 @@ static void eval_jacobian(expr *node)
100100
}
101101
}
102102

103-
static void wsum_hess_init(expr *node)
103+
static void wsum_hess_init_impl(expr *node)
104104
{
105105
/* initialize children's hessians */
106106
hstack_expr *hnode = (hstack_expr *) node;
107107
int nnz = 0;
108108
for (int i = 0; i < hnode->n_args; i++)
109109
{
110-
hnode->args[i]->wsum_hess_init(hnode->args[i]);
110+
wsum_hess_init(hnode->args[i]);
111111
nnz += hnode->args[i]->wsum_hess->nnz;
112112
}
113113

@@ -187,8 +187,9 @@ expr *new_hstack(expr **args, int n_args, int n_vars)
187187
/* Allocate the type-specific struct */
188188
hstack_expr *hnode = (hstack_expr *) calloc(1, sizeof(hstack_expr));
189189
expr *node = &hnode->base;
190-
init_expr(node, args[0]->d1, d2, n_vars, forward, jacobian_init, eval_jacobian,
191-
is_affine, wsum_hess_init, wsum_hess_eval, free_type_data);
190+
init_expr(node, args[0]->d1, d2, n_vars, forward, jacobian_init_impl,
191+
eval_jacobian, is_affine, wsum_hess_init_impl, wsum_hess_eval,
192+
free_type_data);
192193

193194
/* Set type-specific fields (deep copy args array) */
194195
hnode->args = (expr **) calloc(n_args, sizeof(expr *));

src/affine/index.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ static void forward(expr *node, const double *u)
5757
}
5858
}
5959

60-
static void jacobian_init(expr *node)
60+
static void jacobian_init_impl(expr *node)
6161
{
6262
expr *x = node->left;
6363
index_expr *idx = (index_expr *) node;
64-
x->jacobian_init(x);
64+
jacobian_init(x);
6565

6666
CSR_Matrix *Jx = x->jacobian;
6767
CSR_Matrix *J = new_csr_matrix(node->size, node->n_vars, Jx->nnz);
@@ -96,12 +96,12 @@ static void eval_jacobian(expr *node)
9696
}
9797
}
9898

99-
static void wsum_hess_init(expr *node)
99+
static void wsum_hess_init_impl(expr *node)
100100
{
101101
expr *x = node->left;
102102

103103
/* initialize child's wsum_hess */
104-
x->wsum_hess_init(x);
104+
wsum_hess_init(x);
105105

106106
/* for setting weight vector to evaluate hessian of child */
107107
node->work->dwork = (double *) calloc(x->size, sizeof(double));
@@ -166,8 +166,9 @@ expr *new_index(expr *child, int d1, int d2, const int *indices, int n_idxs)
166166
index_expr *idx = (index_expr *) calloc(1, sizeof(index_expr));
167167
expr *node = &idx->base;
168168

169-
init_expr(node, d1, d2, child->n_vars, forward, jacobian_init, eval_jacobian,
170-
is_affine, wsum_hess_init, eval_wsum_hess, free_type_data);
169+
init_expr(node, d1, d2, child->n_vars, forward, jacobian_init_impl,
170+
eval_jacobian, is_affine, wsum_hess_init_impl, eval_wsum_hess,
171+
free_type_data);
171172

172173
node->left = child;
173174
expr_retain(child);

src/affine/left_matmul.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,13 @@ static void free_type_data(expr *node)
104104
lnode->param_source = NULL;
105105
}
106106

107-
static void jacobian_init(expr *node)
107+
static void jacobian_init_impl(expr *node)
108108
{
109109
expr *x = node->left;
110110
left_matmul_expr *lnode = (left_matmul_expr *) node;
111111

112112
/* initialize child's jacobian and precompute sparsity of its CSC */
113-
x->jacobian_init(x);
113+
jacobian_init(x);
114114
lnode->Jchild_CSC = csr_to_csc_fill_sparsity(x->jacobian, node->work->iwork);
115115

116116
/* precompute sparsity of this node's jacobian in CSC and CSR */
@@ -136,11 +136,11 @@ static void eval_jacobian(expr *node)
136136
csc_to_csr_fill_values(J_CSC, node->jacobian, lnode->csc_to_csr_work);
137137
}
138138

139-
static void wsum_hess_init(expr *node)
139+
static void wsum_hess_init_impl(expr *node)
140140
{
141141
/* initialize child's hessian */
142142
expr *x = node->left;
143-
x->wsum_hess_init(x);
143+
wsum_hess_init(x);
144144

145145
/* allocate this node's hessian with the same sparsity as child's */
146146
node->wsum_hess = new_csr_copy_sparsity(x->wsum_hess);
@@ -220,8 +220,8 @@ expr *new_left_matmul(expr *param_node, expr *u, const CSR_Matrix *A)
220220
left_matmul_expr *lnode =
221221
(left_matmul_expr *) calloc(1, sizeof(left_matmul_expr));
222222
expr *node = &lnode->base;
223-
init_expr(node, d1, d2, u->n_vars, forward, jacobian_init, eval_jacobian,
224-
is_affine, wsum_hess_init, eval_wsum_hess, free_type_data);
223+
init_expr(node, d1, d2, u->n_vars, forward, jacobian_init_impl, eval_jacobian,
224+
is_affine, wsum_hess_init_impl, eval_wsum_hess, free_type_data);
225225
node->left = u;
226226
expr_retain(u);
227227

@@ -274,8 +274,8 @@ expr *new_left_matmul_dense(expr *param_node, expr *u, int m, int n,
274274
left_matmul_expr *lnode =
275275
(left_matmul_expr *) calloc(1, sizeof(left_matmul_expr));
276276
expr *node = &lnode->base;
277-
init_expr(node, d1, d2, u->n_vars, forward, jacobian_init, eval_jacobian,
278-
is_affine, wsum_hess_init, eval_wsum_hess, free_type_data);
277+
init_expr(node, d1, d2, u->n_vars, forward, jacobian_init_impl, eval_jacobian,
278+
is_affine, wsum_hess_init_impl, eval_wsum_hess, free_type_data);
279279
node->left = u;
280280
expr_retain(u);
281281

0 commit comments

Comments
 (0)