Foundry/tensor/tests/test_tensor.c

3391 lines
93 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
#include "../../lib/test.h"
#include "../tensor.h"
#include <math.h>
/* ============================================================= */
/* 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;
}