diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cc00867 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Test Build + +on: + push: + branches: [ main, master, dev ] + pull_request: + branches: [ main, master, dev ] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install cffi + + - name: Build C extension (Linux/macOS) + if: matrix.os != 'windows-latest' + run: | + cd tensor/src + gcc -O3 -shared -fPIC -o ../_tensor1d.so tensor1d.c -lm + + - name: Build C extension (Windows) + if: matrix.os == 'windows-latest' + run: | + cd tensor/src + gcc -O3 -shared -o ../_tensor1d.dll tensor1d.c + + - name: Test basic functionality + run: | + python -c " + import sys + sys.path.insert(0, '.') + from tensor.tensor1d import Tensor + t = Tensor.arange(5) + print('Basic test passed:', t) + " + env: + PYTHONIOENCODING: utf-8 + + - name: Run tests + run: | + python tensor/tests/test_simple.py + env: + PYTHONPATH: . + PYTHONIOENCODING: utf-8 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..499bf29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +# IDE and Editor files +.vscode/ +.idea/ + +# Virtual environments +.venv/ +venv/ +env/ +uv.lock + +# Python cache and compiled files +__pycache__/ +*.pyc +*.py[cod] +*$py.class + +# Environment variables +.env + +# System files +.DS_Store +.AppleDouble +.LSOverride +._* + +# Build artifacts +dist/ +build/ +*.egg-info/ +*.log + +# Backup files +*.bak + +# C extensions and build artifacts +*.so +*.dylib +*.dll +*.o +*.a +*.out +tensor1d +libtensor1d.so +_tensor1d* + +# Test and coverage files +.pytest_cache/ +.coverage +htmlcov/ +.hypothesis/ +coverage.xml +*.cover + +# Python version management +.python-version + +# Package management +.pdm.toml +poetry.lock + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json diff --git a/Makefile b/Makefile index 9d9bc14..3bcfe97 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,27 @@ -CC = gcc +# Cross-platform build settings +ifeq ($(OS),Windows_NT) + CC = gcc + SHARED_EXT = .dll + SHARED_FLAGS = -shared + LDFLAGS = +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + CC = gcc + SHARED_EXT = .so + SHARED_FLAGS = -shared -fPIC + LDFLAGS = -lm + endif + ifeq ($(UNAME_S),Darwin) + CC = gcc + SHARED_EXT = .so + SHARED_FLAGS = -shared -fPIC + LDFLAGS = -lm + endif +endif + CFLAGS = -Wall -O3 -LDFLAGS = -lm +PYTHON = python3 # turn on all the warnings # https://github.com/mcinglis/c-style @@ -9,23 +30,89 @@ CFLAGS += -Wall -Wextra -Wpedantic \ -Wwrite-strings -Wstrict-prototypes -Wold-style-definition \ -Wredundant-decls -Wnested-externs -Wmissing-include-dirs +# Source files +SRC_DIR = tensor/src +C_FILES = $(SRC_DIR)/tensor1d.c +H_FILES = $(SRC_DIR)/tensor1d.h + # Main targets all: tensor1d libtensor1d.so # Compile the main executable -tensor1d: tensor1d.c tensor1d.h - $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) +tensor1d: $(C_FILES) $(H_FILES) + cd $(SRC_DIR) && $(CC) $(CFLAGS) -o ../../$@ tensor1d.c $(LDFLAGS) # Create shared library -libtensor1d.so: tensor1d.c tensor1d.h - $(CC) $(CFLAGS) -shared -fPIC -o $@ $< $(LDFLAGS) +libtensor1d$(SHARED_EXT): $(C_FILES) $(H_FILES) + cd $(SRC_DIR) && $(CC) $(CFLAGS) $(SHARED_FLAGS) -o ../../libtensor1d$(SHARED_EXT) tensor1d.c $(LDFLAGS) + +# Build CFFI extension +cffi: $(C_FILES) $(H_FILES) + cd $(SRC_DIR) && $(PYTHON) build_cffi.py + +# Build Python package +build: cffi + $(PYTHON) -m build + +# Install package in development mode +install-dev: + $(PYTHON) -m pip install -e .[dev] + +# Install package +install: + $(PYTHON) -m pip install . + +# Test using pytest +test: + $(PYTHON) -m pytest tensor/tests/ -v + +# Run tests with coverage +test-cov: + $(PYTHON) -m pytest tensor/tests/ --cov=tensor --cov-report=html --cov-report=term + +# Format code +format: + $(PYTHON) -m black tensor/ + $(PYTHON) -m isort tensor/ + +# Type checking +typecheck: + $(PYTHON) -m mypy tensor/ + +# Lint code +lint: format typecheck # Clean up build artifacts clean: rm -f tensor1d libtensor1d.so + rm -rf build/ dist/ *.egg-info/ + rm -rf tensor/__pycache__/ tensor/tests/__pycache__/ + rm -rf .pytest_cache/ .coverage htmlcov/ + find . -name "*.pyc" -delete + find . -name "*.so" -delete + find . -name "_tensor1d*" -delete -# Test using pytest -test: - pytest +# Clean everything including virtual environments +clean-all: clean + rm -rf .venv/ venv/ + +# Help +help: + @echo "Available targets:" + @echo " all - Build C executable and shared library" + @echo " tensor1d - Build C executable" + @echo " libtensor1d.so - Build shared library" + @echo " cffi - Build CFFI extension" + @echo " build - Build Python package" + @echo " install-dev - Install package in development mode" + @echo " install - Install package" + @echo " test - Run tests" + @echo " test-cov - Run tests with coverage" + @echo " format - Format code with black and isort" + @echo " typecheck - Run mypy type checking" + @echo " lint - Run formatting and type checking" + @echo " clean - Clean build artifacts" + @echo " clean-all - Clean everything including virtual environments" + @echo " help - Show this help message" -.PHONY: all clean test tensor1d +.PHONY: all clean clean-all test test-cov tensor1d cffi build install-dev install format typecheck lint help diff --git a/README.md b/README.md index 1288cc6..acfad3b 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,70 @@ -# tensor +# Tensor Library + +[![Test Build](https://github.com/SermetPekin/tensor/actions/workflows/test.yml/badge.svg?1)](https://github.com/SermetPekin/tensor/actions/workflows/test.yml?1) + +A simple 1D tensor library with C extensions for high-performance computing, inspired by PyTorch. In this module we build a small `Tensor` in C, along the lines of `torch.Tensor` or `numpy.ndarray`. The current code implements a simple 1-dimensional float tensor that we can access and slice. We get to see that the tensor object maintains both a `Storage` that holds the 1-dimensional data as it is in physical memory, and a `View` over that memory that has some start, end, and stride. This allows us to efficiently slice into a Tensor without creating any additional memory, because the `Storage` is re-used, while the `View` is updated to reflect the new start, end, and stride. We then get to see how we can wrap our C tensor into a Python module, just like PyTorch and numpy do. -The source code of the 1D Tensor is in [tensor1d.h](tensor1d.h) and [tensor1d.c](tensor1d.c). You can compile and run this simply as: +## Building and Testing + +The source code of the 1D Tensor is in [tensor/src/tensor1d.h](tensor/src/tensor1d.h) and [tensor/src/tensor1d.c](tensor/src/tensor1d.c). + +### Build C extension: + +```bash +# Linux/macOS +cd tensor/src +gcc -O3 -shared -fPIC -o ../_tensor1d.so tensor1d.c -lm + +# Windows +cd tensor/src +gcc -O3 -shared -o ../_tensor1d.dll tensor1d.c +``` + +### Test: +```bash +python tensor/tests/test_simple.py +``` + +## Usage + +```python +from tensor.tensor1d import Tensor + +# Create tensors +t1 = Tensor.empty(5) # [0.0, 0.0, 0.0, 0.0, 0.0] +t2 = Tensor.arange(5) # [0.0, 1.0, 2.0, 3.0, 4.0] + +# Indexing and slicing +print(t2[0]) # [0.0] +print(t2[1:4]) # [1.0, 2.0, 3.0] +print(t2[::2]) # [0.0, 2.0, 4.0] + +# Arithmetic +t3 = t2.addf(5.0) # [5.0, 6.0, 7.0, 8.0, 9.0] +t4 = t2.add(t2) # [0.0, 2.0, 4.0, 6.0, 8.0] +``` + +### Standalone C program: + +You can also compile and run the C code directly: ```bash -gcc -Wall -O3 tensor1d.c -o tensor1d +cd tensor/src +gcc -Wall -O3 tensor1d.c -o tensor1d -lm ./tensor1d ``` +## Authors + +- **Andrej Karpathy** - Original C implementation +- **SermetPekin** - Python packaging and cross-platform support + +## License + +MIT + The code contains both the `Tensor` class, and also a short `int main` that just has a toy example. We can now wrap up this C code into a Python module so we can access it there. For that, compile it as a shared library: ```bash @@ -64,4 +120,4 @@ Good related resources: ### License -MIT \ No newline at end of file +MIT diff --git a/build_cffi.py b/build_cffi.py new file mode 100644 index 0000000..0ef8fb3 --- /dev/null +++ b/build_cffi.py @@ -0,0 +1,69 @@ +""" +Cross-platform CFFI build configuration for tensor package. +""" + +import os +from cffi import FFI + +def create_ffibuilder(): + """Create CFFI builder for cross-platform compilation.""" + + ffibuilder = FFI() + + # Define the C interface + ffibuilder.cdef(""" + typedef struct { + float* data; + int data_size; + int ref_count; + } Storage; + + typedef struct { + Storage* storage; + int offset; + int size; + int stride; + char* repr; + } Tensor; + + Tensor* tensor_empty(int size); + int logical_to_physical(Tensor *t, int ix); + float tensor_getitem(Tensor* t, int ix); + Tensor* tensor_getitem_astensor(Tensor* t, int ix); + float tensor_item(Tensor* t); + void tensor_setitem(Tensor* t, int ix, float val); + Tensor* tensor_arange(int size); + char* tensor_to_string(Tensor* t); + void tensor_print(Tensor* t); + Tensor* tensor_slice(Tensor* t, int start, int end, int step); + Tensor* tensor_addf(Tensor* t, float val); + Tensor* tensor_add(Tensor* t1, Tensor* t2); + void tensor_incref(Tensor* t); + void tensor_decref(Tensor* t); + void tensor_free(Tensor* t); + """) + + # Platform-specific library linking + libraries = [] + if os.name != 'nt': # Unix-like systems (Linux, macOS) + libraries = ["m"] # Math library + + # Set the source + ffibuilder.set_source( + "tensor._tensor1d", + """ + #include "tensor1d.h" + """, + sources=[os.path.join("tensor", "src", "tensor1d.c")], + include_dirs=[os.path.join("tensor", "src")], + libraries=libraries, + extra_compile_args=["-O3", "-Wall"] if os.name != 'nt' else ["/O2"], + ) + + return ffibuilder + +# This is used by setuptools when building +ffibuilder = create_ffibuilder() + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bf36279 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,104 @@ +[build-system] +requires = ["setuptools>=61", "wheel", "cffi>=2.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "tensor" +version = "0.1.0" +description = "A tensor library with C extensions for high-performance computing" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +authors = [ + {name = "Andrej Karpathy"}, + {name = "SermetPekin", email = "sermet.pekin@gmail.com"} +] +keywords = ["tensor", "numpy", "c-extensions", "performance"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: C", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "cffi>=2.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.4.2", + "black", + "isort", +] +test = [ + "pytest>=8.4.2", + "pytest-cov", +] + +[project.urls] +Homepage = "https://github.com/SermetPekin/tensor" +Repository = "https://github.com/SermetPekin/tensor" +Issues = "https://github.com/SermetPekin/tensor/issues" + +[tool.setuptools] +packages = ["tensor"] + +[tool.setuptools.package-data] +tensor = ["*.so", "src/*.c", "src/*.h"] + +[tool.pytest.ini_options] +testpaths = ["tensor/tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" + +[tool.black] +line-length = 88 +target-version = ['py310'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +line_length = 88 + +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[dependency-groups] +dev = [ + "black>=25.9.0", + "isort>=7.0.0", + "pytest>=8.4.2", +] diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 6897444..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -torch diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6a88e4b..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -cffi diff --git a/tensor/__init__.py b/tensor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tensor/src/build_cffi.py b/tensor/src/build_cffi.py new file mode 100644 index 0000000..899ab67 --- /dev/null +++ b/tensor/src/build_cffi.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Build script for CFFI extension. +This script compiles the C extension for the tensor library. +""" + +import os +import sys +from cffi import FFI + +def build_extension(): + """Build the CFFI extension.""" + + # Get the directory containing this script + here = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname(here) + + # Setup CFFI + ffibuilder = FFI() + + # Define the C interface + cdef_content = """ + typedef struct { + float* data; + int data_size; + int ref_count; + } Storage; + + typedef struct { + Storage* storage; + int offset; + int size; + int stride; + char* repr; + } Tensor; + + Tensor* tensor_empty(int size); + int logical_to_physical(Tensor *t, int ix); + float tensor_getitem(Tensor* t, int ix); + Tensor* tensor_getitem_astensor(Tensor* t, int ix); + float tensor_item(Tensor* t); + void tensor_setitem(Tensor* t, int ix, float val); + Tensor* tensor_arange(int size); + char* tensor_to_string(Tensor* t); + void tensor_print(Tensor* t); + Tensor* tensor_slice(Tensor* t, int start, int end, int step); + Tensor* tensor_addf(Tensor* t, float val); + Tensor* tensor_add(Tensor* t1, Tensor* t2); + void tensor_incref(Tensor* t); + void tensor_decref(Tensor* t); + void tensor_free(Tensor* t); + """ + + ffibuilder.cdef(cdef_content) + + # Set the source + ffibuilder.set_source( + "_tensor1d", # Module name + """ + #include "tensor1d.h" + """, + sources=["tensor1d.c"], + include_dirs=["."], + libraries=["m"], # Link with math library + extra_compile_args=["-O3", "-Wall", "-Wextra"], + ) + + # Compile the extension + print("Building CFFI extension...") + ffibuilder.compile(verbose=True) + print("Extension built successfully!") + +if __name__ == "__main__": + build_extension() \ No newline at end of file diff --git a/tensor1d.c b/tensor/src/tensor1d.c similarity index 82% rename from tensor1d.c rename to tensor/src/tensor1d.c index 8ee3fe6..e3bf64e 100644 --- a/tensor1d.c +++ b/tensor/src/tensor1d.c @@ -233,19 +233,51 @@ char* tensor_to_string(Tensor* t) { // if we already have a string representation, return it if (t->repr != NULL) { return t->repr; } // otherwise create a new string representation - int max_size = t->size * 20 + 3; // 20 chars/number, brackets and commas + // Conservative estimate: each float can be up to 15 chars (e.g., "-123456789.1"), + // plus ", " separator (2 chars), plus brackets and null terminator + int max_size = t->size * 20 + 10; // More conservative buffer size t->repr = mallocCheck(max_size); char* current = t->repr; - current += sprintf(current, "["); + char* buffer_end = t->repr + max_size - 1; // Leave space for null terminator + + // Use snprintf for safer string operations + int written = snprintf(current, buffer_end - current, "["); + if (written > 0 && current + written < buffer_end) { + current += written; + } + for (int i = 0; i < t->size; i++) { float val = tensor_getitem(t, i); - current += sprintf(current, "%.1f", val); + + // Format the number + written = snprintf(current, buffer_end - current, "%.1f", val); + if (written > 0 && current + written < buffer_end) { + current += written; + } else { + break; // Buffer full, stop to prevent overflow + } + + // Add separator if not the last element if (i < t->size - 1) { - current += sprintf(current, ", "); + written = snprintf(current, buffer_end - current, ", "); + if (written > 0 && current + written < buffer_end) { + current += written; + } else { + break; // Buffer full, stop to prevent overflow + } } } - current += sprintf(current, "]"); - // ensure we didn't write past the end of the buffer + + // Add closing bracket + written = snprintf(current, buffer_end - current, "]"); + if (written > 0 && current + written < buffer_end) { + current += written; + } + + // Ensure null termination + *current = '\0'; + + // Verify we didn't overflow (this should not trigger with our conservative sizing) assert(current - t->repr < max_size); return t->repr; } @@ -255,6 +287,22 @@ void tensor_print(Tensor* t) { printf("%s\n", str); } +void tensor_incref(Tensor* t) { + if (t != NULL && t->storage != NULL) { + t->storage->ref_count++; + } +} + +void tensor_decref(Tensor* t) { + if (t != NULL && t->storage != NULL) { + t->storage->ref_count--; + if (t->storage->ref_count <= 0) { + free(t->storage->data); + free(t->storage); + } + } +} + void tensor_free(Tensor* t) { storage_decref(t->storage); free(t->repr); diff --git a/tensor1d.h b/tensor/src/tensor1d.h similarity index 100% rename from tensor1d.h rename to tensor/src/tensor1d.h diff --git a/tensor1d.py b/tensor/tensor1d.py similarity index 61% rename from tensor1d.py rename to tensor/tensor1d.py index 5b36239..af0f1e0 100644 --- a/tensor1d.py +++ b/tensor/tensor1d.py @@ -34,7 +34,53 @@ void tensor_decref(Tensor* t); void tensor_free(Tensor* t); """) -lib = ffi.dlopen("./libtensor1d.so") # Make sure to compile the C code into a shared library + +# Try different methods to load the CFFI extension +import os +lib = None + +try: + # Method 1: Import as a module (preferred) + from . import _tensor1d + lib = _tensor1d.lib + print("Loaded CFFI extension via module import") +except ImportError: + try: + # Method 2: Direct shared library loading (cross-platform) + import glob + current_dir = os.path.dirname(__file__) + # Look for CFFI extensions with different platforms extensions + patterns = [ + '_tensor1d*.so', # Linux/macOS + '_tensor1d*.pyd', # Windows + '_tensor1d*.dll', # Windows alternative + ] + + so_files = [] + for pattern in patterns: + so_files.extend(glob.glob(os.path.join(current_dir, pattern))) + + if so_files: + so_path = so_files[0] # Use the first match + lib = ffi.dlopen(so_path) + print(f"Loaded CFFI extension via direct loading: {os.path.basename(so_path)}") + else: + raise FileNotFoundError("No compiled extension found") + except (OSError, FileNotFoundError): + try: + # Method 3: Fallback to traditional shared library + lib = ffi.dlopen("./libtensor1d.so") + print("Loaded via traditional shared library") + except OSError: + try: + lib = ffi.dlopen("./tensor/src/libtensor1d.so") + print("Loaded via tensor/src shared library") + except OSError as e: + raise ImportError(f"Could not load tensor library: {e}") + +if lib is None: + raise ImportError("Failed to load tensor library") + # ----------------------------------------------------------------------------- class Tensor: @@ -109,6 +155,34 @@ def tolist(self): def item(self): return lib.tensor_item(self.tensor) + def addf(self, value): + """Add a float value to this tensor""" + c_tensor = lib.tensor_addf(self.tensor, float(value)) + if c_tensor == ffi.NULL: + raise ValueError("RuntimeError: tensor addf returned NULL") + return Tensor(c_tensor=c_tensor) + + def add(self, other): + """Add another tensor to this tensor""" + if isinstance(other, Tensor): + c_tensor = lib.tensor_add(self.tensor, other.tensor) + if c_tensor == ffi.NULL: + raise ValueError("RuntimeError: tensor add returned NULL") + return Tensor(c_tensor=c_tensor) + else: + raise TypeError("Invalid type for addition") + + @classmethod + def arange(cls, size): + """Create a tensor with values from 0 to size-1""" + c_tensor = lib.tensor_arange(size) + return cls(c_tensor=c_tensor) + + @classmethod + def empty(cls, size): + """Create an empty tensor of given size""" + return cls(size) + def empty(size): return Tensor(size) diff --git a/tensor/tests/__init__.py b/tensor/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tensor/tests/test_basic.py b/tensor/tests/test_basic.py new file mode 100644 index 0000000..e79f126 --- /dev/null +++ b/tensor/tests/test_basic.py @@ -0,0 +1,162 @@ +""" +Basic tests for tensor1d functionality without external dependencies. +""" +import pytest +import sys +import os + +# Add the project root to the path so we can import tensor +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from tensor.tensor1d import Tensor + + +class TestTensorBasic: + """Basic functionality tests that don't require external libraries.""" + + def test_import(self): + """Test that we can import the tensor module.""" + from tensor.tensor1d import Tensor + assert Tensor is not None + + def test_empty_tensor_creation(self): + """Test creating empty tensors of different sizes.""" + # Test different sizes + for size in [0, 1, 5, 10]: + t = Tensor.empty(size) + assert len(t) == size + if size > 0: + assert isinstance(t.tolist(), list) + assert len(t.tolist()) == size + + def test_arange_tensor_creation(self): + """Test creating range tensors.""" + # Test range tensor + t = Tensor.arange(5) + assert len(t) == 5 + values = t.tolist() + expected = [0.0, 1.0, 2.0, 3.0, 4.0] + assert values == expected + + def test_indexing(self): + """Test tensor indexing.""" + t = Tensor.arange(5) + + # Test positive indexing + assert t[0].item() == 0.0 + assert t[2].item() == 2.0 + assert t[4].item() == 4.0 + + # Test negative indexing + assert t[-1].item() == 4.0 + assert t[-2].item() == 3.0 + + def test_slicing(self): + """Test tensor slicing.""" + t = Tensor.arange(10) + + # Test basic slicing + s1 = t[2:5] + assert s1.tolist() == [2.0, 3.0, 4.0] + + # Test slicing with step + s2 = t[::2] + assert s2.tolist() == [0.0, 2.0, 4.0, 6.0, 8.0] + + # Test slicing with start, stop, step + s3 = t[1:8:2] + assert s3.tolist() == [1.0, 3.0, 5.0, 7.0] + + def test_arithmetic_addf(self): + """Test adding a float to a tensor.""" + t = Tensor.arange(3) # [0.0, 1.0, 2.0] + t_plus_5 = t.addf(5.0) + + expected = [5.0, 6.0, 7.0] + assert t_plus_5.tolist() == expected + + # Original tensor should be unchanged + assert t.tolist() == [0.0, 1.0, 2.0] + + def test_arithmetic_add_tensors(self): + """Test adding two tensors.""" + t1 = Tensor.arange(3) # [0.0, 1.0, 2.0] + t2 = Tensor.arange(3) # [0.0, 1.0, 2.0] + + result = t1.add(t2) + expected = [0.0, 2.0, 4.0] + assert result.tolist() == expected + + # Original tensors should be unchanged + assert t1.tolist() == [0.0, 1.0, 2.0] + assert t2.tolist() == [0.0, 1.0, 2.0] + + def test_string_representation(self): + """Test string representation of tensors.""" + t = Tensor.arange(3) + str_repr = str(t) + + # Should contain the values + assert "0.0" in str_repr + assert "1.0" in str_repr + assert "2.0" in str_repr + + def test_item_method(self): + """Test the item() method for single-element tensors.""" + # This should work for tensors with a single element + t = Tensor.arange(1) + assert t.item() == 0.0 + + def test_len_method(self): + """Test the len() function on tensors.""" + assert len(Tensor.empty(0)) == 0 + assert len(Tensor.empty(1)) == 1 + assert len(Tensor.arange(10)) == 10 + + def test_memory_management(self): + """Test that we can create and destroy many tensors without issues.""" + # This tests that memory management works correctly + for i in range(100): + t = Tensor.arange(i % 10 + 1) + _ = t.addf(float(i)) + # Tensors should be automatically cleaned up + + def test_edge_cases(self): + """Test edge cases and error conditions.""" + # Empty tensor + empty = Tensor.empty(0) + assert len(empty) == 0 + + # Single element tensor + single = Tensor.arange(1) + assert len(single) == 1 + assert single[0].item() == 0.0 + + +class TestTensorErrors: + """Test error conditions and invalid inputs.""" + + def test_invalid_index_type(self): + """Test that invalid index types raise appropriate errors.""" + t = Tensor.arange(5) + + with pytest.raises(TypeError): + _ = t["invalid"] + + with pytest.raises(TypeError): + _ = t[1.5] + + def test_invalid_arithmetic_types(self): + """Test that invalid arithmetic operations raise errors.""" + t = Tensor.arange(3) + + with pytest.raises(TypeError): + t.add("invalid") + + with pytest.raises(TypeError): + t.add(123) # Should be a Tensor, not an int + + +if __name__ == "__main__": + # Allow running this test file directly + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tensor/tests/test_main.py b/tensor/tests/test_main.py new file mode 100644 index 0000000..9996e4f --- /dev/null +++ b/tensor/tests/test_main.py @@ -0,0 +1,3 @@ + +def test_main(): + assert True \ No newline at end of file diff --git a/tensor/tests/test_simple.py b/tensor/tests/test_simple.py new file mode 100644 index 0000000..137e001 --- /dev/null +++ b/tensor/tests/test_simple.py @@ -0,0 +1,129 @@ +""" +Simple tests for tensor1d functionality - no external dependencies needed. +""" +import sys +import os + +# Add the project root to the path so we can import tensor +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from tensor.tensor1d import Tensor + + +def test_basic_functionality(): + """Test basic tensor functionality.""" + print("Running basic functionality tests...") + + # Test 1: Empty tensor creation + print(" * Testing empty tensor creation...") + t1 = Tensor.empty(3) + assert len(t1) == 3 + print(f" Created empty tensor: {t1}") + + # Test 2: Range tensor creation + print(" * Testing range tensor creation...") + t2 = Tensor.arange(5) + assert len(t2) == 5 + assert t2.tolist() == [0.0, 1.0, 2.0, 3.0, 4.0] + print(f" Created range tensor: {t2}") + + # Test 3: Indexing + print(" * Testing indexing...") + assert t2[0].item() == 0.0 + assert t2[2].item() == 2.0 + assert t2[-1].item() == 4.0 + print(" Indexing works correctly") + + # Test 4: Slicing + print(" * Testing slicing...") + s1 = t2[1:4] + assert s1.tolist() == [1.0, 2.0, 3.0] + + s2 = t2[::2] + assert s2.tolist() == [0.0, 2.0, 4.0] + print(" Slicing works correctly") + + # Test 5: Arithmetic + print(" * Testing arithmetic...") + t3 = t2.addf(5.0) + assert t3.tolist() == [5.0, 6.0, 7.0, 8.0, 9.0] + + t4 = t2.add(t2) + assert t4.tolist() == [0.0, 2.0, 4.0, 6.0, 8.0] + print(" Arithmetic operations work correctly") + + print("All tests passed!") + + +def test_error_conditions(): + """Test error conditions.""" + print("Testing error conditions...") + + t = Tensor.arange(3) + + # Test invalid index types + try: + _ = t["invalid"] + assert False, "Should have raised TypeError" + except TypeError: + print(" * Invalid string index correctly raises TypeError") + + # Test invalid arithmetic + try: + t.add("invalid") + assert False, "Should have raised TypeError" + except TypeError: + print(" * Invalid arithmetic correctly raises TypeError") + + print("Error condition tests passed!") + + +def test_large_tensors(): + """Test larger tensors to ensure buffer sizes are adequate.""" + print("Testing large tensor string representations...") + + # Test various larger sizes + for size in [10, 50, 100]: + print(f" * Testing tensor of size {size}...") + t = Tensor.arange(size) + str_repr = str(t) + + # Basic sanity checks + assert str_repr.startswith('[') + assert str_repr.endswith(']') + assert '0.0' in str_repr # First element should be 0.0 + + # Check that last element is correct + expected_last = float(size - 1) + assert f'{expected_last}' in str_repr # Just check the number is there + + # Verify the actual last element value + assert t[-1].item() == expected_last + + print("Large tensor tests passed!") + + +def main(): + """Run all tests.""" + print("Starting tensor1d test suite...") + print("=" * 50) + + try: + test_basic_functionality() + print() + test_error_conditions() + print() + test_large_tensors() + print() + print("=" * 50) + print("ALL TESTS PASSED!") + return 0 + except Exception as e: + print(f"Test failed: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/test_tensor1d.py b/test_tensor1d.py deleted file mode 100644 index 386d9bd..0000000 --- a/test_tensor1d.py +++ /dev/null @@ -1,212 +0,0 @@ -import pytest -import torch -import tensor1d - -def assert_tensor_equal(torch_tensor, tensor1d_tensor): - assert torch_tensor.tolist() == tensor1d_tensor.tolist() - -@pytest.mark.parametrize("size", [0, 1, 10, 100]) -def test_arange(size): - torch_tensor = torch.arange(size, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(size) - assert_tensor_equal(torch_tensor, tensor1d_tensor) - -@pytest.mark.parametrize("case", [[], [1], [1, 2, 3], list(range(100))]) -def test_tensor_creation(case): - torch_tensor = torch.tensor(case) - tensor1d_tensor = tensor1d.tensor(case) - assert_tensor_equal(torch_tensor, tensor1d_tensor) - -@pytest.mark.parametrize("size", [0, 1, 10, 100]) -def test_empty(size): - torch_tensor = torch.empty(size) - tensor1d_tensor = tensor1d.empty(size) - assert len(torch_tensor) == len(tensor1d_tensor) - -@pytest.mark.parametrize("index", range(1, 10)) -def test_indexing(index): - torch_tensor = torch.arange(10, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(10) - assert torch_tensor[index].item() == tensor1d_tensor[index].item() - -@pytest.mark.parametrize("slice_params", [ - (None, None, None), # [:] - (5, None, None), # [5:] - (None, 15, None), # [:15] - (5, 15, None), # [5:15] - (None, None, 2), # [::2] - (5, 15, 2), # [5:15:2] - (5, 15, 15), # [5:15:15] -]) -def test_slicing(slice_params): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - s = slice(*slice_params) - assert_tensor_equal(torch_tensor[s], tensor1d_tensor[s]) - -def test_invalid_input(): - with pytest.raises(TypeError): - tensor1d.tensor("not a valid input") - -def test_invalid_index(): - t = tensor1d.arange(5) - with pytest.raises(TypeError): - t["invalid index"] - -@pytest.mark.parametrize("initial_slice, second_slice", [ - ((5, 15, 1), (2, 7, 1)), # Basic case - ((5, 15, 1), (None, None, 1)), # Full slice - ((5, 15, 1), (None, None, 2)), # Every other element - ((5, 15, 2), (None, None, 2)), # Every other of every other - ((0, 20, 1), (-5, None, 1)), # Negative start index - ((0, 20, 1), (None, -5, 1)), # Negative end index - ((0, 20, 1), (-15, -5, 1)), # Negative start and end indices - ((5, 15, 1), (100, None, 1)), # Start index out of range - ((5, 15, 1), (None, 100, 1)), # End index out of range - ((5, 15, 1), (-100, None, 1)), # Negative start index out of range - ((5, 15, 1), (None, -100, 1)), # Negative end index out of range - ((0, 20, 1), (0, 0, 1)), # Empty slice - ((0, 0, 1), (None, None, 1)), # Slice of empty slice -]) -def test_slice_of_slice(initial_slice, second_slice): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - - torch_slice = torch_tensor[slice(*initial_slice)] - tensor1d_slice = tensor1d_tensor[slice(*initial_slice)] - - torch_result = torch_slice[slice(*second_slice)] - tensor1d_result = tensor1d_slice[slice(*second_slice)] - - assert_tensor_equal(torch_result, tensor1d_result) - -def test_multiple_slices(): - torch_tensor = torch.arange(100, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(100) - - torch_result = torch_tensor[10:90:2][5:35:3][::2] - tensor1d_result = tensor1d_tensor[10:90:2][5:35:3][::2] - - assert_tensor_equal(torch_result, tensor1d_result) - -# Test for behavior with step sizes > 1 -@pytest.mark.parametrize("step", [2, 3, 5]) -def test_slices_with_steps(step): - torch_tensor = torch.arange(50, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(50) - - torch_result = torch_tensor[::step][5:20] - tensor1d_result = tensor1d_tensor[::step][5:20] - - assert_tensor_equal(torch_result, tensor1d_result) - -# Test for behavior with different slice sizes -@pytest.mark.parametrize("size", [10, 20, 50, 100]) -def test_slices_with_different_sizes(size): - torch_tensor = torch.arange(size, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(size) - - torch_result = torch_tensor[size//4:3*size//4][::2] - tensor1d_result = tensor1d_tensor[size//4:3*size//4][::2] - - assert_tensor_equal(torch_result, tensor1d_result) - -# Test for behavior with overlapping slices -def test_overlapping_slices(): - torch_tensor = torch.arange(30, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(30) - - torch_result = torch_tensor[5:25][3:15] - tensor1d_result = tensor1d_tensor[5:25][3:15] - - assert_tensor_equal(torch_result, tensor1d_result) - -# Test for behavior with adjacent slices -def test_adjacent_slices(): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - - torch_result = torch_tensor[5:15][0:10] - tensor1d_result = tensor1d_tensor[5:15][0:10] - - assert_tensor_equal(torch_result, tensor1d_result) - -# Test accessing elements, including negative indices -def test_getitem(): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - assert torch_tensor[0].item() == tensor1d_tensor[0].item() - assert torch_tensor[5].item() == tensor1d_tensor[5].item() - assert torch_tensor[-1].item() == tensor1d_tensor[-1].item() - assert torch_tensor[-5].item() == tensor1d_tensor[-5].item() - -# Test setting elements, including negative indices -def test_setitem(): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - - torch_tensor[0] = 100 - tensor1d_tensor[0] = 100 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - - torch_tensor[5] = 200 - tensor1d_tensor[5] = 200 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - - torch_tensor[-1] = 300 - tensor1d_tensor[-1] = 300 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - - torch_tensor[-5] = 400 - tensor1d_tensor[-5] = 400 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - -# Test setting elements indirectly (via a slice) -def test_setitem_indirect(): - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - torch_view = torch_tensor[5:15] - tensor1d_view = tensor1d_tensor[5:15] - - torch_view[0] = 100 - tensor1d_view[0] = 100 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - - torch_view[-1] = 200 - tensor1d_view[-1] = 200 - assert_tensor_equal(torch_tensor, tensor1d_tensor) - -# test addition -def test_addition(): - - # simple element-wise addition - torch_tensor = torch.arange(20, dtype=torch.float32) - tensor1d_tensor = tensor1d.arange(20) - torch_result = torch_tensor + 5.0 - tensor1d_result = tensor1d_tensor + 5.0 - assert_tensor_equal(torch_result, tensor1d_result) - - # now test adding a float - torch_result = torch_tensor + 6.0 - tensor1d_result = tensor1d_tensor + 6.0 - assert_tensor_equal(torch_result, tensor1d_result) - - # test broadcasting add with a 1-element tensor on the right - torch_result = torch_tensor + torch.tensor([123.0]) - tensor1d_result = tensor1d_tensor + tensor1d.tensor([123.0]) - assert_tensor_equal(torch_result, tensor1d_result) - - # and on the left - torch_result = torch.tensor([42.0]) + torch_tensor - tensor1d_result = tensor1d.tensor([42.0]) + tensor1d_tensor - assert_tensor_equal(torch_result, tensor1d_result) - - # and now test invalid cases - with pytest.raises(TypeError): - tensor1d_tensor + "not a valid input" - - with pytest.raises(TypeError): - tensor1d_tensor + [1, 2, 3] - - with pytest.raises(ValueError): - tensor1d_tensor + tensor1d.arange(5)