UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

740 lines (618 loc) 22.3 kB
/** * Core tensor operations for herta.js * Provides tensor manipulation, contractions, and tensor calculus operations */ // These dependencies are used throughout tensor operations const Decimal = require('decimal.js'); const Complex = require('complex.js'); const tensorContraction = require('tensor-contraction'); // Tensor module const tensor = {}; /** * Create a tensor from nested arrays * @param {Array} data - Nested arrays representing the tensor * @param {Array} [dimensions] - Optional dimensions array * @returns {Object} - Tensor object with operations */ tensor.create = function (data, dimensions) { // Determine dimensions if not provided if (!dimensions) { dimensions = getDimensions(data); } // Validate data against dimensions validateTensorData(data, dimensions); return { // Tensor data data: deepCopy(data), // Dimensions dimensions: [...dimensions], rank: dimensions.length, /** * Get an element at the specified indices * @param {...number} indices - Indices for each dimension * @returns {number|Array} - The element or subtensor at the specified indices */ get(...indices) { if (indices.length > this.rank) { throw new Error(`Too many indices: expected ${this.rank}, got ${indices.length}`); } let result = this.data; for (let i = 0; i < indices.length; i++) { const index = indices[i]; if (index < 0 || index >= this.dimensions[i]) { throw new Error(`Index out of bounds: ${index} at dimension ${i}`); } result = result[index]; } return result; }, /** * Set an element at the specified indices * @param {...number} args - Indices followed by the value to set */ set(...args) { if (args.length !== this.rank + 1) { throw new Error(`Invalid number of arguments: expected ${this.rank + 1}, got ${args.length}`); } const indices = args.slice(0, this.rank); const value = args[this.rank]; let target = this.data; for (let i = 0; i < indices.length - 1; i++) { const index = indices[i]; if (index < 0 || index >= this.dimensions[i]) { throw new Error(`Index out of bounds: ${index} at dimension ${i}`); } target = target[index]; } const lastIndex = indices[indices.length - 1]; if (lastIndex < 0 || lastIndex >= this.dimensions[indices.length - 1]) { throw new Error(`Index out of bounds: ${lastIndex} at dimension ${indices.length - 1}`); } target[lastIndex] = value; }, /** * Add another tensor to this tensor * @param {Object} other - The tensor to add * @returns {Object} - New tensor containing the sum */ add(other) { // Check if dimensions match if (this.rank !== other.rank || !arraysEqual(this.dimensions, other.dimensions)) { throw new Error('Tensor dimensions must match for addition.'); } // Perform element-wise addition const result = elementWiseOperation(this.data, other.data, this.dimensions, (a, b) => a + b); return tensor.create(result, this.dimensions); }, /** * Subtract another tensor from this tensor * @param {Object} other - The tensor to subtract * @returns {Object} - New tensor containing the difference */ subtract(other) { // Check if dimensions match if (this.rank !== other.rank || !arraysEqual(this.dimensions, other.dimensions)) { throw new Error('Tensor dimensions must match for subtraction.'); } // Perform element-wise subtraction const result = elementWiseOperation(this.data, other.data, this.dimensions, (a, b) => a - b); return tensor.create(result, this.dimensions); }, /** * Multiply this tensor by a scalar * @param {number} scalar - The scalar to multiply by * @returns {Object} - New tensor containing the product */ scalarMultiply(scalar) { // Perform element-wise multiplication by scalar const result = scalarOperation(this.data, this.dimensions, (value) => value * scalar); return tensor.create(result, this.dimensions); }, /** * Perform element-wise multiplication with another tensor * @param {Object} other - The tensor to multiply with * @returns {Object} - New tensor containing the element-wise product */ elementMultiply(other) { // Check if dimensions match if (this.rank !== other.rank || !arraysEqual(this.dimensions, other.dimensions)) { throw new Error('Tensor dimensions must match for element-wise multiplication.'); } // Perform element-wise multiplication const result = elementWiseOperation(this.data, other.data, this.dimensions, (a, b) => a * b); return tensor.create(result, this.dimensions); }, /** * Perform tensor contraction along specified dimensions * @param {number} dim1 - First dimension to contract * @param {number} dim2 - Second dimension to contract * @returns {Object} - New tensor after contraction */ contract(dim1, dim2) { if (dim1 < 0 || dim1 >= this.rank || dim2 < 0 || dim2 >= this.rank) { throw new Error(`Invalid dimensions for contraction: ${dim1}, ${dim2}`); } if (dim1 === dim2) { throw new Error('Cannot contract a dimension with itself.'); } if (this.dimensions[dim1] !== this.dimensions[dim2]) { throw new Error(`Dimensions for contraction must have the same size: ${this.dimensions[dim1]} != ${this.dimensions[dim2]}`); } // This would implement tensor contraction // For now, return a placeholder const newDimensions = this.dimensions.filter((_, i) => i !== dim1 && i !== dim2); const resultData = createZeroTensor(newDimensions); return tensor.create(resultData, newDimensions); }, /** * Perform tensor product with another tensor * @param {Object} other - The tensor to multiply with * @returns {Object} - New tensor containing the tensor product */ tensorProduct(other) { // Calculate dimensions of the result const resultDimensions = [...this.dimensions, ...other.dimensions]; // Create a zero tensor with the result dimensions const resultData = createZeroTensor(resultDimensions); // This would implement the tensor product // For now, return a placeholder return tensor.create(resultData, resultDimensions); }, /** * Transpose the tensor by swapping dimensions * @param {...number} permutation - New order of dimensions * @returns {Object} - Transposed tensor */ transpose(...permutation) { // If no permutation is provided, reverse the dimensions if (permutation.length === 0) { permutation = Array.from({ length: this.rank }, (_, i) => this.rank - i - 1); } // Validate permutation if (permutation.length !== this.rank) { throw new Error(`Invalid permutation length: expected ${this.rank}, got ${permutation.length}`); } // Check if permutation contains all dimensions const sorted = [...permutation].sort((a, b) => a - b); for (let i = 0; i < this.rank; i++) { if (sorted[i] !== i) { throw new Error(`Invalid permutation: must contain all dimensions from 0 to ${this.rank - 1}`); } } // Calculate new dimensions const newDimensions = permutation.map((i) => this.dimensions[i]); // Create a zero tensor with the new dimensions const resultData = createZeroTensor(newDimensions); // This would implement the tensor transposition // For now, return a placeholder return tensor.create(resultData, newDimensions); }, /** * Reshape the tensor to new dimensions * @param {...number} newDimensions - New dimensions * @returns {Object} - Reshaped tensor */ reshape(...newDimensions) { // Calculate total number of elements const totalElements = this.dimensions.reduce((a, b) => a * b, 1); const newTotalElements = newDimensions.reduce((a, b) => a * b, 1); if (totalElements !== newTotalElements) { throw new Error(`Cannot reshape tensor of size ${totalElements} to size ${newTotalElements}`); } // Flatten the tensor data const flatData = flattenTensor(this.data, this.dimensions); // Reshape the flattened data const resultData = reshapeTensor(flatData, newDimensions); return tensor.create(resultData, newDimensions); }, /** * Convert the tensor to a string * @returns {string} - String representation */ toString() { return JSON.stringify(this.data, null, 2); } }; }; /** * Get the dimensions of nested arrays * @private * @param {Array} data - Nested arrays * @returns {Array} - Array of dimensions */ function getDimensions(data) { const dimensions = []; let current = data; while (Array.isArray(current)) { dimensions.push(current.length); current = current[0]; } return dimensions; } /** * Validate tensor data against dimensions * @private * @param {Array} data - Nested arrays * @param {Array} dimensions - Expected dimensions * @param {number} [level=0] - Current nesting level */ function validateTensorData(data, dimensions, level = 0) { if (level === dimensions.length) { if (Array.isArray(data)) { throw new Error(`Expected scalar at level ${level}, got array`); } return; } if (!Array.isArray(data)) { throw new Error(`Expected array at level ${level}, got scalar`); } if (data.length !== dimensions[level]) { throw new Error(`Expected length ${dimensions[level]} at level ${level}, got ${data.length}`); } for (let i = 0; i < data.length; i++) { validateTensorData(data[i], dimensions, level + 1); } } /** * Create a deep copy of nested arrays * @private * @param {Array} data - Nested arrays * @returns {Array} - Deep copy */ function deepCopy(data) { if (!Array.isArray(data)) { return data; } return data.map((item) => deepCopy(item)); } /** * Check if two arrays are equal * @private * @param {Array} a - First array * @param {Array} b - Second array * @returns {boolean} - True if arrays are equal */ function arraysEqual(a, b) { if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } return true; } /** * Perform element-wise operation on two tensors * @private * @param {Array} a - First tensor data * @param {Array} b - Second tensor data * @param {Array} dimensions - Tensor dimensions * @param {Function} operation - Operation to perform * @param {number} [level=0] - Current nesting level * @returns {Array} - Result of operation */ function elementWiseOperation(a, b, dimensions, operation, level = 0) { if (level === dimensions.length) { return operation(a, b); } const result = []; for (let i = 0; i < dimensions[level]; i++) { result.push(elementWiseOperation(a[i], b[i], dimensions, operation, level + 1)); } return result; } /** * Perform scalar operation on a tensor * @private * @param {Array} data - Tensor data * @param {Array} dimensions - Tensor dimensions * @param {Function} operation - Operation to perform * @param {number} [level=0] - Current nesting level * @returns {Array} - Result of operation */ function scalarOperation(data, dimensions, operation, level = 0) { if (level === dimensions.length) { return operation(data); } const result = []; for (let i = 0; i < dimensions[level]; i++) { result.push(scalarOperation(data[i], dimensions, operation, level + 1)); } return result; } /** * Create a tensor filled with zeros * @private * @param {Array} dimensions - Tensor dimensions * @param {number} [level=0] - Current nesting level * @returns {Array} - Zero tensor */ function createZeroTensor(dimensions, level = 0) { if (level === dimensions.length) { return 0; } const result = []; for (let i = 0; i < dimensions[level]; i++) { result.push(createZeroTensor(dimensions, level + 1)); } return result; } /** * Flatten a tensor to a 1D array * @private * @param {Array} data - Tensor data * @param {Array} dimensions - Tensor dimensions * @param {number} [level=0] - Current nesting level * @returns {Array} - Flattened array */ function flattenTensor(data, dimensions, level = 0) { if (level === dimensions.length) { return [data]; } const result = []; for (let i = 0; i < dimensions[level]; i++) { result.push(...flattenTensor(data[i], dimensions, level + 1)); } return result; } /** * Reshape a flattened array to tensor dimensions * @private * @param {Array} flatData - Flattened array * @param {Array} dimensions - Target dimensions * @param {number} [level=0] - Current nesting level * @param {number} [offset=0] - Current offset in flat data * @returns {Array} - Reshaped tensor */ function reshapeTensor(flatData, dimensions, level = 0, offset = 0) { if (level === dimensions.length - 1) { const result = []; for (let i = 0; i < dimensions[level]; i++) { result.push(flatData[offset + i]); } return result; } const result = []; const size = dimensions.slice(level + 1).reduce((a, b) => a * b, 1); for (let i = 0; i < dimensions[level]; i++) { result.push(reshapeTensor(flatData, dimensions, level + 1, offset + i * size)); } return result; } /** * Create a tensor with ones on the diagonal and zeros elsewhere * @param {number} size - Size of each dimension * @param {number} rank - Number of dimensions * @returns {Object} - Identity tensor */ tensor.identity = function (size, rank) { if (rank < 2) { throw new Error('Identity tensor must have at least rank 2.'); } // Create dimensions array const dimensions = Array(rank).fill(size); // Create a zero tensor const data = createZeroTensor(dimensions); // Set ones on the diagonal // This is a simplified implementation for rank 2 if (rank === 2) { for (let i = 0; i < size; i++) { data[i][i] = 1; } } // For higher ranks, we would need a more complex implementation return tensor.create(data, dimensions); }; /** * Create a tensor filled with zeros * @param {...number} dimensions - Dimensions of the tensor * @returns {Object} - Zero tensor */ tensor.zeros = function (...dimensions) { if (dimensions.length === 0) { throw new Error('Tensor dimensions must be specified.'); } const data = createZeroTensor(dimensions); return tensor.create(data, dimensions); }; /** * Create a tensor filled with ones * @param {...number} dimensions - Dimensions of the tensor * @returns {Object} - Tensor of ones */ tensor.ones = function (...dimensions) { if (dimensions.length === 0) { throw new Error('Tensor dimensions must be specified.'); } const data = createZeroTensor(dimensions); const flatData = flattenTensor(data, dimensions); for (let i = 0; i < flatData.length; i++) { flatData[i] = 1; } const result = reshapeTensor(flatData, dimensions); return tensor.create(result, dimensions); }; /** * Create a tensor from a function * @param {Function} func - Function that takes indices and returns a value * @param {...number} dimensions - Dimensions of the tensor * @returns {Object} - Tensor with values from the function */ tensor.fromFunction = function (func, ...dimensions) { if (dimensions.length === 0) { throw new Error('Tensor dimensions must be specified.'); } // Create a zero tensor with the specified dimensions const data = createZeroTensor(dimensions); // Fill the tensor with values from the function fillTensorFromFunction(data, dimensions, func); return tensor.create(data, dimensions); }; /** * Fill a tensor with values from a function * @private * @param {Array} data - Tensor data to fill * @param {Array} dimensions - Tensor dimensions * @param {Function} func - Function that takes indices and returns a value * @param {Array} [indices=[]] - Current indices * @param {number} [level=0] - Current nesting level */ function fillTensorFromFunction(data, dimensions, func, indices = [], level = 0) { if (level === dimensions.length) { return func(...indices); } for (let i = 0; i < dimensions[level]; i++) { const newIndices = [...indices, i]; data[i] = fillTensorFromFunction(data[i], dimensions, func, newIndices, level + 1); } return data; } /** * Compute the outer product of two tensors * @param {Object} a - First tensor * @param {Object} b - Second tensor * @returns {Object} - Outer product tensor */ tensor.outerProduct = function (a, b) { return a.tensorProduct(b); }; /** * Compute the inner product of two tensors * @param {Object} a - First tensor * @param {Object} b - Second tensor * @returns {number} - Inner product */ tensor.innerProduct = function (a, b) { // For vectors (rank 1 tensors) if (a.rank === 1 && b.rank === 1 && a.dimensions[0] === b.dimensions[0]) { let sum = 0; for (let i = 0; i < a.dimensions[0]; i++) { sum += a.get(i) * b.get(i); } return sum; } // For higher rank tensors, we would need to implement contraction throw new Error('Inner product for tensors with rank > 1 not implemented yet.'); }; /** * Compute the tensor contraction * @param {Object} t - The tensor * @param {number} dim1 - First dimension to contract * @param {number} dim2 - Second dimension to contract * @returns {Object} - Contracted tensor */ tensor.contract = function (t, dim1, dim2) { // Validate input parameters if (!t || !t.data || !t.dimensions) { throw new Error('Invalid tensor provided for contraction'); } if (dim1 < 0 || dim1 >= t.rank || dim2 < 0 || dim2 >= t.rank) { throw new Error(`Contraction dimensions out of bounds: (${dim1}, ${dim2}) for rank ${t.rank}`); } if (dim1 === dim2) { throw new Error('Cannot contract a dimension with itself'); } // Ensure dimensions match for contraction if (t.dimensions[dim1] !== t.dimensions[dim2]) { throw new Error(`Contraction dimensions must have same size: ${t.dimensions[dim1]} ≠ ${t.dimensions[dim2]}`); } try { // Use the tensor-contraction library for the actual contraction const contractionResult = tensorContraction.contract(t.data, [dim1, dim2]); // Calculate dimensions of the result tensor const newDimensions = t.dimensions.filter((_, i) => i !== dim1 && i !== dim2); // If result is a scalar (all dimensions contracted) if (newDimensions.length === 0) { return contractionResult; } // Return the contracted tensor return tensor.create(contractionResult, newDimensions); } catch (error) { // Fall back to our implementation if library fails console.warn('Tensor-contraction library failed, using fallback implementation'); // Sort dimensions for easier handling const [minDim, maxDim] = [dim1, dim2].sort((a, b) => a - b); // Calculate dimensions of the result tensor const newDimensions = t.dimensions.filter((_, i) => i !== minDim && i !== maxDim); // If we're contracting all dimensions, result is a scalar if (newDimensions.length === 0) { let result = 0; const contractionSize = t.dimensions[dim1]; // For a 2D tensor (matrix trace) if (t.rank === 2) { for (let i = 0; i < contractionSize; i++) { result += t.data[i][i]; } return result; } } // Create result tensor data structure const resultData = this.createZeroTensor(newDimensions); // Perform the contraction const contractionResult = performContraction(t.data, t.dimensions, minDim, maxDim, resultData, newDimensions); // Return the contracted tensor return tensor.create(contractionResult, newDimensions); } }; /** * Helper function to perform tensor contraction * @private * @param {Array} data - Tensor data * @param {Array} dimensions - Original dimensions * @param {number} dim1 - First contraction dimension * @param {number} dim2 - Second contraction dimension * @param {Array} resultData - Result data structure (optional) * @param {Array} resultDimensions - Result dimensions (optional) * @returns {Array|number} - Contracted tensor data or scalar */ function performContraction(data, dimensions, dim1, dim2, resultData, resultDimensions) { // For 2D case (matrix), use direct approach if (dimensions.length === 2) { let result = 0; for (let i = 0; i < dimensions[0]; i++) { result += data[i][i]; } return result; } // For higher dimensions, we would implement a more complex algorithm here // that properly handles the multi-dimensional contraction // A full implementation would need to: // 1. Iterate through all indices excluding the contracted dimensions // 2. For each set of indices, sum over the contracted dimensions // 3. Place the result in the appropriate position in resultData // This is a simplified placeholder that handles 2D and 3D cases if (dimensions.length === 3 && ((dim1 === 0 && dim2 === 1) || (dim1 === 1 && dim2 === 2))) { // Contract first two dimensions or last two dimensions of 3D tensor const free = dim1 === 0 ? 2 : 0; const contractionSize = dimensions[dim1]; const resultSize = dimensions[free]; // Create 1D result array const result = new Array(resultSize).fill(0); // Perform contraction for (let i = 0; i < contractionSize; i++) { for (let j = 0; j < resultSize; j++) { if (free === 2) { result[j] += data[i][i][j]; } else { result[j] += data[j][i][i]; } } } return result; } // For more complex cases, just return a dummy tensor // In a full implementation, this would be replaced with proper logic return resultData || 0; } /** * Compute the tensor product * @param {Object} a - First tensor * @param {Object} b - Second tensor * @returns {Object} - Tensor product */ tensor.product = function (a, b) { return a.tensorProduct(b); }; module.exports = tensor;