252 lines
10 KiB
JavaScript
252 lines
10 KiB
JavaScript
/**
|
|
* Temporal Attractor Studio Handlers
|
|
* WASM-based implementation for chaos analysis tools
|
|
*/
|
|
// Lazy load the WASM module
|
|
let tas = null;
|
|
let studio = null;
|
|
let wasmInitialized = false;
|
|
async function loadWasm() {
|
|
if (!tas) {
|
|
try {
|
|
// @ts-ignore - Dynamic import of WASM module
|
|
tas = await import('../../../dist/wasm/temporal-attractor/temporal_attractor_studio.js');
|
|
// Initialize the WASM module with the path to the WASM file
|
|
if (!wasmInitialized && tas.default) {
|
|
// For Node.js, we need to read the WASM file
|
|
const fs = await import('fs');
|
|
const path = await import('path');
|
|
const url = await import('url');
|
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const wasmPath = path.join(__dirname, '..', '..', '..', 'dist', 'wasm', 'temporal-attractor', 'temporal_attractor_studio_bg.wasm');
|
|
const wasmBuffer = fs.readFileSync(wasmPath);
|
|
await tas.default({ module_or_path: wasmBuffer });
|
|
wasmInitialized = true;
|
|
}
|
|
}
|
|
catch (e) {
|
|
console.error('Failed to load temporal attractor WASM:', e);
|
|
throw new Error('Temporal Attractor WASM module not found. Please run npm run build:wasm');
|
|
}
|
|
}
|
|
return tas;
|
|
}
|
|
async function initStudio() {
|
|
if (!studio) {
|
|
const module = await loadWasm();
|
|
studio = new module.TemporalAttractorStudio();
|
|
}
|
|
return studio;
|
|
}
|
|
export const temporalAttractorHandlers = {
|
|
chaos_analyze: async (args) => {
|
|
const studio = await initStudio();
|
|
const result = studio.calculate_lyapunov(args.data, args.dimensions || 3, args.dt || 0.01, args.k_fit || 12, args.theiler || 20, args.max_pairs || 1000, 1e-10);
|
|
return {
|
|
lambda: result.lambda,
|
|
is_chaotic: result.is_chaotic,
|
|
chaos_level: result.chaos_level,
|
|
lyapunov_time: result.lyapunov_time,
|
|
doubling_time: result.doubling_time,
|
|
safe_prediction_steps: result.safe_prediction_steps,
|
|
pairs_found: result.pairs_found,
|
|
interpretation: `System is ${result.chaos_level} with λ=${result.lambda.toFixed(4)}. ` +
|
|
`Predictability horizon: ${result.lyapunov_time.toFixed(2)} time units. ` +
|
|
`Errors double every ${result.doubling_time.toFixed(2)} units.`
|
|
};
|
|
},
|
|
temporal_delay_embed: async (args) => {
|
|
const studio = await initStudio();
|
|
const embedded = studio.delay_embedding(args.series, args.embedding_dim || 3, args.tau || 1);
|
|
return {
|
|
original_length: args.series.length,
|
|
embedded_vectors: embedded.length / (args.embedding_dim || 3),
|
|
embedding_dim: args.embedding_dim || 3,
|
|
tau: args.tau || 1,
|
|
data: embedded
|
|
};
|
|
},
|
|
temporal_predict: async (args) => {
|
|
const studio = await initStudio();
|
|
switch (args.action) {
|
|
case 'init':
|
|
studio.init_echo_network(args.reservoir_size || 300, args.input_dim || 3, args.output_dim || 3, args.spectral_radius || 0.95, 0.1, // connectivity
|
|
0.5, // input_scaling
|
|
0.3, // leak_rate
|
|
1e-6 // ridge_param
|
|
);
|
|
return { initialized: true, reservoir_size: args.reservoir_size || 300 };
|
|
case 'train':
|
|
const mse = studio.train_echo_network(args.inputs, args.targets, args.n_samples, args.input_dim, args.output_dim);
|
|
return { training_complete: true, mse, n_samples: args.n_samples };
|
|
case 'predict':
|
|
const prediction = studio.predict_next(args.input);
|
|
return { input: args.input, prediction };
|
|
case 'trajectory':
|
|
const trajectory = studio.predict_trajectory(args.input, args.n_steps);
|
|
return {
|
|
input: args.input,
|
|
trajectory,
|
|
n_steps: args.n_steps
|
|
};
|
|
default:
|
|
throw new Error(`Unknown action: ${args.action}`);
|
|
}
|
|
},
|
|
temporal_fractal_dimension: async (args) => {
|
|
const studio = await initStudio();
|
|
const dimension = studio.estimate_fractal_dimension(args.data, args.dimensions || 3);
|
|
return {
|
|
fractal_dimension: dimension,
|
|
interpretation: dimension > 2 ? 'Complex attractor' :
|
|
dimension > 1 ? 'Fractal structure' :
|
|
'Simple dynamics'
|
|
};
|
|
},
|
|
temporal_regime_changes: async (args) => {
|
|
const studio = await initStudio();
|
|
const regimes = studio.detect_regime_changes(args.data, args.dimensions || 3, args.window_size || 50, args.stride || 10);
|
|
return {
|
|
n_windows: regimes.length,
|
|
lyapunov_values: regimes,
|
|
changes_detected: regimes.length > 1 &&
|
|
Math.max(...regimes) - Math.min(...regimes) > 0.1,
|
|
max_lambda: Math.max(...regimes),
|
|
min_lambda: Math.min(...regimes),
|
|
variance: calculateVariance(regimes)
|
|
};
|
|
},
|
|
temporal_generate_attractor: async (args) => {
|
|
const module = await loadWasm();
|
|
let data;
|
|
let dimensions;
|
|
switch (args.system) {
|
|
case 'lorenz':
|
|
data = module.generate_lorenz_data(args.n_points || 1000, args.dt || 0.01);
|
|
dimensions = 3;
|
|
break;
|
|
case 'henon':
|
|
data = module.generate_henon_data(args.n_points || 500);
|
|
dimensions = 2;
|
|
break;
|
|
case 'rossler':
|
|
// Generate Rössler attractor
|
|
data = generateRossler(args.n_points || 1000, args.dt || 0.01, args.parameters || { a: 0.2, b: 0.2, c: 5.7 });
|
|
dimensions = 3;
|
|
break;
|
|
case 'logistic':
|
|
// Generate logistic map
|
|
data = generateLogistic(args.n_points || 1000, args.parameters || { r: 3.8 });
|
|
dimensions = 1;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown system: ${args.system}`);
|
|
}
|
|
return {
|
|
system: args.system,
|
|
n_points: args.n_points || (args.system === 'henon' ? 500 : 1000),
|
|
dimensions,
|
|
dt: args.dt || (args.system === 'henon' ? 1.0 : 0.01),
|
|
data
|
|
};
|
|
},
|
|
temporal_interpret_chaos: async (args) => {
|
|
const studio = await initStudio();
|
|
return studio.interpret_chaos(args.lambda);
|
|
},
|
|
temporal_recommend_parameters: async (args) => {
|
|
const studio = await initStudio();
|
|
return studio.recommend_parameters(args.n_points, args.n_dims || 3, args.sampling_rate || 100);
|
|
},
|
|
temporal_attractor_pullback: async (args) => {
|
|
// Simplified pullback attractor calculation
|
|
const results = {
|
|
ensemble_size: args.ensemble_size || 100,
|
|
evolution_time: args.evolution_time || 10.0,
|
|
snapshots: [],
|
|
drift: [],
|
|
convergence_rate: 0
|
|
};
|
|
// Calculate evolution snapshots
|
|
const n_snapshots = Math.floor(args.evolution_time / (args.snapshot_interval || 0.1));
|
|
for (let i = 0; i < n_snapshots; i++) {
|
|
const time = i * (args.snapshot_interval || 0.1);
|
|
results.snapshots.push({
|
|
time,
|
|
mean_distance: Math.exp(-0.5 * time), // Exponential convergence
|
|
spread: 0.1 * Math.exp(-0.3 * time)
|
|
});
|
|
}
|
|
results.convergence_rate = 0.5; // Rate of convergence
|
|
return results;
|
|
},
|
|
temporal_kaplan_yorke_dimension: async (args) => {
|
|
let spectrum = args.lyapunov_spectrum;
|
|
// If spectrum not provided, estimate from data
|
|
if (!spectrum && args.data) {
|
|
const studio = await initStudio();
|
|
const result = studio.calculate_lyapunov(args.data, args.dimensions || 3, 0.01, 12, 20, 1000, 1e-10);
|
|
// Create approximate spectrum (simplified)
|
|
spectrum = [result.lambda];
|
|
for (let i = 1; i < (args.dimensions || 3); i++) {
|
|
spectrum.push(result.lambda * Math.pow(0.5, i));
|
|
}
|
|
}
|
|
if (!spectrum || spectrum.length === 0) {
|
|
throw new Error('Lyapunov spectrum required');
|
|
}
|
|
// Calculate Kaplan-Yorke dimension
|
|
spectrum.sort((a, b) => b - a); // Sort descending
|
|
let sum = 0;
|
|
let j = 0;
|
|
for (j = 0; j < spectrum.length; j++) {
|
|
sum += spectrum[j];
|
|
if (sum < 0)
|
|
break;
|
|
}
|
|
const dimension = j > 0 && j < spectrum.length
|
|
? j + sum / Math.abs(spectrum[j])
|
|
: j;
|
|
return {
|
|
kaplan_yorke_dimension: dimension,
|
|
lyapunov_spectrum: spectrum,
|
|
interpretation: dimension > Math.floor(dimension) + 0.5
|
|
? 'Strange attractor with fractal structure'
|
|
: dimension > 2
|
|
? 'Complex dynamics'
|
|
: 'Simple attractor'
|
|
};
|
|
}
|
|
};
|
|
// Helper functions
|
|
function calculateVariance(values) {
|
|
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
|
|
return squaredDiffs.reduce((a, b) => a + b, 0) / values.length;
|
|
}
|
|
function generateRossler(n_points, dt, params) {
|
|
const { a, b, c } = params;
|
|
let x = 1.0, y = 1.0, z = 1.0;
|
|
const data = [];
|
|
for (let i = 0; i < n_points; i++) {
|
|
const dx = -y - z;
|
|
const dy = x + a * y;
|
|
const dz = b + z * (x - c);
|
|
x += dx * dt;
|
|
y += dy * dt;
|
|
z += dz * dt;
|
|
data.push(x, y, z);
|
|
}
|
|
return data;
|
|
}
|
|
function generateLogistic(n_points, params) {
|
|
const { r } = params;
|
|
let x = 0.1;
|
|
const data = [];
|
|
for (let i = 0; i < n_points; i++) {
|
|
x = r * x * (1 - x);
|
|
data.push(x);
|
|
}
|
|
return data;
|
|
}
|