solver-core-compute
Version:
Shared solver library for floorplan optimization - compatible with Node.js and Deno
325 lines (259 loc) • 8.9 kB
text/typescript
/**
* 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);
}