diff --git a/README.md b/README.md index fe57c68..49d616e 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ A web-based tool for finding the optimal cell configuration in lithium battery p - **Pack Configuration**: Support for any SxP configuration (e.g., 6S2P, 4S3P, 12S4P) - **Cell Matching**: Optimize by capacity (mAh) and internal resistance (mΩ) - **Multiple Algorithms**: - - Genetic Algorithm (fast, recommended) - - Simulated Annealing (good for escaping local minima) - Exhaustive Search (optimal for small configurations) - **Surplus Cell Support**: Use more cells than needed; the algorithm selects the best subset - **Live Progress**: Watch the optimization in real-time diff --git a/index.html b/index.html index 8b61372..dc269ef 100644 --- a/index.html +++ b/index.html @@ -112,9 +112,9 @@
@@ -313,7 +313,8 @@ Git · Based on research by - Wang et al., 2013 + Wang et al., + 2013

This tool is for educational purposes. Always consult professional guidance for battery pack assembly. diff --git a/js/matching-worker.js b/js/matching-worker.js index 99b58ce..f01605b 100644 --- a/js/matching-worker.js +++ b/js/matching-worker.js @@ -122,411 +122,6 @@ class StatsTracker { } } -// ============================================================================= -// 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 // =============================================================================