herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
750 lines (630 loc) • 23.3 kB
JavaScript
/**
* Optimization module for herta.js
* Provides various optimization algorithms and techniques
*/
const Decimal = require('decimal.js');
const matrix = require('../core/matrix');
const arithmetic = require('../core/arithmetic');
const optimization = {};
/**
* Gradient descent optimization algorithm
* @param {Function} costFunction - Cost function to minimize f(x)
* @param {Function} gradientFunction - Gradient of the cost function ∇f(x)
* @param {Array} initialPoint - Starting point for optimization
* @param {Object} options - Optimization options
* @param {number} [options.learningRate=0.01] - Step size for gradient descent
* @param {number} [options.maxIterations=1000] - Maximum number of iterations
* @param {number} [options.tolerance=1e-6] - Convergence tolerance
* @returns {Object} - Result containing optimized point and convergence info
*/
optimization.gradientDescent = function (costFunction, gradientFunction, initialPoint, options = {}) {
const learningRate = options.learningRate || 0.01;
const maxIterations = options.maxIterations || 1000;
const tolerance = options.tolerance || 1e-6;
let currentPoint = [...initialPoint];
let iteration = 0;
let currentCost = costFunction(currentPoint);
let converged = false;
const costs = [currentCost];
while (iteration < maxIterations && !converged) {
const gradient = gradientFunction(currentPoint);
// Update point: x = x - learningRate * gradient
const newPoint = currentPoint.map((xi, i) => xi - learningRate * gradient[i]);
const newCost = costFunction(newPoint);
costs.push(newCost);
// Check convergence
const improvement = Math.abs(currentCost - newCost);
if (improvement < tolerance) {
converged = true;
}
currentPoint = newPoint;
currentCost = newCost;
iteration++;
}
return {
point: currentPoint,
value: currentCost,
iterations: iteration,
converged,
costs
};
};
/**
* Stochastic gradient descent with momentum
* @param {Function} costFunction - Cost function that takes point and data index
* @param {Function} gradientFunction - Gradient function that takes point and data index
* @param {Array} initialPoint - Starting point
* @param {number} dataSize - Size of the dataset
* @param {Object} options - Optimization options
* @returns {Object} - Optimization result
*/
optimization.sgdWithMomentum = function (costFunction, gradientFunction, initialPoint, dataSize, options = {}) {
const learningRate = options.learningRate || 0.01;
const momentum = options.momentum || 0.9;
const maxIterations = options.maxIterations || 1000;
const batchSize = options.batchSize || 32;
let currentPoint = [...initialPoint];
let velocity = Array(currentPoint.length).fill(0);
const costs = [];
for (let epoch = 0; epoch < maxIterations; epoch++) {
// Shuffle data indices
const indices = Array.from({ length: dataSize }, (_, i) => i);
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}
// Process in mini-batches
for (let i = 0; i < dataSize; i += batchSize) {
const batchIndices = indices.slice(i, i + batchSize);
const batchGradients = [];
// Compute gradients for batch
for (const idx of batchIndices) {
batchGradients.push(gradientFunction(currentPoint, idx));
}
// Average gradients
const avgGradient = batchGradients.reduce((acc, g) => acc.map((v, i) => v + g[i] / batchGradients.length), Array(currentPoint.length).fill(0));
// Update with momentum
velocity = velocity.map((v, i) => momentum * v - learningRate * avgGradient[i]);
currentPoint = currentPoint.map((p, i) => p + velocity[i]);
}
costs.push(costFunction(currentPoint));
}
return {
point: currentPoint,
value: costFunction(currentPoint),
iterations: maxIterations,
costs
};
};
/**
* Newton's method for optimization
* @param {Function} costFunction - Cost function to minimize
* @param {Function} gradientFunction - Gradient of the cost function
* @param {Function} hessianFunction - Hessian (matrix of second derivatives)
* @param {Array} initialPoint - Starting point
* @param {Object} options - Optimization options
* @returns {Object} - Optimization result
*/
optimization.newtonMethod = function (costFunction, gradientFunction, hessianFunction, initialPoint, options = {}) {
const maxIterations = options.maxIterations || 100;
const tolerance = options.tolerance || 1e-6;
let currentPoint = [...initialPoint];
let iteration = 0;
let converged = false;
const costs = [];
while (iteration < maxIterations && !converged) {
const gradient = gradientFunction(currentPoint);
const hessian = hessianFunction(currentPoint);
// Calculate step: dx = -H^(-1) * gradient
const hessianInverse = matrix.inverse(hessian);
const step = matrix.multiply(hessianInverse, gradient.map((g) => -g));
// Update: x = x + dx
const newPoint = currentPoint.map((x, i) => x + step[i]);
const currentCost = costFunction(currentPoint);
const newCost = costFunction(newPoint);
costs.push(newCost);
// Check convergence
const improvement = Math.abs(currentCost - newCost);
if (improvement < tolerance) {
converged = true;
}
currentPoint = newPoint;
iteration++;
}
return {
point: currentPoint,
value: costFunction(currentPoint),
iterations: iteration,
converged,
costs
};
};
/**
* Implement linear programming using Simplex method
* @param {Array} objectiveCoefficients - Coefficients of the objective function to maximize
* @param {Array} constraints - Array of constraint objects {coefficients, limit, type}
* @param {Object} options - Optimization options
* @returns {Object} - Optimization result
*/
optimization.linearProgramming = function (objectiveCoefficients, constraints, options = {}) {
// Note: This is a simplified version of the Simplex algorithm
// A full implementation would be more complex
const maxIterations = options.maxIterations || 1000;
const n = objectiveCoefficients.length; // Number of variables
const m = constraints.length; // Number of constraints
// Create tableau
const tableau = [];
// Objective function row (negative for maximization problem)
const objectiveRow = [0, ...objectiveCoefficients.map((c) => -c)];
for (let i = 0; i < m; i++) {
objectiveRow.push(0);
}
tableau.push(objectiveRow);
// Constraint rows
for (let i = 0; i < m; i++) {
const row = [constraints[i].limit];
for (let j = 0; j < n; j++) {
row.push(constraints[i].coefficients[j]);
}
// Add slack variables
for (let j = 0; j < m; j++) {
row.push(i === j ? 1 : 0);
}
tableau.push(row);
}
// Perform simplex iterations
let iteration = 0;
while (iteration < maxIterations) {
// Find the pivot column (most negative coefficient in objective row)
let pivotCol = -1;
let minCoeff = 0;
for (let j = 1; j < tableau[0].length; j++) {
if (tableau[0][j] < minCoeff) {
minCoeff = tableau[0][j];
pivotCol = j;
}
}
// If no negative coefficients, we're done
if (pivotCol === -1) break;
// Find pivot row (minimum ratio test)
let pivotRow = -1;
let minRatio = Infinity;
for (let i = 1; i < tableau.length; i++) {
if (tableau[i][pivotCol] > 0) {
const ratio = tableau[i][0] / tableau[i][pivotCol];
if (ratio < minRatio) {
minRatio = ratio;
pivotRow = i;
}
}
}
// If no suitable pivot found, problem is unbounded
if (pivotRow === -1) {
return { status: 'unbounded' };
}
// Pivot operation
const pivotValue = tableau[pivotRow][pivotCol];
// Scale pivot row
for (let j = 0; j < tableau[pivotRow].length; j++) {
tableau[pivotRow][j] /= pivotValue;
}
// Update other rows
for (let i = 0; i < tableau.length; i++) {
if (i !== pivotRow) {
const factor = tableau[i][pivotCol];
for (let j = 0; j < tableau[i].length; j++) {
tableau[i][j] -= factor * tableau[pivotRow][j];
}
}
}
iteration++;
}
// Extract solution
const solution = Array(n).fill(0);
const basicVars = Array(m).fill(-1);
// Identify basic variables
for (let j = 1; j <= n; j++) {
let basicVarRow = -1;
let count = 0;
for (let i = 1; i < tableau.length; i++) {
if (Math.abs(tableau[i][j] - 1) < 1e-10) {
count++;
basicVarRow = i;
} else if (Math.abs(tableau[i][j]) > 1e-10) {
count++;
}
}
if (count === 1 && basicVarRow !== -1) {
basicVars[basicVarRow - 1] = j - 1;
solution[j - 1] = tableau[basicVarRow][0];
}
}
return {
status: 'optimal',
solution,
objectiveValue: -tableau[0][0],
tableau
};
};
/**
* Particle Swarm Optimization (PSO)
* @param {Function} costFunction - Function to optimize
* @param {Array} bounds - Bounds for each dimension [[min1, max1], [min2, max2], ...]
* @param {Object} options - PSO options
* @returns {Object} - Optimization result
*/
optimization.particleSwarm = function (costFunction, bounds, options = {}) {
const numParticles = options.numParticles || 30;
const dimensions = bounds.length;
const maxIterations = options.maxIterations || 100;
const inertiaWeight = options.inertiaWeight || 0.7;
const cognitiveWeight = options.cognitiveWeight || 1.5;
const socialWeight = options.socialWeight || 1.5;
// Initialize particles
const particles = [];
for (let i = 0; i < numParticles; i++) {
// Random position within bounds
const position = bounds.map(([min, max]) => min + Math.random() * (max - min));
// Random velocity
const velocity = bounds.map(([min, max]) => (Math.random() - 0.5) * (max - min) * 0.1);
// Best position for this particle is initially its starting position
const personalBest = [...position];
const personalBestValue = costFunction(position);
particles.push({
position,
velocity,
personalBest,
personalBestValue
});
}
// Track global best
let globalBest = [...particles[0].personalBest];
let globalBestValue = particles[0].personalBestValue;
for (let i = 1; i < particles.length; i++) {
if (particles[i].personalBestValue < globalBestValue) {
globalBestValue = particles[i].personalBestValue;
globalBest = [...particles[i].personalBest];
}
}
// Main PSO loop
for (let iteration = 0; iteration < maxIterations; iteration++) {
for (let i = 0; i < numParticles; i++) {
const particle = particles[i];
// Update velocity
for (let d = 0; d < dimensions; d++) {
// Cognitive component (personal best)
const cognitive = cognitiveWeight * Math.random() * (particle.personalBest[d] - particle.position[d]);
// Social component (global best)
const social = socialWeight * Math.random() * (globalBest[d] - particle.position[d]);
// Update velocity with inertia
particle.velocity[d] = inertiaWeight * particle.velocity[d] + cognitive + social;
}
// Update position
for (let d = 0; d < dimensions; d++) {
particle.position[d] += particle.velocity[d];
// Bound check
particle.position[d] = Math.max(bounds[d][0], Math.min(bounds[d][1], particle.position[d]));
}
// Evaluate new position
const value = costFunction(particle.position);
// Update personal best
if (value < particle.personalBestValue) {
particle.personalBestValue = value;
particle.personalBest = [...particle.position];
// Update global best
if (value < globalBestValue) {
globalBestValue = value;
globalBest = [...particle.position];
}
}
}
}
return {
solution: globalBest,
value: globalBestValue
};
};
/**
* Simulated Annealing optimization algorithm
* @param {Function} costFunction - Function to minimize
* @param {Function} neighborFunction - Function that generates a neighboring solution
* @param {Array} initialSolution - Starting solution
* @param {Object} options - Annealing options
* @returns {Object} - Optimization result
*/
optimization.simulatedAnnealing = function (costFunction, neighborFunction, initialSolution, options = {}) {
const maxIterations = options.maxIterations || 1000;
const initialTemperature = options.initialTemperature || 100;
const coolingRate = options.coolingRate || 0.95;
const minTemperature = options.minTemperature || 1e-6;
let currentSolution = [...initialSolution];
let currentCost = costFunction(currentSolution);
let bestSolution = [...currentSolution];
let bestCost = currentCost;
let temperature = initialTemperature;
const costs = [currentCost];
const temperatures = [initialTemperature];
for (let i = 0; i < maxIterations && temperature > minTemperature; i++) {
// Generate neighbor solution
const newSolution = neighborFunction(currentSolution, temperature);
const newCost = costFunction(newSolution);
// Calculate acceptance probability
let acceptanceProbability = 1;
if (newCost > currentCost) {
acceptanceProbability = Math.exp((currentCost - newCost) / temperature);
}
// Accept or reject new solution
if (Math.random() < acceptanceProbability) {
currentSolution = [...newSolution];
currentCost = newCost;
// Update best solution if needed
if (newCost < bestCost) {
bestSolution = [...newSolution];
bestCost = newCost;
}
}
// Cool down
temperature *= coolingRate;
temperatures.push(temperature);
costs.push(currentCost);
}
return {
solution: bestSolution,
cost: bestCost,
history: {
costs,
temperatures
}
};
};
/**
* Genetic Algorithm optimization
* @param {Function} fitnessFunction - Function that evaluates fitness of a solution (higher is better)
* @param {Function} createIndividual - Function that creates a random individual
* @param {Function} crossover - Function that performs crossover between two parents
* @param {Function} mutate - Function that mutates an individual
* @param {Object} options - Genetic algorithm options
* @returns {Object} - Optimization result
*/
optimization.geneticAlgorithm = function (fitnessFunction, createIndividual, crossover, mutate, options = {}) {
const populationSize = options.populationSize || 100;
const maxGenerations = options.maxGenerations || 100;
const crossoverRate = options.crossoverRate || 0.8;
const mutationRate = options.mutationRate || 0.1;
const elitism = options.elitism || true;
const tournamentSize = options.tournamentSize || 5;
// Initialize population
let population = [];
for (let i = 0; i < populationSize; i++) {
population.push(createIndividual());
}
// Evaluate initial population
let fitnesses = population.map((individual) => fitnessFunction(individual));
// Track best solution over generations
let bestIndividual = [...population[fitnesses.indexOf(Math.max(...fitnesses))]];
let bestFitness = Math.max(...fitnesses);
const fitnessHistory = [bestFitness];
// Generational loop
for (let generation = 0; generation < maxGenerations; generation++) {
const newPopulation = [];
// Elitism: preserve best individual
if (elitism) {
newPopulation.push([...bestIndividual]);
}
// Create new individuals until we fill the population
while (newPopulation.length < populationSize) {
// Tournament selection for parents
const parent1 = tournamentSelect(population, fitnesses, tournamentSize);
const parent2 = tournamentSelect(population, fitnesses, tournamentSize);
// Crossover
let child1; let
child2;
if (Math.random() < crossoverRate) {
[child1, child2] = crossover(parent1, parent2);
} else {
child1 = [...parent1];
child2 = [...parent2];
}
// Mutation
if (Math.random() < mutationRate) {
child1 = mutate(child1);
}
if (Math.random() < mutationRate) {
child2 = mutate(child2);
}
newPopulation.push(child1);
if (newPopulation.length < populationSize) {
newPopulation.push(child2);
}
}
// Replace population
population = newPopulation;
// Evaluate new population
fitnesses = population.map((individual) => fitnessFunction(individual));
// Update best solution
const genBestIndex = fitnesses.indexOf(Math.max(...fitnesses));
if (fitnesses[genBestIndex] > bestFitness) {
bestIndividual = [...population[genBestIndex]];
bestFitness = fitnesses[genBestIndex];
}
fitnessHistory.push(bestFitness);
}
return {
solution: bestIndividual,
fitness: bestFitness,
fitnessHistory
};
// Helper function for tournament selection
function tournamentSelect(population, fitnesses, tournamentSize) {
const indices = [];
for (let i = 0; i < tournamentSize; i++) {
indices.push(Math.floor(Math.random() * population.length));
}
let bestIndex = indices[0];
for (let i = 1; i < indices.length; i++) {
if (fitnesses[indices[i]] > fitnesses[bestIndex]) {
bestIndex = indices[i];
}
}
return population[bestIndex];
}
};
/**
* Differential Evolution optimization algorithm
* @param {Function} costFunction - Function to minimize
* @param {Array} bounds - Bounds for each dimension [[min1, max1], [min2, max2], ...]
* @param {Object} options - DE options
* @returns {Object} - Optimization result
*/
optimization.differentialEvolution = function (costFunction, bounds, options = {}) {
const populationSize = options.populationSize || 10 * bounds.length; // Default: 10*dimension
const maxGenerations = options.maxGenerations || 1000;
const F = options.scalingFactor || 0.8; // Differential weight
const CR = options.crossoverRate || 0.9; // Crossover probability
// Initialize population within bounds
let population = [];
for (let i = 0; i < populationSize; i++) {
const individual = bounds.map(([min, max]) => min + Math.random() * (max - min));
population.push(individual);
}
// Evaluate initial population
let costs = population.map((individual) => costFunction(individual));
// Track best solution
const bestIndex = costs.indexOf(Math.min(...costs));
let bestSolution = [...population[bestIndex]];
let bestCost = costs[bestIndex];
const costHistory = [bestCost];
// Generation loop
for (let generation = 0; generation < maxGenerations; generation++) {
// Create new population
const newPopulation = [];
const newCosts = [];
for (let i = 0; i < populationSize; i++) {
// Select three random distinct individuals different from current
const [a, b, c] = selectThreeIndices(populationSize, i);
// Create mutant vector through differential mutation
const mutant = population[a].map((x, j) => x + F * (population[b][j] - population[c][j]));
// Ensure mutant is within bounds
for (let j = 0; j < mutant.length; j++) {
if (mutant[j] < bounds[j][0]) mutant[j] = bounds[j][0];
if (mutant[j] > bounds[j][1]) mutant[j] = bounds[j][1];
}
// Crossover: create trial vector
const trial = [];
const jRand = Math.floor(Math.random() * bounds.length); // Ensure at least one parameter is inherited
for (let j = 0; j < bounds.length; j++) {
if (Math.random() < CR || j === jRand) {
trial.push(mutant[j]);
} else {
trial.push(population[i][j]);
}
}
// Selection: compare trial with current individual
const trialCost = costFunction(trial);
if (trialCost <= costs[i]) {
newPopulation.push(trial);
newCosts.push(trialCost);
// Update best solution if needed
if (trialCost < bestCost) {
bestSolution = [...trial];
bestCost = trialCost;
}
} else {
newPopulation.push([...population[i]]);
newCosts.push(costs[i]);
}
}
population = newPopulation;
costs = newCosts;
costHistory.push(bestCost);
}
return {
solution: bestSolution,
cost: bestCost,
costHistory
};
// Helper function to select three random distinct indices
function selectThreeIndices(populationSize, exclude) {
const indices = [];
while (indices.length < 3) {
const idx = Math.floor(Math.random() * populationSize);
if (idx !== exclude && !indices.includes(idx)) {
indices.push(idx);
}
}
return indices;
}
};
/**
* COBYLA (Constrained Optimization By Linear Approximation) algorithm
* @param {Function} objective - Objective function to minimize
* @param {Array} constraints - Array of constraint functions, each should return <= 0 for feasible points
* @param {Array} initialPoint - Starting point
* @param {Object} options - COBYLA options
* @returns {Object} - Optimization result
*/
optimization.cobyla = function (objective, constraints, initialPoint, options = {}) {
const maxIterations = options.maxIterations || 1000;
const rhoBeg = options.rhoBeg || 1.0; // Initial trust region radius
const rhoEnd = options.rhoEnd || 1e-6; // Final trust region radius
const constraintPenalty = options.constraintPenalty || 1000.0;
// Combine objective and constraints into a single function
function merit(x) {
let value = objective(x);
// Apply penalty for constraint violations
for (const constraint of constraints) {
const violation = constraint(x);
if (violation > 0) {
value += constraintPenalty * violation;
}
}
return value;
}
// Implement simplified COBYLA algorithm
// (For a full implementation, we would need a more complex trust region method)
let x = [...initialPoint];
let rho = rhoBeg;
const n = x.length;
const costs = [merit(x)];
let iteration = 0;
while (iteration < maxIterations && rho >= rhoEnd) {
let improved = false;
// Try perturbations in each direction
for (let i = 0; i < n; i++) {
// Current point
const currentValue = merit(x);
// Try positive perturbation
const xPlus = [...x];
xPlus[i] += rho;
const valuePlus = merit(xPlus);
// Try negative perturbation
const xMinus = [...x];
xMinus[i] -= rho;
const valueMinus = merit(xMinus);
// Update if improved
if (valuePlus < currentValue || valueMinus < currentValue) {
if (valuePlus < valueMinus) {
x = xPlus;
} else {
x = xMinus;
}
improved = true;
break; // We found an improvement, move to next iteration
}
}
// If no improvement, reduce trust region
if (!improved) {
rho *= 0.5;
}
costs.push(merit(x));
iteration++;
}
return {
solution: x,
cost: objective(x),
iterations: iteration,
isFeasible: constraints.every((c) => c(x) <= 0),
costHistory: costs
};
};
module.exports = optimization;