herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
465 lines (386 loc) • 13.6 kB
JavaScript
/**
* Chaos Theory module for herta.js
* Provides tools for analyzing and generating chaotic systems and fractals
*/
const Decimal = require('decimal.js');
const matrix = require('../core/matrix');
const arithmetic = require('../core/arithmetic');
const complex = require('../core/complex');
const chaosTheory = {};
/**
* Calculate the Lyapunov exponent for a 1D map
* Measures the rate of separation of infinitesimally close trajectories
* @param {Function} map - The one-dimensional map function
* @param {number} x0 - Initial value
* @param {Object} options - Calculation options
* @returns {number} - The Lyapunov exponent
*/
chaosTheory.lyapunovExponent = function (map, x0, options = {}) {
const iterations = options.iterations || 10000;
const discardTransient = options.discardTransient || 1000;
let x = x0;
let sum = 0;
// Discard transient behavior
for (let i = 0; i < discardTransient; i++) {
x = map(x);
}
// Calculate Lyapunov exponent
for (let i = 0; i < iterations; i++) {
const derivative = options.derivative
? options.derivative(x)
: (map(x + 1e-8) - map(x)) / 1e-8;
sum += Math.log(Math.abs(derivative));
x = map(x);
}
return sum / iterations;
};
/**
* Generate the bifurcation diagram for a parametrized map
* @param {Function} map - Function(x, r) where r is the parameter
* @param {Object} options - Generation options
* @returns {Array} - Points for the bifurcation diagram
*/
chaosTheory.bifurcationDiagram = function (map, options = {}) {
const rStart = options.rStart || 2.8;
const rEnd = options.rEnd || 4.0;
const rSteps = options.rSteps || 1000;
const xInitial = options.xInitial || 0.5;
const iterations = options.iterations || 1000;
const discardTransient = options.discardTransient || 500;
const points = [];
const rStep = (rEnd - rStart) / rSteps;
for (let rIndex = 0; rIndex <= rSteps; rIndex++) {
const r = rStart + rIndex * rStep;
let x = xInitial;
// Discard transient behavior
for (let i = 0; i < discardTransient; i++) {
x = map(x, r);
}
// Record steady state behavior
for (let i = 0; i < iterations; i++) {
x = map(x, r);
if (i % options.recordEvery || 1 === 0) {
points.push({ r, x });
}
}
}
return points;
};
/**
* Calculate Feigenbaum constant approximation
* @param {Function} map - Parametrized map function(x, r)
* @param {Array} initialBifurcationPoints - Array of initial bifurcation parameter values
* @returns {number} - Approximation of Feigenbaum constant (≈ 4.669)
*/
chaosTheory.feigenbaumConstant = function (map, initialBifurcationPoints) {
if (initialBifurcationPoints.length < 4) {
throw new Error('At least 4 bifurcation points are needed');
}
const deltas = [];
for (let i = 0; i < initialBifurcationPoints.length - 2; i++) {
const delta = (initialBifurcationPoints[i + 1] - initialBifurcationPoints[i])
/ (initialBifurcationPoints[i + 2] - initialBifurcationPoints[i + 1]);
deltas.push(delta);
}
return deltas[deltas.length - 1];
};
/**
* Generate iterations of the Mandelbrot set for visualization
* @param {Object} options - Generation options
* @returns {Array} - 2D array of iteration counts
*/
chaosTheory.mandelbrotSet = function (options = {}) {
const width = options.width || 800;
const height = options.height || 800;
const xMin = options.xMin || -2.0;
const xMax = options.xMax || 1.0;
const yMin = options.yMin || -1.5;
const yMax = options.yMax || 1.5;
const maxIterations = options.maxIterations || 1000;
const escapeRadius = options.escapeRadius || 2.0;
const iterations = Array(height).fill().map(() => Array(width).fill(0));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const re = xMin + (xMax - xMin) * x / (width - 1);
const im = yMin + (yMax - yMin) * y / (height - 1);
let zRe = 0; let
zIm = 0;
let iter;
for (iter = 0; iter < maxIterations; iter++) {
// z = z^2 + c
const zRe2 = zRe * zRe;
const zIm2 = zIm * zIm;
if (zRe2 + zIm2 > escapeRadius * escapeRadius) {
break;
}
zIm = 2 * zRe * zIm + im;
zRe = zRe2 - zIm2 + re;
}
iterations[y][x] = iter;
}
}
return iterations;
};
/**
* Generate Julia set for a given complex parameter c
* @param {Object} options - Generation options
* @returns {Array} - 2D array of iteration counts
*/
chaosTheory.juliaSet = function (options = {}) {
const width = options.width || 800;
const height = options.height || 800;
const xMin = options.xMin || -2.0;
const xMax = options.xMax || 2.0;
const yMin = options.yMin || -2.0;
const yMax = options.yMax || 2.0;
const cRe = options.cRe || -0.7;
const cIm = options.cIm || 0.27015;
const maxIterations = options.maxIterations || 1000;
const escapeRadius = options.escapeRadius || 2.0;
const iterations = Array(height).fill().map(() => Array(width).fill(0));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let zRe = xMin + (xMax - xMin) * x / (width - 1);
let zIm = yMin + (yMax - yMin) * y / (height - 1);
let iter;
for (iter = 0; iter < maxIterations; iter++) {
// z = z^2 + c
const zRe2 = zRe * zRe;
const zIm2 = zIm * zIm;
if (zRe2 + zIm2 > escapeRadius * escapeRadius) {
break;
}
zIm = 2 * zRe * zIm + cIm;
zRe = zRe2 - zIm2 + cRe;
}
iterations[y][x] = iter;
}
}
return iterations;
};
/**
* Computes the fractal dimension using the box-counting method
* @param {Function} isInSet - Function that takes (x, y) and returns boolean if the point is in the set
* @param {Object} bounds - {xMin, xMax, yMin, yMax} defining the area to analyze
* @param {Object} options - Calculation options
* @returns {number} - The estimated fractal dimension
*/
chaosTheory.boxCountingDimension = function (isInSet, bounds, options = {}) {
const minBoxSize = options.minBoxSize || 2;
const maxBoxSize = options.maxBoxSize || 128;
const width = bounds.xMax - bounds.xMin;
const height = bounds.yMax - bounds.yMin;
const sizes = [];
const counts = [];
// Try different box sizes
for (let boxSize = minBoxSize; boxSize <= maxBoxSize; boxSize *= 2) {
const numBoxesX = Math.ceil(width / boxSize);
const numBoxesY = Math.ceil(height / boxSize);
let boxCount = 0;
// Check each box
for (let boxY = 0; boxY < numBoxesY; boxY++) {
for (let boxX = 0; boxX < numBoxesX; boxX++) {
let boxHasPoint = false;
// Check sample points in box
const samplePoints = options.samplesPerBox || 5;
for (let sy = 0; sy < samplePoints; sy++) {
for (let sx = 0; sx < samplePoints; sx++) {
const x = bounds.xMin + (boxX + sx / samplePoints) * boxSize;
const y = bounds.yMin + (boxY + sy / samplePoints) * boxSize;
if (isInSet(x, y)) {
boxHasPoint = true;
break;
}
}
if (boxHasPoint) break;
}
if (boxHasPoint) {
boxCount++;
}
}
}
sizes.push(boxSize);
counts.push(boxCount);
}
// Linear regression on log-log scale to find dimension
const n = sizes.length;
let sumX = 0; let sumY = 0; let sumXY = 0; let
sumX2 = 0;
for (let i = 0; i < n; i++) {
const x = Math.log(1 / sizes[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 Lorenz attractor trajectory
* @param {Object} options - Generation options
* @returns {Array} - Array of points {x, y, z} representing the trajectory
*/
chaosTheory.lorenzAttractor = function (options = {}) {
const sigma = options.sigma || 10;
const rho = options.rho || 28;
const beta = options.beta || 8 / 3;
const dt = options.dt || 0.01;
const duration = options.duration || 100;
const initialState = options.initialState || { x: 0.1, y: 0, z: 0 };
const points = [{ ...initialState }];
const steps = Math.floor(duration / dt);
let { x } = initialState;
let { y } = initialState;
let { z } = initialState;
for (let i = 0; i < steps; i++) {
const dx = sigma * (y - x);
const dy = x * (rho - z) - y;
const dz = x * y - beta * z;
x += dx * dt;
y += dy * dt;
z += dz * dt;
if (i % (options.saveEvery || 10) === 0) {
points.push({ x, y, z });
}
}
return points;
};
/**
* Implement the logistic map function f(x) = rx(1-x)
* @param {number} x - Value between 0 and 1
* @param {number} r - Control parameter, typically between 0 and 4
* @returns {number} - The next value
*/
chaosTheory.logisticMap = function (x, r) {
return r * x * (1 - x);
};
/**
* Implement the Hénon map, a discrete-time dynamical system
* @param {Object} point - {x, y} current point
* @param {Object} params - {a, b} parameters (classic values: a=1.4, b=0.3)
* @returns {Object} - The next point {x, y}
*/
chaosTheory.henonMap = function (point, params = { a: 1.4, b: 0.3 }) {
const { x, y } = point;
const { a, b } = params;
return {
x: 1 - a * x * x + y,
y: b * x
};
};
/**
* Generate recurrence plot to visualize recurrences of states in phase space
* @param {Array} timeSeries - Array of values representing the time series
* @param {Object} options - Plot options
* @returns {Array} - 2D array representing the recurrence plot
*/
chaosTheory.recurrencePlot = function (timeSeries, options = {}) {
const threshold = options.threshold || 0.1;
const norm = options.norm || 'euclidean';
const dimension = options.dimension || 1;
const delay = options.delay || 1;
// Reconstruct phase space using time delay method if dimension > 1
const vectors = [];
for (let i = 0; i <= timeSeries.length - dimension * delay; i++) {
const vector = [];
for (let j = 0; j < dimension; j++) {
vector.push(timeSeries[i + j * delay]);
}
vectors.push(vector);
}
const n = vectors.length;
const plot = Array(n).fill().map(() => Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
let distance;
if (dimension === 1) {
distance = Math.abs(vectors[i][0] - vectors[j][0]);
} else {
// Calculate distance using specified norm
if (norm === 'euclidean') {
let sum = 0;
for (let k = 0; k < dimension; k++) {
sum += (vectors[i][k] - vectors[j][k]) ** 2;
}
distance = Math.sqrt(sum);
} else if (norm === 'maximum') {
let max = 0;
for (let k = 0; k < dimension; k++) {
max = Math.max(max, Math.abs(vectors[i][k] - vectors[j][k]));
}
distance = max;
}
}
plot[i][j] = distance <= threshold ? 1 : 0;
}
}
return plot;
};
/**
* Calculate Correlation Dimension using the Grassberger-Procaccia algorithm
* @param {Array} timeSeries - Array of values representing the time series
* @param {Object} options - Calculation options
* @returns {Object} - Correlation dimension estimate and data for plotting
*/
chaosTheory.correlationDimension = function (timeSeries, options = {}) {
const dimension = options.dimension || 2;
const delay = options.delay || 1;
const minEpsilon = options.minEpsilon || 0.01;
const maxEpsilon = options.maxEpsilon || 1.0;
const epsilonSteps = options.epsilonSteps || 20;
// Reconstruct phase space using time delay method
const vectors = [];
for (let i = 0; i <= timeSeries.length - dimension * delay; i++) {
const vector = [];
for (let j = 0; j < dimension; j++) {
vector.push(timeSeries[i + j * delay]);
}
vectors.push(vector);
}
const n = vectors.length;
const epsilons = [];
const correlations = [];
// Calculate correlation sum for different epsilon values
for (let step = 0; step < epsilonSteps; step++) {
const epsilon = minEpsilon * (maxEpsilon / minEpsilon) ** (step / (epsilonSteps - 1));
epsilons.push(epsilon);
let correlation = 0;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
let distance = 0;
for (let k = 0; k < dimension; k++) {
distance += (vectors[i][k] - vectors[j][k]) ** 2;
}
distance = Math.sqrt(distance);
if (distance < epsilon) {
correlation += 2; // Count both (i,j) and (j,i)
}
}
}
correlation /= n * (n - 1); // Normalize
correlations.push(correlation);
}
// Linear regression on log-log plot to find dimension
const logEpsilons = epsilons.map((e) => Math.log(e));
const logCorrelations = correlations.map((c) => Math.log(c + 1e-10)); // Avoid log(0)
let sumX = 0; let sumY = 0; let sumXY = 0; let
sumX2 = 0;
const validPoints = logCorrelations.filter((_, i) => correlations[i] > 0).length;
for (let i = 0; i < logEpsilons.length; i++) {
if (correlations[i] > 0) {
sumX += logEpsilons[i];
sumY += logCorrelations[i];
sumXY += logEpsilons[i] * logCorrelations[i];
sumX2 += logEpsilons[i] * logEpsilons[i];
}
}
const slope = (validPoints * sumXY - sumX * sumY) / (validPoints * sumX2 - sumX * sumX);
return {
dimension: slope,
epsilons,
correlations
};
};
module.exports = chaosTheory;