737 lines
25 KiB
JavaScript
737 lines
25 KiB
JavaScript
/**
|
|
* LiXX Cell Pack Matcher - Web Worker
|
|
*
|
|
* Runs matching algorithms in a background thread to keep the UI responsive.
|
|
* Communicates with the main thread via postMessage.
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Utility Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Calculate the coefficient of variation (CV) as a percentage.
|
|
*/
|
|
function coefficientOfVariation(values) {
|
|
if (!values || values.length === 0) return 0;
|
|
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
if (mean === 0) return 0;
|
|
const variance = values.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / values.length;
|
|
return (Math.sqrt(variance) / mean) * 100;
|
|
}
|
|
|
|
/**
|
|
* Shuffle array in place using Fisher-Yates algorithm.
|
|
*/
|
|
function shuffleArray(array) {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Deep clone an array of arrays.
|
|
*/
|
|
function cloneConfiguration(arr) {
|
|
return arr.map(group => [...group]);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Scoring Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Calculate the match score for a pack configuration.
|
|
* Lower score = better match.
|
|
*/
|
|
function calculateScore(configuration, capacityWeight = 0.7, irWeight = 0.3) {
|
|
const groupCapacities = configuration.map(group =>
|
|
group.reduce((sum, cell) => sum + cell.capacity, 0)
|
|
);
|
|
|
|
const groupIRs = configuration.map(group => {
|
|
const irsWithValues = group.filter(cell => cell.ir !== null && cell.ir !== undefined);
|
|
if (irsWithValues.length === 0) return null;
|
|
return irsWithValues.reduce((sum, cell) => sum + cell.ir, 0) / irsWithValues.length;
|
|
}).filter(ir => ir !== null);
|
|
|
|
const withinGroupIRVariances = configuration.map(group => {
|
|
const irsWithValues = group.filter(cell => cell.ir !== null && cell.ir !== undefined);
|
|
if (irsWithValues.length < 2) return 0;
|
|
const irs = irsWithValues.map(cell => cell.ir);
|
|
return coefficientOfVariation(irs);
|
|
});
|
|
|
|
const capacityCV = coefficientOfVariation(groupCapacities);
|
|
const avgWithinGroupIRCV = withinGroupIRVariances.length > 0
|
|
? withinGroupIRVariances.reduce((a, b) => a + b, 0) / withinGroupIRVariances.length
|
|
: 0;
|
|
|
|
const score = (capacityWeight * capacityCV) + (irWeight * avgWithinGroupIRCV);
|
|
|
|
return {
|
|
score,
|
|
capacityCV,
|
|
irCV: avgWithinGroupIRCV,
|
|
groupCapacities,
|
|
groupIRs,
|
|
withinGroupIRVariances
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Statistics Tracker
|
|
// =============================================================================
|
|
|
|
class StatsTracker {
|
|
constructor() {
|
|
this.startTime = Date.now();
|
|
this.iterationTimes = [];
|
|
this.lastIterationTime = this.startTime;
|
|
this.windowSize = 100; // Rolling window for time estimates
|
|
}
|
|
|
|
recordIteration() {
|
|
const now = Date.now();
|
|
const iterTime = now - this.lastIterationTime;
|
|
this.lastIterationTime = now;
|
|
|
|
this.iterationTimes.push(iterTime);
|
|
if (this.iterationTimes.length > this.windowSize) {
|
|
this.iterationTimes.shift();
|
|
}
|
|
}
|
|
|
|
getStats(currentIteration, maxIterations) {
|
|
const elapsedTime = Date.now() - this.startTime;
|
|
const avgIterTime = this.iterationTimes.length > 0
|
|
? this.iterationTimes.reduce((a, b) => a + b, 0) / this.iterationTimes.length
|
|
: 0;
|
|
|
|
const remainingIterations = maxIterations - currentIteration;
|
|
const eta = avgIterTime * remainingIterations;
|
|
|
|
return {
|
|
elapsedTime,
|
|
avgIterTime,
|
|
eta,
|
|
iterationsPerSecond: avgIterTime > 0 ? 1000 / avgIterTime : 0
|
|
};
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Genetic Algorithm
|
|
// =============================================================================
|
|
|
|
class GeneticAlgorithm {
|
|
constructor(cells, serial, parallel, options = {}) {
|
|
this.cells = cells;
|
|
this.serial = serial;
|
|
this.parallel = parallel;
|
|
this.totalCellsNeeded = serial * parallel;
|
|
|
|
this.populationSize = options.populationSize || 50;
|
|
this.maxIterations = options.maxIterations || 5000;
|
|
this.mutationRate = options.mutationRate || 0.15;
|
|
this.eliteCount = options.eliteCount || 5;
|
|
this.capacityWeight = options.capacityWeight ?? 0.7;
|
|
this.irWeight = options.irWeight ?? 0.3;
|
|
|
|
this.stopped = false;
|
|
this.bestSolution = null;
|
|
this.bestScore = Infinity;
|
|
this.stats = new StatsTracker();
|
|
}
|
|
|
|
stop() {
|
|
this.stopped = true;
|
|
}
|
|
|
|
createIndividual(cellPool) {
|
|
const shuffled = shuffleArray([...cellPool]).slice(0, this.totalCellsNeeded);
|
|
const configuration = [];
|
|
|
|
for (let i = 0; i < this.serial; i++) {
|
|
const group = [];
|
|
for (let j = 0; j < this.parallel; j++) {
|
|
group.push(shuffled[i * this.parallel + j]);
|
|
}
|
|
configuration.push(group);
|
|
}
|
|
|
|
return configuration;
|
|
}
|
|
|
|
configToIndices(config) {
|
|
const flat = config.flat();
|
|
return flat.map(cell => this.cells.findIndex(c => c.label === cell.label));
|
|
}
|
|
|
|
indicesToConfig(indices) {
|
|
const configuration = [];
|
|
for (let i = 0; i < this.serial; i++) {
|
|
const group = [];
|
|
for (let j = 0; j < this.parallel; j++) {
|
|
const idx = indices[i * this.parallel + j];
|
|
// Safety check: ensure index is valid
|
|
if (idx >= 0 && idx < this.cells.length) {
|
|
group.push(this.cells[idx]);
|
|
} else {
|
|
// Fallback: use a random valid cell
|
|
group.push(this.cells[Math.floor(Math.random() * this.cells.length)]);
|
|
}
|
|
}
|
|
configuration.push(group);
|
|
}
|
|
return configuration;
|
|
}
|
|
|
|
crossover(parent1, parent2) {
|
|
// Simple two-point crossover with repair
|
|
const length = parent1.length;
|
|
|
|
// 50% chance to just return a copy of one parent (with shuffle)
|
|
if (Math.random() < 0.5) {
|
|
const child = [...parent1];
|
|
// Swap a few random positions
|
|
for (let i = 0; i < 2; i++) {
|
|
const a = Math.floor(Math.random() * length);
|
|
const b = Math.floor(Math.random() * length);
|
|
[child[a], child[b]] = [child[b], child[a]];
|
|
}
|
|
return child;
|
|
}
|
|
|
|
// Otherwise, take half from each parent and repair duplicates
|
|
const midpoint = Math.floor(length / 2);
|
|
const child = [...parent1.slice(0, midpoint), ...parent2.slice(midpoint)];
|
|
|
|
// Find and fix duplicates
|
|
const seen = new Set();
|
|
const duplicatePositions = [];
|
|
const allIndices = new Set(parent1.concat(parent2));
|
|
|
|
for (let i = 0; i < child.length; i++) {
|
|
if (seen.has(child[i])) {
|
|
duplicatePositions.push(i);
|
|
} else {
|
|
seen.add(child[i]);
|
|
}
|
|
}
|
|
|
|
// Find missing indices
|
|
const missing = [];
|
|
for (const idx of allIndices) {
|
|
if (!seen.has(idx)) {
|
|
missing.push(idx);
|
|
}
|
|
}
|
|
|
|
// Replace duplicates with missing values
|
|
for (let i = 0; i < duplicatePositions.length && i < missing.length; i++) {
|
|
child[duplicatePositions[i]] = missing[i];
|
|
}
|
|
|
|
return child;
|
|
}
|
|
|
|
mutate(indices, unusedCells) {
|
|
const mutated = [...indices];
|
|
|
|
if (Math.random() < this.mutationRate) {
|
|
if (unusedCells.length > 0 && Math.random() < 0.3) {
|
|
const replaceIdx = Math.floor(Math.random() * mutated.length);
|
|
const unusedCell = unusedCells[Math.floor(Math.random() * unusedCells.length)];
|
|
const unusedIdx = this.cells.findIndex(c => c.label === unusedCell.label);
|
|
mutated[replaceIdx] = unusedIdx;
|
|
} else {
|
|
const i = Math.floor(Math.random() * mutated.length);
|
|
const j = Math.floor(Math.random() * mutated.length);
|
|
[mutated[i], mutated[j]] = [mutated[j], mutated[i]];
|
|
}
|
|
}
|
|
|
|
return mutated;
|
|
}
|
|
|
|
run() {
|
|
// Initialize population
|
|
let population = [];
|
|
for (let i = 0; i < this.populationSize; i++) {
|
|
population.push(this.createIndividual(this.cells));
|
|
}
|
|
|
|
// Evaluate initial population
|
|
let evaluated = population.map(config => ({
|
|
config,
|
|
indices: this.configToIndices(config),
|
|
...calculateScore(config, this.capacityWeight, this.irWeight)
|
|
}));
|
|
|
|
evaluated.sort((a, b) => a.score - b.score);
|
|
|
|
if (evaluated[0].score < this.bestScore) {
|
|
this.bestScore = evaluated[0].score;
|
|
this.bestSolution = evaluated[0];
|
|
}
|
|
|
|
// Calculate total combinations for display
|
|
const totalCombinations = this.factorial(this.cells.length) /
|
|
(this.factorial(this.cells.length - this.totalCellsNeeded) *
|
|
Math.pow(this.factorial(this.parallel), this.serial) *
|
|
this.factorial(this.serial));
|
|
|
|
// Main evolution loop
|
|
for (let iteration = 0; iteration < this.maxIterations && !this.stopped; iteration++) {
|
|
const newPopulation = [];
|
|
|
|
// Keep elite individuals
|
|
for (let i = 0; i < this.eliteCount && i < evaluated.length; i++) {
|
|
newPopulation.push(evaluated[i].indices);
|
|
}
|
|
|
|
// Generate rest through crossover and mutation
|
|
while (newPopulation.length < this.populationSize) {
|
|
const tournament1 = evaluated.slice(0, Math.ceil(evaluated.length / 2));
|
|
const tournament2 = evaluated.slice(0, Math.ceil(evaluated.length / 2));
|
|
const parent1 = tournament1[Math.floor(Math.random() * tournament1.length)];
|
|
const parent2 = tournament2[Math.floor(Math.random() * tournament2.length)];
|
|
|
|
let child = this.crossover(parent1.indices, parent2.indices);
|
|
|
|
// Safety: ensure all indices are valid
|
|
child = child.map(idx => {
|
|
if (idx >= 0 && idx < this.cells.length) return idx;
|
|
return Math.floor(Math.random() * this.cells.length);
|
|
});
|
|
|
|
const usedLabels = new Set(child.map(idx => this.cells[idx].label));
|
|
const unusedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
|
|
|
child = this.mutate(child, unusedCells);
|
|
newPopulation.push(child);
|
|
}
|
|
|
|
// Evaluate new population
|
|
evaluated = newPopulation.map(indices => {
|
|
const config = this.indicesToConfig(indices);
|
|
return {
|
|
config,
|
|
indices,
|
|
...calculateScore(config, this.capacityWeight, this.irWeight)
|
|
};
|
|
});
|
|
|
|
evaluated.sort((a, b) => a.score - b.score);
|
|
|
|
if (evaluated[0].score < this.bestScore) {
|
|
this.bestScore = evaluated[0].score;
|
|
this.bestSolution = evaluated[0];
|
|
}
|
|
|
|
this.stats.recordIteration();
|
|
|
|
// Send progress update every 10 iterations
|
|
if (iteration % 10 === 0 || iteration === this.maxIterations - 1) {
|
|
const stats = this.stats.getStats(iteration, this.maxIterations);
|
|
|
|
self.postMessage({
|
|
type: 'progress',
|
|
data: {
|
|
iteration,
|
|
maxIterations: this.maxIterations,
|
|
bestScore: this.bestScore,
|
|
currentBest: this.bestSolution,
|
|
totalCombinations,
|
|
evaluatedCombinations: (iteration + 1) * this.populationSize,
|
|
...stats
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
|
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
|
|
|
return {
|
|
configuration: this.bestSolution.config,
|
|
score: this.bestScore,
|
|
capacityCV: this.bestSolution.capacityCV,
|
|
irCV: this.bestSolution.irCV,
|
|
groupCapacities: this.bestSolution.groupCapacities,
|
|
excludedCells,
|
|
iterations: this.maxIterations,
|
|
elapsedTime: Date.now() - this.stats.startTime
|
|
};
|
|
}
|
|
|
|
factorial(n) {
|
|
if (n <= 1) return 1;
|
|
if (n > 20) return Infinity; // Prevent overflow
|
|
let result = 1;
|
|
for (let i = 2; i <= n; i++) result *= i;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Simulated Annealing
|
|
// =============================================================================
|
|
|
|
class SimulatedAnnealing {
|
|
constructor(cells, serial, parallel, options = {}) {
|
|
this.cells = cells;
|
|
this.serial = serial;
|
|
this.parallel = parallel;
|
|
this.totalCellsNeeded = serial * parallel;
|
|
|
|
this.maxIterations = options.maxIterations || 5000;
|
|
this.initialTemp = options.initialTemp || 100;
|
|
this.coolingRate = options.coolingRate || 0.995;
|
|
this.capacityWeight = options.capacityWeight ?? 0.7;
|
|
this.irWeight = options.irWeight ?? 0.3;
|
|
|
|
this.stopped = false;
|
|
this.bestSolution = null;
|
|
this.bestScore = Infinity;
|
|
this.stats = new StatsTracker();
|
|
}
|
|
|
|
stop() {
|
|
this.stopped = true;
|
|
}
|
|
|
|
createInitialConfig() {
|
|
const shuffled = shuffleArray([...this.cells]).slice(0, this.totalCellsNeeded);
|
|
const configuration = [];
|
|
|
|
for (let i = 0; i < this.serial; i++) {
|
|
const group = [];
|
|
for (let j = 0; j < this.parallel; j++) {
|
|
group.push(shuffled[i * this.parallel + j]);
|
|
}
|
|
configuration.push(group);
|
|
}
|
|
|
|
return configuration;
|
|
}
|
|
|
|
getNeighbor(config) {
|
|
const newConfig = cloneConfiguration(config);
|
|
const usedLabels = new Set(config.flat().map(c => c.label));
|
|
const unusedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
|
|
|
const moveType = Math.random();
|
|
|
|
if (unusedCells.length > 0 && moveType < 0.3) {
|
|
const groupIdx = Math.floor(Math.random() * this.serial);
|
|
const cellIdx = Math.floor(Math.random() * this.parallel);
|
|
const unusedCell = unusedCells[Math.floor(Math.random() * unusedCells.length)];
|
|
newConfig[groupIdx][cellIdx] = unusedCell;
|
|
} else if (moveType < 0.65) {
|
|
const group1 = Math.floor(Math.random() * this.serial);
|
|
let group2 = Math.floor(Math.random() * this.serial);
|
|
while (group2 === group1 && this.serial > 1) {
|
|
group2 = Math.floor(Math.random() * this.serial);
|
|
}
|
|
const cell1 = Math.floor(Math.random() * this.parallel);
|
|
const cell2 = Math.floor(Math.random() * this.parallel);
|
|
|
|
const temp = newConfig[group1][cell1];
|
|
newConfig[group1][cell1] = newConfig[group2][cell2];
|
|
newConfig[group2][cell2] = temp;
|
|
} else {
|
|
const groupIdx = Math.floor(Math.random() * this.serial);
|
|
if (this.parallel >= 2) {
|
|
const cell1 = Math.floor(Math.random() * this.parallel);
|
|
let cell2 = Math.floor(Math.random() * this.parallel);
|
|
while (cell2 === cell1) {
|
|
cell2 = Math.floor(Math.random() * this.parallel);
|
|
}
|
|
const temp = newConfig[groupIdx][cell1];
|
|
newConfig[groupIdx][cell1] = newConfig[groupIdx][cell2];
|
|
newConfig[groupIdx][cell2] = temp;
|
|
}
|
|
}
|
|
|
|
return newConfig;
|
|
}
|
|
|
|
run() {
|
|
let current = this.createInitialConfig();
|
|
let currentScore = calculateScore(current, this.capacityWeight, this.irWeight);
|
|
|
|
this.bestSolution = { config: cloneConfiguration(current), ...currentScore };
|
|
this.bestScore = currentScore.score;
|
|
|
|
let temperature = this.initialTemp;
|
|
let acceptedMoves = 0;
|
|
let totalMoves = 0;
|
|
|
|
for (let iteration = 0; iteration < this.maxIterations && !this.stopped; iteration++) {
|
|
const neighbor = this.getNeighbor(current);
|
|
const neighborScore = calculateScore(neighbor, this.capacityWeight, this.irWeight);
|
|
|
|
const delta = neighborScore.score - currentScore.score;
|
|
totalMoves++;
|
|
|
|
if (delta < 0 || Math.random() < Math.exp(-delta / temperature)) {
|
|
current = neighbor;
|
|
currentScore = neighborScore;
|
|
acceptedMoves++;
|
|
|
|
if (currentScore.score < this.bestScore) {
|
|
this.bestScore = currentScore.score;
|
|
this.bestSolution = { config: cloneConfiguration(current), ...currentScore };
|
|
}
|
|
}
|
|
|
|
temperature *= this.coolingRate;
|
|
this.stats.recordIteration();
|
|
|
|
if (iteration % 50 === 0 || iteration === this.maxIterations - 1) {
|
|
const stats = this.stats.getStats(iteration, this.maxIterations);
|
|
|
|
self.postMessage({
|
|
type: 'progress',
|
|
data: {
|
|
iteration,
|
|
maxIterations: this.maxIterations,
|
|
bestScore: this.bestScore,
|
|
currentBest: this.bestSolution,
|
|
temperature,
|
|
acceptanceRate: totalMoves > 0 ? (acceptedMoves / totalMoves * 100) : 0,
|
|
evaluatedCombinations: iteration + 1,
|
|
...stats
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
|
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
|
|
|
return {
|
|
configuration: this.bestSolution.config,
|
|
score: this.bestScore,
|
|
capacityCV: this.bestSolution.capacityCV,
|
|
irCV: this.bestSolution.irCV,
|
|
groupCapacities: this.bestSolution.groupCapacities,
|
|
excludedCells,
|
|
iterations: this.maxIterations,
|
|
elapsedTime: Date.now() - this.stats.startTime
|
|
};
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Exhaustive Search
|
|
// =============================================================================
|
|
|
|
class ExhaustiveSearch {
|
|
constructor(cells, serial, parallel, options = {}) {
|
|
this.cells = cells;
|
|
this.serial = serial;
|
|
this.parallel = parallel;
|
|
this.totalCellsNeeded = serial * parallel;
|
|
|
|
this.capacityWeight = options.capacityWeight ?? 0.7;
|
|
this.irWeight = options.irWeight ?? 0.3;
|
|
this.maxIterations = options.maxIterations || 100000;
|
|
|
|
this.stopped = false;
|
|
this.bestSolution = null;
|
|
this.bestScore = Infinity;
|
|
this.stats = new StatsTracker();
|
|
}
|
|
|
|
stop() {
|
|
this.stopped = true;
|
|
}
|
|
|
|
*combinations(array, k) {
|
|
if (k === 0) {
|
|
yield [];
|
|
return;
|
|
}
|
|
if (array.length < k) return;
|
|
|
|
const [first, ...rest] = array;
|
|
for (const combo of this.combinations(rest, k - 1)) {
|
|
yield [first, ...combo];
|
|
}
|
|
yield* this.combinations(rest, k);
|
|
}
|
|
|
|
*generatePartitions(cells, groupSize, numGroups) {
|
|
if (numGroups === 0) {
|
|
yield [];
|
|
return;
|
|
}
|
|
|
|
if (cells.length < groupSize * numGroups) return;
|
|
|
|
for (const group of this.combinations(cells, groupSize)) {
|
|
const remaining = cells.filter(c => !group.includes(c));
|
|
for (const rest of this.generatePartitions(remaining, groupSize, numGroups - 1)) {
|
|
yield [group, ...rest];
|
|
}
|
|
}
|
|
}
|
|
|
|
calculateTotalCombinations() {
|
|
// Formula: C(n, k) * C(n-k, k) * ... / numGroups! for identical groups
|
|
const n = this.cells.length;
|
|
const k = this.parallel;
|
|
const numGroups = this.serial;
|
|
|
|
let total = 1;
|
|
let remaining = n;
|
|
|
|
for (let i = 0; i < numGroups; i++) {
|
|
total *= this.binomial(remaining, k);
|
|
remaining -= k;
|
|
}
|
|
|
|
// Divide by numGroups! if groups are interchangeable
|
|
// (but for battery packs, position matters, so we don't divide)
|
|
|
|
return total;
|
|
}
|
|
|
|
binomial(n, k) {
|
|
if (k > n) return 0;
|
|
if (k === 0 || k === n) return 1;
|
|
|
|
let result = 1;
|
|
for (let i = 0; i < k; i++) {
|
|
result = result * (n - i) / (i + 1);
|
|
}
|
|
return Math.round(result);
|
|
}
|
|
|
|
run() {
|
|
let iteration = 0;
|
|
const totalCombinations = this.calculateTotalCombinations();
|
|
|
|
const cellCombos = this.cells.length > this.totalCellsNeeded
|
|
? this.combinations(this.cells, this.totalCellsNeeded)
|
|
: [[...this.cells]];
|
|
|
|
for (const cellSubset of cellCombos) {
|
|
if (this.stopped) break;
|
|
|
|
for (const partition of this.generatePartitions(cellSubset, this.parallel, this.serial)) {
|
|
if (this.stopped) break;
|
|
|
|
const scoreResult = calculateScore(partition, this.capacityWeight, this.irWeight);
|
|
|
|
if (scoreResult.score < this.bestScore) {
|
|
this.bestScore = scoreResult.score;
|
|
this.bestSolution = { config: partition, ...scoreResult };
|
|
}
|
|
|
|
iteration++;
|
|
this.stats.recordIteration();
|
|
|
|
if (iteration % 500 === 0) {
|
|
const stats = this.stats.getStats(iteration, Math.min(totalCombinations, this.maxIterations));
|
|
|
|
self.postMessage({
|
|
type: 'progress',
|
|
data: {
|
|
iteration,
|
|
maxIterations: Math.min(totalCombinations, this.maxIterations),
|
|
bestScore: this.bestScore,
|
|
currentBest: this.bestSolution,
|
|
totalCombinations,
|
|
evaluatedCombinations: iteration,
|
|
...stats
|
|
}
|
|
});
|
|
}
|
|
|
|
if (iteration >= this.maxIterations) {
|
|
this.stopped = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final progress update
|
|
const stats = this.stats.getStats(iteration, Math.min(totalCombinations, this.maxIterations));
|
|
self.postMessage({
|
|
type: 'progress',
|
|
data: {
|
|
iteration,
|
|
maxIterations: Math.min(totalCombinations, this.maxIterations),
|
|
bestScore: this.bestScore,
|
|
currentBest: this.bestSolution,
|
|
totalCombinations,
|
|
evaluatedCombinations: iteration,
|
|
...stats
|
|
}
|
|
});
|
|
|
|
const usedLabels = new Set(this.bestSolution.config.flat().map(c => c.label));
|
|
const excludedCells = this.cells.filter(c => !usedLabels.has(c.label));
|
|
|
|
return {
|
|
configuration: this.bestSolution.config,
|
|
score: this.bestScore,
|
|
capacityCV: this.bestSolution.capacityCV,
|
|
irCV: this.bestSolution.irCV,
|
|
groupCapacities: this.bestSolution.groupCapacities,
|
|
excludedCells,
|
|
iterations: iteration,
|
|
elapsedTime: Date.now() - this.stats.startTime
|
|
};
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Worker Message Handler
|
|
// =============================================================================
|
|
|
|
let currentAlgorithm = null;
|
|
|
|
self.onmessage = function (e) {
|
|
const { type, data } = e.data;
|
|
|
|
switch (type) {
|
|
case 'start':
|
|
const { cells, serial, parallel, algorithm, options } = data;
|
|
|
|
switch (algorithm) {
|
|
case 'genetic':
|
|
currentAlgorithm = new GeneticAlgorithm(cells, serial, parallel, options);
|
|
break;
|
|
case 'simulated-annealing':
|
|
currentAlgorithm = new SimulatedAnnealing(cells, serial, parallel, options);
|
|
break;
|
|
case 'exhaustive':
|
|
currentAlgorithm = new ExhaustiveSearch(cells, serial, parallel, options);
|
|
break;
|
|
}
|
|
|
|
try {
|
|
const result = currentAlgorithm.run();
|
|
self.postMessage({ type: 'complete', data: result });
|
|
} catch (error) {
|
|
self.postMessage({ type: 'error', data: error.message });
|
|
}
|
|
|
|
currentAlgorithm = null;
|
|
break;
|
|
|
|
case 'stop':
|
|
if (currentAlgorithm) {
|
|
currentAlgorithm.stop();
|
|
}
|
|
break;
|
|
}
|
|
};
|