UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

535 lines (445 loc) 14.5 kB
/** * Utility functions for herta.js * Provides common utilities used across the framework */ const Decimal = require('decimal.js'); const Complex = require('complex.js'); // Utilities module const utils = {}; /** * Check if a value is a number (including numeric strings) * @param {any} value - The value to check * @returns {boolean} - True if the value is a number */ utils.isNumber = function (value) { if (typeof value === 'number') return true; if (typeof value === 'string') { return !isNaN(value) && !isNaN(parseFloat(value)); } return false; }; /** * Check if a value is a complex number * @param {any} value - The value to check * @returns {boolean} - True if the value is a complex number */ utils.isComplex = function (value) { return value instanceof Complex; }; /** * Check if a value is a matrix * @param {any} value - The value to check * @returns {boolean} - True if the value is a matrix */ utils.isMatrix = function (value) { return Array.isArray(value) && value.length > 0 && Array.isArray(value[0]); }; /** * Check if a value is a tensor (array with depth > 2) * @param {any} value - The value to check * @returns {boolean} - True if the value is a tensor */ utils.isTensor = function (value) { if (!Array.isArray(value) || value.length === 0) return false; if (!Array.isArray(value[0])) return false; // Check if at least one element in the first row is an array return value[0].some((item) => Array.isArray(item)); }; /** * Format a number to a specified precision * @param {number} value - The number to format * @param {number} [precision=14] - The number of significant digits * @returns {string} - The formatted number */ utils.format = function (value, precision = 14) { if (typeof value === 'number') { return new Decimal(value).toPrecision(precision); } return String(value); }; /** * Deep clone an object or array * @param {any} obj - The object to clone * @returns {any} - The cloned object */ utils.clone = function (obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof Complex) return new Complex(obj); if (Array.isArray(obj)) { return obj.map((item) => utils.clone(item)); } const result = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { result[key] = utils.clone(obj[key]); } } return result; }; /** * Check if two values are approximately equal * @param {number|Complex} a - First value * @param {number|Complex} b - Second value * @param {number} [epsilon=1e-12] - Tolerance * @returns {boolean} - True if values are approximately equal */ utils.approxEqual = function (a, b, epsilon = 1e-12) { if (utils.isComplex(a) || utils.isComplex(b)) { const aComplex = utils.isComplex(a) ? a : new Complex(a); const bComplex = utils.isComplex(b) ? b : new Complex(b); return Math.abs(aComplex.re - bComplex.re) < epsilon && Math.abs(aComplex.im - bComplex.im) < epsilon; } return Math.abs(a - b) < epsilon; }; /** * Generate a range of numbers * @param {number} start - Start value (inclusive) * @param {number} end - End value (inclusive) * @param {number} [step=1] - Step size * @returns {Array} - Array of numbers in the range */ utils.range = function (start, end, step = 1) { const result = []; if (step === 0) { throw new Error('Step cannot be zero'); } if ((step > 0 && start > end) || (step < 0 && start < end)) { return result; } for (let i = start; step > 0 ? i <= end : i >= end; i += step) { result.push(i); } return result; }; /** * Convert degrees to radians * @param {number} degrees - Angle in degrees * @returns {number} - Angle in radians */ utils.toRadians = function (degrees) { return degrees * (Math.PI / 180); }; /** * Convert radians to degrees * @param {number} radians - Angle in radians * @returns {number} - Angle in degrees */ utils.toDegrees = function (radians) { return radians * (180 / Math.PI); }; /** * Parse a mathematical expression string * @param {string} expr - The expression to parse * @returns {Object} - Parsed expression object */ utils.parseExpression = function (expr) { // This would implement a proper expression parser // For now, return a placeholder return { type: 'expression', value: expr }; }; /** * Check if a value is prime * @param {number} n - The number to check * @returns {boolean} - True if the number is prime */ utils.isPrime = function (n) { if (n <= 1) return false; if (n <= 3) return true; if (n % 2 === 0 || n % 3 === 0) return false; const limit = Math.sqrt(n); for (let i = 5; i <= limit; i += 6) { if (n % i === 0 || n % (i + 2) === 0) return false; } return true; }; /** * Calculate the factorial of a number * @param {number} n - The number * @returns {number} - The factorial */ utils.factorial = function (n) { if (n < 0) { throw new Error('Factorial is not defined for negative numbers'); } if (n === 0 || n === 1) return 1; let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result; }; /** * Calculate the binomial coefficient (n choose k) * @param {number} n - Total number of items * @param {number} k - Number of items to choose * @returns {number} - The binomial coefficient */ utils.binomial = function (n, k) { if (k < 0 || k > n) return 0; if (k === 0 || k === n) return 1; // Use symmetry to reduce calculations k = Math.min(k, n - k); let result = 1; for (let i = 1; i <= k; i++) { result *= (n - (i - 1)); result /= i; } return result; }; /** * Calculate the greatest common divisor (GCD) of two numbers * @param {number} a - First number * @param {number} b - Second number * @returns {number} - The GCD */ utils.gcd = function (a, b) { a = Math.abs(a); b = Math.abs(b); while (b !== 0) { const temp = b; b = a % b; a = temp; } return a; }; /** * Calculate the least common multiple (LCM) of two numbers * @param {number} a - First number * @param {number} b - Second number * @returns {number} - The LCM */ utils.lcm = function (a, b) { return Math.abs(a * b) / utils.gcd(a, b); }; /** * Calculate the mean of an array of numbers * @param {Array<number>} values - Array of numbers * @returns {number} - The arithmetic mean */ utils.mean = function (values) { if (!Array.isArray(values) || values.length === 0) { throw new Error('Input must be a non-empty array'); } const sum = values.reduce((acc, val) => acc + val, 0); return sum / values.length; }; /** * Calculate the median of an array of numbers * @param {Array<number>} values - Array of numbers * @returns {number} - The median value */ utils.median = function (values) { if (!Array.isArray(values) || values.length === 0) { throw new Error('Input must be a non-empty array'); } const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); if (sorted.length % 2 === 0) { return (sorted[mid - 1] + sorted[mid]) / 2; } return sorted[mid]; }; /** * Calculate the variance of an array of numbers * @param {Array<number>} values - Array of numbers * @param {boolean} [sample=true] - If true, calculates sample variance, otherwise population variance * @returns {number} - The variance */ utils.variance = function (values, sample = true) { if (!Array.isArray(values) || values.length === 0) { throw new Error('Input must be a non-empty array'); } const mean = utils.mean(values); const squaredDiffs = values.map((val) => (val - mean) ** 2); const sum = squaredDiffs.reduce((acc, val) => acc + val, 0); return sum / (values.length - (sample ? 1 : 0)); }; /** * Calculate the standard deviation of an array of numbers * @param {Array<number>} values - Array of numbers * @param {boolean} [sample=true] - If true, calculates sample standard deviation, otherwise population * @returns {number} - The standard deviation */ utils.standardDeviation = function (values, sample = true) { return Math.sqrt(utils.variance(values, sample)); }; /** * Calculate the covariance between two arrays of numbers * @param {Array<number>} xValues - First array of numbers * @param {Array<number>} yValues - Second array of numbers * @param {boolean} [sample=true] - If true, calculates sample covariance, otherwise population * @returns {number} - The covariance */ utils.covariance = function (xValues, yValues, sample = true) { if (!Array.isArray(xValues) || !Array.isArray(yValues) || xValues.length === 0 || yValues.length === 0) { throw new Error('Inputs must be non-empty arrays'); } if (xValues.length !== yValues.length) { throw new Error('Arrays must have the same length'); } const xMean = utils.mean(xValues); const yMean = utils.mean(yValues); let sum = 0; for (let i = 0; i < xValues.length; i++) { sum += (xValues[i] - xMean) * (yValues[i] - yMean); } return sum / (xValues.length - (sample ? 1 : 0)); }; /** * Calculate the Pearson correlation coefficient between two arrays of numbers * @param {Array<number>} xValues - First array of numbers * @param {Array<number>} yValues - Second array of numbers * @returns {number} - The correlation coefficient (between -1 and 1) */ utils.correlation = function (xValues, yValues) { const xStdDev = utils.standardDeviation(xValues); const yStdDev = utils.standardDeviation(yValues); if (xStdDev === 0 || yStdDev === 0) { return 0; // No correlation when there's no variation } return utils.covariance(xValues, yValues) / (xStdDev * yStdDev); }; /** * Perform numerical integration using the trapezoidal rule * @param {Function} func - The function to integrate * @param {number} a - Lower bound * @param {number} b - Upper bound * @param {number} [n=100] - Number of intervals * @returns {number} - The approximate integral value */ utils.integrate = function (func, a, b, n = 100) { if (typeof func !== 'function') { throw new Error('First argument must be a function'); } if (a >= b) { throw new Error('Upper bound must be greater than lower bound'); } const h = (b - a) / n; let sum = 0.5 * (func(a) + func(b)); for (let i = 1; i < n; i++) { const x = a + i * h; sum += func(x); } return h * sum; }; /** * Find the root of a function using the Newton-Raphson method * @param {Function} func - The function to find the root of * @param {Function} derivative - The derivative of the function * @param {number} initialGuess - Initial guess for the root * @param {number} [tolerance=1e-10] - Convergence tolerance * @param {number} [maxIterations=100] - Maximum number of iterations * @returns {number} - The approximate root */ utils.findRoot = function (func, derivative, initialGuess, tolerance = 1e-10, maxIterations = 100) { if (typeof func !== 'function' || typeof derivative !== 'function') { throw new Error('First two arguments must be functions'); } let x = initialGuess; let iteration = 0; while (iteration < maxIterations) { const fx = func(x); if (Math.abs(fx) < tolerance) { return x; // Converged to a root } const dfx = derivative(x); if (dfx === 0) { throw new Error('Derivative is zero, cannot continue'); } const xNew = x - fx / dfx; if (Math.abs(xNew - x) < tolerance) { return xNew; // Converged to a root } x = xNew; iteration++; } throw new Error(`Method did not converge after ${maxIterations} iterations`); }; /** * Calculate the probability density function (PDF) of a normal distribution * @param {number} x - The value to calculate the PDF at * @param {number} [mean=0] - The mean of the distribution * @param {number} [stdDev=1] - The standard deviation of the distribution * @returns {number} - The PDF value */ utils.normalPDF = function (x, mean = 0, stdDev = 1) { if (stdDev <= 0) { throw new Error('Standard deviation must be positive'); } const coefficient = 1 / (stdDev * Math.sqrt(2 * Math.PI)); const exponent = -((x - mean) ** 2) / (2 * stdDev ** 2); return coefficient * Math.exp(exponent); }; /** * Calculate the cumulative distribution function (CDF) of a normal distribution * @param {number} x - The value to calculate the CDF at * @param {number} [mean=0] - The mean of the distribution * @param {number} [stdDev=1] - The standard deviation of the distribution * @returns {number} - The CDF value (between 0 and 1) */ utils.normalCDF = function (x, mean = 0, stdDev = 1) { if (stdDev <= 0) { throw new Error('Standard deviation must be positive'); } // Use error function approximation for normal CDF const z = (x - mean) / (stdDev * Math.sqrt(2)); return 0.5 * (1 + utils.erf(z)); }; /** * Error function approximation * @param {number} x - Input value * @returns {number} - Error function value */ utils.erf = function (x) { // Constants for approximation const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; // Save the sign const sign = x < 0 ? -1 : 1; x = Math.abs(x); // Approximation formula const t = 1.0 / (1.0 + p * x); const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; }; /** * Perform polynomial interpolation using Lagrange method * @param {Array<number>} xValues - Array of x coordinates * @param {Array<number>} yValues - Array of y coordinates * @returns {Function} - Interpolation function that takes an x value and returns the interpolated y value */ utils.interpolate = function (xValues, yValues) { if (!Array.isArray(xValues) || !Array.isArray(yValues)) { throw new Error('Inputs must be arrays'); } if (xValues.length !== yValues.length) { throw new Error('Arrays must have the same length'); } if (xValues.length === 0) { throw new Error('Arrays cannot be empty'); } // Return the interpolation function return function (x) { let result = 0; for (let i = 0; i < xValues.length; i++) { let term = yValues[i]; for (let j = 0; j < xValues.length; j++) { if (j !== i) { term *= (x - xValues[j]) / (xValues[i] - xValues[j]); } } result += term; } return result; }; }; module.exports = utils;