UNPKG

solver-core-compute

Version:

Shared solver library for floorplan optimization - compatible with Node.js and Deno

325 lines (259 loc) 8.9 kB
/** * Local Search Algorithm * Deno-compatible version */ import type { Position, Constraint, LSConfig, LSNeighbor, LSSolution } from '../types/index.js'; import { calculateCost } from '../constraint-evaluator.js'; export class LocalSearchSolver { private config: LSConfig; private bestSolution: LSSolution | null = null; private tabuList: Set<string> = new Set(); constructor(config?: Partial<LSConfig>) { this.config = { maxIterations: 1000, stepSize: 5, perturbation: 10, neighborsPerIteration: 5, restartLimit: 3, tabuListSize: 10, ...config }; } public solve( initialPositions: Position[], constraints: Constraint[], objective?: (positions: Position[]) => number, timeLimit?: number ): LSSolution { const startTime = Date.now(); const maxTime = timeLimit || 1000; // Default 1000ms let currentPositions = this.copyPositions(initialPositions); let currentCost = calculateCost(currentPositions, constraints, objective); let bestCost = currentCost; let bestPositions = this.copyPositions(currentPositions); let iterations = 0; let improvements = 0; let restarts = 0; let neighborhoodEvaluations = 0; for (let restart = 0; restart < this.config.restartLimit; restart++) { let localOptimum = false; let iterationSinceImprovement = 0; while ( iterations < this.config.maxIterations && iterationSinceImprovement < 100 && !localOptimum && Date.now() - startTime < maxTime ) { const neighbors = this.generateNeighbors( currentPositions, this.config.neighborsPerIteration ); neighborhoodEvaluations += neighbors.length; let bestNeighbor = this.getBestNeighbor( neighbors, constraints, objective ); if (bestNeighbor && bestNeighbor.cost < currentCost) { const moveKey = this.getMoveKey(currentPositions, bestNeighbor.positions); if (!this.tabuList.has(moveKey) || bestNeighbor.cost < bestCost - 0.001) { currentPositions = bestNeighbor.positions; currentCost = bestNeighbor.cost; if (currentCost < bestCost) { bestCost = currentCost; bestPositions = this.copyPositions(currentPositions); improvements++; } iterationSinceImprovement = 0; if (bestCost === 0) { break; } } } else { iterationSinceImprovement++; localOptimum = true; } iterations++; this.updateTabuList(this.getMoveKey(currentPositions, currentPositions)); } if (bestCost === 0) { break; } if (iterationSinceImprovement >= 100) { restarts++; currentPositions = this.randomPerturbation(bestPositions, this.config.perturbation); currentCost = calculateCost(currentPositions, constraints, objective); iterationSinceImprovement = 0; if (currentCost < bestCost) { bestCost = currentCost; bestPositions = this.copyPositions(currentPositions); } } } this.bestSolution = { positions: bestPositions, cost: bestCost, iterations, algorithm: `local-search-${Date.now() - startTime}ms`, metrics: { improvements, restarts, neighborhoodEvaluations } }; return this.bestSolution; } private generateNeighbors(positions: Position[], count: number): LSNeighbor[] { const neighbors: LSNeighbor[] = []; for (let i = 0; i < count; i++) { const operator = Math.floor(Math.random() * 4); switch (operator) { case 0: neighbors.push({ ...this.movePosition(positions), moveType: 'single_move' }); break; case 1: neighbors.push({ ...this.swapPositions(positions), moveType: 'swap' }); break; case 2: neighbors.push({ ...this.rotatePosition(positions), moveType: 'rotate' }); break; case 3: neighbors.push({ ...this.multiMove(positions), moveType: 'multi_move' }); break; } } return neighbors; } private movePosition(positions: Position[]): LSNeighbor { const newPositions = this.copyPositions(positions); const index = Math.floor(Math.random() * newPositions.length); const angle = Math.random() * 2 * Math.PI; const distance = this.config.stepSize; newPositions[index].x += Math.cos(angle) * distance; newPositions[index].y += Math.sin(angle) * distance; return { positions: newPositions, cost: 0, moveType: 'single_move' }; } private swapPositions(positions: Position[]): LSNeighbor { const newPositions = this.copyPositions(positions); const i = Math.floor(Math.random() * newPositions.length); let j = Math.floor(Math.random() * newPositions.length); while (j === i && newPositions.length > 1) { j = Math.floor(Math.random() * newPositions.length); } const tempX = newPositions[i].x; const tempY = newPositions[i].y; newPositions[i].x = newPositions[j].x; newPositions[i].y = newPositions[j].y; newPositions[j].x = tempX; newPositions[j].y = tempY; return { positions: newPositions, cost: 0, moveType: 'swap' }; } private rotatePosition(positions: Position[]): LSNeighbor { const newPositions = this.copyPositions(positions); const index = Math.floor(Math.random() * newPositions.length); const rotation = (Math.random() - 0.5) * 90; newPositions[index].rotation = (newPositions[index].rotation || 0) + rotation; const temp = newPositions[index].width; newPositions[index].width = newPositions[index].height; newPositions[index].height = temp; return { positions: newPositions, cost: 0, moveType: 'rotate' }; } private multiMove(positions: Position[]): LSNeighbor { const newPositions = this.copyPositions(positions); const count = Math.floor(Math.random() * 3) + 1; for (let k = 0; k < count; k++) { const index = Math.floor(Math.random() * newPositions.length); const angle = Math.random() * 2 * Math.PI; const distance = this.config.stepSize / 2; newPositions[index].x += Math.cos(angle) * distance; newPositions[index].y += Math.sin(angle) * distance; } return { positions: newPositions, cost: 0, moveType: 'multi_move' }; } private getBestNeighbor( neighbors: LSNeighbor[], constraints: Constraint[], objective?: (positions: Position[]) => number ): LSNeighbor | null { if (neighbors.length === 0) return null; let bestNeighbor = neighbors[0]; bestNeighbor.cost = calculateCost(bestNeighbor.positions, constraints, objective); for (let i = 1; i < neighbors.length; i++) { neighbors[i].cost = calculateCost(neighbors[i].positions, constraints, objective); if (neighbors[i].cost < bestNeighbor.cost) { bestNeighbor = neighbors[i]; } } return bestNeighbor; } private randomPerturbation(positions: Position[], magnitude: number): Position[] { return positions.map(p => ({ ...p, x: p.x + (Math.random() - 0.5) * magnitude, y: p.y + (Math.random() - 0.5) * magnitude })); } private getMoveKey(positions1: Position[], positions2: Position[]): string { let key = ''; for (let i = 0; i < Math.min(positions1.length, positions2.length); i++) { const dx = positions2[i].x - positions1[i].x; const dy = positions2[i].y - positions1[i].y; key += `${dx.toFixed(1)},${dy.toFixed(1)};`; } return key || ''; } private updateTabuList(moveKey: string) { if (!moveKey) return; this.tabuList.add(moveKey); if (this.tabuList.size > this.config.tabuListSize!) { const firstKey = this.tabuList.values().next().value as string; this.tabuList.delete(firstKey); } } private copyPositions(positions: Position[]): Position[] { return positions.map(p => ({ ...p })); } public getBestSolution() { return this.bestSolution; } public getStats() { return { iterations: this.bestSolution?.iterations || 0, improvements: this.bestSolution?.metrics.improvements || 0, restarts: this.bestSolution?.metrics.restarts || 0, neighborhoodEvaluations: this.bestSolution?.metrics.neighborhoodEvaluations || 0, algorithm: 'local-search' }; } } export function createLocalSearchSolver(config?: Partial<LSConfig>): LocalSearchSolver { return new LocalSearchSolver(config); }