Skip to content

Commit f07cb1a

Browse files
Transurgeonclaude
andcommitted
[WIP] Add upper_tri and diag_mat affine atoms (#54)
* Add upper_tri and diag_mat affine atoms Both are thin wrappers that compute flat column-major index arrays and delegate to new_index. diag_mat extracts the diagonal of a square matrix into a vector. upper_tri extracts strict upper triangular elements (excluding diagonal). Also removes a duplicate new_diag_vec declaration in affine.h. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix upper_tri to use row-major ordering to match CVXPY CVXPY's upper_tri returns elements row-by-row (i outer, j inner), which differs from the engine's typical column-major convention. Swap the loop nesting to iterate rows-then-columns for compatibility. Add a 4x4 forward test that distinguishes the two orderings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove SPDX headers and redundant 3x3 upper_tri test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Upgrade upper_tri jacobian and hessian tests to 4x4 The 3x3 tests produced identical indices for both row-major and column-major orderings, so they didn't actually verify the fix. 4x4 indices [4,8,12,9,13,14] differ from column-major [4,8,9,12,13,14]. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Run clang-format on changed files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ba24274 commit f07cb1a

File tree

10 files changed

+421
-0
lines changed

10 files changed

+421
-0
lines changed

include/atoms/affine.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ expr *new_index(expr *child, int d1, int d2, const int *indices, int n_idxs);
3838
expr *new_reshape(expr *child, int d1, int d2);
3939
expr *new_broadcast(expr *child, int target_d1, int target_d2);
4040
expr *new_diag_vec(expr *child);
41+
expr *new_diag_mat(expr *child);
42+
expr *new_upper_tri(expr *child);
4143
expr *new_transpose(expr *child);
4244

4345
/* Left matrix multiplication: A @ f(x) where A is a constant or parameter

src/atoms/affine/diag_mat.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2026 Daniel Cederberg and William Zhang
3+
*
4+
* This file is part of the DNLP-differentiation-engine project.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
#include "atoms/affine.h"
19+
#include <assert.h>
20+
#include <stdlib.h>
21+
22+
/* Extract diagonal from a square matrix into a column vector.
23+
* For an (n, n) matrix in column-major order, diagonal element i
24+
* is at flat index i * (n + 1). */
25+
26+
expr *new_diag_mat(expr *child)
27+
{
28+
assert(child->d1 == child->d2);
29+
int n = child->d1;
30+
31+
int *indices = (int *) malloc((size_t) n * sizeof(int));
32+
for (int i = 0; i < n; i++)
33+
{
34+
indices[i] = i * (n + 1);
35+
}
36+
37+
expr *node = new_index(child, n, 1, indices, n);
38+
free(indices);
39+
return node;
40+
}

src/atoms/affine/upper_tri.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2026 Daniel Cederberg and William Zhang
3+
*
4+
* This file is part of the DNLP-differentiation-engine project.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
#include "atoms/affine.h"
19+
#include <assert.h>
20+
#include <stdlib.h>
21+
22+
/* Extract strict upper triangular elements (excluding diagonal)
23+
* from a square matrix, in ROW-MAJOR order to match CVXPY.
24+
*
25+
* NOTE: This is an exception to the engine's column-major
26+
* convention. CVXPY's upper_tri iterates row-by-row across
27+
* columns (i outer, j inner), so we do the same here for
28+
* compatibility.
29+
*
30+
* For an (n, n) column-major matrix, element (i, j) with
31+
* i < j is at flat index j * n + i.
32+
* Output has n * (n - 1) / 2 elements. */
33+
34+
expr *new_upper_tri(expr *child)
35+
{
36+
assert(child->d1 == child->d2);
37+
int n = child->d1;
38+
int n_elems = n * (n - 1) / 2;
39+
40+
int *indices = NULL;
41+
if (n_elems > 0)
42+
{
43+
indices = (int *) malloc((size_t) n_elems * sizeof(int));
44+
int k = 0;
45+
for (int i = 0; i < n; i++)
46+
{
47+
for (int j = i + 1; j < n; j++)
48+
{
49+
indices[k++] = j * n + i;
50+
}
51+
}
52+
assert(k == n_elems);
53+
}
54+
55+
expr *node = new_index(child, n_elems, 1, indices, n_elems);
56+
free(indices);
57+
return node;
58+
}

tests/all_tests.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
#ifndef PROFILE_ONLY
88
#include "forward_pass/affine/test_add.h"
99
#include "forward_pass/affine/test_broadcast.h"
10+
#include "forward_pass/affine/test_diag_mat.h"
1011
#include "forward_pass/affine/test_hstack.h"
1112
#include "forward_pass/affine/test_left_matmul_dense.h"
1213
#include "forward_pass/affine/test_linear_op.h"
1314
#include "forward_pass/affine/test_neg.h"
1415
#include "forward_pass/affine/test_promote.h"
1516
#include "forward_pass/affine/test_sum.h"
17+
#include "forward_pass/affine/test_upper_tri.h"
1618
#include "forward_pass/affine/test_variable_parameter.h"
1719
#include "forward_pass/affine/test_vstack.h"
1820
#include "forward_pass/bivariate_full_dom/test_matmul.h"
@@ -23,6 +25,7 @@
2325
#include "forward_pass/other/test_prod_axis_one.h"
2426
#include "forward_pass/other/test_prod_axis_zero.h"
2527
#include "jacobian_tests/affine/test_broadcast.h"
28+
#include "jacobian_tests/affine/test_diag_mat.h"
2629
#include "jacobian_tests/affine/test_hstack.h"
2730
#include "jacobian_tests/affine/test_index.h"
2831
#include "jacobian_tests/affine/test_left_matmul.h"
@@ -33,6 +36,7 @@
3336
#include "jacobian_tests/affine/test_sum.h"
3437
#include "jacobian_tests/affine/test_trace.h"
3538
#include "jacobian_tests/affine/test_transpose.h"
39+
#include "jacobian_tests/affine/test_upper_tri.h"
3640
#include "jacobian_tests/affine/test_vector_mult.h"
3741
#include "jacobian_tests/affine/test_vstack.h"
3842
#include "jacobian_tests/bivariate_full_dom/test_elementwise_mult.h"
@@ -61,6 +65,7 @@
6165
#include "utils/test_linalg_utils_matmul_chain_rule.h"
6266
#include "utils/test_matrix.h"
6367
#include "wsum_hess/affine/test_broadcast.h"
68+
#include "wsum_hess/affine/test_diag_mat.h"
6469
#include "wsum_hess/affine/test_hstack.h"
6570
#include "wsum_hess/affine/test_index.h"
6671
#include "wsum_hess/affine/test_left_matmul.h"
@@ -69,6 +74,7 @@
6974
#include "wsum_hess/affine/test_sum.h"
7075
#include "wsum_hess/affine/test_trace.h"
7176
#include "wsum_hess/affine/test_transpose.h"
77+
#include "wsum_hess/affine/test_upper_tri.h"
7278
#include "wsum_hess/affine/test_vector_mult.h"
7379
#include "wsum_hess/affine/test_vstack.h"
7480
#include "wsum_hess/bivariate_full_dom/test_matmul.h"
@@ -128,6 +134,8 @@ int main(void)
128134
mu_run_test(test_forward_prod_axis_one, tests_run);
129135
mu_run_test(test_matmul, tests_run);
130136
mu_run_test(test_left_matmul_dense, tests_run);
137+
mu_run_test(test_diag_mat_forward, tests_run);
138+
mu_run_test(test_upper_tri_forward_4x4, tests_run);
131139

132140
printf("\n--- Jacobian Tests ---\n");
133141
mu_run_test(test_neg_jacobian, tests_run);
@@ -208,6 +216,10 @@ int main(void)
208216
mu_run_test(test_jacobian_right_matmul_log_vector, tests_run);
209217
mu_run_test(test_jacobian_matmul, tests_run);
210218
mu_run_test(test_jacobian_transpose, tests_run);
219+
mu_run_test(test_diag_mat_jacobian_variable, tests_run);
220+
mu_run_test(test_diag_mat_jacobian_of_log, tests_run);
221+
mu_run_test(test_upper_tri_jacobian_variable, tests_run);
222+
mu_run_test(test_upper_tri_jacobian_of_log, tests_run);
211223

212224
printf("\n--- Weighted Sum of Hessian Tests ---\n");
213225
mu_run_test(test_wsum_hess_log, tests_run);
@@ -275,6 +287,8 @@ int main(void)
275287
mu_run_test(test_wsum_hess_trace_log_variable, tests_run);
276288
mu_run_test(test_wsum_hess_trace_composite, tests_run);
277289
mu_run_test(test_wsum_hess_transpose, tests_run);
290+
mu_run_test(test_wsum_hess_diag_mat_log, tests_run);
291+
mu_run_test(test_wsum_hess_upper_tri_log, tests_run);
278292
mu_run_test(test_wsum_hess_exp_sum, tests_run);
279293
mu_run_test(test_wsum_hess_exp_sum_mult, tests_run);
280294
mu_run_test(test_wsum_hess_exp_sum_matmul, tests_run);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include <stdio.h>
2+
3+
#include "atoms/affine.h"
4+
#include "expr.h"
5+
#include "minunit.h"
6+
#include "test_helpers.h"
7+
8+
const char *test_diag_mat_forward(void)
9+
{
10+
/* 3x3 matrix variable (column-major): [1,2,3,4,5,6,7,8,9]
11+
* Matrix: 1 4 7
12+
* 2 5 8
13+
* 3 6 9
14+
* Diagonal: (0,0)=1, (1,1)=5, (2,2)=9 */
15+
double u[9] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0};
16+
expr *var = new_variable(3, 3, 0, 9);
17+
expr *dm = new_diag_mat(var);
18+
19+
mu_assert("diag_mat d1", dm->d1 == 3);
20+
mu_assert("diag_mat d2", dm->d2 == 1);
21+
22+
dm->forward(dm, u);
23+
24+
double expected[3] = {1.0, 5.0, 9.0};
25+
mu_assert("diag_mat forward", cmp_double_array(dm->value, expected, 3));
26+
27+
free_expr(dm);
28+
return 0;
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#include <stdio.h>
2+
3+
#include "atoms/affine.h"
4+
#include "expr.h"
5+
#include "minunit.h"
6+
#include "test_helpers.h"
7+
8+
const char *test_upper_tri_forward_4x4(void)
9+
{
10+
/* 4x4 matrix variable (column-major): [1..16]
11+
* Matrix: 1 5 9 13
12+
* 2 6 10 14
13+
* 3 7 11 15
14+
* 4 8 12 16
15+
* Upper tri in row-major order (matching CVXPY):
16+
* Row 0: (0,1)=5, (0,2)=9, (0,3)=13
17+
* Row 1: (1,2)=10, (1,3)=14
18+
* Row 2: (2,3)=15
19+
* Flat indices: 4, 8, 12, 9, 13, 14
20+
*
21+
* NOTE: column-major order would give [4, 8, 9, 12, 13, 14]
22+
* instead. This test verifies the row-major ordering. */
23+
double u[16];
24+
for (int k = 0; k < 16; k++)
25+
{
26+
u[k] = (double) (k + 1);
27+
}
28+
expr *var = new_variable(4, 4, 0, 16);
29+
expr *ut = new_upper_tri(var);
30+
31+
mu_assert("upper_tri 4x4 d1", ut->d1 == 6);
32+
mu_assert("upper_tri 4x4 d2", ut->d2 == 1);
33+
34+
ut->forward(ut, u);
35+
36+
double expected[6] = {5.0, 9.0, 13.0, 10.0, 14.0, 15.0};
37+
mu_assert("upper_tri forward 4x4", cmp_double_array(ut->value, expected, 6));
38+
39+
free_expr(ut);
40+
return 0;
41+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include <stdio.h>
2+
3+
#include "atoms/affine.h"
4+
#include "atoms/elementwise_restricted_dom.h"
5+
#include "expr.h"
6+
#include "minunit.h"
7+
#include "test_helpers.h"
8+
9+
const char *test_diag_mat_jacobian_variable(void)
10+
{
11+
/* diag_mat of a 2x2 variable (4 vars total)
12+
* Diagonal indices in column-major: [0, 3]
13+
* Jacobian is 2x4 CSR: row 0 has col 0, row 1 has col 3 */
14+
double u[4] = {1.0, 2.0, 3.0, 4.0};
15+
expr *var = new_variable(2, 2, 0, 4);
16+
expr *dm = new_diag_mat(var);
17+
18+
dm->forward(dm, u);
19+
jacobian_init(dm);
20+
dm->eval_jacobian(dm);
21+
22+
double expected_x[2] = {1.0, 1.0};
23+
int expected_p[3] = {0, 1, 2};
24+
int expected_i[2] = {0, 3};
25+
26+
mu_assert("diag_mat jac vals", cmp_double_array(dm->jacobian->x, expected_x, 2));
27+
mu_assert("diag_mat jac p", cmp_int_array(dm->jacobian->p, expected_p, 3));
28+
mu_assert("diag_mat jac i", cmp_int_array(dm->jacobian->i, expected_i, 2));
29+
30+
free_expr(dm);
31+
return 0;
32+
}
33+
34+
const char *test_diag_mat_jacobian_of_log(void)
35+
{
36+
/* diag_mat(log(X)) where X is 2x2 variable
37+
* X = [[1, 3], [2, 4]] (column-major: [1, 2, 3, 4])
38+
* Diagonal: x[0]=1, x[3]=4
39+
* d/dx log at diagonal positions:
40+
* Row 0: 1/1 = 1.0 at col 0
41+
* Row 1: 1/4 = 0.25 at col 3 */
42+
double u[4] = {1.0, 2.0, 3.0, 4.0};
43+
expr *var = new_variable(2, 2, 0, 4);
44+
expr *log_node = new_log(var);
45+
expr *dm = new_diag_mat(log_node);
46+
47+
dm->forward(dm, u);
48+
jacobian_init(dm);
49+
dm->eval_jacobian(dm);
50+
51+
double expected_x[2] = {1.0, 0.25};
52+
int expected_i[2] = {0, 3};
53+
54+
mu_assert("diag_mat log jac vals",
55+
cmp_double_array(dm->jacobian->x, expected_x, 2));
56+
mu_assert("diag_mat log jac cols",
57+
cmp_int_array(dm->jacobian->i, expected_i, 2));
58+
59+
free_expr(dm);
60+
return 0;
61+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#include <stdio.h>
2+
3+
#include "atoms/affine.h"
4+
#include "atoms/elementwise_restricted_dom.h"
5+
#include "expr.h"
6+
#include "minunit.h"
7+
#include "test_helpers.h"
8+
9+
const char *test_upper_tri_jacobian_variable(void)
10+
{
11+
/* upper_tri of a 4x4 variable (16 vars total)
12+
* Row-major upper tri indices: [4, 8, 12, 9, 13, 14]
13+
* Jacobian is 6x16 CSR: row k has a single 1.0 at col indices[k] */
14+
double u[16];
15+
for (int k = 0; k < 16; k++)
16+
{
17+
u[k] = (double) (k + 1);
18+
}
19+
expr *var = new_variable(4, 4, 0, 16);
20+
expr *ut = new_upper_tri(var);
21+
22+
ut->forward(ut, u);
23+
jacobian_init(ut);
24+
ut->eval_jacobian(ut);
25+
26+
double expected_x[6] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
27+
int expected_p[7] = {0, 1, 2, 3, 4, 5, 6};
28+
int expected_i[6] = {4, 8, 12, 9, 13, 14};
29+
30+
mu_assert("upper_tri jac vals",
31+
cmp_double_array(ut->jacobian->x, expected_x, 6));
32+
mu_assert("upper_tri jac p", cmp_int_array(ut->jacobian->p, expected_p, 7));
33+
mu_assert("upper_tri jac i", cmp_int_array(ut->jacobian->i, expected_i, 6));
34+
35+
free_expr(ut);
36+
return 0;
37+
}
38+
39+
const char *test_upper_tri_jacobian_of_log(void)
40+
{
41+
/* upper_tri(log(X)) where X is 4x4 variable
42+
* Row-major upper tri indices: [4, 8, 12, 9, 13, 14]
43+
* Values at those positions: u[4]=5, u[8]=9, u[12]=13,
44+
* u[9]=10, u[13]=14, u[14]=15
45+
* d/dx log at those positions: 1/5, 1/9, 1/13, 1/10, 1/14, 1/15 */
46+
double u[16];
47+
for (int k = 0; k < 16; k++)
48+
{
49+
u[k] = (double) (k + 1);
50+
}
51+
expr *var = new_variable(4, 4, 0, 16);
52+
expr *log_node = new_log(var);
53+
expr *ut = new_upper_tri(log_node);
54+
55+
ut->forward(ut, u);
56+
jacobian_init(ut);
57+
ut->eval_jacobian(ut);
58+
59+
double expected_x[6] = {0.2, 1.0 / 9.0, 1.0 / 13.0, 0.1, 1.0 / 14.0, 1.0 / 15.0};
60+
int expected_i[6] = {4, 8, 12, 9, 13, 14};
61+
62+
mu_assert("upper_tri log jac vals",
63+
cmp_double_array(ut->jacobian->x, expected_x, 6));
64+
mu_assert("upper_tri log jac cols",
65+
cmp_int_array(ut->jacobian->i, expected_i, 6));
66+
67+
free_expr(ut);
68+
return 0;
69+
}

0 commit comments

Comments
 (0)