diff --git a/README.md b/README.md index aec592a..5f6e7ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # neuralnet-cpp -![C++](https://img.shields.io/badge/c++-%2300599C.svg?&logo=c%2B%2B&logoColor=white) +![C++](https://img.shields.io/badge/C%2B%2B-23-blue.svg) ![C++ Unit Tests](https://github.com/lucaswychan/neuralnet-cpp/actions/workflows/cpp_test.yaml/badge.svg) [![GitHub license badge](https://img.shields.io/github/license/lucaswychan/neural-stock-prophet?color=blue)](https://opensource.org/licenses/MIT) @@ -22,18 +22,21 @@ More to come. ## Get Started -Make sure you have [CMake](https://cmake.org/) installed. +This project requires C++23, GCC >= 13.3, and CMake >= 3.20 to compile. Please make sure you have [GCC](https://gcc.gnu.org) and [CMake](https://cmake.org/) installed. -For Mac OS, run the following commands: +For **Mac OS**, run the following commands: ```bash brew install cmake +brew install gcc ``` -For Linux, run the following commands: +For **Linux**, run the following commands: ```bash -sudo apt-get install cmake +sudo apt update +sudo apt install cmake +sudo apt install build-essential ``` Get the repository: @@ -59,28 +62,7 @@ Run the example: I implemented a tensor from scratch as well and integrate it to my neural network implementation. The detailed implementation of `Tensor` can be found in [`include/core/tensor.hpp`](include/core/tensor.hpp). -`Tensor` provides a lot of useful methods such as `add`, `sub`, `mul`, `div`, `matmul`, `transpose`, etc. You can find the detailed documentation in [`include/core/tensor.hpp`](include/core/tensor.hpp). - -Note that `Tensor` currently only supports up to 3-dimensional vectors. - -### Example usage - -```cpp -#include "tensor.hpp" - -// default type is double -Tensor<> your_tensor = { { 1.2, 2.3, 3.4 }, { 4.5, 5.6, 6.7 } }; // shape: (2, 3) - -// Or you can create a tensor with a specific type -Tensor your_int_tensor = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } } // shape: (3, 3); - -// Lots of operations are supported, including element-wise operations, matrix multiplication, etc. -Tensor<> transposed_tensor = your_tensor.transpose(); // shape: (3, 2) - -// You can also create a tensor from a vector -vector> your_vec = { { 1.2, 2.3, 3.4 }, { 4.5, 5.6, 6.7 } }; -Tensor<> your_tensor_from_vec = Tensor<>(your_vec); -``` +For more details about tensor, please refer to [tensor tutorial](docs/tensor.md). ## Module API @@ -110,3 +92,5 @@ class MyModule : public nn::Module { Please refer to the [TODO list](https://github.com/lucaswychan/neuralnet-cpp/blob/main/TODO.md). ## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/TODO.md b/TODO.md index df9f728..ab743c2 100644 --- a/TODO.md +++ b/TODO.md @@ -7,8 +7,13 @@ More functionalities to come. - [x] ReLU activation - [x] Mean Squared Error loss - [x] Cross Entropy loss +- [x] Accuracy Metric - [] Convolutional layer - [] Pooling layer +- [] Attention layer +- [] LayerNorm layer +- [] Embedding layer - [] Transformer layer +- [] AutoEncoder - [] Recurrent layer -- [] Embedding layer +- [] **CUDA Support** diff --git a/data/mnist/raw/t10k-images-idx3-ubyte b/data/mnist/raw/t10k-images-idx3-ubyte new file mode 100644 index 0000000..1170b2c Binary files /dev/null and b/data/mnist/raw/t10k-images-idx3-ubyte differ diff --git a/data/mnist/raw/t10k-images-idx3-ubyte.gz b/data/mnist/raw/t10k-images-idx3-ubyte.gz new file mode 100644 index 0000000..5ace8ea Binary files /dev/null and b/data/mnist/raw/t10k-images-idx3-ubyte.gz differ diff --git a/data/mnist/raw/t10k-labels-idx1-ubyte b/data/mnist/raw/t10k-labels-idx1-ubyte new file mode 100644 index 0000000..d1c3a97 Binary files /dev/null and b/data/mnist/raw/t10k-labels-idx1-ubyte differ diff --git a/data/mnist/raw/t10k-labels-idx1-ubyte.gz b/data/mnist/raw/t10k-labels-idx1-ubyte.gz new file mode 100644 index 0000000..a7e1415 Binary files /dev/null and b/data/mnist/raw/t10k-labels-idx1-ubyte.gz differ diff --git a/data/mnist/raw/train-images-idx3-ubyte b/data/mnist/raw/train-images-idx3-ubyte new file mode 100644 index 0000000..bbce276 Binary files /dev/null and b/data/mnist/raw/train-images-idx3-ubyte differ diff --git a/data/mnist/raw/train-images-idx3-ubyte.gz b/data/mnist/raw/train-images-idx3-ubyte.gz new file mode 100644 index 0000000..b50e4b6 Binary files /dev/null and b/data/mnist/raw/train-images-idx3-ubyte.gz differ diff --git a/data/mnist/raw/train-labels-idx1-ubyte b/data/mnist/raw/train-labels-idx1-ubyte new file mode 100644 index 0000000..d6b4c5d Binary files /dev/null and b/data/mnist/raw/train-labels-idx1-ubyte differ diff --git a/data/mnist/raw/train-labels-idx1-ubyte.gz b/data/mnist/raw/train-labels-idx1-ubyte.gz new file mode 100644 index 0000000..707a576 Binary files /dev/null and b/data/mnist/raw/train-labels-idx1-ubyte.gz differ diff --git a/docs/tensor.md b/docs/tensor.md new file mode 100644 index 0000000..77c3cb8 --- /dev/null +++ b/docs/tensor.md @@ -0,0 +1,240 @@ +# Tensor Tutorial + +`Tensor` provides a lot of useful methods such as `add`, `sub`, `mul`, `div`, `matmul`, `dtype`, etc. You can find the detailed documentation in [`include/core/tensor.hpp`](include/core/tensor.hpp). + +Note that `Tensor` currently only supports up to 3-dimensional vectors. + +**Guide :** + +- [Create a tensor](#creteate-a-tensor) +- [Access tensor metadata](#access-tensor-metadata) +- [Index tensor](#index-tensor) +- [Visualize tensor](#visualize-tensor) +- [Perform arithmetic operations](#perform-arithmetic-operations) +- [Reshape tensor](#reshape-tensor) +- [Convert tensor data type](#convert-tensor-data-type) +- [Filter the unwanted elements](#filter-the-unwanted-elements) +- [Perform function mapping](#perform-function-mapping) +- [Max, Min, Argmax, Argmin](#max-min-argmax-argmin) + +### Creteate a tensor + +You can create your tensor from C++ array, or using `vector` in C++ STL. You can create a tensor with different variable type, even with your custom class. + +```cpp +#include "tensor.hpp" + +// default type is double +Tensor<> your_tensor = { { 1.2, 2.3, 3.4 }, { 4.5, 5.6, 6.7 } }; // shape: (2, 3) + +// Or you can create a tensor with a specific type +Tensor your_int_tensor = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } } // shape: (3, 3); + +// Lots of operations are supported, including element-wise operations, matrix multiplication, etc. +Tensor<> transposed_tensor = your_tensor.transpose(); // shape: (3, 2) + +// You can also create a tensor from a vector +vector> your_vec = { { 1.2, 2.3, 3.4 }, { 4.5, 5.6, 6.7 } }; +Tensor<> your_tensor_from_vec = Tensor<>(your_vec); +``` + +## Access tensor metadata + +Several function are provided to access the tensor's shape, dimensions, and size. + +```cpp +Tensor<> tensor = { { 1.2, 2.3, 3.4 }, { 4.5, 5.6, 6.7 } }; + +// it stores the shapes of each dimension. +vector shapes = tensor.shapes() +// {2, 3} + +// it stores the number of dimensions of tensor. +size_t num_dimensions = tensor.ndim() +// 2 + +// it stores the total number of elements in tensor. +size_t size = tensor.size() +// 6 +``` + +## Index tensor + +Without calling any function or creating a vector, you can directly index your tensor with just `[]`. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +int first_element = A[0, 0]; +// 1 + +// Negative indexing is allowed as well +int last_element = A[-1, -1]; +// 6 + +for (int i = 0, i < A.shapes()[0]; ++i) { + for (int j = 0; j < A.shapes()[1]; ++j) { + cout << A[i, j] << " "; + } +} +cout << endl; +``` + +## Visualize tensor + +Instead of using `cout` all the time, `print` is provided for convenience. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +A.print() +/* +[[1, 2, 3], +[4, 5, 6]] +*/ +``` + +## Perform arithmetic operations + +You can also perform typical tensor's arithmetic operations including `add`, `sub`, `mul`, `div`, `matmul`, `transpose`, etc. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +Tensor B = { { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } }; // 3 x 3 + +Tensor A_2 = A * A; +/* +{ { 1, 4, 9 }, + { 16, 25, 36 } } +*/ + +Tensor B_plus_B = B + B; +/* +{ { 2, 4, 6 }, + { 8, 10, 12 }, + { 14, 16, 18 } } +*/ + +Tensor B_x2 = B * 2 +/* +{ { 2, 4, 6 }, + { 8, 10, 12 }, + { 14, 16, 18 } } +*/ + +Tensor A_matmul_B = A.matmul(B); +/* +{ { 30, 36, 42 } + { 66, 81, 96 } } +*/ + +Tensor A_T = A.transpose(); +/* +{ { 1, 4 }, + { 2, 5 }, + { 3, 6 } } +*/ +``` + +## Reshape tensor + +You can reshape your tensor. Note that your new shapes must have the same number of elements. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +vector new_shapes = {6}; +A.reshape(new_shapes); // or A.reshape({6}) + +size_t new_ndim = A.ndim() +// 1 + +vector other_shapes = {3, 4}; +A.reshape(other_shapes); // Error !!!!! +``` + +## Convert tensor data type + +If you don't like the current tensor's data type, feel free to convert it to other data type using `dtype`. Since it is a template function, you should specify the desired type in the template argument instead of the funcion argument. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +Tensor A_float = A.dtype(); + +Tensor<> A_double = A.dtype(); // since the default type of tensor is double +``` + +## Filter the unwanted elements + +Sometimes some of the elements in the tensor should be filtered (such as ReLU operation). Simply use `filter` to filter the unwanted elements. It takes a boolean function as argument to test each element of the tensor. It should return true if the element passes the test. All elements that fail the test are set to 0 + +```cpp +Tensor A = { { -1, 2, 3 }, + { 4, -5, 6 } }; // 2 x 3 + +Tensor A_filtered = A.filter([](int x) { return x > 0; }); +/* +{ { 0, 2, 3 }, + { 4, 0, 6 } } +*/ +``` + +## Perform function mapping + +Function mapping also can be applied to the tensor, simply by using `map`. It takes a function as argument to perform element-wise transformation to the tensor. + +```cpp +Tensor A = { { 1, 2, 3 }, + { 4, 5, 6 } }; // 2 x 3 + +Tensor A_mapped = A.map([](int x) { return 5 * x; }); +/* +{ { 5, 10, 15 }, + { 20, 25, 30 } } +*/ +``` + +## Max, Min, Argmax, Argmin + +Row-wise max, min, argmax, argmin operations are also provided. Currently only support 1-D and 2-D tensor. + +```cpp +Tensor B = { { 10, 2, 3 }, + { 4, 5, 60 }, + { 7, 80, 90 } }; // 3 x 3 + +Tensor B_max = B.max(); +// { 10, 60, 90 } + +Tensor B_argmax = B.argmax(); +// { 0, 2, 2 } + +Tensor B_max = B.min(); +// { 2, 4, 7 } + +Tensor B_argmax = B.argmin(); +// { 1, 0, 0 } + +Tensor tensor_1d = { 1, 2, 30, 4, 5 }; + +Tensor tensor_1d_max = tensor_1d.max(); +// { 30 } + +Tensor tensor_1d_argmax = tensor_1d.argmax(); +// { 2 } + +Tensor tensor_1d_max = tensor_1d.min(); +// { 1 } + +Tensor tensor_1d_argmax = tensor_1d.argmin(); +// { 0 } +``` diff --git a/include/core/tensor.hpp b/include/core/tensor.hpp index 92c81b5..39e592c 100644 --- a/include/core/tensor.hpp +++ b/include/core/tensor.hpp @@ -8,23 +8,22 @@ using namespace std; template class Tensor { private: - vector shapes_; // store the dimensions of the tensor - size_t size_; // store the number of elements in the tensor vector data_; // data is stored as a 1D vector + vector shapes_; // store the dimensions of the tensor // Helper function to calculate the index in the 1D vector for a given set of indices expressed in the form of N-D vector - size_t calculateIndex(const vector& idxs) const { - size_t index = 0; + size_t calculate_idx(const vector& idxs) const { + size_t idx = 0; size_t multiplier = 1; for (size_t i = this->ndim(); i-- > 0;) { - index += idxs[i] * multiplier; + idx += idxs[i] * multiplier; multiplier *= this->shapes_[i]; } - return index; + return idx; } // Helper function for printing since we don't know the number of dimensions - void printRecursive(size_t dim, size_t offset) const { + void print_recursive(size_t dim, size_t offset) const { if (dim == this->ndim() - 1) { // Last dimension cout << "["; for (size_t i = 0; i < this->shapes_[dim]; ++i) { @@ -39,7 +38,7 @@ class Tensor { stride *= this->shapes_[i]; } for (size_t i = 0; i < this->shapes_[dim]; ++i) { - printRecursive(dim + 1, offset + i * stride); + print_recursive(dim + 1, offset + i * stride); if (i < this->shapes_[dim] - 1) cout << ", " << endl; } cout << "]" << endl; @@ -48,20 +47,32 @@ class Tensor { // Helper function for operator[] overloading template - const vector getIdxs(Indices... indices) const { + const vector get_idxs(Indices... indices) const { // Convert variadic arguments to vector - vector idxs({static_cast(indices)...}); + vector idxs({static_cast(indices)...}); + vector normalized_idxs; + + // for better performance, reserve the size of the vector + normalized_idxs.reserve(idxs.size()); // Check bounds for (size_t i = 0; i < idxs.size(); ++i) { - if (idxs[i] < 0 || idxs[i] >= this->shapes_[i]) { - throw std::out_of_range("Tensor: Index out of bounds"); - } + size_t normalized_idx = this->normalize_index(idxs[i], this->shapes_[i]); + normalized_idxs.push_back(normalized_idx); } - return idxs; + return normalized_idxs; } + /** + * Reduces a 1D or 2D tensor along its rows using the specified reduction operation. + * + * @tparam U The data type of the resulting tensor. Defaults to the type of the current tensor. + * @param op The reduction operation to perform. Supported operations are MAX, MIN, ARGMAX, and ARGMIN. + * @return A Tensor of shape (num_rows, 1) containing the reduced values or indices. + * @throws runtime_error if the tensor's number of dimensions is greater than 2. + */ + template Tensor reduce(ReduceOp op) const { if (this->ndim() > 2) { @@ -127,7 +138,7 @@ class Tensor { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { switch (op) { case ArithmeticOp::ADD: result.data_[i] = this->data_[i] + other.data_[i]; @@ -146,13 +157,35 @@ class Tensor { return result; } + // Helper function to convert negative indices to positive + size_t normalize_index(int idx, size_t dim_size) const { + if (idx < 0) idx += dim_size; + if (idx < 0 || idx >= dim_size) { + throw std::out_of_range("Index out of bounds after index normalization"); + } + return idx; + } + + // Helper function to apply slice to a dimension + vector apply_slice(const Slice& slice, size_t dim_size) const { + vector indices; + // cout << "In apply_slice, start: " << slice.start << " stop: " << slice.stop << " step: " << slice.step << endl; + size_t start = normalize_index(slice.start, dim_size); + size_t stop = slice.stop == INT_MAX ? dim_size : normalize_index(slice.stop - 1, dim_size) + 1; + size_t step = slice.step; + + // cout << "start applying slice" << endl; + for (size_t i = start; i < stop; i += step) { + // cout << "i: " << i << endl; + indices.push_back(i); + } + return indices; + } + // Declare friendship so that TensorView can access private members of Tensor template friend Tensor dtype_impl(const Tensor& tensor); - template - friend class TensorView; - public: Tensor() = default; @@ -163,21 +196,23 @@ class Tensor { } // 1D tensor constructor - Tensor(const initializer_list& data) : size_(data.size()), data_(data.begin(), data.end()) { - this->shapes_ = vector { data.size() }; + Tensor(const initializer_list& data_1d) { + this->data_ = vector(data_1d.begin(), data_1d.end()); + this->shapes_ = vector { data_1d.size() }; } - Tensor(const vector& data) : size_(data.size()), data_(data.begin(), data.end()) { - this->shapes_ = vector { data.size() }; + Tensor(const vector& data_1d) { + this->data_ = data_1d; + this->shapes_ = vector { data_1d.size() }; } // 2D tensor constructor Tensor(const initializer_list>& data_2d) { const size_t n = data_2d.size(), m = data_2d.begin()->size(); - this->size_ = n * m; this->shapes_ = vector { n, m }; + this->data_.reserve(n * m); // Optimize memory allocation for (const initializer_list& row : data_2d) { this->data_.insert(this->data_.end(), row.begin(), row.end()); } @@ -185,10 +220,10 @@ class Tensor { Tensor(const vector>& data_2d) { const size_t n = data_2d.size(), m = data_2d.begin()->size(); - this->size_ = n * m; this->shapes_ = vector { n, m }; + this->data_.reserve(n * m); // Optimize memory allocation for (const vector& row : data_2d) { this->data_.insert(this->data_.end(), row.begin(), row.end()); } @@ -197,10 +232,10 @@ class Tensor { // 3D tensor constructor Tensor(const initializer_list>>& data_3d) { const size_t n = data_3d.size(), m = data_3d.begin()->size(), l = data_3d.begin()->begin()->size(); - this->size_ = n * m * l; this->shapes_ = vector { n, m, l }; + this->data_.reserve(n * m * l); // Optimize memory allocation for (const initializer_list>& matrix : data_3d) { for (const initializer_list& row : matrix) { this->data_.insert(this->data_.end(), row.begin(), row.end()); @@ -210,10 +245,10 @@ class Tensor { Tensor(const vector>>& data_3d) { const size_t n = data_3d.size(), m = data_3d.begin()->size(), l = data_3d.begin()->begin()->size(); - this->size_ = n * m * l; this->shapes_ = vector { n, m, l }; + this->data_.reserve(n * m * l); // Optimize memory allocation for (const vector>& matrix : data_3d) { for (const vector& row : matrix) { this->data_.insert(this->data_.end(), row.begin(), row.end()); @@ -224,12 +259,11 @@ class Tensor { // certin value constructor Tensor(const vector& shape, const T& value) { this->shapes_ = shape; - this->size_ = 1; - for (const size_t& s : shape) { - this->size_ *= s; + size_t size = 1; + for (const size_t& dim : shape) { + size *= dim; } - - this->data_.resize(this->size_, value); + this->data_.resize(size, value); } // copy constructor @@ -262,7 +296,7 @@ class Tensor { Tensor mul(const T& scaler) const { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { result.data_[i] = this->data_[i] * scaler; } return result; @@ -332,9 +366,9 @@ class Tensor { /// @brief Flatten the tensor into 1D. /// @return A new 1D tensor with the same elements as the original tensor. Tensor flatten() const { - Tensor result({ this->size_ }, static_cast(0)); + Tensor result({ this->size() }, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { result.data_[i] = this->data_[i]; } @@ -345,7 +379,7 @@ class Tensor { /// @details This function only changes the shape of the tensor, and does not modify the underlying data. /// @post The shape of the tensor is changed to 1D, with the same elements as the original tensor. void flatten() { - this->shapes_ = { this->size_ }; + this->shapes_ = { this->size() }; return; } @@ -355,7 +389,6 @@ class Tensor { if (this == &other) return *this; this->shapes_ = other.shapes_; - this->size_ = other.size_; this->data_ = other.data_; return *this; } @@ -365,7 +398,7 @@ class Tensor { Tensor abs() const { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { result.data_[i] = std::abs(this->data_[i]); } return result; @@ -373,27 +406,29 @@ class Tensor { /// @brief Filter the tensor with the given function /// @param func a function to test each element of the tensor. It should return true if the element passes the test - /// @return a new tensor with the same shape as the original, but all elements that fail the test are set to 0 + /// @return a new tensor with the same shape as the original, but all elements that fail the test are set to 0. Tensor filter(bool (*func)(T)) const { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { if (func(this->data_[i])) { result.data_[i] = this->data_[i]; } } + return result; } /// @brief Perform element-wise transformation with a function - /// @param func a function perform element-wise transformation to the tensor + /// @param func a function to perform element-wise transformation to the tensor /// @return a new tensor with the same shape as the original, but with each element transformed by the given func Tensor map(T (*func)(T)) const { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { result.data_[i] = func(this->data_[i]); } + return result; } @@ -402,7 +437,7 @@ class Tensor { T sum() const { T sum = static_cast(0); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { sum += this->data_[i]; } @@ -419,7 +454,7 @@ class Tensor { Tensor result(this->shapes_, static_cast(0)); - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { result.data_[i] = this->data_[i] == other.data_[i]; } @@ -434,7 +469,7 @@ class Tensor { throw runtime_error("Shape mismatch"); } - for (size_t i = 0; i < this->size_; i++) { + for (size_t i = 0; i < this->size(); i++) { if (this->data_[i] != other.data_[i]) { return false; } @@ -477,71 +512,22 @@ class Tensor { return dtype_impl(*this); } - - /** - * @brief Get a view of a specific slice along a specified dimension. - * - * This function creates a `TensorView` that represents a reference-like view - * into the tensor data along a specified dimension. The view does not contain - * actual data but provides access to the underlying tensor data. - * - * @tparam Dim The dimension along which the slice is taken. - * @param index The index of the slice along the specified dimension. - * @return A `TensorView` representing the view of the specified slice. - * @throws std::std::out_of_range if the index is out of bounds for the given dimension. - */ - - template - TensorView slice(size_t index) { - if (index >= this->shapes_[Dim]) throw std::out_of_range("Index out of bounds"); - - // Calculate strides for the resulting view - vector view_strides; - size_t slice_size = 1; - for (size_t i = 0; i < this->ndim(); ++i) { - if (i != Dim) { - view_strides.push_back(slice_size); - slice_size *= this->shapes_[i]; - } + /// @brief Reshape the tensor to the specified new shape. + /// @details This function changes the shape of the tensor without altering the data. + /// The total number of elements must remain the same; otherwise, an exception is thrown. + /// @param new_shape The desired shape for the tensor. + /// @throws runtime_error if the new shape is not compatible with the current number of elements. + void reshape(const vector& new_shape) { + size_t new_size = 1; + for (const size_t& dim : new_shape) { + new_size *= dim; } - // Create initial indices with the fixed dimension - vector indices(this->ndim(), 0); - indices[Dim] = index; - - // It will only return a view, not the real row or col data - return TensorView(*this, indices, view_strides, Dim, slice_size); - } - - - /** - * @brief Get a view of a specific row in the tensor. - * - * This function creates a `TensorView` that represents a reference-like view - * into the tensor data for a specific row. The view does not contain actual data - * but provides access to the underlying tensor data. - * - * @param index The index of the row to view. - * @return A `TensorView` representing the view of the specified row. - * @throws std::std::out_of_range if the index is out of bounds for the row dimension. - */ - inline TensorView row(size_t index) { - return this->slice<0>(index); - } + if (new_size != this->size()) { + throw runtime_error("New shape must be compatible with the original shape"); + } - /** - * @brief Get a view of a specific column in the tensor. - * - * This function creates a `TensorView` that represents a reference-like view - * into the tensor data for a specific column. The view does not contain actual data - * but provides access to the underlying tensor data. - * - * @param index The index of the column to view. - * @return A `TensorView` representing the view of the specified column. - * @throws std::std::out_of_range if the index is out of bounds for the column dimension. - */ - inline TensorView col(size_t index) { - return this->slice<1>(index); + this->shapes_ = new_shape; } // Get the dimension of the tensor @@ -549,11 +535,14 @@ class Tensor { return this->shapes_.size(); } - inline const vector& shapes() const { return this->shapes_; } + inline const size_t size() const { + return this->data_.size(); + } + - inline const size_t size() const { return this->size_; } + inline const vector& shapes() const { return this->shapes_; } - inline void print() const { printRecursive(0, 0); } + inline void print() const { print_recursive(0, 0); } // ========================================operators overloading======================================== inline Tensor operator+(const Tensor& other) const { return this->add(other); } @@ -586,17 +575,117 @@ class Tensor { // lvalue operator overloading template T& operator[](Indices... indices) { - // ((cout << ',' << std::forward(indices)), ...); - // cout << endl; + vector idxs = this->get_idxs(indices...); + return this->data_[calculate_idx(idxs)]; + } - vector idxs = this->getIdxs(indices...); - return this->data_[calculateIndex(idxs)]; + T& operator[](const vector& indices) { + return this->data_[calculate_idx(indices)]; } // rvalue operator overloading template - T operator[](Indices... indices) const { - vector idxs = this->getIdxs(indices...); - return this->data_[calculateIndex(idxs)]; + const T& operator[](Indices... indices) const { + vector idxs = this->get_idxs(indices...); + return this->data_[calculate_idx(idxs)]; + } + + const T& operator[](const vector& indices) const { + return this->data_[calculate_idx(indices)]; + } + + /** + * @brief Advanced indexing using a combination of integers, strings, and slices. + * + * This function allows for flexible indexing into the tensor, similar to Python's + * advanced indexing. It supports integer indices, string-based slices, and the ellipsis + * ("...") for automatic dimension completion. The function expands slices and handles + * ellipsis to generate the appropriate sub-tensor. + * + * @param indices A vector of indices where each index can be an integer, a string + * representing a slice, or a special ellipsis ("..."). + * @return A new tensor that is indexed from the current tensor according to the given indices. + * + * @throw std::invalid_argument if an index type is invalid or if more than one ellipsis is used. + */ + using IndexType = variant; + Tensor index(const vector& indices) const { + vector> expanded_indices; + + // Handle ellipsis and expand slices + // cout << "Start expanding indices" << endl; + for (size_t i = 0; i < indices.size(); ++i) { + const auto& idx = indices[i]; + + if (auto str_idx = get_if(&idx)) { + Slice slice = Slice::parse(*str_idx); + expanded_indices.push_back(apply_slice(slice, this->shapes_[i])); + } + else if (auto int_idx = get_if(&idx)) { + expanded_indices.push_back({normalize_index(*int_idx, this->shapes_[i])}); + } + else if (auto slice_idx = get_if(&idx)) { + expanded_indices.push_back(apply_slice(*slice_idx, this->shapes_[i])); + } + else { + throw std::invalid_argument("Invalid index type"); + } + } + + // Calculate new dimensions + vector new_dims; + for (const vector& expanded_idx : expanded_indices) { + if (expanded_idx[0] != -1) { // Not None/newaxis + if (expanded_idx.size() > 1) { + new_dims.push_back(expanded_idx.size()); + } + } + else { + new_dims.push_back(1); + } + } + + // cout << "Start printing new_dims" << endl; + // cout << "new_dims size: " << new_dims.size() << endl; + // for (size_t i = 0; i < new_dims.size(); ++i) { + // cout << new_dims[i] << " "; + // } + + // Create result tensor + Tensor result(new_dims, static_cast(0)); + + // Fill result tensor + vector current_indices(expanded_indices.size()); + vector result_indices; + + // Recursive lambda to fill result tensor + function fill_tensor = [&](size_t depth) { + if (depth == expanded_indices.size()) { + result_indices.clear(); + for (int i = 0; i < expanded_indices.size(); ++i) { + if (expanded_indices[i][0] != -1 && expanded_indices[i].size() > 1) { + result_indices.push_back(current_indices[i]); + } + } + + vector original_indices; + for (int i = 0; i < expanded_indices.size(); ++i) { + if (expanded_indices[i][0] != -1) { + original_indices.push_back(expanded_indices[i][current_indices[i]]); + } + } + + result[result_indices] = (*this)[original_indices]; + return; + } + + for (int i = 0; i < expanded_indices[depth].size(); ++i) { + current_indices[depth] = i; + fill_tensor(depth + 1); + } + }; + + fill_tensor(0); + return result; } }; \ No newline at end of file diff --git a/include/utils/tensor_utils.hpp b/include/utils/tensor_utils.hpp index a8eae5e..d82e48e 100644 --- a/include/utils/tensor_utils.hpp +++ b/include/utils/tensor_utils.hpp @@ -1,7 +1,14 @@ #pragma once +#include #include #include #include +#include +#include +#include +#include +#include +#include using namespace std; @@ -14,36 +21,6 @@ class Tensor; template Tensor dtype_impl(const Tensor& tensor); -// TensorView class to provide a reference-like view into tensor data -template -class TensorView { - Tensor& tensor_; // Reference to the original tensor - vector indices_; - vector strides_; - size_t slice_dim_; - size_t size_; - -public: - // Constructor - TensorView(Tensor& tensor, const vector& indices, const vector& strides, size_t slice_dim, size_t size) - : tensor_(tensor) - , indices_(indices) - , strides_(strides) - , slice_dim_(slice_dim) - , size_(size) {} - - // Indexing operator - U& operator[](size_t idx); - - inline size_t size() const { return this->size_; } - - // Iterator support - inline U* begin() { return &operator[](0); } - inline U* end() { return &operator[](this->size_); } - inline const U* begin() const { return &operator[](0); } - inline const U* end() const { return &operator[](this->size_); } -}; - // for max, min ,argmax, argmin reduction enum class ReduceOp { MAX, @@ -60,41 +37,55 @@ enum class ArithmeticOp { DIV }; +// Slice struct to handle Python-like slicing +struct Slice { + int start; + int stop; + int step; + + Slice(int start_ = 0, int stop_ = -1, int step_ = 1) + : start(start_), stop(stop_), step(step_) {} + + static Slice parse(const string& slice_str) { + Slice result; + istringstream ss(slice_str); + string token; + vector values; + vector is_empty; + + while (getline(ss, token, ':')) { + // cout << "token: " << token << endl; + if (token.empty()) { + values.push_back(-1); + is_empty.push_back(1); + } else { + values.push_back(stoi(token)); + is_empty.push_back(0); + } + } + + result.start = is_empty[0] == 1 ? 0 : values[0]; + result.stop = is_empty[1] == 1 ? INT_MAX : values[1]; + + if (values.size() == 3) { + result.step = is_empty[2] == 1 ? 1 : values[2]; + } + + // cout << "start: " << result.start << " stop: " << result.stop << " step: " << result.step << endl; + return result; + } +}; + // ================================================definition================================================ template Tensor dtype_impl(const Tensor& tensor) { Tensor result; result.shapes_ = tensor.shapes_; - result.size_ = tensor.size_; result.data_.resize(tensor.data_.size()); std::transform(tensor.data_.begin(), tensor.data_.end(), result.data_.begin(), [](const U& val) { return static_cast(val); }); return result; -} - -// TensorView class to provide a reference-like view into tensor data -template // Indexing operator -U& TensorView::operator[](size_t idx) { - if (idx >= this->size_) throw std::out_of_range("TensorView: Index out of bounds"); - - vector full_indices = this->indices_; - size_t remaining = idx; - - // Convert linear index back to multidimensional indices - size_t curr_dim = 0; - for (size_t i = 0; i < this->tensor_.ndim(); ++i) { - if (i != this->slice_dim_) { - full_indices[i] = remaining / strides_[curr_dim]; - remaining %= this->strides_[curr_dim]; - ++curr_dim; - } - } - - // Calculate final linear index in original data - size_t final_idx = this->tensor_.calculateIndex(full_indices); - - return this->tensor_.data_[final_idx]; } \ No newline at end of file diff --git a/tests/core/tensor_test.cpp b/tests/core/tensor_test.cpp new file mode 100644 index 0000000..b644c78 --- /dev/null +++ b/tests/core/tensor_test.cpp @@ -0,0 +1,317 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" +#include "tensor.hpp" + +TEST_CASE("TensorTest - Constructor and Destructor") { + Tensor<> tensor; + // No explicit assertions needed, just verify no crashes +} + +TEST_CASE("TensorTest - Scaler Constructor") { + Tensor<> tensor(10.0f); + CHECK(tensor.shapes()[0] == 1); + CHECK(tensor.ndim() == 1); + CHECK(tensor.size() == 1); + CHECK(tensor[0] == 10); +} + +TEST_CASE("TensorTest - 1D Tensor Constructor from initializer_list") { + Tensor<> tensor_1d = {1.0f, 2.0f, 3.0f, 4.0f}; + CHECK(tensor_1d.shapes()[0] == 4); + CHECK(tensor_1d.ndim() == 1); + CHECK(tensor_1d.size() == 4); + CHECK(tensor_1d[0] == 1.0f); + CHECK(tensor_1d[1] == 2.0f); + CHECK(tensor_1d[2] == 3.0f); + CHECK(tensor_1d[3] == 4.0f); + + Tensor<> tensor_1d_1val = {0}; + CHECK(tensor_1d_1val.shapes()[0] == 1); + CHECK(tensor_1d_1val.ndim() == 1); + CHECK(tensor_1d_1val.size() == 1); + CHECK(tensor_1d_1val[0] == 0.0f); +} + +TEST_CASE("TensorTest - 2D Tensor Constructor from initializer_list") { + Tensor<> tensor_2d = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + CHECK(tensor_2d.shapes()[0] == 2); + CHECK(tensor_2d.shapes()[1] == 2); + CHECK(tensor_2d.ndim() == 2); + CHECK(tensor_2d.size() == 4); + CHECK(tensor_2d[0, 0] == 1.0f); + CHECK(tensor_2d[0, 1] == 2.0f); + CHECK(tensor_2d[1, 0] == 3.0f); + CHECK(tensor_2d[1, 1] == 4.0f); + + Tensor<> tensor_2d_1row = {{0.0f, 0.0f}}; + CHECK(tensor_2d_1row.shapes()[0] == 1); + CHECK(tensor_2d_1row.shapes()[1] == 2); + CHECK(tensor_2d_1row.ndim() == 2); + CHECK(tensor_2d_1row.size() == 2); + CHECK(tensor_2d_1row[0, 0] == 0.0f); + CHECK(tensor_2d_1row[0, 1] == 0.0f); + + // Tensor<> tensor_2d_1col({{0.0f}, {0.0f}}); + // CHECK(tensor_2d_1col.shapes()[0] == 2); + // CHECK(tensor_2d_1col.shapes()[1] == 1); + // CHECK(tensor_2d_1col.ndim() == 2); + // CHECK(tensor_2d_1col.size() == 2); + // CHECK(tensor_2d_1col[0, 0] == 0.0f); + // CHECK(tensor_2d_1col[1, 0] == 0.0f); +} + +TEST_CASE("TensorTest - 3D Tensor Constructor from initializer_list") { + Tensor<> tensor = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + CHECK(tensor.shapes()[0] == 2); + CHECK(tensor.shapes()[1] == 2); + CHECK(tensor.shapes()[2] == 2); + CHECK(tensor.ndim() == 3); + CHECK(tensor.size() == 8); + CHECK(tensor[0, 0, 0] == 1.0f); + CHECK(tensor[0, 0, 1] == 2.0f); + CHECK(tensor[0, 1, 0] == 3.0f); + CHECK(tensor[0, 1, 1] == 4.0f); + CHECK(tensor[1, 1, 1] == 8.0f); + + Tensor<> tensor2 = {{{0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.0f, 0.0f}, {0.0f, 0.0f}}}; + CHECK(tensor2.shapes()[0] == 2); + CHECK(tensor2.shapes()[1] == 2); + CHECK(tensor2.shapes()[2] == 2); + CHECK(tensor2.ndim() == 3); + CHECK(tensor2.size() == 8); + CHECK(tensor2[0, 0, 0] == 0.0f); + CHECK(tensor2[0, 0, 1] == 0.0f); + CHECK(tensor2[0, 1, 0] == 0.0f); + CHECK(tensor2[0, 1, 1] == 0.0f); + CHECK(tensor2[1, 1, 1] == 0.0f); +} + +TEST_CASE("TensorTest - 1D Tensor Constructor from vector") { + vector data = {1.0f, 2.0f, 3.0f, 4.0f}; + Tensor<> tensor1 = data; + CHECK(tensor1.shapes()[0] == 4); + CHECK(tensor1.ndim() == 1); + CHECK(tensor1.size() == 4); + CHECK(tensor1[0] == 1.0f); + CHECK(tensor1[1] == 2.0f); + CHECK(tensor1[2] == 3.0f); + CHECK(tensor1[3] == 4.0f); + + vector data2 = {0}; + Tensor<> tensor2 = data2; + CHECK(tensor2.shapes()[0] == 1); + CHECK(tensor2.ndim() == 1); + CHECK(tensor2.size() == 1); + CHECK(tensor2[0] == 0.0f); +} + +TEST_CASE("TensorTest - 2D Tensor Constructor from vector") { + vector> data = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + Tensor<> tensor = data; + CHECK(tensor.shapes()[0] == 2); + CHECK(tensor.shapes()[1] == 2); + CHECK(tensor.ndim() == 2); + CHECK(tensor.size() == 4); + CHECK(tensor[0, 0] == 1.0f); + CHECK(tensor[0, 1] == 2.0f); + CHECK(tensor[1, 0] == 3.0f); + CHECK(tensor[1, 1] == 4.0f); + + vector> data2 = {{0.0f, 0.0f}}; + Tensor<> tensor2 = data2; + CHECK(tensor2.shapes()[0] == 1); + CHECK(tensor2.shapes()[1] == 2); + CHECK(tensor2.ndim() == 2); + CHECK(tensor2.size() == 2); + CHECK(tensor2[0, 0] == 0.0f); + CHECK(tensor2[0, 1] == 0.0f); +} + +TEST_CASE("TensorTest - 3D Tensor Constructor from vector") { + vector>> data = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + Tensor<> tensor = data; + CHECK(tensor.shapes()[0] == 2); + CHECK(tensor.shapes()[1] == 2); + CHECK(tensor.shapes()[2] == 2); + CHECK(tensor.ndim() == 3); + CHECK(tensor.size() == 8); + CHECK(tensor[0, 0, 0] == 1.0f); + CHECK(tensor[0, 0, 1] == 2.0f); + CHECK(tensor[0, 1, 0] == 3.0f); + CHECK(tensor[0, 1, 1] == 4.0f); + CHECK(tensor[1, 1, 1] == 8.0f); + + vector>> data2 = {{{0.0f, 0.0f}, {0.0f, 0.0f}}, {{0.0f, 0.0f}, {0.0f, 0.0f}}}; + Tensor<> tensor2 = data2; + CHECK(tensor2.shapes()[0] == 2); + CHECK(tensor2.shapes()[1] == 2); + CHECK(tensor2.shapes()[2] == 2); + CHECK(tensor2.ndim() == 3); + CHECK(tensor2.size() == 8); + CHECK(tensor2[0, 0, 0] == 0.0f); + CHECK(tensor2[0, 0, 1] == 0.0f); + CHECK(tensor2[0, 1, 0] == 0.0f); + CHECK(tensor2[0, 1, 1] == 0.0f); + CHECK(tensor2[1, 1, 1] == 0.0f); +} + +TEST_CASE("TensorTest - Copy Constructor") { + // 1D tensor + Tensor<> tensor1 = {1.0f, 2.0f, 3.0f, 4.0f}; + Tensor<> test_tensor = tensor1; + CHECK(test_tensor.shapes()[0] == 4); + CHECK(test_tensor.ndim() == 1); + CHECK(test_tensor.size() == 4); + CHECK(test_tensor[0] == 1.0f); + CHECK(test_tensor[1] == 2.0f); + CHECK(test_tensor[2] == 3.0f); + CHECK(test_tensor[3] == 4.0f); + + // Scalar tensor + Tensor<> tensor2 = {0.0f}; + test_tensor = tensor2; + CHECK(test_tensor.shapes()[0] == 1); + CHECK(test_tensor.ndim() == 1); + CHECK(test_tensor.size() == 1); + CHECK(test_tensor[0] == 0.0f); + + // 2D tensor + Tensor<> tensor3 = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + test_tensor = tensor3; + CHECK(test_tensor.shapes()[0] == 2); + CHECK(test_tensor.shapes()[1] == 2); + CHECK(test_tensor.ndim() == 2); + CHECK(test_tensor.size() == 4); + CHECK(test_tensor[0, 0] == 1.0f); + CHECK(test_tensor[0, 1] == 2.0f); + CHECK(test_tensor[1, 0] == 3.0f); + CHECK(test_tensor[1, 1] == 4.0f); + + // 3D tensor + Tensor<> tensor4 = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + test_tensor = tensor4; + CHECK(test_tensor.shapes()[0] == 2); + CHECK(test_tensor.shapes()[1] == 2); + CHECK(test_tensor.shapes()[2] == 2); + CHECK(test_tensor.ndim() == 3); + CHECK(test_tensor.size() == 8); + CHECK(test_tensor[0, 0, 0] == 1.0f); + CHECK(test_tensor[0, 0, 1] == 2.0f); + CHECK(test_tensor[0, 1, 0] == 3.0f); + CHECK(test_tensor[0, 1, 1] == 4.0f); + CHECK(test_tensor[1, 1, 1] == 8.0f); +} + +TEST_CASE("TensorTest - Move Constructor") { + // 1D tensor + Tensor<> tensor1 = {1.0f, 2.0f, 3.0f, 4.0f}; + Tensor<> test_tensor = std::move(tensor1); + CHECK(test_tensor.shapes()[0] == 4); + CHECK(test_tensor.ndim() == 1); + CHECK(test_tensor.size() == 4); + CHECK(test_tensor[0] == 1.0f); + CHECK(test_tensor[1] == 2.0f); + CHECK(test_tensor[2] == 3.0f); + CHECK(test_tensor[3] == 4.0f); + + // Scalar tensor + Tensor<> tensor2 = {0.0f}; + test_tensor = std::move(tensor2); + CHECK(test_tensor.shapes()[0] == 1); + CHECK(test_tensor.ndim() == 1); + CHECK(test_tensor.size() == 1); + CHECK(test_tensor[0] == 0.0f); + + // 2D tensor + Tensor<> tensor3 = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + test_tensor = std::move(tensor3); + CHECK(test_tensor.shapes()[0] == 2); + CHECK(test_tensor.shapes()[1] == 2); + CHECK(test_tensor.ndim() == 2); + CHECK(test_tensor.size() == 4); + CHECK(test_tensor[0, 0] == 1.0f); + CHECK(test_tensor[0, 1] == 2.0f); + CHECK(test_tensor[1, 0] == 3.0f); + CHECK(test_tensor[1, 1] == 4.0f); + + // 3D tensor + Tensor<> tensor4 = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + test_tensor = std::move(tensor4); + CHECK(test_tensor.shapes()[0] == 2); + CHECK(test_tensor.shapes()[1] == 2); + CHECK(test_tensor.shapes()[2] == 2); + CHECK(test_tensor.ndim() == 3); + CHECK(test_tensor.size() == 8); + CHECK(test_tensor[0, 0, 0] == 1.0f); + CHECK(test_tensor[0, 0, 1] == 2.0f); + CHECK(test_tensor[0, 1, 0] == 3.0f); + CHECK(test_tensor[0, 1, 1] == 4.0f); + CHECK(test_tensor[1, 1, 1] == 8.0f); +} + +TEST_CASE("TensorTest - Certain Value Constructor") { + Tensor<> tensor_1d({1}, 0.0f); + CHECK(tensor_1d.shapes()[0] == 1); + CHECK(tensor_1d.ndim() == 1); + CHECK(tensor_1d.size() == 1); + CHECK(tensor_1d[0] == 0.0f); + + Tensor<> tensor_2d({2, 2}, 10.0f); + CHECK(tensor_2d.shapes()[0] == 2); + CHECK(tensor_2d.shapes()[1] == 2); + CHECK(tensor_2d.ndim() == 2); + CHECK(tensor_2d.size() == 4); + CHECK(tensor_2d[0, 0] == 10.0f); + CHECK(tensor_2d[0, 1] == 10.0f); + CHECK(tensor_2d[1, 0] == 10.0f); + CHECK(tensor_2d[1, 1] == 10.0f); + + + Tensor<> tensor_3d({2, 2, 2}, 5.0f); + CHECK(tensor_3d.shapes()[0] == 2); + CHECK(tensor_3d.shapes()[1] == 2); + CHECK(tensor_3d.shapes()[2] == 2); + CHECK(tensor_3d.ndim() == 3); + CHECK(tensor_3d.size() == 8); + CHECK(tensor_3d[0, 0, 0] == 5.0f); + CHECK(tensor_3d[0, 0, 1] == 5.0f); + CHECK(tensor_3d[0, 1, 0] == 5.0f); + CHECK(tensor_3d[0, 1, 1] == 5.0f); + CHECK(tensor_3d[1, 1, 1] == 5.0f); +} + +TEST_CASE("TensorTest - Indexing Operator") { + Tensor<> tensor = {1.0f, 2.0f, 3.0f, 4.0f}; + CHECK(tensor[0] == 1.0f); + CHECK(tensor[1] == 2.0f); + CHECK(tensor[2] == 3.0f); + CHECK(tensor[3] == 4.0f); + + tensor = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + CHECK(tensor[0, 0] == 1.0f); + CHECK(tensor[0, 1] == 2.0f); + CHECK(tensor[1, 0] == 3.0f); + CHECK(tensor[1, 1] == 4.0f); + + tensor = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + CHECK(tensor[0, 0, 0] == 1.0f); + CHECK(tensor[0, 0, 1] == 2.0f); + CHECK(tensor[0, 1, 0] == 3.0f); + CHECK(tensor[0, 1, 1] == 4.0f); + CHECK(tensor[1, 1, 1] == 8.0f); +} + +TEST_CASE("TensorTest - Indexing Operator - Out of Bound") { + Tensor<> tensor = {1.0f, 2.0f, 3.0f, 4.0f}; + CHECK_THROWS(tensor[4]); + + tensor = {{1.0f, 2.0f}, {3.0f, 4.0f}}; + CHECK_THROWS(tensor[2, 0]); + CHECK_THROWS(tensor[0, 2]); + + tensor = {{{1.0f, 2.0f}, {3.0f, 4.0f}}, {{5.0f, 6.0f}, {7.0f, 8.0f}}}; + CHECK_THROWS(tensor[2, 0, 0]); + CHECK_THROWS(tensor[0, 2, 0]); + CHECK_THROWS(tensor[0, 0, 2]); +} + diff --git a/unit_tests.sh b/unit_tests.sh index 0f599aa..f09f0b2 100755 --- a/unit_tests.sh +++ b/unit_tests.sh @@ -1,4 +1,4 @@ cd build-tests cmake -DBUILD_TESTS=ON -Wno-dev .. make -ctest \ No newline at end of file +ctest --rerun-failed --output-on-failure \ No newline at end of file