/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ #include "../../lib/test.h" #include "../tensor.h" #include /* ============================================================= */ /* Creation Tests */ /* ============================================================= */ static void test_zeros_creates_tensor(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_zeros(shape, 2, TM_F32); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(2, tm_ndim(t)); ASSERT_INT_EQ(2, tm_size(t, 0)); ASSERT_INT_EQ(3, tm_size(t, 1)); ASSERT_INT_EQ(6, tm_numel(t)); /* Check all zeros */ for (int64_t i = 0; i < 6; i++) { ASSERT_FLT_EQ(0.0f, tm_get_f32(t, i)); } tm_free(t); } static void test_ones_creates_tensor(void) { int64_t shape[] = {3}; tm_tensor* t = tm_ones(shape, 1, TM_F32); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(1, tm_ndim(t)); ASSERT_INT_EQ(3, tm_numel(t)); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(1.0f, tm_get_f32(t, i)); } tm_free(t); } static void test_full_creates_tensor(void) { int64_t shape[] = {2, 2}; tm_tensor* t = tm_full(shape, 2, TM_F32, 3.14); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(4, tm_numel(t)); for (int64_t i = 0; i < 4; i++) { ASSERT_FLT_NEAR(3.14f, tm_get_f32(t, i), 0.001f); } tm_free(t); } static void test_randn_creates_tensor(void) { tm_manual_seed(42); int64_t shape[] = {100}; tm_tensor* t = tm_randn(shape, 1, TM_F32); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(100, tm_numel(t)); /* Check approximate normal distribution */ float sum = 0; for (int64_t i = 0; i < 100; i++) { sum += tm_get_f32(t, i); } float mean = sum / 100.0f; /* Mean should be close to 0 for normal distribution */ ASSERT_FLT_NEAR(0.0f, mean, 0.5f); tm_free(t); } static void test_clone_creates_copy(void) { int64_t shape[] = {2, 2}; tm_tensor* t = tm_full(shape, 2, TM_F32, 5.0); tm_tensor* c = tm_clone(t); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(tm_numel(t), tm_numel(c)); /* Modify original, clone should be unchanged */ tm_set_f32(t, 0, 99.0f); ASSERT_FLT_EQ(5.0f, tm_get_f32(c, 0)); tm_free(t); tm_free(c); } /* ============================================================= */ /* Accessor Tests */ /* ============================================================= */ static void test_get_set_f32(void) { int64_t shape[] = {4}; tm_tensor* t = tm_zeros(shape, 1, TM_F32); tm_set_f32(t, 2, 42.0f); ASSERT_FLT_EQ(42.0f, tm_get_f32(t, 2)); ASSERT_FLT_EQ(0.0f, tm_get_f32(t, 0)); tm_free(t); } static void test_data_pointer(void) { int64_t shape[] = {3}; tm_tensor* t = tm_ones(shape, 1, TM_F32); float* data = tm_data_f32(t); ASSERT_NOT_NULL(data); ASSERT_FLT_EQ(1.0f, data[0]); data[1] = 7.0f; ASSERT_FLT_EQ(7.0f, tm_get_f32(t, 1)); tm_free(t); } /* ============================================================= */ /* Shape Tests */ /* ============================================================= */ static void test_reshape(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_ones(shape, 2, TM_F32); int64_t new_shape[] = {6}; tm_tensor* r = tm_reshape(t, new_shape, 1); ASSERT_NOT_NULL(r); ASSERT_INT_EQ(1, tm_ndim(r)); ASSERT_INT_EQ(6, tm_size(r, 0)); ASSERT_INT_EQ(6, tm_numel(r)); tm_free(t); tm_free(r); } static void test_transpose(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_zeros(shape, 2, TM_F32); /* Set t[0, 1] = 5.0 */ float* data = tm_data_f32(t); data[0 * 3 + 1] = 5.0f; tm_tensor* tr = tm_transpose(t, 0, 1); ASSERT_NOT_NULL(tr); ASSERT_INT_EQ(3, tm_size(tr, 0)); ASSERT_INT_EQ(2, tm_size(tr, 1)); tm_free(t); tm_free(tr); } static void test_is_contiguous(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_zeros(shape, 2, TM_F32); ASSERT_TRUE(tm_is_contiguous(t)); /* 2D transpose now returns a contiguous copy for matmul compatibility */ tm_tensor* tr = tm_transpose(t, 0, 1); ASSERT_TRUE(tm_is_contiguous(tr)); /* Now contiguous */ ASSERT_INT_EQ(tr->shape[0], 3); /* Shape is transposed */ ASSERT_INT_EQ(tr->shape[1], 2); tm_free(t); tm_free(tr); } /* ============================================================= */ /* Element-wise Op Tests */ /* ============================================================= */ static void test_add(void) { int64_t shape[] = {3}; tm_tensor* a = tm_full(shape, 1, TM_F32, 2.0); tm_tensor* b = tm_full(shape, 1, TM_F32, 3.0); tm_tensor* c = tm_add(a, b); ASSERT_NOT_NULL(c); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(5.0f, tm_get_f32(c, i)); } tm_free(a); tm_free(b); tm_free(c); } static void test_sub(void) { int64_t shape[] = {3}; tm_tensor* a = tm_full(shape, 1, TM_F32, 5.0); tm_tensor* b = tm_full(shape, 1, TM_F32, 3.0); tm_tensor* c = tm_sub(a, b); ASSERT_NOT_NULL(c); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(2.0f, tm_get_f32(c, i)); } tm_free(a); tm_free(b); tm_free(c); } static void test_mul(void) { int64_t shape[] = {3}; tm_tensor* a = tm_full(shape, 1, TM_F32, 2.0); tm_tensor* b = tm_full(shape, 1, TM_F32, 4.0); tm_tensor* c = tm_mul(a, b); ASSERT_NOT_NULL(c); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(8.0f, tm_get_f32(c, i)); } tm_free(a); tm_free(b); tm_free(c); } static void test_mul_scalar(void) { int64_t shape[] = {3}; tm_tensor* a = tm_full(shape, 1, TM_F32, 3.0); tm_tensor* c = tm_mul_scalar(a, 2.0); ASSERT_NOT_NULL(c); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(6.0f, tm_get_f32(c, i)); } tm_free(a); tm_free(c); } static void test_exp(void) { int64_t shape[] = {2}; tm_tensor* t = tm_zeros(shape, 1, TM_F32); tm_set_f32(t, 0, 0.0f); tm_set_f32(t, 1, 1.0f); tm_tensor* e = tm_exp(t); ASSERT_FLT_NEAR(1.0f, tm_get_f32(e, 0), 0.001f); /* exp(0) = 1 */ ASSERT_FLT_NEAR(2.718f, tm_get_f32(e, 1), 0.01f); /* exp(1) ≈ 2.718 */ tm_free(t); tm_free(e); } static void test_relu(void) { int64_t shape[] = {4}; tm_tensor* t = tm_zeros(shape, 1, TM_F32); tm_set_f32(t, 0, -2.0f); tm_set_f32(t, 1, -0.5f); tm_set_f32(t, 2, 0.0f); tm_set_f32(t, 3, 3.0f); tm_tensor* r = tm_relu(t); ASSERT_FLT_EQ(0.0f, tm_get_f32(r, 0)); ASSERT_FLT_EQ(0.0f, tm_get_f32(r, 1)); ASSERT_FLT_EQ(0.0f, tm_get_f32(r, 2)); ASSERT_FLT_EQ(3.0f, tm_get_f32(r, 3)); tm_free(t); tm_free(r); } /* ============================================================= */ /* Matrix Op Tests */ /* ============================================================= */ static void test_matmul_2d(void) { /* A = [[1, 2], [3, 4]] (2x2) */ /* B = [[5, 6], [7, 8]] (2x2) */ /* C = A @ B = [[19, 22], [43, 50]] */ int64_t shape[] = {2, 2}; tm_tensor* a = tm_zeros(shape, 2, TM_F32); tm_tensor* b = tm_zeros(shape, 2, TM_F32); float* ad = tm_data_f32(a); ad[0] = 1; ad[1] = 2; ad[2] = 3; ad[3] = 4; float* bd = tm_data_f32(b); bd[0] = 5; bd[1] = 6; bd[2] = 7; bd[3] = 8; tm_tensor* c = tm_matmul(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(2, tm_ndim(c)); ASSERT_INT_EQ(2, tm_size(c, 0)); ASSERT_INT_EQ(2, tm_size(c, 1)); float* cd = tm_data_f32(c); ASSERT_FLT_EQ(19.0f, cd[0]); ASSERT_FLT_EQ(22.0f, cd[1]); ASSERT_FLT_EQ(43.0f, cd[2]); ASSERT_FLT_EQ(50.0f, cd[3]); tm_free(a); tm_free(b); tm_free(c); } static void test_matmul_different_shapes(void) { /* A = (3, 4), B = (4, 2) -> C = (3, 2) */ int64_t shape_a[] = {3, 4}; int64_t shape_b[] = {4, 2}; tm_tensor* a = tm_ones(shape_a, 2, TM_F32); tm_tensor* b = tm_ones(shape_b, 2, TM_F32); tm_tensor* c = tm_matmul(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(3, tm_size(c, 0)); ASSERT_INT_EQ(2, tm_size(c, 1)); /* Each element should be 4 (sum of 4 ones) */ for (int64_t i = 0; i < 6; i++) { ASSERT_FLT_EQ(4.0f, tm_get_f32(c, i)); } tm_free(a); tm_free(b); tm_free(c); } /* ============================================================= */ /* Reduction Tests */ /* ============================================================= */ static void test_sum_all(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_ones(shape, 2, TM_F32); tm_tensor* s = tm_sum(t, -1, false); ASSERT_NOT_NULL(s); ASSERT_INT_EQ(1, tm_numel(s)); ASSERT_FLT_EQ(6.0f, tm_get_f32(s, 0)); tm_free(t); tm_free(s); } static void test_mean_all(void) { int64_t shape[] = {4}; tm_tensor* t = tm_zeros(shape, 1, TM_F32); tm_set_f32(t, 0, 1.0f); tm_set_f32(t, 1, 2.0f); tm_set_f32(t, 2, 3.0f); tm_set_f32(t, 3, 4.0f); tm_tensor* m = tm_mean(t, -1, false); ASSERT_NOT_NULL(m); ASSERT_FLT_EQ(2.5f, tm_get_f32(m, 0)); tm_free(t); tm_free(m); } /* ============================================================= */ /* NN Op Tests */ /* ============================================================= */ static void test_softmax(void) { int64_t shape[] = {1, 3}; tm_tensor* t = tm_zeros(shape, 2, TM_F32); tm_set_f32(t, 0, 1.0f); tm_set_f32(t, 1, 2.0f); tm_set_f32(t, 2, 3.0f); tm_tensor* s = tm_softmax(t, 1); ASSERT_NOT_NULL(s); /* Softmax values should sum to 1 */ float sum = tm_get_f32(s, 0) + tm_get_f32(s, 1) + tm_get_f32(s, 2); ASSERT_FLT_NEAR(1.0f, sum, 0.001f); /* Values should be in increasing order */ ASSERT_TRUE(tm_get_f32(s, 0) < tm_get_f32(s, 1)); ASSERT_TRUE(tm_get_f32(s, 1) < tm_get_f32(s, 2)); tm_free(t); tm_free(s); } static void test_layer_norm(void) { int64_t shape[] = {2, 4}; tm_tensor* t = tm_zeros(shape, 2, TM_F32); float* data = tm_data_f32(t); /* Row 1: [1, 2, 3, 4] */ data[0] = 1; data[1] = 2; data[2] = 3; data[3] = 4; /* Row 2: [4, 4, 4, 4] */ data[4] = 4; data[5] = 4; data[6] = 4; data[7] = 4; tm_tensor* ln = tm_layer_norm(t, NULL, NULL, 1e-5f); ASSERT_NOT_NULL(ln); /* Normalized values should have mean ~0 and std ~1 per row */ float* lnd = tm_data_f32(ln); /* Check row 1 mean */ float mean = (lnd[0] + lnd[1] + lnd[2] + lnd[3]) / 4.0f; ASSERT_FLT_NEAR(0.0f, mean, 0.01f); /* Row 2 should be all zeros (constant input) */ ASSERT_FLT_NEAR(0.0f, lnd[4], 0.01f); ASSERT_FLT_NEAR(0.0f, lnd[5], 0.01f); tm_free(t); tm_free(ln); } /* ============================================================= */ /* Autograd Tests */ /* ============================================================= */ static void test_autograd_add(void) { int64_t shape[] = {3}; tm_tensor* a = tm_ones(shape, 1, TM_F32); tm_tensor* b = tm_full(shape, 1, TM_F32, 2.0); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); tm_tensor* c = tm_add(a, b); tm_tensor* loss = tm_sum(c, -1, false); ASSERT_TRUE(loss->requires_grad); tm_backward(loss); /* Gradient of sum(a + b) w.r.t a and b is all ones */ ASSERT_NOT_NULL(a->grad); ASSERT_NOT_NULL(b->grad); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_EQ(1.0f, tm_get_f32(a->grad, i)); ASSERT_FLT_EQ(1.0f, tm_get_f32(b->grad, i)); } tm_free(a); tm_free(b); tm_free(c); tm_free(loss); } static void test_autograd_mul(void) { int64_t shape[] = {2}; tm_tensor* a = tm_full(shape, 1, TM_F32, 3.0); tm_tensor* b = tm_full(shape, 1, TM_F32, 4.0); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); tm_tensor* c = tm_mul(a, b); tm_tensor* loss = tm_sum(c, -1, false); tm_backward(loss); /* d(sum(a*b))/da = b, d(sum(a*b))/db = a */ for (int64_t i = 0; i < 2; i++) { ASSERT_FLT_EQ(4.0f, tm_get_f32(a->grad, i)); /* grad = b */ ASSERT_FLT_EQ(3.0f, tm_get_f32(b->grad, i)); /* grad = a */ } tm_free(a); tm_free(b); tm_free(c); tm_free(loss); } static void test_no_grad_context(void) { int64_t shape[] = {2}; tm_tensor* a = tm_ones(shape, 1, TM_F32); tm_tensor* b = tm_ones(shape, 1, TM_F32); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); bool prev = tm_no_grad_begin(); tm_tensor* c = tm_add(a, b); /* In no_grad context, output should not require grad */ ASSERT_FALSE(c->requires_grad); tm_no_grad_end(prev); tm_free(a); tm_free(b); tm_free(c); } /* ============================================================= */ /* Edge Case Tests */ /* ============================================================= */ static void test_null_safety(void) { ASSERT_NULL(tm_add(NULL, NULL)); ASSERT_NULL(tm_matmul(NULL, NULL)); ASSERT_NULL(tm_reshape(NULL, NULL, 0)); ASSERT_INT_EQ(0, tm_numel(NULL)); ASSERT_INT_EQ(0, tm_ndim(NULL)); tm_free(NULL); /* Should not crash */ ASSERT_TRUE(1); } static void test_scalar_tensor(void) { int64_t shape[] = {1}; tm_tensor* t = tm_full(shape, 1, TM_F32, 42.0); ASSERT_INT_EQ(1, tm_numel(t)); ASSERT_FLT_EQ(42.0f, tm_get_f32(t, 0)); tm_free(t); } /* ============================================================= */ /* Broadcasting Tests */ /* ============================================================= */ static void test_broadcast_add_1d_to_2d(void) { /* Test [4, 3] + [3] -> [4, 3] */ int64_t shape_a[] = {4, 3}; int64_t shape_b[] = {3}; tm_tensor* a = tm_ones(shape_a, 2, TM_F32); tm_tensor* b = tm_full(shape_b, 1, TM_F32, 2.0); tm_tensor* c = tm_add(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(2, tm_ndim(c)); ASSERT_INT_EQ(4, tm_size(c, 0)); ASSERT_INT_EQ(3, tm_size(c, 1)); /* All elements should be 3.0 (1.0 + 2.0) */ for (int64_t i = 0; i < 12; i++) { ASSERT_FLT_NEAR(3.0f, tm_get_f32(c, i), 0.0001f); } tm_free(a); tm_free(b); tm_free(c); } static void test_broadcast_mul_scalar_like(void) { /* Test [4, 3] * [1] -> [4, 3] */ int64_t shape_a[] = {4, 3}; int64_t shape_b[] = {1}; tm_tensor* a = tm_full(shape_a, 2, TM_F32, 3.0); tm_tensor* b = tm_full(shape_b, 1, TM_F32, 2.0); tm_tensor* c = tm_mul(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(12, tm_numel(c)); for (int64_t i = 0; i < 12; i++) { ASSERT_FLT_NEAR(6.0f, tm_get_f32(c, i), 0.0001f); } tm_free(a); tm_free(b); tm_free(c); } static void test_broadcast_sub(void) { /* Test [2, 3] - [3] -> [2, 3] */ int64_t shape_a[] = {2, 3}; int64_t shape_b[] = {3}; tm_tensor* a = tm_full(shape_a, 2, TM_F32, 5.0); tm_tensor* b = tm_zeros(shape_b, 1, TM_F32); float* bd = tm_data_f32(b); bd[0] = 1.0f; bd[1] = 2.0f; bd[2] = 3.0f; tm_tensor* c = tm_sub(a, b); ASSERT_NOT_NULL(c); float* cd = tm_data_f32(c); /* Row 0: 5-1=4, 5-2=3, 5-3=2 */ ASSERT_FLT_NEAR(4.0f, cd[0], 0.0001f); ASSERT_FLT_NEAR(3.0f, cd[1], 0.0001f); ASSERT_FLT_NEAR(2.0f, cd[2], 0.0001f); /* Row 1: same pattern */ ASSERT_FLT_NEAR(4.0f, cd[3], 0.0001f); ASSERT_FLT_NEAR(3.0f, cd[4], 0.0001f); ASSERT_FLT_NEAR(2.0f, cd[5], 0.0001f); tm_free(a); tm_free(b); tm_free(c); } static void test_broadcast_div(void) { /* Test [2, 2] / [2] -> [2, 2] */ int64_t shape_a[] = {2, 2}; int64_t shape_b[] = {2}; tm_tensor* a = tm_full(shape_a, 2, TM_F32, 6.0); tm_tensor* b = tm_zeros(shape_b, 1, TM_F32); float* bd = tm_data_f32(b); bd[0] = 2.0f; bd[1] = 3.0f; tm_tensor* c = tm_div(a, b); ASSERT_NOT_NULL(c); float* cd = tm_data_f32(c); /* 6/2=3, 6/3=2 for each row */ ASSERT_FLT_NEAR(3.0f, cd[0], 0.0001f); ASSERT_FLT_NEAR(2.0f, cd[1], 0.0001f); ASSERT_FLT_NEAR(3.0f, cd[2], 0.0001f); ASSERT_FLT_NEAR(2.0f, cd[3], 0.0001f); tm_free(a); tm_free(b); tm_free(c); } static void test_broadcast_backward(void) { /* Test that gradients are summed correctly for broadcast */ int64_t shape_a[] = {2, 3}; int64_t shape_b[] = {3}; tm_tensor* a = tm_ones(shape_a, 2, TM_F32); tm_tensor* b = tm_full(shape_b, 1, TM_F32, 2.0); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); tm_tensor* c = tm_add(a, b); /* Create gradient for c (all ones) */ c->grad = tm_ones(c->shape, c->ndim, TM_F32); /* Call backward */ tm_backward(c); /* a's gradient should be all 1s (same shape as a) */ ASSERT_NOT_NULL(a->grad); for (int64_t i = 0; i < 6; i++) { ASSERT_FLT_NEAR(1.0f, tm_get_f32(a->grad, i), 0.0001f); } /* b's gradient should be summed over the broadcast dim (2 rows) */ ASSERT_NOT_NULL(b->grad); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_NEAR(2.0f, tm_get_f32(b->grad, i), 0.0001f); } tm_free(a); tm_free(b); tm_free(c); } /* ============================================================= */ /* P0 Feature Tests */ /* ============================================================= */ static void test_arange(void) { tm_tensor* t = tm_arange(0, 5, 1, TM_F32); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(5, tm_numel(t)); ASSERT_FLT_EQ(0.0f, tm_get_f32(t, 0)); ASSERT_FLT_EQ(4.0f, tm_get_f32(t, 4)); tm_free(t); /* Negative step */ tm_tensor* t2 = tm_arange(5, 0, -1, TM_F32); ASSERT_NOT_NULL(t2); ASSERT_INT_EQ(5, tm_numel(t2)); ASSERT_FLT_EQ(5.0f, tm_get_f32(t2, 0)); ASSERT_FLT_EQ(1.0f, tm_get_f32(t2, 4)); tm_free(t2); } static void test_cat(void) { int64_t shape[] = {2, 3}; tm_tensor* a = tm_ones(shape, 2, TM_F32); tm_tensor* b = tm_full(shape, 2, TM_F32, 2.0); tm_tensor* tensors[] = {a, b}; tm_tensor* c = tm_cat(tensors, 2, 0); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(2, tm_ndim(c)); ASSERT_INT_EQ(4, tm_size(c, 0)); /* 2 + 2 */ ASSERT_INT_EQ(3, tm_size(c, 1)); /* First half should be 1s, second half 2s */ ASSERT_FLT_EQ(1.0f, tm_get_f32(c, 0)); ASSERT_FLT_EQ(2.0f, tm_get_f32(c, 6)); tm_free(a); tm_free(b); tm_free(c); } static void test_stack(void) { int64_t shape[] = {2, 3}; tm_tensor* a = tm_ones(shape, 2, TM_F32); tm_tensor* b = tm_full(shape, 2, TM_F32, 2.0); tm_tensor* tensors[] = {a, b}; tm_tensor* c = tm_stack(tensors, 2, 0); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(3, tm_ndim(c)); ASSERT_INT_EQ(2, tm_size(c, 0)); /* Stack dimension */ ASSERT_INT_EQ(2, tm_size(c, 1)); ASSERT_INT_EQ(3, tm_size(c, 2)); tm_free(a); tm_free(b); tm_free(c); } static void test_split(void) { int64_t shape[] = {6, 3}; tm_tensor* t = tm_arange(0, 18, 1, TM_F32); t = tm_reshape(t, shape, 2); int num_chunks; tm_tensor** chunks = tm_split(t, 2, 0, &num_chunks); ASSERT_NOT_NULL(chunks); ASSERT_INT_EQ(3, num_chunks); for (int i = 0; i < num_chunks; i++) { ASSERT_NOT_NULL(chunks[i]); ASSERT_INT_EQ(2, tm_size(chunks[i], 0)); ASSERT_INT_EQ(3, tm_size(chunks[i], 1)); tm_free(chunks[i]); } free(chunks); tm_free(t); } static void test_permute(void) { int64_t shape[] = {2, 3, 4}; tm_tensor* t = tm_randn(shape, 3, TM_F32); int dims[] = {1, 2, 0}; tm_tensor* p = tm_permute(t, dims, 3); ASSERT_NOT_NULL(p); ASSERT_INT_EQ(3, tm_size(p, 0)); ASSERT_INT_EQ(4, tm_size(p, 1)); ASSERT_INT_EQ(2, tm_size(p, 2)); tm_free(t); tm_free(p); } static void test_flatten(void) { int64_t shape[] = {2, 3, 4}; tm_tensor* t = tm_randn(shape, 3, TM_F32); tm_tensor* f = tm_flatten(t, 0, -1); ASSERT_NOT_NULL(f); ASSERT_INT_EQ(1, tm_ndim(f)); ASSERT_INT_EQ(24, tm_numel(f)); tm_free(t); tm_free(f); } static void test_select(void) { int64_t shape[] = {3, 4}; tm_tensor* t = tm_arange(0, 12, 1, TM_F32); t = tm_reshape(t, shape, 2); tm_tensor* s = tm_select(t, 0, 1); /* Select row 1 */ ASSERT_NOT_NULL(s); ASSERT_INT_EQ(1, tm_ndim(s)); ASSERT_INT_EQ(4, tm_numel(s)); ASSERT_FLT_EQ(4.0f, tm_get_f32(s, 0)); ASSERT_FLT_EQ(5.0f, tm_get_f32(s, 1)); tm_free(t); tm_free(s); } static void test_silu(void) { int64_t shape[] = {4}; float data[] = {-1.0f, 0.0f, 1.0f, 2.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_silu(t); ASSERT_NOT_NULL(out); /* SiLU(0) = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 1), 0.0001f); /* SiLU(x) = x * sigmoid(x) */ float sig1 = 1.0f / (1.0f + expf(-1.0f)); ASSERT_FLT_NEAR(1.0f * sig1, tm_get_f32(out, 2), 0.0001f); tm_free(t); tm_free(out); } static void test_rms_norm(void) { int64_t shape[] = {2, 4}; tm_tensor* t = tm_ones(shape, 2, TM_F32); /* Without weight */ tm_tensor* out = tm_rms_norm(t, NULL, 1e-5f); ASSERT_NOT_NULL(out); /* RMS of all 1s is 1, so normalized should be ~1 */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 0), 0.01f); tm_free(t); tm_free(out); } static void test_linear(void) { /* x: [2, 3], weight: [4, 3], bias: [4] -> out: [2, 4] */ int64_t x_shape[] = {2, 3}; int64_t w_shape[] = {4, 3}; int64_t b_shape[] = {4}; tm_tensor* x = tm_ones(x_shape, 2, TM_F32); tm_tensor* w = tm_ones(w_shape, 2, TM_F32); tm_tensor* b = tm_full(b_shape, 1, TM_F32, 1.0); tm_tensor* out = tm_linear(x, w, b); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(2, tm_size(out, 0)); ASSERT_INT_EQ(4, tm_size(out, 1)); /* Each output = sum(1*1 * 3) + 1 = 4 */ ASSERT_FLT_NEAR(4.0f, tm_get_f32(out, 0), 0.0001f); tm_free(x); tm_free(w); tm_free(b); tm_free(out); } static void test_triu(void) { int64_t shape[] = {3, 3}; tm_tensor* t = tm_ones(shape, 2, TM_F32); tm_tensor* upper = tm_triu(t, 0); ASSERT_NOT_NULL(upper); /* Lower triangle should be 0 */ float* d = tm_data_f32(upper); ASSERT_FLT_EQ(0.0f, d[3]); /* (1,0) */ ASSERT_FLT_EQ(0.0f, d[6]); /* (2,0) */ ASSERT_FLT_EQ(0.0f, d[7]); /* (2,1) */ /* Upper triangle should be 1 */ ASSERT_FLT_EQ(1.0f, d[0]); /* (0,0) */ ASSERT_FLT_EQ(1.0f, d[2]); /* (0,2) */ tm_free(t); tm_free(upper); } static void test_sin_cos(void) { int64_t shape[] = {4}; float data[] = {0.0f, 3.14159f/2, 3.14159f, 3.14159f*2}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* s = tm_sin(t); tm_tensor* c = tm_cos(t); ASSERT_NOT_NULL(s); ASSERT_NOT_NULL(c); ASSERT_FLT_NEAR(0.0f, tm_get_f32(s, 0), 0.001f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(s, 1), 0.001f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(c, 0), 0.001f); ASSERT_FLT_NEAR(0.0f, tm_get_f32(c, 1), 0.001f); tm_free(t); tm_free(s); tm_free(c); } static void test_clamp(void) { int64_t shape[] = {5}; float data[] = {-2.0f, -0.5f, 0.5f, 1.5f, 3.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_clamp(t, 0.0, 1.0); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(0.5f, tm_get_f32(out, 2)); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 3)); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 4)); tm_free(t); tm_free(out); } static void test_argmax(void) { int64_t shape[] = {2, 3}; float data[] = {1.0f, 3.0f, 2.0f, 5.0f, 4.0f, 6.0f}; tm_tensor* t = tm_from_data(data, shape, 2, TM_F32); t->owns_data = false; tm_tensor* am = tm_argmax(t, -1, false); ASSERT_NOT_NULL(am); ASSERT_INT_EQ(1, tm_numel(am)); /* Global argmax is at index 5 (value 6.0) */ int64_t* idx = (int64_t*)am->data; ASSERT_INT_EQ(5, idx[0]); tm_free(t); tm_free(am); } static void test_logsumexp(void) { int64_t shape[] = {3}; float data[] = {1.0f, 2.0f, 3.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* lse = tm_logsumexp(t, -1, false); ASSERT_NOT_NULL(lse); /* logsumexp([1,2,3]) = log(e^1 + e^2 + e^3) ≈ 3.407 */ float expected = logf(expf(1.0f) + expf(2.0f) + expf(3.0f)); ASSERT_FLT_NEAR(expected, tm_get_f32(lse, 0), 0.001f); tm_free(t); tm_free(lse); } static void test_abs(void) { int64_t shape[] = {4}; float data[] = {-1.0f, 0.0f, 1.0f, -2.5f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_abs(t); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 2)); ASSERT_FLT_EQ(2.5f, tm_get_f32(out, 3)); tm_free(t); tm_free(out); } static void test_pow(void) { int64_t shape[] = {3}; float data[] = {2.0f, 3.0f, 4.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_pow(t, 2.0); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(4.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(9.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(16.0f, tm_get_f32(out, 2)); tm_free(t); tm_free(out); } static void test_where(void) { int64_t shape[] = {4}; float cond_data[] = {1.0f, 0.0f, 1.0f, 0.0f}; float a_data[] = {1.0f, 2.0f, 3.0f, 4.0f}; float b_data[] = {10.0f, 20.0f, 30.0f, 40.0f}; tm_tensor* cond = tm_from_data(cond_data, shape, 1, TM_F32); tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); cond->owns_data = false; a->owns_data = false; b->owns_data = false; tm_tensor* out = tm_where(cond, a, b); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 0)); /* cond=true, pick a */ ASSERT_FLT_EQ(20.0f, tm_get_f32(out, 1)); /* cond=false, pick b */ ASSERT_FLT_EQ(3.0f, tm_get_f32(out, 2)); ASSERT_FLT_EQ(40.0f, tm_get_f32(out, 3)); tm_free(cond); tm_free(a); tm_free(b); tm_free(out); } static void test_masked_fill(void) { int64_t shape[] = {4}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; float mask_data[] = {0.0f, 1.0f, 0.0f, 1.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); tm_tensor* mask = tm_from_data(mask_data, shape, 1, TM_F32); t->owns_data = false; mask->owns_data = false; tm_tensor* out = tm_masked_fill(t, mask, -99.0f); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(-99.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(3.0f, tm_get_f32(out, 2)); ASSERT_FLT_EQ(-99.0f, tm_get_f32(out, 3)); tm_free(t); tm_free(mask); tm_free(out); } static void test_eye(void) { tm_tensor* t = tm_eye(3, TM_F32); ASSERT_NOT_NULL(t); ASSERT_INT_EQ(2, tm_ndim(t)); ASSERT_INT_EQ(3, tm_size(t, 0)); ASSERT_INT_EQ(3, tm_size(t, 1)); float* d = tm_data_f32(t); ASSERT_FLT_EQ(1.0f, d[0]); ASSERT_FLT_EQ(0.0f, d[1]); ASSERT_FLT_EQ(1.0f, d[4]); ASSERT_FLT_EQ(1.0f, d[8]); tm_free(t); } static void test_rsqrt(void) { int64_t shape[] = {3}; float data[] = {1.0f, 4.0f, 9.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_rsqrt(t); ASSERT_NOT_NULL(out); ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 0), 0.0001f); ASSERT_FLT_NEAR(0.5f, tm_get_f32(out, 1), 0.0001f); ASSERT_FLT_NEAR(1.0f/3.0f, tm_get_f32(out, 2), 0.0001f); tm_free(t); tm_free(out); } static void test_sub_div_scalar(void) { int64_t shape[] = {3}; float data[] = {10.0f, 20.0f, 30.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* sub_out = tm_sub_scalar(t, 5.0); ASSERT_NOT_NULL(sub_out); ASSERT_FLT_EQ(5.0f, tm_get_f32(sub_out, 0)); ASSERT_FLT_EQ(15.0f, tm_get_f32(sub_out, 1)); tm_tensor* div_out = tm_div_scalar(t, 10.0); ASSERT_NOT_NULL(div_out); ASSERT_FLT_EQ(1.0f, tm_get_f32(div_out, 0)); ASSERT_FLT_EQ(2.0f, tm_get_f32(div_out, 1)); tm_free(t); tm_free(sub_out); tm_free(div_out); } static void test_inplace_ops(void) { int64_t shape[] = {4}; tm_tensor* t = tm_full(shape, 1, TM_F32, 5.0); /* Test zero_ */ tm_zero_(t); ASSERT_FLT_EQ(0.0f, tm_get_f32(t, 0)); /* Test clamp_ */ tm_fill_(t, 10.0f); tm_clamp_(t, 0.0f, 5.0f); ASSERT_FLT_EQ(5.0f, tm_get_f32(t, 0)); /* Test copy_ */ tm_tensor* src = tm_full(shape, 1, TM_F32, 42.0); tm_copy_(t, src); ASSERT_FLT_EQ(42.0f, tm_get_f32(t, 0)); tm_free(t); tm_free(src); } /* ============================================================= */ /* P1/P2 Feature Tests */ /* ============================================================= */ static void test_comparison_ops(void) { int64_t shape[] = {4}; float a_data[] = {1.0f, 2.0f, 3.0f, 4.0f}; float b_data[] = {1.0f, 3.0f, 2.0f, 4.0f}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; /* Test eq */ tm_tensor* eq = tm_eq(a, b); ASSERT_NOT_NULL(eq); int32_t* eq_data = (int32_t*)eq->data; ASSERT_INT_EQ(1, eq_data[0]); /* 1 == 1 */ ASSERT_INT_EQ(0, eq_data[1]); /* 2 != 3 */ ASSERT_INT_EQ(1, eq_data[3]); /* 4 == 4 */ /* Test lt */ tm_tensor* lt = tm_lt(a, b); int32_t* lt_data = (int32_t*)lt->data; ASSERT_INT_EQ(0, lt_data[0]); /* 1 < 1 = false */ ASSERT_INT_EQ(1, lt_data[1]); /* 2 < 3 = true */ ASSERT_INT_EQ(0, lt_data[2]); /* 3 < 2 = false */ /* Test gt */ tm_tensor* gt = tm_gt(a, b); int32_t* gt_data = (int32_t*)gt->data; ASSERT_INT_EQ(0, gt_data[1]); /* 2 > 3 = false */ ASSERT_INT_EQ(1, gt_data[2]); /* 3 > 2 = true */ tm_free(a); tm_free(b); tm_free(eq); tm_free(lt); tm_free(gt); } static void test_maximum_minimum(void) { int64_t shape[] = {4}; float a_data[] = {1.0f, 5.0f, 3.0f, 2.0f}; float b_data[] = {2.0f, 3.0f, 4.0f, 1.0f}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; tm_tensor* max_t = tm_maximum(a, b); ASSERT_NOT_NULL(max_t); ASSERT_FLT_EQ(2.0f, tm_get_f32(max_t, 0)); ASSERT_FLT_EQ(5.0f, tm_get_f32(max_t, 1)); ASSERT_FLT_EQ(4.0f, tm_get_f32(max_t, 2)); tm_tensor* min_t = tm_minimum(a, b); ASSERT_NOT_NULL(min_t); ASSERT_FLT_EQ(1.0f, tm_get_f32(min_t, 0)); ASSERT_FLT_EQ(3.0f, tm_get_f32(min_t, 1)); ASSERT_FLT_EQ(3.0f, tm_get_f32(min_t, 2)); tm_free(a); tm_free(b); tm_free(max_t); tm_free(min_t); } static void test_leaky_relu(void) { int64_t shape[] = {4}; float data[] = {-2.0f, -1.0f, 1.0f, 2.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_leaky_relu(t, 0.1f); ASSERT_NOT_NULL(out); ASSERT_FLT_NEAR(-0.2f, tm_get_f32(out, 0), 0.0001f); ASSERT_FLT_NEAR(-0.1f, tm_get_f32(out, 1), 0.0001f); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 2)); ASSERT_FLT_EQ(2.0f, tm_get_f32(out, 3)); tm_free(t); tm_free(out); } static void test_gather(void) { int64_t shape[] = {3, 4}; tm_tensor* t = tm_arange(0, 12, 1, TM_F32); t = tm_reshape(t, shape, 2); /* Index tensor for gathering along dim 1 */ int64_t idx_shape[] = {3, 2}; int32_t idx_data[] = {0, 2, 1, 3, 0, 1}; tm_tensor* idx = tm_from_data(idx_data, idx_shape, 2, TM_I32); idx->owns_data = false; tm_tensor* out = tm_gather(t, 1, idx); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(3, tm_size(out, 0)); ASSERT_INT_EQ(2, tm_size(out, 1)); /* Row 0: indices [0,2] -> values [0, 2] */ ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(2.0f, tm_get_f32(out, 1)); tm_free(t); tm_free(idx); tm_free(out); } static void test_index_select(void) { int64_t shape[] = {4, 3}; tm_tensor* t = tm_arange(0, 12, 1, TM_F32); t = tm_reshape(t, shape, 2); int64_t idx_shape[] = {2}; int32_t idx_data[] = {0, 2}; tm_tensor* idx = tm_from_data(idx_data, idx_shape, 1, TM_I32); idx->owns_data = false; tm_tensor* out = tm_index_select(t, 0, idx); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(2, tm_size(out, 0)); ASSERT_INT_EQ(3, tm_size(out, 1)); /* Should get rows 0 and 2 */ ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 0)); /* row 0, col 0 */ ASSERT_FLT_EQ(6.0f, tm_get_f32(out, 3)); /* row 2, col 0 */ tm_free(t); tm_free(idx); tm_free(out); } static void test_narrow(void) { int64_t shape[] = {4, 5}; tm_tensor* t = tm_arange(0, 20, 1, TM_F32); t = tm_reshape(t, shape, 2); tm_tensor* out = tm_narrow(t, 0, 1, 2); /* Rows 1-2 */ ASSERT_NOT_NULL(out); ASSERT_INT_EQ(2, tm_size(out, 0)); ASSERT_INT_EQ(5, tm_size(out, 1)); ASSERT_FLT_EQ(5.0f, tm_get_f32(out, 0)); /* First element of row 1 */ tm_free(t); tm_free(out); } static void test_expand(void) { int64_t shape[] = {1, 3}; float data[] = {1.0f, 2.0f, 3.0f}; tm_tensor* t = tm_from_data(data, shape, 2, TM_F32); t->owns_data = false; int64_t new_shape[] = {4, 3}; tm_tensor* out = tm_expand(t, new_shape, 2); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(4, tm_size(out, 0)); ASSERT_INT_EQ(3, tm_size(out, 1)); /* All rows should have the same values */ ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 3)); ASSERT_FLT_EQ(2.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(2.0f, tm_get_f32(out, 4)); tm_free(t); tm_free(out); } static void test_repeat(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_arange(0, 6, 1, TM_F32); t = tm_reshape(t, shape, 2); int64_t repeats[] = {2, 1}; tm_tensor* out = tm_repeat(t, repeats, 2); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(4, tm_size(out, 0)); ASSERT_INT_EQ(3, tm_size(out, 1)); /* Row 0 and row 2 should be the same */ ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(0.0f, tm_get_f32(out, 6)); tm_free(t); tm_free(out); } static void test_cumsum(void) { int64_t shape[] = {3}; float data[] = {1.0f, 2.0f, 3.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_cumsum(t, 0); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(1.0f, tm_get_f32(out, 0)); ASSERT_FLT_EQ(3.0f, tm_get_f32(out, 1)); ASSERT_FLT_EQ(6.0f, tm_get_f32(out, 2)); tm_free(t); tm_free(out); } static void test_var_std(void) { int64_t shape[] = {4}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; /* mean = 2.5 */ tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; /* Variance (unbiased): sum((x - mean)^2) / (n-1) */ /* (1.5^2 + 0.5^2 + 0.5^2 + 1.5^2) / 3 = 5/3 ≈ 1.667 */ tm_tensor* var = tm_var(t, -1, false, true); ASSERT_NOT_NULL(var); ASSERT_FLT_NEAR(1.6667f, tm_get_f32(var, 0), 0.01f); tm_tensor* std = tm_std(t, -1, false, true); ASSERT_NOT_NULL(std); ASSERT_FLT_NEAR(sqrtf(5.0f/3.0f), tm_get_f32(std, 0), 0.01f); tm_free(t); tm_free(var); tm_free(std); } static void test_prod(void) { int64_t shape[] = {4}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_prod(t, -1, false); ASSERT_NOT_NULL(out); ASSERT_FLT_EQ(24.0f, tm_get_f32(out, 0)); /* 1*2*3*4 = 24 */ tm_free(t); tm_free(out); } static void test_type_conversion(void) { int64_t shape[] = {3}; float data[] = {1.5f, 2.7f, 3.9f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; /* Convert to int (truncates) */ tm_tensor* int_t = tm_int(t); ASSERT_NOT_NULL(int_t); ASSERT_INT_EQ(TM_I32, int_t->dtype); int32_t* int_data = (int32_t*)int_t->data; ASSERT_INT_EQ(1, int_data[0]); ASSERT_INT_EQ(2, int_data[1]); ASSERT_INT_EQ(3, int_data[2]); /* Convert to double */ tm_tensor* dbl_t = tm_double(t); ASSERT_NOT_NULL(dbl_t); ASSERT_INT_EQ(TM_F64, dbl_t->dtype); double* dbl_data = (double*)dbl_t->data; ASSERT_TRUE(fabs(dbl_data[0] - 1.5) < 0.001); tm_free(t); tm_free(int_t); tm_free(dbl_t); } static void test_chunk(void) { int64_t shape[] = {6, 3}; tm_tensor* t = tm_arange(0, 18, 1, TM_F32); t = tm_reshape(t, shape, 2); int actual_chunks; tm_tensor** chunks = tm_chunk(t, 3, 0, &actual_chunks); ASSERT_NOT_NULL(chunks); ASSERT_INT_EQ(3, actual_chunks); for (int i = 0; i < actual_chunks; i++) { ASSERT_NOT_NULL(chunks[i]); ASSERT_INT_EQ(2, tm_size(chunks[i], 0)); ASSERT_INT_EQ(3, tm_size(chunks[i], 1)); tm_free(chunks[i]); } free(chunks); tm_free(t); } static void test_isnan_isinf(void) { int64_t shape[] = {4}; float data[] = {1.0f, NAN, INFINITY, -INFINITY}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* nan_mask = tm_isnan(t); ASSERT_NOT_NULL(nan_mask); int32_t* nan_data = (int32_t*)nan_mask->data; ASSERT_INT_EQ(0, nan_data[0]); ASSERT_INT_EQ(1, nan_data[1]); ASSERT_INT_EQ(0, nan_data[2]); tm_tensor* inf_mask = tm_isinf(t); ASSERT_NOT_NULL(inf_mask); int32_t* inf_data = (int32_t*)inf_mask->data; ASSERT_INT_EQ(0, inf_data[0]); ASSERT_INT_EQ(0, inf_data[1]); ASSERT_INT_EQ(1, inf_data[2]); ASSERT_INT_EQ(1, inf_data[3]); tm_free(t); tm_free(nan_mask); tm_free(inf_mask); } /* Full parity tests */ static void test_mm(void) { int64_t a_shape[] = {2, 3}; int64_t b_shape[] = {3, 4}; tm_tensor* a = tm_ones(a_shape, 2, TM_F32); tm_tensor* b = tm_full(b_shape, 2, TM_F32, 2.0); tm_tensor* c = tm_mm(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(2, c->ndim); ASSERT_INT_EQ(2, tm_size(c, 0)); ASSERT_INT_EQ(4, tm_size(c, 1)); ASSERT_FLT_EQ(6.0f, tm_get_f32(c, 0)); /* 3 * 2.0 = 6.0 */ tm_free(a); tm_free(b); tm_free(c); } static void test_dot(void) { int64_t shape[] = {4}; float a_data[] = {1.0f, 2.0f, 3.0f, 4.0f}; float b_data[] = {2.0f, 3.0f, 4.0f, 5.0f}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; tm_tensor* c = tm_dot(a, b); ASSERT_NOT_NULL(c); /* dot = 1*2 + 2*3 + 3*4 + 4*5 = 2 + 6 + 12 + 20 = 40 */ ASSERT_FLT_EQ(40.0f, tm_get_f32(c, 0)); tm_free(a); tm_free(b); tm_free(c); } static void test_mv(void) { int64_t mat_shape[] = {2, 3}; int64_t vec_shape[] = {3}; tm_tensor* mat = tm_ones(mat_shape, 2, TM_F32); float vec_data[] = {1.0f, 2.0f, 3.0f}; tm_tensor* vec = tm_from_data(vec_data, vec_shape, 1, TM_F32); vec->owns_data = false; tm_tensor* result = tm_mv(mat, vec); ASSERT_NOT_NULL(result); ASSERT_INT_EQ(1, result->ndim); ASSERT_INT_EQ(2, result->numel); ASSERT_FLT_EQ(6.0f, tm_get_f32(result, 0)); /* 1+2+3 = 6 */ ASSERT_FLT_EQ(6.0f, tm_get_f32(result, 1)); tm_free(mat); tm_free(vec); tm_free(result); } static void test_outer(void) { int64_t shape[] = {3}; float a_data[] = {1.0f, 2.0f, 3.0f}; float b_data[] = {4.0f, 5.0f}; int64_t b_shape[] = {2}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, b_shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; tm_tensor* c = tm_outer(a, b); ASSERT_NOT_NULL(c); ASSERT_INT_EQ(2, c->ndim); ASSERT_INT_EQ(3, tm_size(c, 0)); ASSERT_INT_EQ(2, tm_size(c, 1)); ASSERT_FLT_EQ(4.0f, tm_get_f32(c, 0)); /* 1*4 */ ASSERT_FLT_EQ(10.0f, tm_get_f32(c, 3)); /* 2*5 */ tm_free(a); tm_free(b); tm_free(c); } static void test_floor_ceil_round(void) { int64_t shape[] = {4}; float data[] = {1.2f, 2.7f, -1.5f, 3.5f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* f = tm_floor(t); ASSERT_FLT_EQ(1.0f, tm_get_f32(f, 0)); ASSERT_FLT_EQ(2.0f, tm_get_f32(f, 1)); ASSERT_FLT_EQ(-2.0f, tm_get_f32(f, 2)); tm_tensor* c = tm_ceil(t); ASSERT_FLT_EQ(2.0f, tm_get_f32(c, 0)); ASSERT_FLT_EQ(3.0f, tm_get_f32(c, 1)); ASSERT_FLT_EQ(-1.0f, tm_get_f32(c, 2)); tm_tensor* r = tm_round(t); ASSERT_FLT_EQ(1.0f, tm_get_f32(r, 0)); ASSERT_FLT_EQ(3.0f, tm_get_f32(r, 1)); tm_free(t); tm_free(f); tm_free(c); tm_free(r); } static void test_logical_ops(void) { int64_t shape[] = {4}; float a_data[] = {1.0f, 0.0f, 1.0f, 0.0f}; float b_data[] = {1.0f, 1.0f, 0.0f, 0.0f}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; tm_tensor* and_result = tm_logical_and(a, b); ASSERT_NOT_NULL(and_result); int32_t* and_data = (int32_t*)and_result->data; ASSERT_INT_EQ(1, and_data[0]); /* 1 && 1 */ ASSERT_INT_EQ(0, and_data[1]); /* 0 && 1 */ ASSERT_INT_EQ(0, and_data[2]); /* 1 && 0 */ ASSERT_INT_EQ(0, and_data[3]); /* 0 && 0 */ tm_tensor* or_result = tm_logical_or(a, b); int32_t* or_data = (int32_t*)or_result->data; ASSERT_INT_EQ(1, or_data[0]); ASSERT_INT_EQ(1, or_data[1]); ASSERT_INT_EQ(1, or_data[2]); ASSERT_INT_EQ(0, or_data[3]); tm_tensor* not_result = tm_logical_not(a); int32_t* not_data = (int32_t*)not_result->data; ASSERT_INT_EQ(0, not_data[0]); ASSERT_INT_EQ(1, not_data[1]); tm_free(a); tm_free(b); tm_free(and_result); tm_free(or_result); tm_free(not_result); } static void test_flip_roll(void) { int64_t shape[] = {4}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; int flip_dims[] = {0}; tm_tensor* flipped = tm_flip(t, flip_dims, 1); ASSERT_NOT_NULL(flipped); ASSERT_FLT_EQ(4.0f, tm_get_f32(flipped, 0)); ASSERT_FLT_EQ(1.0f, tm_get_f32(flipped, 3)); tm_tensor* rolled = tm_roll(t, 2, 0); ASSERT_NOT_NULL(rolled); ASSERT_FLT_EQ(3.0f, tm_get_f32(rolled, 0)); ASSERT_FLT_EQ(4.0f, tm_get_f32(rolled, 1)); ASSERT_FLT_EQ(1.0f, tm_get_f32(rolled, 2)); tm_free(t); tm_free(flipped); tm_free(rolled); } static void test_extra_inplace_ops(void) { int64_t shape[] = {4}; tm_tensor* a = tm_full(shape, 1, TM_F32, 4.0); tm_tensor* b = tm_full(shape, 1, TM_F32, 2.0); tm_sub_(a, b); ASSERT_FLT_EQ(2.0f, tm_get_f32(a, 0)); tm_mul_(a, b); ASSERT_FLT_EQ(4.0f, tm_get_f32(a, 0)); tm_div_(a, b); ASSERT_FLT_EQ(2.0f, tm_get_f32(a, 0)); tm_exp_(a); ASSERT_FLT_NEAR(expf(2.0f), tm_get_f32(a, 0), 0.001f); tm_free(a); a = tm_full(shape, 1, TM_F32, 2.0); tm_log_(a); ASSERT_FLT_NEAR(logf(2.0f), tm_get_f32(a, 0), 0.001f); tm_free(a); a = tm_full(shape, 1, TM_F32, 4.0); tm_sqrt_(a); ASSERT_FLT_EQ(2.0f, tm_get_f32(a, 0)); tm_free(a); a = tm_full(shape, 1, TM_F32, -3.0); tm_abs_(a); ASSERT_FLT_EQ(3.0f, tm_get_f32(a, 0)); tm_neg_(a); ASSERT_FLT_EQ(-3.0f, tm_get_f32(a, 0)); tm_free(a); a = tm_full(shape, 1, TM_F32, -2.0); tm_relu_(a); ASSERT_FLT_EQ(0.0f, tm_get_f32(a, 0)); tm_free(a); a = tm_full(shape, 1, TM_F32, 0.0); tm_sigmoid_(a); ASSERT_FLT_EQ(0.5f, tm_get_f32(a, 0)); tm_free(a); tm_free(b); } static void test_equal_allclose(void) { int64_t shape[] = {3}; float a_data[] = {1.0f, 2.0f, 3.0f}; float b_data[] = {1.0f, 2.0f, 3.0f}; float c_data[] = {1.0f, 2.0f, 4.0f}; tm_tensor* a = tm_from_data(a_data, shape, 1, TM_F32); tm_tensor* b = tm_from_data(b_data, shape, 1, TM_F32); tm_tensor* c = tm_from_data(c_data, shape, 1, TM_F32); a->owns_data = false; b->owns_data = false; c->owns_data = false; ASSERT_TRUE(tm_equal(a, b)); ASSERT_TRUE(!tm_equal(a, c)); ASSERT_TRUE(tm_allclose(a, b, 1e-5, 1e-8)); tm_free(a); tm_free(b); tm_free(c); } static void test_like_functions(void) { int64_t shape[] = {2, 3}; tm_tensor* t = tm_randn(shape, 2, TM_F32); tm_tensor* f = tm_full_like(t, 5.0); ASSERT_NOT_NULL(f); ASSERT_INT_EQ(2, f->ndim); ASSERT_INT_EQ(2, tm_size(f, 0)); ASSERT_INT_EQ(3, tm_size(f, 1)); ASSERT_FLT_EQ(5.0f, tm_get_f32(f, 0)); tm_tensor* e = tm_empty_like(t); ASSERT_NOT_NULL(e); ASSERT_INT_EQ(2, e->ndim); tm_tensor* r = tm_rand_like(t); ASSERT_NOT_NULL(r); ASSERT_INT_EQ(2, r->ndim); tm_tensor* rn = tm_randn_like(t); ASSERT_NOT_NULL(rn); ASSERT_INT_EQ(2, rn->ndim); tm_free(t); tm_free(f); tm_free(e); tm_free(r); tm_free(rn); } static void test_cumprod(void) { int64_t shape[] = {4}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* cp = tm_cumprod(t, 0); ASSERT_NOT_NULL(cp); ASSERT_FLT_EQ(1.0f, tm_get_f32(cp, 0)); ASSERT_FLT_EQ(2.0f, tm_get_f32(cp, 1)); ASSERT_FLT_EQ(6.0f, tm_get_f32(cp, 2)); ASSERT_FLT_EQ(24.0f, tm_get_f32(cp, 3)); tm_free(t); tm_free(cp); } static void test_any_all(void) { int64_t shape[] = {4}; float data1[] = {0.0f, 0.0f, 1.0f, 0.0f}; float data2[] = {1.0f, 1.0f, 1.0f, 1.0f}; float data3[] = {0.0f, 0.0f, 0.0f, 0.0f}; tm_tensor* t1 = tm_from_data(data1, shape, 1, TM_F32); tm_tensor* t2 = tm_from_data(data2, shape, 1, TM_F32); tm_tensor* t3 = tm_from_data(data3, shape, 1, TM_F32); t1->owns_data = false; t2->owns_data = false; t3->owns_data = false; tm_tensor* any1 = tm_any(t1, -1, false); ASSERT_NOT_NULL(any1); ASSERT_INT_EQ(1, ((int32_t*)any1->data)[0]); tm_tensor* any3 = tm_any(t3, -1, false); ASSERT_INT_EQ(0, ((int32_t*)any3->data)[0]); tm_tensor* all2 = tm_all(t2, -1, false); ASSERT_INT_EQ(1, ((int32_t*)all2->data)[0]); tm_tensor* all1 = tm_all(t1, -1, false); ASSERT_INT_EQ(0, ((int32_t*)all1->data)[0]); tm_free(t1); tm_free(t2); tm_free(t3); tm_free(any1); tm_free(any3); tm_free(all2); tm_free(all1); } static void test_norm(void) { int64_t shape[] = {3}; float data[] = {3.0f, 4.0f, 0.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; /* L2 norm: sqrt(9 + 16 + 0) = 5 */ tm_tensor* n2 = tm_norm(t, 2.0, -1, false); ASSERT_NOT_NULL(n2); ASSERT_FLT_EQ(5.0f, tm_get_f32(n2, 0)); /* L1 norm: 3 + 4 + 0 = 7 */ tm_tensor* n1 = tm_norm(t, 1.0, -1, false); ASSERT_FLT_EQ(7.0f, tm_get_f32(n1, 0)); tm_free(t); tm_free(n2); tm_free(n1); } static void test_elu(void) { int64_t shape[] = {4}; float data[] = {1.0f, 0.0f, -1.0f, -2.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* e = tm_elu(t, 1.0f); ASSERT_NOT_NULL(e); ASSERT_FLT_EQ(1.0f, tm_get_f32(e, 0)); ASSERT_FLT_EQ(0.0f, tm_get_f32(e, 1)); /* For x < 0: alpha * (exp(x) - 1) */ ASSERT_FLT_NEAR(expf(-1.0f) - 1.0f, tm_get_f32(e, 2), 0.001f); tm_free(t); tm_free(e); } static void test_trig_functions(void) { int64_t shape[] = {1}; float data[] = {0.5f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* tan_t = tm_tan(t); ASSERT_FLT_NEAR(tanf(0.5f), tm_get_f32(tan_t, 0), 0.001f); tm_tensor* asin_t = tm_asin(t); ASSERT_FLT_NEAR(asinf(0.5f), tm_get_f32(asin_t, 0), 0.001f); tm_tensor* acos_t = tm_acos(t); ASSERT_FLT_NEAR(acosf(0.5f), tm_get_f32(acos_t, 0), 0.001f); tm_tensor* atan_t = tm_atan(t); ASSERT_FLT_NEAR(atanf(0.5f), tm_get_f32(atan_t, 0), 0.001f); tm_tensor* sinh_t = tm_sinh(t); ASSERT_FLT_NEAR(sinhf(0.5f), tm_get_f32(sinh_t, 0), 0.001f); tm_tensor* cosh_t = tm_cosh(t); ASSERT_FLT_NEAR(coshf(0.5f), tm_get_f32(cosh_t, 0), 0.001f); tm_free(t); tm_free(tan_t); tm_free(asin_t); tm_free(acos_t); tm_free(atan_t); tm_free(sinh_t); tm_free(cosh_t); } /* NN Ops tests */ static void test_hardswish(void) { int64_t shape[] = {5}; float data[] = {-4.0f, -3.0f, 0.0f, 3.0f, 4.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* out = tm_hardswish(t); ASSERT_NOT_NULL(out); /* hardswish(-4) = -4 * 0 / 6 = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 0), 0.01f); /* hardswish(0) = 0 * 3/6 = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 2), 0.01f); /* hardswish(3) = 3 * 6/6 = 3 */ ASSERT_FLT_NEAR(3.0f, tm_get_f32(out, 3), 0.01f); /* hardswish(4) = 4 * 6/6 = 4 */ ASSERT_FLT_NEAR(4.0f, tm_get_f32(out, 4), 0.01f); tm_free(t); tm_free(out); } static void test_batch_norm(void) { /* [batch=2, channels=2, length=3] */ int64_t shape[] = {2, 2, 3}; tm_tensor* t = tm_randn(shape, 3, TM_F32); tm_manual_seed(42); /* No weight/bias (identity transform) */ tm_tensor* out = tm_batch_norm(t, NULL, NULL, NULL, NULL, true, 0.1f, 1e-5f); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(3, out->ndim); ASSERT_INT_EQ(2, tm_size(out, 0)); ASSERT_INT_EQ(2, tm_size(out, 1)); ASSERT_INT_EQ(3, tm_size(out, 2)); tm_free(t); tm_free(out); } static void test_group_norm(void) { /* [batch=1, channels=4, length=2] */ int64_t shape[] = {1, 4, 2}; tm_tensor* t = tm_randn(shape, 3, TM_F32); /* 2 groups */ tm_tensor* out = tm_group_norm(t, 2, NULL, NULL, 1e-5f); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(3, out->ndim); tm_free(t); tm_free(out); } static void test_instance_norm(void) { /* [batch=2, channels=3, length=4] */ int64_t shape[] = {2, 3, 4}; tm_tensor* t = tm_randn(shape, 3, TM_F32); tm_tensor* out = tm_instance_norm(t, NULL, NULL, 1e-5f); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(3, out->ndim); tm_free(t); tm_free(out); } static void test_scaled_dot_product_attention(void) { /* Q, K, V: [batch=1, heads=2, seq=3, head_dim=4] */ int64_t shape[] = {1, 2, 3, 4}; tm_tensor* Q = tm_randn(shape, 4, TM_F32); tm_tensor* K = tm_randn(shape, 4, TM_F32); tm_tensor* V = tm_randn(shape, 4, TM_F32); tm_tensor* out = tm_scaled_dot_product_attention(Q, K, V, NULL, 0.0f, false); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(4, out->ndim); ASSERT_INT_EQ(1, tm_size(out, 0)); ASSERT_INT_EQ(2, tm_size(out, 1)); ASSERT_INT_EQ(3, tm_size(out, 2)); ASSERT_INT_EQ(4, tm_size(out, 3)); /* Test with causal mask */ tm_tensor* out_causal = tm_scaled_dot_product_attention(Q, K, V, NULL, 0.0f, true); ASSERT_NOT_NULL(out_causal); tm_free(Q); tm_free(K); tm_free(V); tm_free(out); tm_free(out_causal); } static void test_split_merge_heads(void) { /* x: [batch=2, seq=4, embed_dim=8] */ int64_t shape[] = {2, 4, 8}; tm_tensor* x = tm_randn(shape, 3, TM_F32); /* Split into 2 heads */ tm_tensor* split = tm_split_heads(x, 2); ASSERT_NOT_NULL(split); ASSERT_INT_EQ(4, split->ndim); ASSERT_INT_EQ(2, tm_size(split, 0)); /* batch */ ASSERT_INT_EQ(2, tm_size(split, 1)); /* heads */ ASSERT_INT_EQ(4, tm_size(split, 2)); /* seq */ ASSERT_INT_EQ(4, tm_size(split, 3)); /* head_dim */ /* Merge back */ tm_tensor* merged = tm_merge_heads(split); ASSERT_NOT_NULL(merged); ASSERT_INT_EQ(3, merged->ndim); ASSERT_INT_EQ(2, tm_size(merged, 0)); /* batch */ ASSERT_INT_EQ(4, tm_size(merged, 1)); /* seq */ ASSERT_INT_EQ(8, tm_size(merged, 2)); /* embed_dim */ tm_free(x); tm_free(split); tm_free(merged); } static void test_conv1d(void) { /* input: [batch=1, channels=2, length=5] */ int64_t in_shape[] = {1, 2, 5}; tm_tensor* input = tm_ones(in_shape, 3, TM_F32); /* weight: [out_channels=3, in_channels=2, kernel=3] */ int64_t w_shape[] = {3, 2, 3}; tm_tensor* weight = tm_ones(w_shape, 3, TM_F32); tm_tensor* out = tm_conv1d(input, weight, NULL, 1, 0, 1, 1); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(3, out->ndim); ASSERT_INT_EQ(1, tm_size(out, 0)); ASSERT_INT_EQ(3, tm_size(out, 1)); /* out_channels */ ASSERT_INT_EQ(3, tm_size(out, 2)); /* (5 - 3)/1 + 1 = 3 */ /* Each output is sum of 2*3 ones = 6 */ ASSERT_FLT_NEAR(6.0f, tm_get_f32(out, 0), 0.01f); tm_free(input); tm_free(weight); tm_free(out); } static void test_conv2d(void) { /* input: [batch=1, channels=1, H=4, W=4] */ int64_t in_shape[] = {1, 1, 4, 4}; tm_tensor* input = tm_ones(in_shape, 4, TM_F32); /* weight: [out_channels=1, in_channels=1, kH=2, kW=2] */ int64_t w_shape[] = {1, 1, 2, 2}; tm_tensor* weight = tm_ones(w_shape, 4, TM_F32); tm_tensor* out = tm_conv2d(input, weight, NULL, 1, 1, 0, 0, 1, 1, 1); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(4, out->ndim); ASSERT_INT_EQ(1, tm_size(out, 0)); ASSERT_INT_EQ(1, tm_size(out, 1)); ASSERT_INT_EQ(3, tm_size(out, 2)); /* (4 - 2)/1 + 1 = 3 */ ASSERT_INT_EQ(3, tm_size(out, 3)); /* Each output is sum of 2*2 ones = 4 */ ASSERT_FLT_NEAR(4.0f, tm_get_f32(out, 0), 0.01f); tm_free(input); tm_free(weight); tm_free(out); } static void test_pooling(void) { /* 1D pooling: [batch=1, channels=1, length=4] */ int64_t shape1[] = {1, 1, 4}; float data1[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* t1 = tm_from_data(data1, shape1, 3, TM_F32); t1->owns_data = false; tm_tensor* avg1 = tm_avg_pool1d(t1, 2, 2, 0); ASSERT_NOT_NULL(avg1); ASSERT_INT_EQ(2, tm_size(avg1, 2)); /* (4-2)/2 + 1 = 2 */ ASSERT_FLT_NEAR(1.5f, tm_get_f32(avg1, 0), 0.01f); /* avg(1,2) */ ASSERT_FLT_NEAR(3.5f, tm_get_f32(avg1, 1), 0.01f); /* avg(3,4) */ tm_tensor* max1 = tm_max_pool1d(t1, 2, 2, 0); ASSERT_NOT_NULL(max1); ASSERT_FLT_NEAR(2.0f, tm_get_f32(max1, 0), 0.01f); /* max(1,2) */ ASSERT_FLT_NEAR(4.0f, tm_get_f32(max1, 1), 0.01f); /* max(3,4) */ /* 2D pooling: [batch=1, channels=1, H=2, W=2] */ int64_t shape2[] = {1, 1, 2, 2}; float data2[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* t2 = tm_from_data(data2, shape2, 4, TM_F32); t2->owns_data = false; tm_tensor* avg2 = tm_avg_pool2d(t2, 2, 2, 1, 1, 0, 0); ASSERT_NOT_NULL(avg2); ASSERT_INT_EQ(1, tm_size(avg2, 2)); ASSERT_FLT_NEAR(2.5f, tm_get_f32(avg2, 0), 0.01f); /* avg(1,2,3,4) */ tm_free(t1); tm_free(avg1); tm_free(max1); tm_free(t2); tm_free(avg2); } static void test_adaptive_pooling(void) { /* 1D: [batch=1, channels=1, length=6] -> output_size=2 */ int64_t shape1[] = {1, 1, 6}; tm_tensor* t1 = tm_arange(1, 7, 1, TM_F32); t1 = tm_reshape(t1, shape1, 3); tm_tensor* out1 = tm_adaptive_avg_pool1d(t1, 2); ASSERT_NOT_NULL(out1); ASSERT_INT_EQ(2, tm_size(out1, 2)); /* First 3 elements: avg(1,2,3) = 2, last 3: avg(4,5,6) = 5 */ ASSERT_FLT_NEAR(2.0f, tm_get_f32(out1, 0), 0.01f); ASSERT_FLT_NEAR(5.0f, tm_get_f32(out1, 1), 0.01f); tm_free(t1); tm_free(out1); } /* ============================================================= */ /* Loss Function Tests */ /* ============================================================= */ static void test_l1_loss(void) { /* L1 loss = mean(|pred - target|) */ int64_t shape[] = {4}; float pred_data[] = {1.0f, 2.0f, 3.0f, 4.0f}; float target_data[] = {1.5f, 1.5f, 3.5f, 3.5f}; tm_tensor* pred = tm_from_data(pred_data, shape, 1, TM_F32); pred->owns_data = false; tm_tensor* target = tm_from_data(target_data, shape, 1, TM_F32); target->owns_data = false; tm_tensor* loss = tm_l1_loss(pred, target); ASSERT_NOT_NULL(loss); /* |1-1.5| + |2-1.5| + |3-3.5| + |4-3.5| = 0.5+0.5+0.5+0.5 = 2.0, avg = 0.5 */ ASSERT_FLT_NEAR(0.5f, tm_get_f32(loss, 0), 0.001f); tm_free(pred); tm_free(target); tm_free(loss); } static void test_smooth_l1_loss(void) { /* Smooth L1 with beta=1.0 */ int64_t shape[] = {4}; float pred_data[] = {0.0f, 0.0f, 0.0f, 0.0f}; float target_data[] = {0.5f, 1.0f, 1.5f, 2.0f}; tm_tensor* pred = tm_from_data(pred_data, shape, 1, TM_F32); pred->owns_data = false; tm_tensor* target = tm_from_data(target_data, shape, 1, TM_F32); target->owns_data = false; tm_tensor* loss = tm_smooth_l1_loss(pred, target, 1.0f); ASSERT_NOT_NULL(loss); /* For |x| < 1: 0.5*x^2, else |x| - 0.5 * 0.5: 0.5*0.25 = 0.125 * 1.0: 1.0 - 0.5 = 0.5 * 1.5: 1.5 - 0.5 = 1.0 * 2.0: 2.0 - 0.5 = 1.5 * Total = 3.125, avg = 0.78125 */ ASSERT_FLT_NEAR(0.78125f, tm_get_f32(loss, 0), 0.01f); tm_free(pred); tm_free(target); tm_free(loss); } static void test_nll_loss(void) { /* NLL loss with log probabilities */ int64_t logp_shape[] = {2, 3}; /* batch=2, classes=3 */ /* log_softmax values (approximately) */ float logp_data[] = { -0.4f, -1.2f, -2.0f, /* batch 0: class 0 is most likely */ -2.0f, -0.4f, -1.2f /* batch 1: class 1 is most likely */ }; int64_t target_shape[] = {2}; int32_t target_data[] = {0, 1}; /* correct classes */ tm_tensor* log_probs = tm_from_data(logp_data, logp_shape, 2, TM_F32); log_probs->owns_data = false; tm_tensor* targets = tm_from_data(target_data, target_shape, 1, TM_I32); targets->owns_data = false; tm_tensor* loss = tm_nll_loss(log_probs, targets); ASSERT_NOT_NULL(loss); /* -(-0.4 + -0.4) / 2 = 0.4 */ ASSERT_FLT_NEAR(0.4f, tm_get_f32(loss, 0), 0.01f); tm_free(log_probs); tm_free(targets); tm_free(loss); } static void test_kl_div(void) { /* KL divergence test: KL(target || exp(input)) * Uses very divergent distributions to ensure positive result */ int64_t shape[] = {1, 3}; /* Log probabilities (input) - very low prob for class 0 */ float input_data[] = {-5.0f, -0.5f, -0.5f}; /* Target probabilities - concentrated on class 0 */ float target_data[] = {0.9f, 0.05f, 0.05f}; tm_tensor* input = tm_from_data(input_data, shape, 2, TM_F32); input->owns_data = false; tm_tensor* target = tm_from_data(target_data, shape, 2, TM_F32); target->owns_data = false; tm_tensor* loss = tm_kl_div(input, target); ASSERT_NOT_NULL(loss); /* KL divergence should be non-negative */ ASSERT_TRUE(tm_get_f32(loss, 0) >= 0); tm_free(input); tm_free(target); tm_free(loss); } static void test_binary_cross_entropy(void) { /* BCE loss */ int64_t shape[] = {4}; float pred_data[] = {0.9f, 0.1f, 0.8f, 0.2f}; float target_data[] = {1.0f, 0.0f, 1.0f, 0.0f}; tm_tensor* pred = tm_from_data(pred_data, shape, 1, TM_F32); pred->owns_data = false; tm_tensor* target = tm_from_data(target_data, shape, 1, TM_F32); target->owns_data = false; tm_tensor* loss = tm_binary_cross_entropy(pred, target); ASSERT_NOT_NULL(loss); /* All predictions are fairly correct, loss should be small */ ASSERT_TRUE(tm_get_f32(loss, 0) < 0.5f); tm_free(pred); tm_free(target); tm_free(loss); } static void test_half_conversion(void) { /* Float16 conversion test */ int64_t shape[] = {4}; float data[] = {1.0f, 2.5f, -3.0f, 0.0f}; tm_tensor* t = tm_from_data(data, shape, 1, TM_F32); t->owns_data = false; tm_tensor* h = tm_half(t); ASSERT_NOT_NULL(h); ASSERT_INT_EQ(TM_F16, h->dtype); /* Convert back to float and check values */ tm_tensor* f = tm_float(h); ASSERT_NOT_NULL(f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(f, 0), 0.01f); ASSERT_FLT_NEAR(2.5f, tm_get_f32(f, 1), 0.01f); ASSERT_FLT_NEAR(-3.0f, tm_get_f32(f, 2), 0.01f); ASSERT_FLT_NEAR(0.0f, tm_get_f32(f, 3), 0.01f); tm_free(t); tm_free(h); tm_free(f); } /* ============================================================= */ /* Additional Loss Function Tests */ /* ============================================================= */ static void test_cosine_embedding_loss(void) { /* Test cosine embedding loss */ int64_t x_shape[] = {2, 3}; float x1_data[] = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f}; float x2_data[] = {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}; /* First same, second orthogonal */ float y_data[] = {1.0f, -1.0f}; /* First should be similar, second dissimilar */ tm_tensor* x1 = tm_from_data(x1_data, x_shape, 2, TM_F32); x1->owns_data = false; tm_tensor* x2 = tm_from_data(x2_data, x_shape, 2, TM_F32); x2->owns_data = false; int64_t y_shape[] = {2}; tm_tensor* y = tm_from_data(y_data, y_shape, 1, TM_F32); y->owns_data = false; tm_tensor* loss = tm_cosine_embedding_loss(x1, x2, y, 0.0f); ASSERT_NOT_NULL(loss); /* First pair: cos_sim=1, y=1, loss=0 * Second pair: cos_sim=0, y=-1, loss=max(0, 0-0)=0 * Total loss = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(loss, 0), 0.01f); tm_free(x1); tm_free(x2); tm_free(y); tm_free(loss); } static void test_triplet_margin_loss(void) { /* Test triplet margin loss */ int64_t shape[] = {2, 3}; float anchor_data[] = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f}; float positive_data[] = {0.9f, 0.1f, 0.0f, 0.1f, 0.9f, 0.0f}; /* Close to anchor */ float negative_data[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f}; /* Far from anchor */ tm_tensor* anchor = tm_from_data(anchor_data, shape, 2, TM_F32); anchor->owns_data = false; tm_tensor* positive = tm_from_data(positive_data, shape, 2, TM_F32); positive->owns_data = false; tm_tensor* negative = tm_from_data(negative_data, shape, 2, TM_F32); negative->owns_data = false; tm_tensor* loss = tm_triplet_margin_loss(anchor, positive, negative, 1.0f, 2.0f); ASSERT_NOT_NULL(loss); /* With positive closer than negative, and margin=1, loss should be manageable */ ASSERT_TRUE(tm_get_f32(loss, 0) >= 0.0f); tm_free(anchor); tm_free(positive); tm_free(negative); tm_free(loss); } static void test_label_smoothing(void) { /* Test cross entropy with label smoothing */ int64_t logits_shape[] = {2, 3}; float logits_data[] = {2.0f, 1.0f, 0.0f, 0.0f, 2.0f, 1.0f}; int64_t targets_shape[] = {2}; int32_t targets_data[] = {0, 1}; tm_tensor* logits = tm_from_data(logits_data, logits_shape, 2, TM_F32); logits->owns_data = false; tm_tensor* targets = tm_from_data(targets_data, targets_shape, 1, TM_I32); targets->owns_data = false; /* With smoothing=0, should equal regular cross entropy */ tm_tensor* loss_no_smooth = tm_cross_entropy_label_smooth(logits, targets, 0.0f); tm_tensor* loss_regular = tm_cross_entropy(logits, targets); ASSERT_NOT_NULL(loss_no_smooth); ASSERT_NOT_NULL(loss_regular); ASSERT_FLT_NEAR(tm_get_f32(loss_regular, 0), tm_get_f32(loss_no_smooth, 0), 0.01f); /* With smoothing > 0, loss should be higher */ tm_tensor* loss_smooth = tm_cross_entropy_label_smooth(logits, targets, 0.1f); ASSERT_NOT_NULL(loss_smooth); ASSERT_TRUE(tm_get_f32(loss_smooth, 0) > tm_get_f32(loss_no_smooth, 0)); tm_free(logits); tm_free(targets); tm_free(loss_no_smooth); tm_free(loss_regular); tm_free(loss_smooth); } /* ============================================================= */ /* Linear Algebra Tests */ /* ============================================================= */ static void test_inverse(void) { /* Test matrix inverse: A * A^-1 = I */ int64_t shape[] = {3, 3}; float data[] = { 1.0f, 2.0f, 3.0f, 0.0f, 1.0f, 4.0f, 5.0f, 6.0f, 0.0f }; tm_tensor* A = tm_from_data(data, shape, 2, TM_F32); A->owns_data = false; tm_tensor* A_inv = tm_inverse(A); ASSERT_NOT_NULL(A_inv); /* Verify A * A_inv ≈ I */ tm_tensor* I_approx = tm_matmul(A, A_inv); ASSERT_NOT_NULL(I_approx); /* Check diagonal is 1, off-diagonal is 0 */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(I_approx, 0), 0.01f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(I_approx, 4), 0.01f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(I_approx, 8), 0.01f); ASSERT_FLT_NEAR(0.0f, tm_get_f32(I_approx, 1), 0.01f); tm_free(A); tm_free(A_inv); tm_free(I_approx); } static void test_det(void) { /* Test determinant */ int64_t shape[] = {2, 2}; float data[] = {1.0f, 2.0f, 3.0f, 4.0f}; tm_tensor* A = tm_from_data(data, shape, 2, TM_F32); A->owns_data = false; tm_tensor* det = tm_det(A); ASSERT_NOT_NULL(det); /* det([[1,2],[3,4]]) = 1*4 - 2*3 = -2 */ ASSERT_FLT_NEAR(-2.0f, tm_get_f32(det, 0), 0.01f); tm_free(A); tm_free(det); } static void test_svd(void) { /* Test SVD: A = U * S * V^T */ int64_t shape[] = {3, 2}; float data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f }; tm_tensor* A = tm_from_data(data, shape, 2, TM_F32); A->owns_data = false; int num_outputs; tm_tensor** usv = tm_svd(A, &num_outputs); ASSERT_NOT_NULL(usv); ASSERT_INT_EQ(3, num_outputs); tm_tensor* U = usv[0]; tm_tensor* S = usv[1]; tm_tensor* V = usv[2]; ASSERT_NOT_NULL(U); ASSERT_NOT_NULL(S); ASSERT_NOT_NULL(V); /* U should be [3, 2], S should be [2], V should be [2, 2] */ ASSERT_INT_EQ(3, tm_size(U, 0)); ASSERT_INT_EQ(2, tm_size(U, 1)); ASSERT_INT_EQ(2, tm_numel(S)); /* Singular values should be positive and decreasing */ ASSERT_TRUE(tm_get_f32(S, 0) > 0); ASSERT_TRUE(tm_get_f32(S, 0) >= tm_get_f32(S, 1)); tm_free(A); tm_free(U); tm_free(S); tm_free(V); free(usv); } static void test_eig(void) { /* Test eigenvalues for symmetric matrix */ int64_t shape[] = {2, 2}; float data[] = {2.0f, 1.0f, 1.0f, 2.0f}; /* Symmetric matrix */ tm_tensor* A = tm_from_data(data, shape, 2, TM_F32); A->owns_data = false; tm_tensor* eig = tm_eig(A); ASSERT_NOT_NULL(eig); ASSERT_INT_EQ(2, tm_numel(eig)); /* Eigenvalues of [[2,1],[1,2]] are 3 and 1 */ float e1 = tm_get_f32(eig, 0); float e2 = tm_get_f32(eig, 1); /* Check that eigenvalues are approximately 3 and 1 (order may vary) */ float sum = e1 + e2; float prod = e1 * e2; ASSERT_FLT_NEAR(4.0f, sum, 0.1f); /* trace = 2 + 2 = 4 */ ASSERT_FLT_NEAR(3.0f, prod, 0.1f); /* det = 4 - 1 = 3 */ tm_free(A); tm_free(eig); } /* ============================================================= */ /* Autograd Extension Tests */ /* ============================================================= */ static void test_retain_grad(void) { /* Test retain_grad - should allow non-leaf to keep gradient */ int64_t shape[] = {2, 2}; tm_tensor* a = tm_ones(shape, 2, TM_F32); tm_set_requires_grad(a, true); tm_tensor* b = tm_mul_scalar(a, 2.0f); tm_retain_grad(b); tm_tensor* c = tm_sum(b, -1, false); tm_backward(c); /* After backward, b should have gradient */ ASSERT_NOT_NULL(b->grad); tm_free(a); tm_free(b); tm_free(c); } static void test_explicit_grad(void) { /* Test tm_grad for explicit gradient computation */ int64_t shape[] = {2}; tm_tensor* x = tm_full(shape, 1, TM_F32, 3.0f); tm_set_requires_grad(x, true); /* y = x^2 (via x * x) */ tm_tensor* y = tm_mul(x, x); tm_tensor* outputs[] = {y}; tm_tensor* inputs[] = {x}; tm_tensor** grads = tm_grad(outputs, 1, inputs, 1, NULL); ASSERT_NOT_NULL(grads); /* dy/dx = 2x = 6 */ if (grads[0]) { ASSERT_FLT_NEAR(6.0f, tm_get_f32(grads[0], 0), 0.1f); tm_free(grads[0]); } free(grads); tm_free(x); tm_free(y); } /* Loss function for Hessian test: L = sum(x^2) = x1^2 + x2^2 */ static tm_tensor* quadratic_loss(tm_tensor** params, int num_params, void* ctx) { (void)ctx; if (num_params != 1 || !params[0]) return NULL; /* L = sum(x^2) */ tm_tensor* sq = tm_mul(params[0], params[0]); tm_tensor* loss = tm_sum(sq, -1, false); tm_free(sq); return loss; } static void test_hessian(void) { /* Test Hessian computation for L = x1^2 + x2^2 * Hessian should be 2*I (diagonal matrix with 2s) */ int64_t shape[] = {2}; tm_tensor* x = tm_full(shape, 1, TM_F32, 1.0f); tm_set_requires_grad(x, true); tm_tensor* params[] = {x}; tm_tensor* H = tm_hessian(params, 1, quadratic_loss, NULL); ASSERT_NOT_NULL(H); /* H should be [[2, 0], [0, 2]] */ ASSERT_INT_EQ(2, H->ndim); ASSERT_INT_EQ(2, H->shape[0]); ASSERT_INT_EQ(2, H->shape[1]); float* h_data = (float*)H->data; /* Diagonal should be ~2 */ ASSERT_FLT_NEAR(2.0f, h_data[0], 0.1f); /* H[0,0] */ ASSERT_FLT_NEAR(2.0f, h_data[3], 0.1f); /* H[1,1] */ /* Off-diagonal should be ~0 */ ASSERT_FLT_NEAR(0.0f, h_data[1], 0.1f); /* H[0,1] */ ASSERT_FLT_NEAR(0.0f, h_data[2], 0.1f); /* H[1,0] */ tm_free(H); tm_free(x); } static void test_hvp(void) { /* Test Hessian-vector product for L = x1^2 + x2^2 * H = [[2, 0], [0, 2]], v = [1, 2] * Hv = [2, 4] */ int64_t shape[] = {2}; tm_tensor* x = tm_full(shape, 1, TM_F32, 1.0f); tm_set_requires_grad(x, true); float v_data[] = {1.0f, 2.0f}; tm_tensor* v = tm_from_data(v_data, shape, 1, TM_F32); tm_tensor* params[] = {x}; tm_tensor* vecs[] = {v}; tm_tensor* hvp = tm_hvp(params, 1, vecs, 1, quadratic_loss, NULL); ASSERT_NOT_NULL(hvp); /* Hv should be [2, 4] */ ASSERT_INT_EQ(1, hvp->ndim); ASSERT_INT_EQ(2, hvp->shape[0]); float* hvp_data = (float*)hvp->data; ASSERT_FLT_NEAR(2.0f, hvp_data[0], 0.2f); ASSERT_FLT_NEAR(4.0f, hvp_data[1], 0.2f); tm_free(hvp); tm_free(v); tm_free(x); } /* Custom autograd function: squared function with custom backward */ static tm_tensor* square_forward(tm_tensor** inputs, int num_inputs, tm_custom_ctx* ctx) { if (num_inputs != 1 || !inputs[0]) return NULL; /* Save input for backward */ tm_save_for_backward(ctx, inputs[0]); /* Return x^2 */ return tm_mul(inputs[0], inputs[0]); } static tm_tensor** square_backward(tm_tensor* grad_output, tm_custom_ctx* ctx, int* num_grads) { int num_saved; tm_tensor** saved = tm_get_saved_tensors(ctx, &num_saved); if (num_saved != 1 || !saved[0]) { *num_grads = 0; return NULL; } /* d(x^2)/dx = 2x */ tm_tensor* two_x = tm_mul_scalar(saved[0], 2.0f); tm_tensor* grad_input = tm_mul(grad_output, two_x); tm_free(two_x); tm_tensor** grads = (tm_tensor**)malloc(sizeof(tm_tensor*)); grads[0] = grad_input; *num_grads = 1; return grads; } static void test_custom_autograd(void) { /* Register and test a custom autograd function */ int fn_id = tm_register_autograd_function("square", square_forward, square_backward); ASSERT_TRUE(fn_id >= 0); /* Test forward pass */ int64_t shape[] = {2}; float x_data[] = {2.0f, 3.0f}; tm_tensor* x = tm_from_data(x_data, shape, 1, TM_F32); tm_set_requires_grad(x, true); tm_tensor* inputs[] = {x}; tm_tensor* y = tm_apply_custom_function("square", inputs, 1); ASSERT_NOT_NULL(y); /* y should be [4, 9] */ float* y_data = (float*)y->data; ASSERT_FLT_NEAR(4.0f, y_data[0], 0.01f); ASSERT_FLT_NEAR(9.0f, y_data[1], 0.01f); /* Test backward pass */ tm_tensor* loss = tm_sum(y, -1, false); tm_backward(loss); /* grad should be 2*x = [4, 6] */ ASSERT_NOT_NULL(x->grad); float* grad = (float*)x->grad->data; ASSERT_FLT_NEAR(4.0f, grad[0], 0.01f); ASSERT_FLT_NEAR(6.0f, grad[1], 0.01f); tm_free(loss); tm_free(y); tm_free(x); /* Cleanup registry */ tm_cleanup_autograd_registry(); } static void test_inplace_add_grad(void) { /* Test tm_add_ with autograd: a += b */ int64_t shape[] = {2}; tm_tensor* a = tm_full(shape, 1, TM_F32, 2.0f); tm_tensor* b = tm_full(shape, 1, TM_F32, 3.0f); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); /* a = [2, 2], b = [3, 3] */ tm_add_(a, b); /* a becomes [5, 5] */ ASSERT_FLT_NEAR(5.0f, tm_get_f32(a, 0), 0.01f); /* Compute loss = sum(a) = 10, backward */ tm_tensor* loss = tm_sum(a, -1, false); tm_backward(loss); /* grad_b should be [1, 1] (gradient flows through) */ ASSERT_NOT_NULL(b->grad); ASSERT_FLT_NEAR(1.0f, tm_get_f32(b->grad, 0), 0.01f); tm_free(loss); tm_free(a); tm_free(b); } static void test_inplace_mul_grad(void) { /* Test tm_mul_ with autograd: a *= b */ int64_t shape[] = {2}; tm_tensor* a = tm_full(shape, 1, TM_F32, 2.0f); tm_tensor* b = tm_full(shape, 1, TM_F32, 3.0f); tm_set_requires_grad(a, true); tm_set_requires_grad(b, true); /* a = [2, 2], b = [3, 3] */ tm_mul_(a, b); /* a becomes [6, 6] */ ASSERT_FLT_NEAR(6.0f, tm_get_f32(a, 0), 0.01f); /* Compute loss = sum(a) = 12, backward */ tm_tensor* loss = tm_sum(a, -1, false); tm_backward(loss); /* grad_b should be [2, 2] (original a values) */ ASSERT_NOT_NULL(b->grad); ASSERT_FLT_NEAR(2.0f, tm_get_f32(b->grad, 0), 0.1f); tm_free(loss); tm_free(a); tm_free(b); } static void test_inplace_exp_grad(void) { /* Test tm_exp_ with autograd */ int64_t shape[] = {2}; tm_tensor* a = tm_full(shape, 1, TM_F32, 1.0f); tm_set_requires_grad(a, true); /* a = [1, 1] -> exp -> [e, e] */ tm_exp_(a); float e = expf(1.0f); ASSERT_FLT_NEAR(e, tm_get_f32(a, 0), 0.01f); /* Compute loss = sum(a), backward */ tm_tensor* loss = tm_sum(a, -1, false); tm_backward(loss); /* grad should be [e, e] (d/dx exp(x) = exp(x)) */ ASSERT_NOT_NULL(a->grad); ASSERT_FLT_NEAR(e, tm_get_f32(a->grad, 0), 0.01f); tm_free(loss); tm_free(a); } static void test_inplace_relu_grad(void) { /* Test tm_relu_ with autograd */ int64_t shape[] = {4}; float data[] = {-2.0f, -1.0f, 1.0f, 2.0f}; tm_tensor* a = tm_from_data(data, shape, 1, TM_F32); tm_set_requires_grad(a, true); tm_relu_(a); /* a should be [0, 0, 1, 2] */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(a, 0), 0.01f); ASSERT_FLT_NEAR(0.0f, tm_get_f32(a, 1), 0.01f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(a, 2), 0.01f); ASSERT_FLT_NEAR(2.0f, tm_get_f32(a, 3), 0.01f); /* Compute loss = sum(a), backward */ tm_tensor* loss = tm_sum(a, -1, false); tm_backward(loss); /* grad should be [0, 0, 1, 1] */ ASSERT_NOT_NULL(a->grad); ASSERT_FLT_NEAR(0.0f, tm_get_f32(a->grad, 0), 0.01f); ASSERT_FLT_NEAR(0.0f, tm_get_f32(a->grad, 1), 0.01f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(a->grad, 2), 0.01f); ASSERT_FLT_NEAR(1.0f, tm_get_f32(a->grad, 3), 0.01f); tm_free(loss); tm_free(a); } static void test_inplace_sigmoid_grad(void) { /* Test tm_sigmoid_ with autograd */ int64_t shape[] = {2}; tm_tensor* a = tm_full(shape, 1, TM_F32, 0.0f); tm_set_requires_grad(a, true); /* sigmoid(0) = 0.5 */ tm_sigmoid_(a); ASSERT_FLT_NEAR(0.5f, tm_get_f32(a, 0), 0.01f); /* Compute loss = sum(a), backward */ tm_tensor* loss = tm_sum(a, -1, false); tm_backward(loss); /* grad should be 0.5 * (1 - 0.5) = 0.25 */ ASSERT_NOT_NULL(a->grad); ASSERT_FLT_NEAR(0.25f, tm_get_f32(a->grad, 0), 0.01f); tm_free(loss); tm_free(a); } /* ============================================================= */ /* Additional NN Activation Tests */ /* ============================================================= */ static void test_mish(void) { int64_t shape[] = {4}; tm_tensor* t = tm_full(shape, 1, TM_F32, 1.0f); tm_tensor* out = tm_mish(t); ASSERT_NOT_NULL(out); /* mish(1) = 1 * tanh(softplus(1)) = 1 * tanh(ln(1+e)) ≈ 0.865 */ ASSERT_FLT_NEAR(0.865f, tm_get_f32(out, 0), 0.01f); tm_free(out); tm_free(t); } static void test_selu(void) { tm_tensor* t = tm_arange(-2.0, 2.0, 1.0, TM_F32); tm_tensor* out = tm_selu(t); ASSERT_NOT_NULL(out); /* selu(1) = scale * 1 ≈ 1.0507 */ ASSERT_FLT_NEAR(1.0507f, tm_get_f32(out, 3), 0.01f); /* selu(-1) = scale * alpha * (exp(-1) - 1) ≈ -1.111 */ ASSERT_TRUE(tm_get_f32(out, 1) < 0.0f); tm_free(out); tm_free(t); } static void test_celu(void) { tm_tensor* t = tm_arange(-2.0, 2.0, 1.0, TM_F32); tm_tensor* out = tm_celu(t, 1.0f); ASSERT_NOT_NULL(out); /* celu(1, 1) = 1 */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 3), 0.01f); /* celu(-1, 1) = 1*(exp(-1) - 1) ≈ -0.632 */ ASSERT_FLT_NEAR(-0.632f, tm_get_f32(out, 1), 0.01f); tm_free(out); tm_free(t); } static void test_relu6(void) { /* tm_arange(-2, 8, 2.5) -> [-2, 0.5, 3, 5.5] (4 elements) */ tm_tensor* t = tm_arange(-2.0, 10.0, 3.0, TM_F32); /* [-2, 1, 4, 7] */ tm_tensor* out = tm_relu6(t); ASSERT_NOT_NULL(out); /* relu6(-2) = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 0), 0.01f); /* relu6(1) = 1 */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 1), 0.01f); /* relu6(4) = 4 */ ASSERT_FLT_NEAR(4.0f, tm_get_f32(out, 2), 0.01f); /* relu6(7) = 6 (capped) */ ASSERT_FLT_NEAR(6.0f, tm_get_f32(out, 3), 0.01f); tm_free(out); tm_free(t); } static void test_glu(void) { /* Create tensor [1, 2, 3, 4] and split along last dim */ tm_tensor* t = tm_arange(1.0, 5.0, 1.0, TM_F32); tm_tensor* out = tm_glu(t, 0); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(2, (int)out->numel); /* a = [1, 2], b = [3, 4] -> a * sigmoid(b) */ /* out[0] = 1 * sigmoid(3) ≈ 0.952 */ ASSERT_FLT_NEAR(0.952f, tm_get_f32(out, 0), 0.01f); tm_free(out); tm_free(t); } static void test_softsign(void) { tm_tensor* t = tm_arange(-1.0, 2.0, 1.0, TM_F32); tm_tensor* out = tm_softsign(t); ASSERT_NOT_NULL(out); /* softsign(-1) = -1/(1+1) = -0.5 */ ASSERT_FLT_NEAR(-0.5f, tm_get_f32(out, 0), 0.01f); /* softsign(0) = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 1), 0.01f); /* softsign(1) = 1/(1+1) = 0.5 */ ASSERT_FLT_NEAR(0.5f, tm_get_f32(out, 2), 0.01f); tm_free(out); tm_free(t); } static void test_tanhshrink(void) { tm_tensor* t = tm_arange(-1.0, 2.0, 1.0, TM_F32); tm_tensor* out = tm_tanhshrink(t); ASSERT_NOT_NULL(out); /* tanhshrink(0) = 0 - tanh(0) = 0 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 1), 0.01f); /* tanhshrink(1) = 1 - tanh(1) ≈ 0.238 */ ASSERT_FLT_NEAR(0.238f, tm_get_f32(out, 2), 0.01f); tm_free(out); tm_free(t); } static void test_hardsigmoid(void) { tm_tensor* t = tm_arange(-4.0, 5.0, 2.0, TM_F32); /* -4, -2, 0, 2, 4 */ tm_tensor* out = tm_hardsigmoid(t); ASSERT_NOT_NULL(out); /* hardsigmoid(-4) = 0 (clamped) */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out, 0), 0.01f); /* hardsigmoid(0) = 0/6 + 0.5 = 0.5 */ ASSERT_FLT_NEAR(0.5f, tm_get_f32(out, 2), 0.01f); /* hardsigmoid(4) = 1 (clamped) */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 4), 0.01f); tm_free(out); tm_free(t); } static void test_threshold_fn(void) { tm_tensor* t = tm_arange(0.0, 4.0, 1.0, TM_F32); tm_tensor* out = tm_threshold(t, 2.0f, -1.0f); ASSERT_NOT_NULL(out); /* threshold(0, 2, -1) = -1 (0 <= 2) */ ASSERT_FLT_NEAR(-1.0f, tm_get_f32(out, 0), 0.01f); /* threshold(2, 2, -1) = -1 (2 <= 2) */ ASSERT_FLT_NEAR(-1.0f, tm_get_f32(out, 2), 0.01f); /* threshold(3, 2, -1) = 3 (3 > 2) */ ASSERT_FLT_NEAR(3.0f, tm_get_f32(out, 3), 0.01f); tm_free(out); tm_free(t); } static void test_pad_fn(void) { /* Test 1D padding */ tm_tensor* t1 = tm_arange(1.0, 4.0, 1.0, TM_F32); /* [1, 2, 3] */ int64_t padding1[] = {1, 2}; /* pad left=1, right=2 */ tm_tensor* out1 = tm_pad(t1, padding1, 2, 0.0f); ASSERT_NOT_NULL(out1); ASSERT_INT_EQ(6, (int)out1->numel); /* 3 + 1 + 2 = 6 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out1, 0), 0.01f); /* pad */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out1, 1), 0.01f); /* original */ ASSERT_FLT_NEAR(3.0f, tm_get_f32(out1, 3), 0.01f); /* original */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(out1, 5), 0.01f); /* pad */ tm_free(out1); tm_free(t1); } static void test_normalize_fn(void) { /* Test with 2D tensor, normalize along dim 1 */ int64_t shape[] = {2, 3}; tm_tensor* t = tm_full(shape, 2, TM_F32, 3.0f); /* [[3,3,3], [3,3,3]] */ tm_tensor* out = tm_normalize(t, 2.0f, 1, 1e-12f); ASSERT_NOT_NULL(out); /* L2 norm of [3,3,3] = sqrt(27) ≈ 5.196 */ /* normalized = 3 / 5.196 ≈ 0.577 */ ASSERT_FLT_NEAR(0.577f, tm_get_f32(out, 0), 0.01f); tm_free(out); tm_free(t); } static void test_interpolate_fn(void) { /* Test 4D tensor [1, 1, 2, 2] -> [1, 1, 4, 4] */ int64_t shape[] = {1, 1, 2, 2}; tm_tensor* t = tm_arange(1.0, 5.0, 1.0, TM_F32); t = tm_reshape(t, shape, 4); int64_t size[] = {4, 4}; tm_tensor* out = tm_interpolate(t, size, 2, "nearest"); ASSERT_NOT_NULL(out); ASSERT_INT_EQ(4, (int)out->shape[2]); ASSERT_INT_EQ(4, (int)out->shape[3]); /* Nearest neighbor: should replicate values */ ASSERT_FLT_NEAR(1.0f, tm_get_f32(out, 0), 0.01f); /* top-left */ tm_free(out); tm_free(t); } /* ============================================================= */ /* Sparse Tensor Tests */ /* ============================================================= */ static void test_sparse_coo_creation(void) { /* Create a 3x3 sparse matrix with 3 non-zeros */ int64_t idx_shape[] = {2, 3}; tm_tensor* indices = tm_empty(idx_shape, 2, TM_I64); int64_t* idx_data = (int64_t*)indices->data; /* Row indices */ idx_data[0] = 0; idx_data[1] = 1; idx_data[2] = 2; /* Col indices */ idx_data[3] = 0; idx_data[4] = 1; idx_data[5] = 2; int64_t val_shape[] = {3}; tm_tensor* values = tm_full(val_shape, 1, TM_F32, 1.0f); int64_t sparse_shape[] = {3, 3}; tm_tensor* sparse = tm_sparse_coo(indices, values, sparse_shape, 2); ASSERT_NOT_NULL(sparse); ASSERT_TRUE(tm_is_sparse(sparse)); ASSERT_INT_EQ(3, (int)tm_sparse_nnz(sparse)); ASSERT_INT_EQ(TM_SPARSE_COO, tm_get_sparse_format(sparse)); tm_free(sparse); tm_free(indices); tm_free(values); } static void test_sparse_csr_creation(void) { /* Create CSR representation of 3x3 identity matrix */ int64_t rp_shape[] = {4}; tm_tensor* row_ptr = tm_empty(rp_shape, 1, TM_I64); int64_t* rp_data = (int64_t*)row_ptr->data; rp_data[0] = 0; rp_data[1] = 1; rp_data[2] = 2; rp_data[3] = 3; int64_t ci_shape[] = {3}; tm_tensor* col_ind = tm_empty(ci_shape, 1, TM_I64); int64_t* ci_data = (int64_t*)col_ind->data; ci_data[0] = 0; ci_data[1] = 1; ci_data[2] = 2; int64_t val_shape[] = {3}; tm_tensor* values = tm_full(val_shape, 1, TM_F32, 1.0f); int64_t sparse_shape[] = {3, 3}; tm_tensor* sparse = tm_sparse_csr(row_ptr, col_ind, values, sparse_shape, 2); ASSERT_NOT_NULL(sparse); ASSERT_TRUE(tm_is_sparse(sparse)); ASSERT_INT_EQ(3, (int)tm_sparse_nnz(sparse)); ASSERT_INT_EQ(TM_SPARSE_CSR, tm_get_sparse_format(sparse)); tm_free(sparse); tm_free(row_ptr); tm_free(col_ind); tm_free(values); } static void test_dense_to_sparse_coo(void) { /* Create dense identity matrix */ tm_tensor* dense = tm_eye(3, TM_F32); /* Convert to sparse */ tm_tensor* sparse = tm_to_sparse(dense); ASSERT_NOT_NULL(sparse); ASSERT_TRUE(tm_is_sparse(sparse)); ASSERT_INT_EQ(3, (int)tm_sparse_nnz(sparse)); tm_free(sparse); tm_free(dense); } static void test_dense_to_sparse_csr(void) { /* Create dense identity matrix */ tm_tensor* dense = tm_eye(3, TM_F32); /* Convert to sparse CSR */ tm_tensor* sparse = tm_to_sparse_csr(dense); ASSERT_NOT_NULL(sparse); ASSERT_TRUE(tm_is_sparse(sparse)); ASSERT_INT_EQ(3, (int)tm_sparse_nnz(sparse)); ASSERT_INT_EQ(TM_SPARSE_CSR, tm_get_sparse_format(sparse)); tm_free(sparse); tm_free(dense); } static void test_sparse_to_dense(void) { /* Create dense identity matrix */ tm_tensor* dense = tm_eye(3, TM_F32); /* Convert to sparse and back */ tm_tensor* sparse = tm_to_sparse(dense); tm_tensor* restored = tm_to_dense(sparse); ASSERT_NOT_NULL(restored); ASSERT_FALSE(tm_is_sparse(restored)); /* Check values match */ for (int64_t i = 0; i < 9; i++) { ASSERT_FLT_NEAR(tm_get_f32(dense, i), tm_get_f32(restored, i), 0.0001f); } tm_free(restored); tm_free(sparse); tm_free(dense); } static void test_sparse_spmm(void) { /* Create sparse identity matrix */ tm_tensor* dense_eye = tm_eye(3, TM_F32); tm_tensor* sparse = tm_to_sparse(dense_eye); /* Create dense matrix to multiply */ int64_t shape[] = {3, 2}; tm_tensor* dense = tm_randn(shape, 2, TM_F32); /* SpMM: I @ dense = dense */ tm_tensor* result = tm_spmm(sparse, dense); ASSERT_NOT_NULL(result); ASSERT_INT_EQ(3, (int)result->shape[0]); ASSERT_INT_EQ(2, (int)result->shape[1]); /* Result should equal dense (identity times dense) */ for (int64_t i = 0; i < 6; i++) { ASSERT_FLT_NEAR(tm_get_f32(dense, i), tm_get_f32(result, i), 0.0001f); } tm_free(result); tm_free(dense); tm_free(sparse); tm_free(dense_eye); } static void test_sparse_spmv(void) { /* Create sparse identity matrix */ tm_tensor* dense_eye = tm_eye(3, TM_F32); tm_tensor* sparse = tm_to_sparse(dense_eye); /* Create vector */ int64_t shape[] = {3}; tm_tensor* vec = tm_full(shape, 1, TM_F32, 2.0f); /* SpMV: I @ vec = vec */ tm_tensor* result = tm_spmv(sparse, vec); ASSERT_NOT_NULL(result); ASSERT_INT_EQ(3, (int)result->numel); for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_NEAR(2.0f, tm_get_f32(result, i), 0.0001f); } tm_free(result); tm_free(vec); tm_free(sparse); tm_free(dense_eye); } static void test_sparse_mul_scalar(void) { /* Create sparse matrix */ tm_tensor* dense = tm_eye(3, TM_F32); tm_tensor* sparse = tm_to_sparse(dense); /* Multiply by scalar */ tm_tensor* scaled = tm_sparse_mul_scalar(sparse, 2.0); ASSERT_NOT_NULL(scaled); ASSERT_TRUE(tm_is_sparse(scaled)); /* Convert back and check values */ tm_tensor* result = tm_to_dense(scaled); ASSERT_FLT_NEAR(2.0f, tm_get_f32(result, 0), 0.0001f); /* [0,0] = 2 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(result, 1), 0.0001f); /* [0,1] = 0 */ ASSERT_FLT_NEAR(2.0f, tm_get_f32(result, 4), 0.0001f); /* [1,1] = 2 */ tm_free(result); tm_free(scaled); tm_free(sparse); tm_free(dense); } static void test_sparse_format_conversion(void) { /* Create COO sparse matrix */ tm_tensor* dense = tm_eye(3, TM_F32); tm_tensor* coo = tm_to_sparse(dense); /* Convert to CSR */ tm_tensor* csr = tm_sparse_coo_to_csr(coo); ASSERT_NOT_NULL(csr); ASSERT_INT_EQ(TM_SPARSE_CSR, tm_get_sparse_format(csr)); /* Convert back to COO */ tm_tensor* coo2 = tm_sparse_csr_to_coo(csr); ASSERT_NOT_NULL(coo2); ASSERT_INT_EQ(TM_SPARSE_COO, tm_get_sparse_format(coo2)); /* Check values are preserved */ tm_tensor* result = tm_to_dense(coo2); for (int64_t i = 0; i < 9; i++) { ASSERT_FLT_NEAR(tm_get_f32(dense, i), tm_get_f32(result, i), 0.0001f); } tm_free(result); tm_free(coo2); tm_free(csr); tm_free(coo); tm_free(dense); } static void test_sparse_add(void) { /* Create two sparse matrices */ tm_tensor* a_dense = tm_eye(3, TM_F32); tm_tensor* b_dense = tm_eye(3, TM_F32); tm_tensor* a = tm_to_sparse(a_dense); tm_tensor* b = tm_to_sparse(b_dense); /* Add them: I + I = 2*I */ tm_tensor* sum = tm_sparse_add(a, b); ASSERT_NOT_NULL(sum); ASSERT_TRUE(tm_is_sparse(sum)); /* Convert to dense and check */ tm_tensor* result = tm_to_dense(sum); ASSERT_FLT_NEAR(2.0f, tm_get_f32(result, 0), 0.0001f); /* [0,0] = 2 */ ASSERT_FLT_NEAR(0.0f, tm_get_f32(result, 1), 0.0001f); /* [0,1] = 0 */ ASSERT_FLT_NEAR(2.0f, tm_get_f32(result, 4), 0.0001f); /* [1,1] = 2 */ tm_free(result); tm_free(sum); tm_free(b); tm_free(a); tm_free(b_dense); tm_free(a_dense); } static void test_sparse_values_indices(void) { /* Create sparse matrix */ tm_tensor* dense = tm_eye(3, TM_F32); tm_tensor* sparse = tm_to_sparse(dense); /* Get values */ tm_tensor* values = tm_sparse_values(sparse); ASSERT_NOT_NULL(values); ASSERT_INT_EQ(3, (int)values->numel); /* All values should be 1.0 */ for (int64_t i = 0; i < 3; i++) { ASSERT_FLT_NEAR(1.0f, tm_get_f32(values, i), 0.0001f); } /* Get indices */ tm_tensor* indices = tm_sparse_indices(sparse); ASSERT_NOT_NULL(indices); ASSERT_INT_EQ(2, indices->ndim); ASSERT_INT_EQ(2, (int)indices->shape[0]); ASSERT_INT_EQ(3, (int)indices->shape[1]); tm_free(indices); tm_free(values); tm_free(sparse); tm_free(dense); } static void test_sparse_coalesce(void) { /* Create sparse matrix and coalesce (should just work) */ tm_tensor* dense = tm_eye(3, TM_F32); tm_tensor* sparse = tm_to_sparse(dense); tm_tensor* coalesced = tm_sparse_coalesce(sparse); ASSERT_NOT_NULL(coalesced); ASSERT_TRUE(tm_is_sparse(coalesced)); ASSERT_INT_EQ(3, (int)tm_sparse_nnz(coalesced)); tm_free(coalesced); tm_free(sparse); tm_free(dense); } /* ============================================================= */ /* Main */ /* ============================================================= */ int main(void) { printf("\n Running tensor tests...\n\n"); /* Creation tests */ RUN_TEST(zeros_creates_tensor); RUN_TEST(ones_creates_tensor); RUN_TEST(full_creates_tensor); RUN_TEST(randn_creates_tensor); RUN_TEST(clone_creates_copy); /* Accessor tests */ RUN_TEST(get_set_f32); RUN_TEST(data_pointer); /* Shape tests */ RUN_TEST(reshape); RUN_TEST(transpose); RUN_TEST(is_contiguous); /* Element-wise tests */ RUN_TEST(add); RUN_TEST(sub); RUN_TEST(mul); RUN_TEST(mul_scalar); RUN_TEST(exp); RUN_TEST(relu); /* Matrix tests */ RUN_TEST(matmul_2d); RUN_TEST(matmul_different_shapes); /* Reduction tests */ RUN_TEST(sum_all); RUN_TEST(mean_all); /* NN op tests */ RUN_TEST(softmax); RUN_TEST(layer_norm); /* Autograd tests */ RUN_TEST(autograd_add); RUN_TEST(autograd_mul); RUN_TEST(no_grad_context); /* Edge case tests */ RUN_TEST(null_safety); RUN_TEST(scalar_tensor); /* Broadcasting tests */ RUN_TEST(broadcast_add_1d_to_2d); RUN_TEST(broadcast_mul_scalar_like); RUN_TEST(broadcast_sub); RUN_TEST(broadcast_div); RUN_TEST(broadcast_backward); /* P0 Feature tests */ RUN_TEST(arange); RUN_TEST(cat); RUN_TEST(stack); RUN_TEST(split); RUN_TEST(permute); RUN_TEST(flatten); RUN_TEST(select); RUN_TEST(silu); RUN_TEST(rms_norm); RUN_TEST(linear); RUN_TEST(triu); RUN_TEST(sin_cos); RUN_TEST(clamp); RUN_TEST(argmax); RUN_TEST(logsumexp); RUN_TEST(abs); RUN_TEST(pow); RUN_TEST(where); RUN_TEST(masked_fill); RUN_TEST(eye); RUN_TEST(rsqrt); RUN_TEST(sub_div_scalar); RUN_TEST(inplace_ops); /* P1/P2 Feature tests */ RUN_TEST(comparison_ops); RUN_TEST(maximum_minimum); RUN_TEST(leaky_relu); RUN_TEST(gather); RUN_TEST(index_select); RUN_TEST(narrow); RUN_TEST(expand); RUN_TEST(repeat); RUN_TEST(cumsum); RUN_TEST(var_std); RUN_TEST(prod); RUN_TEST(type_conversion); RUN_TEST(chunk); RUN_TEST(isnan_isinf); /* Full parity tests */ RUN_TEST(mm); RUN_TEST(dot); RUN_TEST(mv); RUN_TEST(outer); RUN_TEST(floor_ceil_round); RUN_TEST(logical_ops); RUN_TEST(flip_roll); RUN_TEST(extra_inplace_ops); RUN_TEST(equal_allclose); RUN_TEST(like_functions); RUN_TEST(cumprod); RUN_TEST(any_all); RUN_TEST(norm); RUN_TEST(elu); RUN_TEST(trig_functions); /* Additional NN ops tests */ RUN_TEST(hardswish); RUN_TEST(batch_norm); RUN_TEST(group_norm); RUN_TEST(instance_norm); RUN_TEST(scaled_dot_product_attention); RUN_TEST(split_merge_heads); RUN_TEST(conv1d); RUN_TEST(conv2d); RUN_TEST(pooling); RUN_TEST(adaptive_pooling); /* Loss function tests */ RUN_TEST(l1_loss); RUN_TEST(smooth_l1_loss); RUN_TEST(nll_loss); RUN_TEST(kl_div); RUN_TEST(binary_cross_entropy); RUN_TEST(half_conversion); /* Additional loss function tests */ RUN_TEST(cosine_embedding_loss); RUN_TEST(triplet_margin_loss); RUN_TEST(label_smoothing); /* Linear algebra tests */ RUN_TEST(inverse); RUN_TEST(det); RUN_TEST(svd); RUN_TEST(eig); /* Autograd extension tests */ RUN_TEST(retain_grad); RUN_TEST(explicit_grad); /* Higher-order gradient tests */ RUN_TEST(hessian); RUN_TEST(hvp); /* Custom autograd tests */ RUN_TEST(custom_autograd); /* In-place gradient tests */ RUN_TEST(inplace_add_grad); RUN_TEST(inplace_mul_grad); RUN_TEST(inplace_exp_grad); RUN_TEST(inplace_relu_grad); RUN_TEST(inplace_sigmoid_grad); /* Additional NN activation tests */ RUN_TEST(mish); RUN_TEST(selu); RUN_TEST(celu); RUN_TEST(relu6); RUN_TEST(glu); RUN_TEST(softsign); RUN_TEST(tanhshrink); RUN_TEST(hardsigmoid); RUN_TEST(threshold_fn); RUN_TEST(pad_fn); RUN_TEST(normalize_fn); RUN_TEST(interpolate_fn); /* Sparse tensor tests */ RUN_TEST(sparse_coo_creation); RUN_TEST(sparse_csr_creation); RUN_TEST(dense_to_sparse_coo); RUN_TEST(dense_to_sparse_csr); RUN_TEST(sparse_to_dense); RUN_TEST(sparse_spmm); RUN_TEST(sparse_spmv); RUN_TEST(sparse_mul_scalar); RUN_TEST(sparse_format_conversion); RUN_TEST(sparse_add); RUN_TEST(sparse_values_indices); RUN_TEST(sparse_coalesce); TEST_REPORT(); return __ft_failed > 0 ? 1 : 0; }