UNPKG

herta

Version:

Advanced mathematics framework for scientific, engineering, and financial applications

387 lines (319 loc) 11.1 kB
/** * Dynamical Systems module for herta.js * Provides tools for analyzing and simulating dynamical systems */ const arithmetic = require('../core/arithmetic'); const matrix = require('../core/matrix'); const numerical = require('./numerical'); const dynamicalSystems = {}; /** * Iterate a discrete dynamical system * @param {Function} f - The evolution function f(x) * @param {number|Array} x0 - Initial state * @param {number} iterations - Number of iterations * @returns {Array} - Trajectory of the system */ dynamicalSystems.iterate = function (f, x0, iterations) { const trajectory = [x0]; let currentState = x0; for (let i = 0; i < iterations; i++) { currentState = f(currentState); trajectory.push(currentState); } return trajectory; }; /** * Calculate the Lyapunov exponent for a 1D map * @param {Function} f - The map function * @param {number} x0 - Initial state * @param {number} iterations - Number of iterations * @param {number} [discardTransients=100] - Number of initial iterations to discard * @returns {number} - Estimated Lyapunov exponent */ dynamicalSystems.lyapunovExponent = function (f, x0, iterations, discardTransients = 100) { let x = x0; let sum = 0; // Discard transients for (let i = 0; i < discardTransients; i++) { x = f(x); } // Estimate Lyapunov exponent for (let i = 0; i < iterations; i++) { const derivative = (f(x + 1e-10) - f(x)) / 1e-10; // Numerical derivative sum += Math.log(Math.abs(derivative)); x = f(x); } return sum / iterations; }; /** * Calculate the bifurcation diagram for a 1D map * @param {Function} f - The map function f(x, r) where r is the parameter * @param {number} minParam - Minimum parameter value * @param {number} maxParam - Maximum parameter value * @param {number} paramSteps - Number of parameter steps * @param {Object} [options] - Options for the calculation * @returns {Object} - Bifurcation diagram data */ dynamicalSystems.bifurcationDiagram = function (f, minParam, maxParam, paramSteps, options = {}) { const initialX = options.initialX || 0.5; const iterations = options.iterations || 500; const discardTransients = options.discardTransients || 200; const paramStep = (maxParam - minParam) / paramSteps; const diagram = { parameters: [], values: [] }; for (let i = 0; i <= paramSteps; i++) { const r = minParam + i * paramStep; diagram.parameters.push(r); // Create function with fixed parameter const fr = (x) => f(x, r); // Initialize and discard transients let x = initialX; for (let j = 0; j < discardTransients; j++) { x = fr(x); } // Record stable points const values = []; for (let j = 0; j < iterations; j++) { x = fr(x); if (j >= iterations - 50) { // Record only last 50 iterations values.push(x); } } diagram.values.push(values); } return diagram; }; /** * Solve a system of ordinary differential equations using Runge-Kutta 4th order method * @param {Function} derivs - Function that calculates derivatives: derivs(t, y) returns array of derivatives * @param {Array} initialState - Initial state vector * @param {number} tStart - Start time * @param {number} tEnd - End time * @param {number} numSteps - Number of steps * @returns {Object} - Solution object with time and state arrays */ dynamicalSystems.solveODE = function (derivs, initialState, tStart, tEnd, numSteps) { const dt = (tEnd - tStart) / numSteps; const times = Array(numSteps + 1).fill().map((_, i) => tStart + i * dt); const states = Array(numSteps + 1).fill().map(() => Array(initialState.length)); // Set initial state states[0] = [...initialState]; // RK4 integration for (let i = 0; i < numSteps; i++) { const t = times[i]; const y = states[i]; const k1 = derivs(t, y); const k2Args = y.map((yi, j) => yi + k1[j] * dt / 2); const k2 = derivs(t + dt / 2, k2Args); const k3Args = y.map((yi, j) => yi + k2[j] * dt / 2); const k3 = derivs(t + dt / 2, k3Args); const k4Args = y.map((yi, j) => yi + k3[j] * dt); const k4 = derivs(t + dt, k4Args); states[i + 1] = y.map((yi, j) => yi + (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j]) * dt / 6); } return { times, states }; }; /** * Calculate the fixed points of a 1D map * @param {Function} f - The map function * @param {number} min - Minimum search range * @param {number} max - Maximum search range * @param {number} [precision=1e-10] - Precision for fixed point determination * @returns {Array} - Array of fixed points */ dynamicalSystems.fixedPoints = function (f, min, max, precision = 1e-10) { // Define the function g(x) = f(x) - x, whose roots are fixed points const g = (x) => f(x) - x; // Initial sampling const numSamples = 100; const step = (max - min) / numSamples; const samples = []; for (let i = 0; i <= numSamples; i++) { const x = min + i * step; samples.push({ x, y: g(x) }); } // Look for sign changes const fixedPoints = []; for (let i = 0; i < samples.length - 1; i++) { if (samples[i].y * samples[i + 1].y <= 0) { // Potential root found, use bisection method let a = samples[i].x; let b = samples[i + 1].x; let fa = g(a); let fb = g(b); // Bisection search while (Math.abs(b - a) > precision) { const c = (a + b) / 2; const fc = g(c); if (Math.abs(fc) < precision) { // Found fixed point fixedPoints.push(c); break; } if (fa * fc < 0) { b = c; fb = fc; } else { a = c; fa = fc; } } // Add the approximation if (Math.abs(b - a) <= precision) { fixedPoints.push((a + b) / 2); } } } return fixedPoints; }; /** * Calculate the stability of fixed points of a 1D map * @param {Function} f - The map function * @param {Array} fixedPoints - Array of fixed points * @returns {Array} - Array of objects {point, stability} */ dynamicalSystems.stability = function (f, fixedPoints) { return fixedPoints.map((point) => { // Calculate derivative at fixed point const derivative = (f(point + 1e-10) - f(point)) / 1e-10; const absDerivative = Math.abs(derivative); let stability; if (absDerivative < 1) { stability = 'stable'; } else if (absDerivative > 1) { stability = 'unstable'; } else { stability = 'neutral'; } return { point, derivative, stability }; }); }; /** * Generate a Poincaré map for a continuous dynamical system * @param {Function} derivs - Function that calculates derivatives * @param {Array} initialStates - Array of initial states * @param {number} tMax - Maximum integration time * @param {Function} sectionCondition - Function that returns true when trajectory crosses section * @param {number} [dt=0.01] - Time step for integration * @returns {Array} - Array of points on the Poincaré section */ dynamicalSystems.poincareMap = function (derivs, initialStates, tMax, sectionCondition, dt = 0.01) { const result = []; for (const initialState of initialStates) { let t = 0; let state = [...initialState]; let prevState = null; while (t < tMax) { // RK4 step const k1 = derivs(t, state); const k2Args = state.map((s, i) => s + k1[i] * dt / 2); const k2 = derivs(t + dt / 2, k2Args); const k3Args = state.map((s, i) => s + k2[i] * dt / 2); const k3 = derivs(t + dt / 2, k3Args); const k4Args = state.map((s, i) => s + k3[i] * dt); const k4 = derivs(t + dt, k4Args); prevState = [...state]; state = state.map((s, i) => s + (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]) * dt / 6); t += dt; // Check if trajectory crossed the section if (prevState && sectionCondition(state) && !sectionCondition(prevState)) { result.push([...state]); } } } return result; }; /** * Calculate the fractal dimension of a set of points (box-counting dimension) * @param {Array} points - Array of points (each point is an array of coordinates) * @param {number} minSize - Minimum box size * @param {number} maxSize - Maximum box size * @param {number} numSteps - Number of box sizes to try * @returns {number} - Estimated fractal dimension */ dynamicalSystems.fractalDimension = function (points, minSize, maxSize, numSteps) { if (!points.length) return 0; // Find bounds of the point set const dimensions = points[0].length; const min = Array(dimensions).fill(Infinity); const max = Array(dimensions).fill(-Infinity); for (const point of points) { for (let d = 0; d < dimensions; d++) { min[d] = Math.min(min[d], point[d]); max[d] = Math.max(max[d], point[d]); } } // Calculate box counting at different scales const scales = []; const counts = []; const logBase = Math.log(maxSize / minSize) / (numSteps - 1); for (let i = 0; i < numSteps; i++) { const size = maxSize / Math.exp(i * logBase); scales.push(1 / size); // Count occupied boxes const occupied = new Set(); for (const point of points) { // Calculate box indices for this point const indices = point.map((x, d) => Math.floor((x - min[d]) / size)); occupied.add(indices.join(',')); } counts.push(occupied.size); } // Linear regression to find slope (dimension) let sumX = 0; let sumY = 0; let sumXY = 0; let sumX2 = 0; const n = scales.length; for (let i = 0; i < n; i++) { const x = Math.log(scales[i]); const y = Math.log(counts[i]); sumX += x; sumY += y; sumXY += x * y; sumX2 += x * x; } const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); return slope; }; /** * Generate the phase portrait of a 2D dynamical system * @param {Function} derivs - Function that calculates derivatives * @param {number} xMin - Minimum x value * @param {number} xMax - Maximum x value * @param {number} yMin - Minimum y value * @param {number} yMax - Maximum y value * @param {number} numX - Number of grid points in x direction * @param {number} numY - Number of grid points in y direction * @returns {Object} - Phase portrait data */ dynamicalSystems.phasePortrait = function (derivs, xMin, xMax, yMin, yMax, numX, numY) { const dx = (xMax - xMin) / (numX - 1); const dy = (yMax - yMin) / (numY - 1); const portrait = { grid: { x: [], y: [] }, vectors: [] }; // Create grid for (let i = 0; i < numX; i++) { portrait.grid.x.push(xMin + i * dx); } for (let j = 0; j < numY; j++) { portrait.grid.y.push(yMin + j * dy); } // Calculate vector field for (let j = 0; j < numY; j++) { const y = portrait.grid.y[j]; const row = []; for (let i = 0; i < numX; i++) { const x = portrait.grid.x[i]; const derivatives = derivs(0, [x, y]); row.push({ x, y, dx: derivatives[0], dy: derivatives[1] }); } portrait.vectors.push(row); } return portrait; }; module.exports = dynamicalSystems;