UNPKG

ndarray-ts

Version:

A basic N-dimensional array library in TypeScript, similar to NumPy.

299 lines (298 loc) 11.7 kB
"use strict"; /** * @file NdArray.ts * @description The core NdArray class for N-dimensional array operations. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NdArray = void 0; /** * Represents an N-dimensional array, providing functionalities similar to NumPy's ndarray. */ class NdArray { /** * Creates an instance of NdArray. * @param data The flat array of numbers. * @param shape The shape (dimensions) of the array. * @throws {Error} If data length does not match the product of shape dimensions. */ constructor(data, shape) { this.shape = shape; this.ndim = shape.length; this.size = shape.reduce((acc, dim) => acc * dim, 1); if (data.length !== this.size) { throw new Error(`Data length (${data.length}) does not match expected size (${this.size}) for shape ${shape}`); } this.data = [...data]; // Create a copy to ensure immutability of original data array this.strides = this.calculateStrides(shape); } /** * Calculates the strides for each dimension. * Strides indicate how many elements to skip in the flat data array to move to the next element along a dimension. * @param shape The shape of the array. * @returns An array of strides. */ calculateStrides(shape) { const strides = new Array(shape.length).fill(1); for (let i = shape.length - 2; i >= 0; i--) { strides[i] = strides[i + 1] * shape[i + 1]; } return strides; } /** * Calculates the flat index in the data array from multi-dimensional indices. * @param indices An array of indices for each dimension. * @returns The flat index. * @throws {Error} If the number of indices does not match the number of dimensions, * or if any index is out of bounds. */ getFlatIndex(indices) { if (indices.length !== this.ndim) { throw new Error(`Expected ${this.ndim} indices, but got ${indices.length}`); } let flatIndex = 0; for (let i = 0; i < this.ndim; i++) { if (indices[i] < 0 || indices[i] >= this.shape[i]) { throw new Error(`Index ${indices[i]} out of bounds for dimension ${i} with size ${this.shape[i]}`); } flatIndex += indices[i] * this.strides[i]; } return flatIndex; } /** * Creates a new NdArray filled with zeros. * @param shape The shape of the new array. * @returns A new NdArray instance. */ static zeros(shape) { const size = shape.reduce((acc, dim) => acc * dim, 1); const data = new Array(size).fill(0); return new NdArray(data, shape); } /** * Creates a new NdArray filled with ones. * @param shape The shape of the new array. * @returns A new NdArray instance. */ static ones(shape) { const size = shape.reduce((acc, dim) => acc * dim, 1); const data = new Array(size).fill(1); return new NdArray(data, shape); } /** * Creates a new NdArray with evenly spaced values within a given interval. * Similar to NumPy's `arange`. * @param start The start of the interval (inclusive). * @param stop The end of the interval (exclusive). * @param step The spacing between values (default: 1). * @returns A new NdArray instance. */ static arange(start, stop, step = 1) { const data = []; for (let i = start; i < stop; i += step) { data.push(i); } return new NdArray(data, [data.length]); } /** * Returns the element at the specified multi-dimensional indices. * @param indices An array of indices for each dimension. * @returns The value at the given indices. */ get(...indices) { const flatIndex = this.getFlatIndex(indices); return this.data[flatIndex]; } /** * Sets the element at the specified multi-dimensional indices to a new value. * @param indices An array of indices for each dimension. * @param value The new value to set. */ set(indices, value) { const flatIndex = this.getFlatIndex(indices); this.data[flatIndex] = value; } /** * Reshapes the array to a new shape. The total number of elements must remain the same. * @param newShape The new shape (dimensions) for the array. * @returns A new NdArray instance with the reshaped view of the same data. * @throws {Error} If the new shape does not result in the same total number of elements. */ reshape(newShape) { const newSize = newShape.reduce((acc, dim) => acc * dim, 1); if (newSize !== this.size) { throw new Error(`Cannot reshape array of size ${this.size} into shape ${newShape} with size ${newSize}`); } // Create a new instance with a copy of the data and the new shape return new NdArray([...this.data], newShape); } /** * Performs element-wise addition with another NdArray or a scalar. * @param other The other NdArray or a number to add. * @returns A new NdArray with the result. * @throws {Error} If shapes are not compatible for element-wise operation. */ add(other) { const resultData = new Array(this.size); if (typeof other === 'number') { for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] + other; } } else { if (!this.isShapeCompatible(other)) { throw new Error(`Operands could not be broadcast together with shapes ${this.shape} and ${other.shape}`); } for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] + other.data[i]; } } return new NdArray(resultData, [...this.shape]); } /** * Performs element-wise subtraction with another NdArray or a scalar. * @param other The other NdArray or a number to subtract. * @returns A new NdArray with the result. * @throws {Error} If shapes are not compatible for element-wise operation. */ subtract(other) { const resultData = new Array(this.size); if (typeof other === 'number') { for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] - other; } } else { if (!this.isShapeCompatible(other)) { throw new Error(`Operands could not be broadcast together with shapes ${this.shape} and ${other.shape}`); } for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] - other.data[i]; } } return new NdArray(resultData, [...this.shape]); } /** * Performs element-wise multiplication with another NdArray or a scalar. * @param other The other NdArray or a number to multiply. * @returns A new NdArray with the result. * @throws {Error} If shapes are not compatible for element-wise operation. */ multiply(other) { const resultData = new Array(this.size); if (typeof other === 'number') { for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] * other; } } else { if (!this.isShapeCompatible(other)) { throw new Error(`Operands could not be broadcast together with shapes ${this.shape} and ${other.shape}`); } for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] * other.data[i]; } } return new NdArray(resultData, [...this.shape]); } /** * Performs element-wise division with another NdArray or a scalar. * @param other The other NdArray or a number to divide by. * @returns A new NdArray with the result. * @throws {Error} If shapes are not compatible for element-wise operation, or division by zero occurs. */ divide(other) { const resultData = new Array(this.size); if (typeof other === 'number') { if (other === 0) throw new Error("Division by zero"); for (let i = 0; i < this.size; i++) { resultData[i] = this.data[i] / other; } } else { if (!this.isShapeCompatible(other)) { throw new Error(`Operands could not be broadcast together with shapes ${this.shape} and ${other.shape}`); } for (let i = 0; i < this.size; i++) { if (other.data[i] === 0) throw new Error("Division by zero encountered in element-wise division"); resultData[i] = this.data[i] / other.data[i]; } } return new NdArray(resultData, [...this.shape]); } /** * Performs matrix multiplication (dot product) for 2D arrays. * @param other The other NdArray to multiply with. * @returns A new NdArray with the result of the dot product. * @throws {Error} If arrays are not 2D or shapes are incompatible for dot product. */ dot(other) { if (this.ndim !== 2 || other.ndim !== 2) { throw new Error("Dot product currently only supported for 2D arrays."); } if (this.shape[1] !== other.shape[0]) { throw new Error(`Shapes incompatible for dot product: (${this.shape}) vs (${other.shape})`); } const rowsA = this.shape[0]; const colsA = this.shape[1]; const colsB = other.shape[1]; const resultData = new Array(rowsA * colsB).fill(0); const result = new NdArray(resultData, [rowsA, colsB]); for (let i = 0; i < rowsA; i++) { for (let j = 0; j < colsB; j++) { let sum = 0; for (let k = 0; k < colsA; k++) { sum += this.get(i, k) * other.get(k, j); } result.set([i, j], sum); } } return result; } /** * Checks if the shape of another NdArray is compatible for element-wise operations. * For simplicity, this implementation only checks for exact shape match. * A full NumPy-like broadcasting mechanism would be more complex. * @param other The other NdArray. * @returns True if shapes are compatible, false otherwise. */ isShapeCompatible(other) { if (this.ndim !== other.ndim) { return false; } for (let i = 0; i < this.ndim; i++) { if (this.shape[i] !== other.shape[i]) { return false; } } return true; } /** * Returns a string representation of the NdArray. * @returns A string representing the array. */ toString() { let str = `NdArray(shape=${this.shape}, data=[\n`; // Simple 1D/2D representation for readability if (this.ndim === 1) { str += ` [${this.data.join(', ')}]\n`; } else if (this.ndim === 2) { for (let i = 0; i < this.shape[0]; i++) { const row = []; for (let j = 0; j < this.shape[1]; j++) { row.push(this.get(i, j)); } str += ` [${row.join(', ')}]\n`; } } else { // For higher dimensions, just show flat data and shape str += ` ${this.data.join(', ')}\n`; } str += `])`; return str; } } exports.NdArray = NdArray;