UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

834 lines (691 loc) 25.5 kB
/** * Advanced numerical methods module for herta.js * Provides numerical algorithms for scientific computing */ const Decimal = require('decimal.js'); // Numerical methods module const numerical = {}; /** * Root-finding methods */ numerical.roots = {}; /** * Find a root of a function using Newton's method * @param {Function|string} f - The function to find roots for * @param {Function|string} [df] - The derivative of f (optional, will use numerical differentiation if not provided) * @param {number} x0 - Initial guess * @param {Object} [options] - Additional options * @returns {number} - The root of the function */ numerical.roots.newton = function (f, df, x0, options = {}) { // Default options const defaultOptions = { maxIterations: 100, tolerance: 1e-10, epsilon: 1e-8 // For numerical differentiation }; const config = { ...defaultOptions, ...options }; // Handle case where derivative is not provided if (typeof df !== 'function' && typeof df !== 'string') { options = x0 || {}; x0 = df; // Use numerical differentiation df = (x) => { const h = config.epsilon; const fx = typeof f === 'function' ? f(x) : evaluateExpression(f, { x }); const fxh = typeof f === 'function' ? f(x + h) : evaluateExpression(f, { x: x + h }); return (fxh - fx) / h; }; } // Convert to functions if strings const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); const dfFunc = typeof df === 'function' ? df : (x) => evaluateExpression(df, { x }); // Newton's method iteration let x = x0; for (let i = 0; i < config.maxIterations; i++) { const fx = fFunc(x); const dfx = dfFunc(x); // Check for division by zero if (Math.abs(dfx) < config.tolerance) { throw new Error('Derivative too close to zero'); } // Newton step const xNew = x - fx / dfx; // Check for convergence if (Math.abs(xNew - x) < config.tolerance) { return xNew; } x = xNew; } throw new Error(`Newton's method did not converge after ${config.maxIterations} iterations`); }; /** * Find a root of a function using the bisection method * @param {Function|string} f - The function to find roots for * @param {number} a - Lower bound of the interval * @param {number} b - Upper bound of the interval * @param {Object} [options] - Additional options * @returns {number} - The root of the function */ numerical.roots.bisection = function (f, a, b, options = {}) { // Default options const defaultOptions = { maxIterations: 100, tolerance: 1e-10 }; const config = { ...defaultOptions, ...options }; // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); // Check if the function changes sign in the interval const fa = fFunc(a); const fb = fFunc(b); if (fa * fb > 0) { throw new Error('Function must have opposite signs at interval endpoints'); } // Bisection method iteration let left = a; let right = b; for (let i = 0; i < config.maxIterations; i++) { const mid = (left + right) / 2; const fMid = fFunc(mid); // Check for convergence if (Math.abs(right - left) < config.tolerance || Math.abs(fMid) < config.tolerance) { return mid; } // Update interval if (fa * fMid < 0) { right = mid; } else { left = mid; } } throw new Error(`Bisection method did not converge after ${config.maxIterations} iterations`); }; /** * Differential equation solvers */ numerical.ode = {}; /** * Solve an ordinary differential equation using the Runge-Kutta 4th order method * @param {Function|string} f - The ODE function dy/dx = f(x, y) * @param {number} x0 - Initial x value * @param {number} y0 - Initial y value * @param {number} xEnd - End x value * @param {number} steps - Number of steps * @param {Object} [options] - Additional options * @returns {Array} - Array of [x, y] pairs representing the solution */ numerical.ode.rk4 = function (f, x0, y0, xEnd, steps, options = {}) { // Convert to function if string const fFunc = typeof f === 'function' ? f : (x, y) => evaluateExpression(f, { x, y }); const h = (xEnd - x0) / steps; const result = [[x0, y0]]; let x = x0; let y = y0; for (let i = 0; i < steps; i++) { // RK4 steps const k1 = fFunc(x, y); const k2 = fFunc(x + h / 2, y + k1 * h / 2); const k3 = fFunc(x + h / 2, y + k2 * h / 2); const k4 = fFunc(x + h, y + k3 * h); // Update y using weighted average y += (h / 6) * (k1 + 2 * k2 + 2 * k3 + k4); x += h; result.push([x, y]); } return result; }; /** * Optimization methods */ numerical.optimize = {}; /** * Find the minimum of a function using gradient descent * @param {Function|string} f - The function to minimize * @param {Array} initialGuess - Initial guess for the minimum * @param {Object} [options] - Additional options * @returns {Array} - The point that minimizes the function */ numerical.optimize.gradientDescent = function (f, initialGuess, options = {}) { // Default options const defaultOptions = { learningRate: 0.01, maxIterations: 1000, tolerance: 1e-6, epsilon: 1e-8 // For numerical gradient }; const config = { ...defaultOptions, ...options }; // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); // Numerical gradient function function gradient(point) { const grad = []; const h = config.epsilon; for (let i = 0; i < point.length; i++) { const pointPlusH = [...point]; pointPlusH[i] += h; grad.push((fFunc(pointPlusH) - fFunc(point)) / h); } return grad; } // Gradient descent iteration const point = [...initialGuess]; for (let i = 0; i < config.maxIterations; i++) { const grad = gradient(point); // Check for convergence const gradNorm = Math.sqrt(grad.reduce((sum, val) => sum + val * val, 0)); if (gradNorm < config.tolerance) { return point; } // Update point for (let j = 0; j < point.length; j++) { point[j] -= config.learningRate * grad[j]; } } return point; }; /** * Interpolation methods */ numerical.interpolate = {}; /** * Perform polynomial interpolation using Lagrange polynomials * @param {Array} points - Array of [x, y] points to interpolate * @param {number} x - The x value to interpolate at * @returns {number} - The interpolated y value */ numerical.interpolate.lagrange = function (points, x) { let result = 0; for (let i = 0; i < points.length; i++) { let term = points[i][1]; for (let j = 0; j < points.length; j++) { if (i !== j) { term *= (x - points[j][0]) / (points[i][0] - points[j][0]); } } result += term; } return result; }; /** * Placeholder for expression evaluation * @private * @param {string} expr - The expression to evaluate * @param {Object} scope - Variable values * @returns {number} - The evaluated result */ function evaluateExpression(expr, scope) { // This would be implemented with a proper expression parser // For now, return a placeholder implementation try { // Create a function from the expression const vars = Object.keys(scope).join(','); const values = Object.values(scope); return Function(vars, `return ${expr.replace(/\^/g, '**')}`).apply(null, values); } catch (error) { throw new Error(`Error evaluating expression: ${error.message}`); } } /** * Numerical integration methods */ numerical.integrate = {}; /** * Perform numerical integration using the trapezoidal rule * @param {Function|string} f - The function to integrate * @param {number} a - Lower bound * @param {number} b - Upper bound * @param {number} n - Number of intervals * @returns {number} - The approximated integral */ numerical.integrate.trapezoidal = function (f, a, b, n) { // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); const h = (b - a) / n; let sum = 0.5 * (fFunc(a) + fFunc(b)); for (let i = 1; i < n; i++) { const x = a + i * h; sum += fFunc(x); } return h * sum; }; /** * Perform numerical integration using Simpson's rule * @param {Function|string} f - The function to integrate * @param {number} a - Lower bound * @param {number} b - Upper bound * @param {number} n - Number of intervals (must be even) * @returns {number} - The approximated integral */ numerical.integrate.simpson = function (f, a, b, n) { if (n % 2 !== 0) { throw new Error('Number of intervals must be even for Simpson\'s rule'); } // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); const h = (b - a) / n; let sum = fFunc(a) + fFunc(b); for (let i = 1; i < n; i++) { const x = a + i * h; const coef = i % 2 === 0 ? 2 : 4; sum += coef * fFunc(x); } return (h / 3) * sum; }; /** * Perform numerical integration using Gaussian quadrature * @param {Function|string} f - The function to integrate * @param {number} a - Lower bound * @param {number} b - Upper bound * @param {number} n - Number of quadrature points * @returns {number} - The approximated integral */ numerical.integrate.gauss = function (f, a, b, n = 5) { // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); // Gauss-Legendre weights and points for different orders const weights = { 2: [1.0, 1.0], 3: [0.5555555555555556, 0.8888888888888888, 0.5555555555555556], 4: [0.3478548451374538, 0.6521451548625462, 0.6521451548625462, 0.3478548451374538], 5: [0.2369268850561891, 0.4786286704993665, 0.5688888888888889, 0.4786286704993665, 0.2369268850561891] }; const points = { 2: [-0.5773502691896257, 0.5773502691896257], 3: [-0.7745966692414834, 0.0, 0.7745966692414834], 4: [-0.8611363115940526, -0.3399810435848563, 0.3399810435848563, 0.8611363115940526], 5: [-0.9061798459386640, -0.5384693101056831, 0.0, 0.5384693101056831, 0.9061798459386640] }; if (!weights[n] || !points[n]) { throw new Error(`Gaussian quadrature with ${n} points is not implemented`); } // Scale from [-1, 1] to [a, b] const c1 = (b - a) / 2; const c2 = (b + a) / 2; let sum = 0; for (let i = 0; i < n; i++) { const x = c1 * points[n][i] + c2; sum += weights[n][i] * fFunc(x); } return c1 * sum; }; /** * Perform numerical integration using adaptive Simpson's rule * @param {Function|string} f - The function to integrate * @param {number} a - Lower bound * @param {number} b - Upper bound * @param {number} tolerance - Error tolerance * @param {number} maxDepth - Maximum recursion depth * @returns {number} - The approximated integral */ numerical.integrate.adaptiveSimpson = function (f, a, b, tolerance = 1e-10, maxDepth = 20) { // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); function adaptiveSimpsonRecursive(a, b, fa, fm, fb, tolerance, depth) { const m = (a + b) / 2; const h = (b - a) / 6; const fml = fFunc((a + m) / 2); const fmr = fFunc((m + b) / 2); // Simpson's rule on whole interval and subintervals const whole = h * (fa + 4 * fm + fb); const left = h / 2 * (fa + 4 * fml + fm); const right = h / 2 * (fm + 4 * fmr + fb); const difference = Math.abs(left + right - whole); if (difference <= 15 * tolerance || depth >= maxDepth) { return left + right + difference / 15; // Add correction term } return adaptiveSimpsonRecursive(a, m, fa, fml, fm, tolerance / 2, depth + 1) + adaptiveSimpsonRecursive(m, b, fm, fmr, fb, tolerance / 2, depth + 1); } const fa = fFunc(a); const fm = fFunc((a + b) / 2); const fb = fFunc(b); return adaptiveSimpsonRecursive(a, b, fa, fm, fb, tolerance, 0); }; /** * Perform Monte Carlo integration * @param {Function|string} f - The function to integrate * @param {Array} lowerBounds - Lower bounds for each dimension * @param {Array} upperBounds - Upper bounds for each dimension * @param {number} samples - Number of random samples * @returns {Object} - The approximated integral and error estimate */ numerical.integrate.monteCarlo = function (f, lowerBounds, upperBounds, samples = 10000) { // Convert to function if string const fFunc = typeof f === 'function' ? f : (x) => evaluateExpression(f, { x }); if (lowerBounds.length !== upperBounds.length) { throw new Error('Dimension mismatch between lower and upper bounds'); } const dim = lowerBounds.length; const volume = upperBounds.reduce((vol, upper, i) => vol * (upper - lowerBounds[i]), 1); // Generate random samples and evaluate function let sum = 0; let sumSquared = 0; for (let i = 0; i < samples; i++) { // Generate random point within bounds const point = Array(dim).fill(0).map((_, j) => lowerBounds[j] + Math.random() * (upperBounds[j] - lowerBounds[j])); const value = fFunc(...point); sum += value; sumSquared += value * value; } const mean = sum / samples; const variance = sumSquared / samples - mean * mean; const integral = volume * mean; const error = volume * Math.sqrt(variance / samples); return { integral, error }; }; /** * PDE solvers for partial differential equations */ numerical.pde = {}; /** * Solve the 2D heat equation using the explicit finite difference method * u_t = k * (u_xx + u_yy) * @param {Function} initialCondition - Initial temperature distribution u(x,y,0) * @param {Function} boundaryCondition - Boundary values u(x,y,t) at the domain edges * @param {number} xMin - Lower x boundary * @param {number} xMax - Upper x boundary * @param {number} yMin - Lower y boundary * @param {number} yMax - Upper y boundary * @param {number} tMax - Maximum time * @param {number} dx - Spatial step in x * @param {number} dy - Spatial step in y * @param {number} dt - Time step * @param {number} k - Thermal diffusivity * @returns {Array} - 3D array u[t][y][x] representing the solution */ numerical.pde.heat2d = function (initialCondition, boundaryCondition, xMin, xMax, yMin, yMax, tMax, dx = 0.1, dy = 0.1, dt = 0.01, k = 1.0) { // Check stability condition const stability = k * dt * (1 / (dx * dx) + 1 / (dy * dy)); if (stability > 0.5) { throw new Error(`Unstable parameters: dt is too large. Reduce dt or increase dx and dy. Stability value: ${stability}, should be <= 0.5`); } // Setup grid const nx = Math.floor((xMax - xMin) / dx) + 1; const ny = Math.floor((yMax - yMin) / dy) + 1; const nt = Math.floor(tMax / dt) + 1; // Initialize solution array const u = Array(nt).fill().map(() => Array(ny).fill().map(() => Array(nx).fill(0))); // Set initial condition for (let j = 0; j < ny; j++) { for (let i = 0; i < nx; i++) { const x = xMin + i * dx; const y = yMin + j * dy; u[0][j][i] = initialCondition(x, y); } } // Time stepping for (let n = 0; n < nt - 1; n++) { // Set boundary conditions for current time step for (let j = 0; j < ny; j++) { for (let i = 0; i < nx; i++) { const x = xMin + i * dx; const y = yMin + j * dy; const t = n * dt; // Check if point is on boundary if (i === 0 || i === nx - 1 || j === 0 || j === ny - 1) { u[n + 1][j][i] = boundaryCondition(x, y, t); } } } // Update interior points for (let j = 1; j < ny - 1; j++) { for (let i = 1; i < nx - 1; i++) { // Finite difference scheme const uxx = (u[n][j][i + 1] - 2 * u[n][j][i] + u[n][j][i - 1]) / (dx * dx); const uyy = (u[n][j + 1][i] - 2 * u[n][j][i] + u[n][j - 1][i]) / (dy * dy); u[n + 1][j][i] = u[n][j][i] + k * dt * (uxx + uyy); } } } return u; }; /** * Solve the 1D wave equation using the explicit finite difference method * u_tt = c^2 * u_xx * @param {Function} initialPosition - Initial displacement u(x,0) * @param {Function} initialVelocity - Initial velocity u_t(x,0) * @param {Function} boundaryCondition - Boundary values u(x,t) at the domain edges * @param {number} xMin - Lower x boundary * @param {number} xMax - Upper x boundary * @param {number} tMax - Maximum time * @param {number} dx - Spatial step * @param {number} dt - Time step * @param {number} c - Wave speed * @returns {Array} - 2D array u[t][x] representing the solution */ numerical.pde.wave1d = function (initialPosition, initialVelocity, boundaryCondition, xMin, xMax, tMax, dx = 0.1, dt = 0.01, c = 1.0) { // Check Courant–Friedrichs–Lewy condition const cfl = c * dt / dx; if (cfl > 1.0) { throw new Error(`CFL condition violated: c*dt/dx = ${cfl} > 1. Reduce dt or increase dx.`); } // Setup grid const nx = Math.floor((xMax - xMin) / dx) + 1; const nt = Math.floor(tMax / dt) + 1; // Initialize solution array const u = Array(nt).fill().map(() => Array(nx).fill(0)); // Set initial position for (let i = 0; i < nx; i++) { const x = xMin + i * dx; u[0][i] = initialPosition(x); } // Set initial velocity (using central difference for first time step) for (let i = 1; i < nx - 1; i++) { const x = xMin + i * dx; u[1][i] = u[0][i] + dt * initialVelocity(x) + 0.5 * c * c * dt * dt * (u[0][i + 1] - 2 * u[0][i] + u[0][i - 1]) / (dx * dx); } // Set boundary conditions for first time step u[1][0] = boundaryCondition(xMin, dt); u[1][nx - 1] = boundaryCondition(xMax, dt); // Time stepping for (let n = 1; n < nt - 1; n++) { // Set boundary conditions u[n + 1][0] = boundaryCondition(xMin, (n + 1) * dt); u[n + 1][nx - 1] = boundaryCondition(xMax, (n + 1) * dt); // Update interior points for (let i = 1; i < nx - 1; i++) { // Explicit finite difference scheme for wave equation const c2 = c * c; u[n + 1][i] = 2 * u[n][i] - u[n - 1][i] + c2 * dt * dt * (u[n][i + 1] - 2 * u[n][i] + u[n][i - 1]) / (dx * dx); } } return u; }; /** * Spectral methods for solving differential equations */ numerical.spectral = {}; /** * Solve a differential equation using the Fourier spectral method * @param {Function} f - The right-hand side of the equation u_t = f(u, u_x, u_xx, ...) * @param {Function} initialCondition - Initial condition u(x,0) * @param {number} xMin - Lower x boundary * @param {number} xMax - Upper x boundary * @param {number} tMax - Maximum time * @param {number} nx - Number of spatial grid points * @param {number} nt - Number of time steps * @returns {Array} - 2D array u[t][x] representing the solution */ numerical.spectral.fourier = function (f, initialCondition, xMin, xMax, tMax, nx = 64, nt = 100) { // This is a simplified version that requires an FFT implementation // In a real implementation, we'd use a proper FFT library // Mock FFT implementation const fft = function (data) { // In a real implementation, this would be a proper FFT // For now, just return a transformed array of same length return data.map((val, idx) => ({ re: val, im: 0 })); }; const ifft = function (data) { // In a real implementation, this would be a proper inverse FFT // For now, just return the real parts return data.map((val) => val.re); }; // Setup grid const L = xMax - xMin; const dx = L / nx; const dt = tMax / nt; // Initialize solution array const u = Array(nt + 1).fill().map(() => Array(nx).fill(0)); // Set initial condition for (let i = 0; i < nx; i++) { const x = xMin + i * dx; u[0][i] = initialCondition(x); } // Wavenumbers in Fourier space const k = Array(nx).fill(0).map((_, i) => { const k_val = 2 * Math.PI * (i <= nx / 2 ? i : i - nx) / L; return k_val; }); // Time stepping with 4th order Runge-Kutta in Fourier space for (let n = 0; n < nt; n++) { const t = n * dt; // Transform to Fourier space const u_hat = fft(u[n]); // RK4 step 1 const k1 = evaluateRightHandSide(u_hat, k, t, f); // RK4 step 2 const u_hat2 = u_hat.map((val, i) => ({ re: val.re + 0.5 * dt * k1[i].re, im: val.im + 0.5 * dt * k1[i].im })); const k2 = evaluateRightHandSide(u_hat2, k, t + 0.5 * dt, f); // RK4 step 3 const u_hat3 = u_hat.map((val, i) => ({ re: val.re + 0.5 * dt * k2[i].re, im: val.im + 0.5 * dt * k2[i].im })); const k3 = evaluateRightHandSide(u_hat3, k, t + 0.5 * dt, f); // RK4 step 4 const u_hat4 = u_hat.map((val, i) => ({ re: val.re + dt * k3[i].re, im: val.im + dt * k3[i].im })); const k4 = evaluateRightHandSide(u_hat4, k, t + dt, f); // Combine RK4 steps const u_hat_new = u_hat.map((val, i) => ({ re: val.re + (dt / 6) * (k1[i].re + 2 * k2[i].re + 2 * k3[i].re + k4[i].re), im: val.im + (dt / 6) * (k1[i].im + 2 * k2[i].im + 2 * k3[i].im + k4[i].im) })); // Transform back to real space u[n + 1] = ifft(u_hat_new); } return u; // Helper function to evaluate the right-hand side in Fourier space function evaluateRightHandSide(u_hat, k, t, f) { // This is a placeholder - in a real implementation, we would compute // derivatives in Fourier space and evaluate f accordingly return u_hat.map((val, i) => ({ re: -k[i] * k[i] * val.re, // Example: heat equation u_t = u_xx im: -k[i] * k[i] * val.im })); } }; /** * Implement the Chebyshev collocation spectral method * @param {Function} diffEq - The differential equation operator L(u) = f * @param {Function} source - The right-hand side function f * @param {Function} boundaryCondition - Function defining boundary values * @param {number} a - Lower boundary * @param {number} b - Upper boundary * @param {number} n - Number of collocation points * @returns {Object} - Object containing the solution function and grid points */ numerical.spectral.chebyshev = function (diffEq, source, boundaryCondition, a, b, n = 32) { // Generate Chebyshev points in [-1, 1] const x_cheb = Array(n).fill(0).map((_, i) => Math.cos(Math.PI * i / (n - 1))); // Map to domain [a, b] const x = x_cheb.map((x) => 0.5 * ((b - a) * x + (b + a))); // Compute differentiation matrix (simplified) // In a full implementation, we would compute the proper Chebyshev differentiation matrix const D = Array(n).fill().map(() => Array(n).fill(0)); // Fill in the differentiation matrix (this is just a placeholder) for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i !== j) { const ci = (i === 0 || i === n - 1) ? 2 : 1; const cj = (j === 0 || j === n - 1) ? 2 : 1; D[i][j] = (ci / cj) * (-1) ** (i + j) / (x_cheb[i] - x_cheb[j]); } } } // Fill in diagonal elements for (let i = 0; i < n; i++) { let sum = 0; for (let j = 0; j < n; j++) { if (j !== i) sum += D[i][j]; } D[i][i] = -sum; } // Apply boundary conditions and solve the system // This is a simplified placeholder - in a real implementation, // we would properly set up and solve the linear system const u = Array(n).fill(0); // Set boundary conditions u[0] = boundaryCondition(a); u[n - 1] = boundaryCondition(b); // Create interpolation function for the solution const solution = function (x_eval) { // Simple Lagrange interpolation let result = 0; for (let i = 0; i < n; i++) { let basis = 1; for (let j = 0; j < n; j++) { if (i !== j) { basis *= (x_eval - x[j]) / (x[i] - x[j]); } } result += u[i] * basis; } return result; }; return { solution, points: x, values: u }; }; /** * Stochastic differential equation solvers */ numerical.sde = {}; /** * Solve a stochastic differential equation using the Euler-Maruyama method * dX = a(X,t)dt + b(X,t)dW * @param {Function} drift - The drift function a(x,t) * @param {Function} diffusion - The diffusion function b(x,t) * @param {number} x0 - Initial condition * @param {number} t0 - Initial time * @param {number} T - Final time * @param {number} dt - Time step * @param {number} paths - Number of sample paths * @returns {Array} - Array of sample paths, each an array of [t, X] pairs */ numerical.sde.eulerMaruyama = function (drift, diffusion, x0, t0, T, dt = 0.01, paths = 1) { const steps = Math.ceil((T - t0) / dt); const actualDt = (T - t0) / steps; // Generate sample paths const results = []; for (let path = 0; path < paths; path++) { const trajectory = []; let t = t0; let x = x0; trajectory.push([t, x]); for (let step = 0; step < steps; step++) { // Generate normal random number with mean 0 and variance dt const dW = Math.sqrt(actualDt) * randn(); // Euler-Maruyama update x = x + drift(x, t) * actualDt + diffusion(x, t) * dW; t += actualDt; trajectory.push([t, x]); } results.push(trajectory); } return results; // Helper function to generate standard normal random number function randn() { let u = 0; let v = 0; while (u === 0) u = Math.random(); while (v === 0) v = Math.random(); return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); } }; module.exports = numerical;