cleanup
This commit is contained in:
@ -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)
|
- **Pack Configuration**: Support for any SxP configuration (e.g., 6S2P, 4S3P, 12S4P)
|
||||||
- **Cell Matching**: Optimize by capacity (mAh) and internal resistance (mΩ)
|
- **Cell Matching**: Optimize by capacity (mAh) and internal resistance (mΩ)
|
||||||
- **Multiple Algorithms**:
|
- **Multiple Algorithms**:
|
||||||
- Genetic Algorithm (fast, recommended)
|
|
||||||
- Simulated Annealing (good for escaping local minima)
|
|
||||||
- Exhaustive Search (optimal for small configurations)
|
- Exhaustive Search (optimal for small configurations)
|
||||||
- **Surplus Cell Support**: Use more cells than needed; the algorithm selects the best subset
|
- **Surplus Cell Support**: Use more cells than needed; the algorithm selects the best subset
|
||||||
- **Live Progress**: Watch the optimization in real-time
|
- **Live Progress**: Watch the optimization in real-time
|
||||||
|
|||||||
@ -112,9 +112,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="algorithm-select">Algorithm</label>
|
<label for="algorithm-select">Algorithm</label>
|
||||||
<select id="algorithm-select">
|
<select id="algorithm-select">
|
||||||
<option value="genetic">Genetic Algorithm (Fast)</option>
|
<option value="exhaustive">Exhaustive Search (Small packs only)</option>
|
||||||
<option value="simulated-annealing">Simulated Annealing</option>
|
<option value="genetic" disabled>Genetic Algorithm (Fast)</option>
|
||||||
<option value="exhaustive">Exhaustive (Small packs only)</option>
|
<option value="simulated-annealing" disabled>Simulated Annealing</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -313,7 +313,8 @@
|
|||||||
<a href="https://git.mosad.xyz/localhorst/LiXX_Cell_Pack_Matcher" target="_blank" rel="noopener">Git</a>
|
<a href="https://git.mosad.xyz/localhorst/LiXX_Cell_Pack_Matcher" target="_blank" rel="noopener">Git</a>
|
||||||
·
|
·
|
||||||
Based on research by
|
Based on research by
|
||||||
<a href="https://doi.org/10.1016/j.jpowsour.2013.11.064" target="_blank" rel="noopener">Wang et al., 2013</a>
|
<a href="https://doi.org/10.1016/j.jpowsour.2013.11.064" target="_blank" rel="noopener">Wang et al.,
|
||||||
|
2013</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="disclaimer">
|
<p class="disclaimer">
|
||||||
This tool is for educational purposes. Always consult professional guidance for battery pack assembly.
|
This tool is for educational purposes. Always consult professional guidance for battery pack assembly.
|
||||||
|
|||||||
@ -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
|
// Exhaustive Search
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user